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