ILIAS  release_8 Revision v8.24
class.ilTestSequenceDynamicQuestionSet.php
Go to the documentation of this file.
1<?php
2
30{
34 private $db = null;
35
39 private $questionSet = null;
40
44 private $activeId = null;
45
50
54 private $questionTracking = array();
55
60
65
69 private $postponedQuestions = array();
70
75
80
85
90
94 private $correctAnsweredQuestions = array();
95
99 private $wrongAnsweredQuestions = array();
100
105
110
115
122 {
123 $this->db = $db;
124 $this->questionSet = $questionSet;
125 $this->activeId = $activeId;
126
127 $this->newlyTrackedQuestion = null;
128 $this->newlyTrackedQuestionsStatus = null;
129
130 $this->newlyPostponedQuestion = null;
131 $this->newlyPostponedQuestionsCount = null;
132
133 $this->newlyAnsweredQuestion = null;
134 $this->newlyAnsweredQuestionsAnswerStatus = null;
135
136 $this->alreadyCheckedQuestions = array();
137 $this->newlyCheckedQuestion = null;
138
139 $this->preventCheckedQuestionsFromComingUpEnabled = false;
140
141 $this->currentQuestionId = null;
142 }
143
144 public function getActiveId(): ?int
145 {
146 return $this->activeId;
147 }
148
150 {
151 $this->preventCheckedQuestionsFromComingUpEnabled = $preventCheckedQuestionsFromComingUpEnabled;
152 }
153
155 {
157 }
158
162 public function getCurrentQuestionId(): ?int
163 {
165 }
166
171 {
172 $this->currentQuestionId = $currentQuestionId;
173 }
174
175 public function loadFromDb()
176 {
177 $this->loadQuestionTracking();
178 $this->loadAnswerStatus();
179 $this->loadPostponedQuestions();
180 $this->loadCheckedQuestions();
181 }
182
183 private function loadQuestionTracking()
184 {
185 $query = "
186 SELECT question_fi, status
187 FROM tst_seq_qst_tracking
188 WHERE active_fi = %s
189 AND pass = %s
190 ORDER BY orderindex ASC
191 ";
192
193 $res = $this->db->queryF($query, array('integer','integer'), array($this->activeId, 0));
194
195 $this->questionTracking = array();
196
197 while ($row = $this->db->fetchAssoc($res)) {
198 $this->questionTracking[] = array(
199 'qid' => $row['question_fi'],
200 'status' => $row['status']
201 );
202 }
203 }
204
205 private function loadAnswerStatus()
206 {
207 $query = "
208 SELECT question_fi, correctness
209 FROM tst_seq_qst_answstatus
210 WHERE active_fi = %s
211 AND pass = %s
212 ";
213
214 $res = $this->db->queryF($query, array('integer','integer'), array($this->activeId, 0));
215
216 $this->correctAnsweredQuestions = array();
217 $this->wrongAnsweredQuestions = array();
218
219 while ($row = $this->db->fetchAssoc($res)) {
220 if ($row['correctness']) {
221 $this->correctAnsweredQuestions[ $row['question_fi'] ] = $row['question_fi'];
222 } else {
223 $this->wrongAnsweredQuestions[ $row['question_fi'] ] = $row['question_fi'];
224 }
225 }
226 }
227
228 private function loadPostponedQuestions()
229 {
230 $query = "
231 SELECT question_fi, cnt
232 FROM tst_seq_qst_postponed
233 WHERE active_fi = %s
234 AND pass = %s
235 ";
236
237 $res = $this->db->queryF($query, array('integer','integer'), array($this->activeId, 0));
238
239 $this->postponedQuestions = array();
240
241 while ($row = $this->db->fetchAssoc($res)) {
242 $this->postponedQuestions[ $row['question_fi'] ] = $row['cnt'];
243 }
244 }
245
246 private function loadCheckedQuestions()
247 {
248 $res = $this->db->queryF(
249 "SELECT question_fi FROM tst_seq_qst_checked WHERE active_fi = %s AND pass = %s",
250 array('integer','integer'),
251 array($this->getActiveId(), 0)
252 );
253
254 while ($row = $this->db->fetchAssoc($res)) {
255 $this->alreadyCheckedQuestions[ $row['question_fi'] ] = $row['question_fi'];
256 }
257 }
258
259 public function saveToDb()
260 {
261 $this->db->manipulateF(
262 "DELETE FROM tst_sequence WHERE active_fi = %s AND pass = %s",
263 array('integer','integer'),
264 array($this->getActiveId(), 0)
265 );
266
267 $this->db->insert('tst_sequence', array(
268 'active_fi' => array('integer', $this->getActiveId()),
269 'pass' => array('integer', 0),
270 'sequence' => array('clob', null),
271 'postponed' => array('text', null),
272 'hidden' => array('text', null),
273 'tstamp' => array('integer', time())
274 ));
275
282 }
283
284 private function saveNewlyTrackedQuestion()
285 {
286 if ((int) $this->newlyTrackedQuestion) {
287 $newOrderIndex = $this->getNewOrderIndexForQuestionTracking();
288
289 $this->db->replace(
290 'tst_seq_qst_tracking',
291 array(
292 'active_fi' => array('integer', (int) $this->getActiveId()),
293 'pass' => array('integer', 0),
294 'question_fi' => array('integer', (int) $this->newlyTrackedQuestion)
295 ),
296 array(
297 'status' => array('text', $this->newlyTrackedQuestionsStatus),
298 'orderindex' => array('integer', $newOrderIndex)
299 )
300 );
301 }
302 }
303
305 {
306 $query = "
307 SELECT (MAX(orderindex) + 1) new_order_index
308 FROM tst_seq_qst_tracking
309 WHERE active_fi = %s
310 AND pass = %s
311 ";
312
313 $res = $this->db->queryF($query, array('integer','integer'), array($this->getActiveId(), 0));
314
315 $row = $this->db->fetchAssoc($res);
316
317 if ($row['new_order_index']) {
318 return $row['new_order_index'];
319 }
320
321 return 1;
322 }
323
325 {
326 if ((int) $this->newlyAnsweredQuestion) {
327 $this->db->replace(
328 'tst_seq_qst_answstatus',
329 array(
330 'active_fi' => array('integer', (int) $this->getActiveId()),
331 'pass' => array('integer', 0),
332 'question_fi' => array('integer', (int) $this->newlyAnsweredQuestion)
333 ),
334 array(
335 'correctness' => array('integer', (int) $this->newlyAnsweredQuestionsAnswerStatus)
336 )
337 );
338 }
339 }
340
341 private function saveNewlyPostponedQuestion()
342 {
343 if ((int) $this->newlyPostponedQuestion) {
344 $this->db->replace(
345 'tst_seq_qst_postponed',
346 array(
347 'active_fi' => array('integer', (int) $this->getActiveId()),
348 'pass' => array('integer', 0),
349 'question_fi' => array('integer', (int) $this->newlyPostponedQuestion)
350 ),
351 array(
352 'cnt' => array('integer', (int) $this->newlyPostponedQuestionsCount)
353 )
354 );
355 }
356 }
357
359 {
360 $INquestions = $this->db->in('question_fi', array_keys($this->postponedQuestions), true, 'integer');
361
362 $query = "
363 DELETE FROM tst_seq_qst_postponed
364 WHERE active_fi = %s
365 AND pass = %s
366 AND $INquestions
367 ";
368
369 $this->db->manipulateF($query, array('integer','integer'), array($this->getActiveId(), 0));
370 }
371
372 private function saveNewlyCheckedQuestion()
373 {
374 if ((int) $this->newlyCheckedQuestion) {
375 $this->db->replace('tst_seq_qst_checked', array(
376 'active_fi' => array('integer', (int) $this->getActiveId()),
377 'pass' => array('integer', 0),
378 'question_fi' => array('integer', (int) $this->newlyCheckedQuestion)
379 ), array());
380 }
381 }
382
384 {
385 $NOT_IN_checkedQuestions = $this->db->in('question_fi', $this->alreadyCheckedQuestions, true, 'integer');
386
387 // BEGIN: FIX IN QUERY
388 if ($NOT_IN_checkedQuestions == ' 1=2 ') {
389 $NOT_IN_checkedQuestions = ' 1=1 ';
390 }
391 // END: FIX IN QUERY
392
393 $query = "
394 DELETE FROM tst_seq_qst_checked
395 WHERE active_fi = %s
396 AND pass = %s
397 AND $NOT_IN_checkedQuestions
398 ";
399
400 $this->db->manipulateF($query, array('integer', 'integer'), array((int) $this->getActiveId(), 0));
401 }
402
403 public function loadQuestions(ilObjTestDynamicQuestionSetConfig $dynamicQuestionSetConfig, ilTestDynamicQuestionSetFilterSelection $filterSelection)
404 {
405 $this->questionSet->load($dynamicQuestionSetConfig, $filterSelection);
406
407 // echo "<table><tr>";
408// echo "<td width='200'><pre>".print_r($this->questionSet->getActualQuestionSequence(), 1)."</pre></td>";
409// echo "<td width='200'><pre>".print_r($this->correctAnsweredQuestions, 1)."</pre></td>";
410// echo "<td width='200'><pre>".print_r($this->wrongAnsweredQuestions, 1)."</pre></td>";
411// echo "</tr></table>";
412 }
413
414 // -----------------------------------------------------------------------------------------------------------------
415
417 {
418 switch (true) {
419 case !$this->questionSet->questionExists($testSession->getCurrentQuestionId()):
420 case !$this->isFilteredQuestion($testSession->getCurrentQuestionId()):
421
422 $testSession->setCurrentQuestionId(null);
423 }
424
425 foreach ($this->postponedQuestions as $questionId) {
426 if (!$this->questionSet->questionExists($questionId)) {
427 unset($this->postponedQuestions[$questionId]);
428 }
429 }
430
431 foreach ($this->wrongAnsweredQuestions as $questionId) {
432 if (!$this->questionSet->questionExists($questionId)) {
433 unset($this->wrongAnsweredQuestions[$questionId]);
434 }
435 }
436
437 foreach ($this->correctAnsweredQuestions as $questionId) {
438 if (!$this->questionSet->questionExists($questionId)) {
439 unset($this->correctAnsweredQuestions[$questionId]);
440 }
441 }
442 }
443
444 // -----------------------------------------------------------------------------------------------------------------
445
446 public function getUpcomingQuestionId()
447 {
448 if ($questionId = $this->fetchUpcomingQuestionId(true, true)) {
449 return $questionId;
450 }
451
452 if ($questionId = $this->fetchUpcomingQuestionId(false, true)) {
453 return $questionId;
454 }
455
456 if ($questionId = $this->fetchUpcomingQuestionId(true, false)) {
457 return $questionId;
458 }
459
460 if ($questionId = $this->fetchUpcomingQuestionId(false, false)) {
461 return $questionId;
462 }
463
464 return null;
465 }
466
467 private function fetchUpcomingQuestionId($excludePostponedQuestions, $forceNonAnsweredQuestion)
468 {
469 foreach ($this->questionSet->getActualQuestionSequence() as $level => $questions) {
470 $postponedQuestions = array();
471
472 foreach ($questions as $pos => $qId) {
473 if (isset($this->correctAnsweredQuestions[$qId])) {
474 continue;
475 }
476
478 continue;
479 }
480
481 if ($forceNonAnsweredQuestion && isset($this->wrongAnsweredQuestions[$qId])) {
482 continue;
483 }
484
485 if (isset($this->postponedQuestions[$qId])) {
486 $postponedQuestions[$qId] = $this->postponedQuestions[$qId];
487 continue;
488 }
489
490 return $qId;
491 }
492
493 if (!$excludePostponedQuestions && count($postponedQuestions)) {
495 }
496 }
497
498 return null;
499 }
500
501 public function isAnsweredQuestion($questionId): bool
502 {
503 return (
504 isset($this->correctAnsweredQuestions[$questionId])
505 || isset($this->wrongAnsweredQuestions[$questionId])
506 );
507 }
508
509 public function isPostponedQuestion($questionId): bool
510 {
511 return isset($this->postponedQuestions[$questionId]);
512 }
513
514 public function isFilteredQuestion($questionId): bool
515 {
516 foreach ($this->questionSet->getActualQuestionSequence() as $level => $questions) {
517 if (in_array($questionId, $questions)) {
518 return true;
519 }
520 }
521
522 return false;
523 }
524
525 public function trackedQuestionExists(): bool
526 {
527 return (bool) count($this->questionTracking);
528 }
529
530 public function getTrackedQuestionList($currentQuestionId = null): array
531 {
532 $questionList = array();
533
534 if ($currentQuestionId) {
535 $questionList[$currentQuestionId] = $this->questionSet->getQuestionData($currentQuestionId);
536 }
537
538 foreach (array_reverse($this->questionTracking) as $trackedQuestion) {
539 if (!isset($questionList[ $trackedQuestion['qid'] ])) {
540 $questionList[ $trackedQuestion['qid'] ] = $this->questionSet->getQuestionData($trackedQuestion['qid']);
541 }
542 }
543
544 return $questionList;
545 }
546
547 public function resetTrackedQuestionList()
548 {
549 $this->questionTracking = array();
550 }
551
552 public function openQuestionExists(): bool
553 {
554 return count($this->getOpenQuestions()) > 0;
555 }
556
557 public function getOpenQuestions(): array
558 {
559 $completeQuestionIds = array_keys($this->questionSet->getAllQuestionsData());
560
561 $openQuestions = array_diff($completeQuestionIds, $this->correctAnsweredQuestions);
562
563 return $openQuestions;
564 }
565
566 public function getTrackedQuestionCount(): int
567 {
568 $uniqueQuestions = array();
569
570 foreach ($this->questionTracking as $trackedQuestion) {
571 $uniqueQuestions[$trackedQuestion['qid']] = $trackedQuestion['qid'];
572 }
573
574 return count($uniqueQuestions);
575 }
576
577 public function getCurrentPositionIndex($questionId): ?int
578 {
579 $i = 0;
580
581 foreach ($this->getSelectionOrderedSequence() as $qId) {
582 $i++;
583
584 if ($qId == $questionId) {
585 return $i;
586 }
587 }
588
589 return null;
590 }
591
592 public function getLastPositionIndex(): int
593 {
594 return count($this->getSelectionOrderedSequence());
595 }
596
597 // -----------------------------------------------------------------------------------------------------------------
598
599 public function setQuestionUnchecked($questionId)
600 {
601 unset($this->alreadyCheckedQuestions[$questionId]);
602 }
603
604 public function setQuestionChecked($questionId)
605 {
606 $this->newlyCheckedQuestion = $questionId;
607 $this->alreadyCheckedQuestions[$questionId] = $questionId;
608 }
609
610 public function isQuestionChecked($questionId): bool
611 {
612 return isset($this->alreadyCheckedQuestions[$questionId]);
613 }
614
615 public function setQuestionPostponed($questionId)
616 {
617 $this->trackQuestion($questionId, 'postponed');
618
619 if (!isset($this->postponedQuestions[$questionId])) {
620 $this->postponedQuestions[$questionId] = 0;
621 }
622
623 $this->postponedQuestions[$questionId]++;
624
625 $this->newlyPostponedQuestion = $questionId;
626 $this->newlyPostponedQuestionsCount = $this->postponedQuestions[$questionId];
627 }
628
629 public function unsetQuestionPostponed($questionId)
630 {
631 if (isset($this->postponedQuestions[$questionId])) {
632 unset($this->postponedQuestions[$questionId]);
633 }
634 }
635
636 public function setQuestionAnsweredCorrect($questionId)
637 {
638 $this->trackQuestion($questionId, 'correct');
639
640 $this->correctAnsweredQuestions[$questionId] = $questionId;
641
642 if (isset($this->wrongAnsweredQuestions[$questionId])) {
643 unset($this->wrongAnsweredQuestions[$questionId]);
644 }
645
646 $this->newlyAnsweredQuestion = $questionId;
647 $this->newlyAnsweredQuestionsAnswerStatus = true;
648 }
649
650 public function setQuestionAnsweredWrong($questionId)
651 {
652 $this->trackQuestion($questionId, 'wrong');
653
654 $this->wrongAnsweredQuestions[$questionId] = $questionId;
655
656 if (isset($this->correctAnsweredQuestions[$questionId])) {
657 unset($this->correctAnsweredQuestions[$questionId]);
658 }
659
660 $this->newlyAnsweredQuestion = $questionId;
661 $this->newlyAnsweredQuestionsAnswerStatus = false;
662 }
663
664 private function trackQuestion($questionId, $answerStatus)
665 {
666 $this->questionTracking[] = array(
667 'qid' => $questionId, 'status' => $answerStatus
668 );
669
670 $this->newlyTrackedQuestion = $questionId;
671 $this->newlyTrackedQuestionsStatus = $answerStatus;
672 }
673
674 // -----------------------------------------------------------------------------------------------------------------
675
676 public function hasStarted(): bool
677 {
678 return $this->trackedQuestionExists();
679 }
680
681 // -----------------------------------------------------------------------------------------------------------------
682
687 {
688 return $this->questionSet;
689 }
690
691 public function getCompleteQuestionsData(): array
692 {
693 return $this->questionSet->getCompleteQuestionList()->getQuestionDataArray();
694 }
695
696 public function getFilteredQuestionsData(): array
697 {
698 return $this->questionSet->getFilteredQuestionList()->getQuestionDataArray();
699 }
700
701 // -----------------------------------------------------------------------------------------------------------------
702
703 public function getUserSequenceQuestions(): array
704 {
705 //return array_keys( $this->getTrackedQuestionList() );
706
707 $questionSequence = array();
708
709 foreach ($this->questionSet->getActualQuestionSequence() as $level => $questions) {
710 $questionSequence = array_merge($questionSequence, $questions);
711 }
712
713 return $questionSequence;
714 }
715
721 {
722 $minPostponeCount = null;
723 $minPostponeItem = null;
724
725 foreach (array_reverse($postponedQuestions, true) as $qId => $postponeCount) {
726 if ($minPostponeCount === null || $postponeCount <= $minPostponeCount) {
727 $minPostponeCount = $postponeCount;
728 $minPostponeItem = $qId;
729 }
730 }
731 return $minPostponeItem;
732 }
733
734 public function getPass(): int
735 {
736 return 0;
737 }
738
739 // -----------------------------------------------------------------------------------------------------------------
740
742 {
743 $maxPostponeCount = max($postponedQuestions);
744
745 $orderedSequence = array();
746 $postponedCountDomain = array_flip($postponedQuestions);
747
748 for ($i = 1; $i <= $maxPostponeCount; $i++) {
749 if (!isset($postponedCountDomain[$i])) {
750 continue;
751 }
752
753 foreach ($postponedQuestions as $qId => $postponeCount) {
754 if ($postponeCount == $i) {
755 $orderedSequence[] = $qId;
756 }
757 }
758 }
759
760 return $orderedSequence;
761 }
762
763 private function fetchQuestionSequence($nonPostponedQuestions, $nonAnsweredQuestions): array
764 {
765 $questionSequence = array();
766
767 foreach ($this->questionSet->getActualQuestionSequence() as $level => $questions) {
768 $postponedQuestions = array();
769
770 foreach ($questions as $pos => $qId) {
771 if (isset($this->correctAnsweredQuestions[$qId])) {
772 continue;
773 }
774
775 if ($nonAnsweredQuestions && isset($this->wrongAnsweredQuestions[$qId])) {
776 continue;
777 } elseif (!$nonAnsweredQuestions && !isset($this->wrongAnsweredQuestions[$qId])) {
778 continue;
779 }
780
781 if (!$nonPostponedQuestions && isset($this->postponedQuestions[$qId])) {
782 $postponedQuestions[$qId] = $this->postponedQuestions[$qId];
783 continue;
784 } elseif ($nonPostponedQuestions && !isset($this->postponedQuestions[$qId])) {
785 $questionSequence[] = $qId;
786 }
787 }
788
789 if (!$nonPostponedQuestions && count($postponedQuestions)) {
790 $questionSequence = array_merge(
791 $questionSequence,
793 );
794 }
795 }
796
797 return $questionSequence;
798 }
799
800 private function fetchTrackedCorrectAnsweredSequence(): array
801 {
802 $questionSequence = array();
803
804 foreach ($this->questionTracking as $key => $question) {
805 $qId = $question['qid'];
806
807 if (!isset($this->correctAnsweredQuestions[$qId])) {
808 continue;
809 }
810
811 $questionSequence[] = $qId;
812 }
813
814 return $questionSequence;
815 }
816
817 private function getOrderedSequence(): array
818 {
820
821 $nonAnsweredQuestions = $this->fetchQuestionSequence(
822 true,
823 true
824 );
825
826 $postponedNonAnsweredQuestions = $this->fetchQuestionSequence(
827 false,
828 true
829 );
830
832 true,
833 false
834 );
835
836 $postponedWrongAnsweredQuestions = $this->fetchQuestionSequence(
837 false,
838 false
839 );
840
841 $questionOrder = array_merge(
843 $nonAnsweredQuestions,
844 $postponedNonAnsweredQuestions,
846 $postponedWrongAnsweredQuestions
847 );
848
849 return $questionOrder;
850 }
851
852 public function getSelectionOrderedSequence(): array
853 {
854 $sequence = array();
855
856 foreach ($this->getOrderedSequence() as $qId) {
857 if (!$this->getQuestionSet()->getSelectionQuestionList()->isInList($qId)) {
858 continue;
859 }
860
861 $sequence[] = $qId;
862 }
863
864 return $sequence;
865 }
866
867 public function getSequenceSummary($obligationsFilterEnabled = false): array
868 {
869 $questionOrder = $this->getSelectionOrderedSequence();
870
871 $solved_questions = ilObjTest::_getSolvedQuestions($this->getActiveId());
872
873 $key = 1;
874
875 $summary = array();
876
877 foreach ($questionOrder as $qId) {
878 $question = ilObjTest::_instanciateQuestion($qId);
879 if (is_object($question)) {
880 $worked_through = $question->_isWorkedThrough($this->getActiveId(), $question->getId(), $this->getPass());
881 $solved = 0;
882 if (array_key_exists($question->getId(), $solved_questions)) {
883 $solved = $solved_questions[$question->getId()]["solved"];
884 }
885
886 // do not show postponing, since this happens implicit on dircarding solutions (CTM only)
887 //$is_postponed = $this->isPostponedQuestion($question->getId());
888
889 $row = array("nr" => "$key", "title" => $question->getTitle(), "qid" => $question->getId(), "visited" => $worked_through, "solved" => (($solved) ? "1" : "0"), "description" => $question->getComment(), "points" => $question->getMaximumPoints(), "worked_through" => $worked_through, "postponed" => false, "sequence" => $qId, "obligatory" => ilObjTest::isQuestionObligatory($question->getId()), 'isAnswered' => $question->isAnswered($this->getActiveId(), $this->getPass()));
890
891 if (!$obligationsFilterEnabled || $row['obligatory']) {
892 $summary[] = $row;
893 }
894
895 $key++;
896 }
897 }
898
899 return $summary;
900 }
901
903 {
904 $filteredQuestions = $this->questionSet->getFilteredQuestionList()->getQuestionDataArray();
905
906 foreach ($filteredQuestions as $filteredQuestion) {
907 if ($this->isQuestionChecked($filteredQuestion['question_id'])) {
908 return true;
909 }
910 }
911
912 return false;
913 }
914
916 {
917 $filteredQuestions = $this->questionSet->getFilteredQuestionList()->getQuestionDataArray();
918
919 foreach ($filteredQuestions as $filteredQuestion) {
920 if ($this->isQuestionChecked($filteredQuestion['question_id'])) {
921 $this->setQuestionUnchecked($filteredQuestion['question_id']);
922 }
923 }
924 }
925}
static _instanciateQuestion($question_id)
Creates an instance of a question with a given question id.
static _getSolvedQuestions($active_id, $question_fi=null)
get solved questions
static isQuestionObligatory($question_id)
checks wether the question with given id is marked as obligatory or not
setPreventCheckedQuestionsFromComingUpEnabled($preventCheckedQuestionsFromComingUpEnabled)
__construct(ilDBInterface $db, ilTestDynamicQuestionSet $questionSet, $activeId)
Constructor.
loadQuestions(ilObjTestDynamicQuestionSetConfig $dynamicQuestionSetConfig, ilTestDynamicQuestionSetFilterSelection $filterSelection)
fetchQuestionSequence($nonPostponedQuestions, $nonAnsweredQuestions)
fetchUpcomingQuestionId($excludePostponedQuestions, $forceNonAnsweredQuestion)
cleanupQuestions(ilTestSessionDynamicQuestionSet $testSession)
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
$i
Definition: metadata.php:41
string $key
Consumer key/client ID value.
Definition: System.php:193
$query