ILIAS  release_7 Revision v7.30-3-g800a261c036
class.ilLMTracker.php
Go to the documentation of this file.
1<?php
2
3/* Copyright (c) 1998-2014 ILIAS open source, Extended GPL, see docs/LICENSE */
4
13{
17 protected $db;
18
22 protected $lng;
23
27 protected $refinery;
28
32 protected $plugin_admin;
33
37 protected $user;
38
39 const NOT_ATTEMPTED = 0;
40 const IN_PROGRESS = 1;
41 const COMPLETED = 2;
42 const FAILED = 3;
43 const CURRENT = 99;
44
45 protected $lm_ref_id;
46 protected $lm_obj_id;
47 protected $lm_tree;
48 protected $lm_obj_ids = array();
49 protected $tree_arr = array(); // tree array
50 protected $re_arr = array(); // read event data array
51 protected $loaded_for_node = false; // current node for that the tracking data has been loaded
52 protected $dirty = false;
53 protected $page_questions = array();
54 protected $all_questions = array();
55 protected $answer_status = array();
56 protected $has_incorrect_answers = false;
57 protected $current_page_id = 0;
58
59 public static $instances = array();
60 public static $instancesbyobj = array();
61
65
71 private function __construct($a_id, $a_by_obj_id = false, $a_user_id)
72 {
73 global $DIC;
74
75 $this->db = $DIC->database();
76 $this->lng = $DIC->language();
77 $this->plugin_admin = $DIC["ilPluginAdmin"];
78 $this->user = $DIC->user();
79 $this->user_id = $a_user_id;
80 $this->refinery = $DIC['refinery'];
81
82 if ($a_by_obj_id) {
83 $this->lm_ref_id = 0;
84 $this->lm_obj_id = $a_id;
85 } else {
86 $this->lm_ref_id = $a_id;
87 $this->lm_obj_id = ilObject::_lookupObjId($a_id);
88 }
89
90 $this->lm_tree = ilLMTree::getInstance($this->lm_obj_id);
91 }
92
99 public static function getInstance($a_ref_id, $a_user_id = 0)
100 {
101 global $DIC;
102
103 $ilUser = $DIC->user();
104
105 if ($a_user_id == 0) {
106 $a_user_id = $ilUser->getId();
107 }
108
109 if (!isset(self::$instances[$a_ref_id][$a_user_id])) {
110 self::$instances[$a_ref_id][$a_user_id] = new ilLMTracker($a_ref_id, false, $a_user_id);
111 }
112 return self::$instances[$a_ref_id][$a_user_id];
113 }
114
121 public static function getInstanceByObjId($a_obj_id, $a_user_id = 0)
122 {
123 global $DIC;
124
125 $ilUser = $DIC->user();
126
127 if ($a_user_id == 0) {
128 $a_user_id = $ilUser->getId();
129 }
130
131 if (!isset(self::$instancesbyobj[$a_obj_id][$a_user_id])) {
132 self::$instancesbyobj[$a_obj_id][$a_user_id] = new ilLMTracker($a_obj_id, true, $a_user_id);
133 }
134 return self::$instancesbyobj[$a_obj_id][$a_user_id];
135 }
136
140
146 public function trackAccess($a_page_id, $user_id)
147 {
148 if ($user_id == ANONYMOUS_USER_ID) {
149 ilChangeEvent::_recordReadEvent("lm", $this->lm_ref_id, $this->lm_obj_id, $user_id);
150 return;
151 }
152
153 if ($this->lm_ref_id == 0) {
154 throw new ilLMPresentationException("ilLMTracker: No Ref Id given.");
155 }
156
157 // track page and chapter access
158 $this->trackPageAndChapterAccess($a_page_id);
159
160 // track last page access (must be done after calling trackPageAndChapterAccess())
161 $this->trackLastPageAccess($this->user_id, $this->lm_ref_id, $a_page_id);
162
163 // #9483
164 // general learning module lp tracking
166 $this->user_id,
167 $this->lm_obj_id,
168 $this->lm_ref_id,
169 "lm"
170 );
171
172 // obsolete?
173 ilLPStatusWrapper::_updateStatus($this->lm_obj_id, $this->user_id);
174
175 // mark currently loaded data as dirty to force reload if necessary
176 $this->dirty = true;
177 }
178
186 public function trackLastPageAccess($usr_id, $lm_id, $obj_id)
187 {
188 $title = "";
189 $db = $this->db;
190 $db->replace(
191 "lo_access",
192 [
193 "usr_id" => ["integer", $usr_id],
194 "lm_id" => ["integer", $lm_id]
195 ],
196 [
197 "timestamp" => ["timestamp", ilUtil::now()],
198 "obj_id" => ["integer", $obj_id],
199 "lm_title" => ["text", $title]
200 ]
201 );
202 }
203
204
208 protected function trackPageAndChapterAccess($a_page_id)
209 {
211
212 $now = time();
213
214 //
215 // 1. Page access: current page
216 //
217 $set = $ilDB->query("SELECT obj_id FROM lm_read_event" .
218 " WHERE obj_id = " . $ilDB->quote($a_page_id, "integer") .
219 " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
220 if (!$ilDB->fetchAssoc($set)) {
221 $fields = array(
222 "obj_id" => array("integer", $a_page_id),
223 "usr_id" => array("integer", $this->user_id)
224 );
225 // $ilDB->insert("lm_read_event", $fields);
226 $ilDB->replace("lm_read_event", $fields, array()); // #15144
227 }
228
229 // update all parent chapters
230 $ilDB->manipulate("UPDATE lm_read_event SET" .
231 " read_count = read_count + 1 " .
232 " , last_access = " . $ilDB->quote($now, "integer") .
233 " WHERE obj_id = " . $ilDB->quote($a_page_id, "integer") .
234 " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
235
236
237 //
238 // 2. Chapter access: based on last page accessed
239 //
240
241 // get last accessed page
242 $set = $ilDB->query("SELECT * FROM lo_access WHERE " .
243 "usr_id = " . $ilDB->quote($this->user_id, "integer") . " AND " .
244 "lm_id = " . $ilDB->quote($this->lm_ref_id, "integer"));
245 $res = $ilDB->fetchAssoc($set);
246 if ($res["obj_id"]) {
247 $valid_timespan = ilObjUserTracking::_getValidTimeSpan();
248
249 $pg_ts = new ilDateTime($res["timestamp"], IL_CAL_DATETIME);
250 $pg_ts = $pg_ts->get(IL_CAL_UNIX);
251 $pg_id = $res["obj_id"];
252 if (!$this->lm_tree->isInTree($pg_id)) {
253 return;
254 }
255
256 $time_diff = $read_diff = 0;
257
258 // spent_seconds or read_count ?
259 if (($now - $pg_ts) <= $valid_timespan) {
260 $time_diff = $now - $pg_ts;
261 } else {
262 $read_diff = 1;
263 }
264
265 // find parent chapter(s) for that page
266 $parent_st_ids = array();
267 foreach ($this->lm_tree->getPathFull($pg_id) as $item) {
268 if ($item["type"] == "st") {
269 $parent_st_ids[] = $item["obj_id"];
270 }
271 }
272
273 if ($parent_st_ids && ($time_diff || $read_diff)) {
274 // get existing chapter entries
275 $ex_st = array();
276 $set = $ilDB->query("SELECT obj_id FROM lm_read_event" .
277 " WHERE " . $ilDB->in("obj_id", $parent_st_ids, "", "integer") .
278 " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
279 while ($row = $ilDB->fetchAssoc($set)) {
280 $ex_st[] = $row["obj_id"];
281 }
282
283 // add missing chapter entries
284 $missing_st = array_diff($parent_st_ids, $ex_st);
285 if (sizeof($missing_st)) {
286 foreach ($missing_st as $st_id) {
287 $fields = array(
288 "obj_id" => array("integer", $st_id),
289 "usr_id" => array("integer", $this->user_id)
290 );
291 // $ilDB->insert("lm_read_event", $fields);
292 $ilDB->replace("lm_read_event", $fields, array()); // #15144
293 }
294 }
295
296 // update all parent chapters
297 $ilDB->manipulate("UPDATE lm_read_event SET" .
298 " read_count = read_count + " . $ilDB->quote($read_diff, "integer") .
299 " , spent_seconds = spent_seconds + " . $ilDB->quote($time_diff, "integer") .
300 " , last_access = " . $ilDB->quote($now, "integer") .
301 " WHERE " . $ilDB->in("obj_id", $parent_st_ids, "", "integer") .
302 " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
303 }
304 }
305 }
306
307
311
317 public function setCurrentPage($a_val)
318 {
319 $this->current_page_id = $a_val;
320 }
321
327 public function getCurrentPage()
328 {
330 }
331
338 protected function loadLMTrackingData()
339 {
341
342 // we must prevent loading tracking data multiple times during a request where possible
343 // please note that the dirty flag works only to a certain limit
344 // e.g. if questions are answered the flag is not set (yet)
345 // or if pages/chapter are added/deleted the flag is not set
346 if ($this->loaded_for_node === (int) $this->getCurrentPage() && !$this->dirty) {
347 return;
348 }
349
350 $this->loaded_for_node = (int) $this->getCurrentPage();
351 $this->dirty = false;
352
353 // load lm tree in array
354 $this->tree_arr = array();
355 $nodes = $this->lm_tree->getCompleteTree();
356 foreach ($nodes as $node) {
357 $this->tree_arr["childs"][$node["parent"]][] = $node;
358 $this->tree_arr["parent"][$node["child"]] = $node["parent"];
359 $this->tree_arr["nodes"][$node["child"]] = $node;
360 }
361
362 // load all lm obj ids of learning module
363 $this->lm_obj_ids = ilLMObject::_getAllLMObjectsOfLM($this->lm_obj_id);
364
365 // load read event data
366 $this->re_arr = array();
367 $set = $ilDB->query("SELECT * FROM lm_read_event " .
368 " WHERE " . $ilDB->in("obj_id", $this->lm_obj_ids, false, "integer") .
369 " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
370 while ($rec = $ilDB->fetchAssoc($set)) {
371 $this->re_arr[$rec["obj_id"]] = $rec;
372 }
373
374 // load question/pages information
375 $this->page_questions = array();
376 $this->all_questions = array();
377 $q = ilLMPageObject::queryQuestionsOfLearningModule($this->lm_obj_id, "", "", 0, 0);
378 foreach ($q["set"] as $quest) {
379 $this->page_questions[$quest["page_id"]][] = $quest["question_id"];
380 $this->all_questions[] = $quest["question_id"];
381 }
382
383 // load question answer information
384 $this->answer_status = ilPageQuestionProcessor::getAnswerStatus($this->all_questions, $this->user_id);
385
386 $this->has_incorrect_answers = false;
387
388 $has_pred_incorrect_answers = false;
389 $has_pred_incorrect_not_unlocked_answers = false;
390 $this->determineProgressStatus($this->lm_tree->readRootId(), $has_pred_incorrect_answers, $has_pred_incorrect_not_unlocked_answers);
391
392 $this->has_incorrect_answers = $has_pred_incorrect_answers;
393 }
394
400 public function getAllQuestionsCorrect()
401 {
402 $this->loadLMTrackingData();
403 if (count($this->all_questions) > 0 && !$this->has_incorrect_answers) {
404 return true;
405 }
406 return false;
407 }
408
409
416 protected function determineProgressStatus($a_obj_id, &$a_has_pred_incorrect_answers, &$a_has_pred_incorrect_not_unlocked_answers)
417 {
419
420 if (isset($this->tree_arr["nodes"][$a_obj_id])) {
421 $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_answers"] = $a_has_pred_incorrect_answers;
422 $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_not_unlocked_answers"] = $a_has_pred_incorrect_not_unlocked_answers;
423
424 if (is_array($this->tree_arr["childs"][$a_obj_id])) {
425 // sort childs in correct order
426 $this->tree_arr["childs"][$a_obj_id] = ilUtil::sortArray($this->tree_arr["childs"][$a_obj_id], "lft", "asc", true);
427
428 $cnt_completed = 0;
429 foreach ($this->tree_arr["childs"][$a_obj_id] as $c) {
430 // if child is not activated/displayed count child as implicitly completed
431 // rationale: everything that is visible for the learner determines the status
432 // see also bug #14642
433 if (!self::_isNodeVisible($c)) {
434 $cnt_completed++;
435 continue;
436 }
437 $c_stat = $this->determineProgressStatus(
438 $c["child"],
439 $a_has_pred_incorrect_answers,
440 $a_has_pred_incorrect_not_unlocked_answers
441 );
442 if ($status != ilLMTracker::FAILED) {
443 if ($c_stat == ilLMTracker::FAILED) {
444 $status = ilLMTracker::IN_PROGRESS;
445 } elseif ($c_stat == ilLMTracker::IN_PROGRESS) {
446 $status = ilLMTracker::IN_PROGRESS;
447 } elseif ($c_stat == ilLMTracker::COMPLETED || $c_stat == ilLMTracker::CURRENT) {
448 $status = ilLMTracker::IN_PROGRESS;
449 $cnt_completed++;
450 }
451 }
452 // if an item is failed or in progress or (not attempted and contains questions)
453 // the next item has predecessing incorrect answers
454 if ($this->tree_arr["nodes"][$c["child"]]["type"] == "pg") {
455 if ($c_stat == ilLMTracker::FAILED || $c_stat == ilLMTracker::IN_PROGRESS ||
456 ($c_stat == ilLMTracker::NOT_ATTEMPTED && is_array($this->page_questions[$c["child"]]) && count($this->page_questions[$c["child"]]) > 0)) {
457 $a_has_pred_incorrect_answers = true;
458 if (!$this->tree_arr["nodes"][$c["child"]]["unlocked"]) {
459 $a_has_pred_incorrect_not_unlocked_answers = true;
460 }
461 }
462 }
463 }
464 if ($cnt_completed == count($this->tree_arr["childs"][$a_obj_id])) {
465 $status = ilLMTracker::COMPLETED;
466 }
467 } elseif ($this->tree_arr["nodes"][$a_obj_id]["type"] == "pg") {
468 // check read event data
469 if (isset($this->re_arr[$a_obj_id]) && $this->re_arr[$a_obj_id]["read_count"] > 0) {
470 $status = ilLMTracker::COMPLETED;
471 } elseif ($a_obj_id == $this->getCurrentPage()) {
472 $status = ilLMTracker::CURRENT;
473 }
474
475 $unlocked = false;
476 if (is_array($this->page_questions[$a_obj_id])) {
477 // check questions, if one is failed -> failed
478 $unlocked = true;
479 foreach ($this->page_questions[$a_obj_id] as $q_id) {
480 if (is_array($this->answer_status[$q_id])
481 && $this->answer_status[$q_id]["try"] > 0
482 && !$this->answer_status[$q_id]["passed"]) {
483 $status = ilLMTracker::FAILED;
484 if (!$this->answer_status[$q_id]["unlocked"]) {
485 $unlocked = false;
486 }
487 }
488 }
489
490 // check questions, if one is not answered -> in progress
491 if ($status != ilLMTracker::FAILED) {
492 foreach ($this->page_questions[$a_obj_id] as $q_id) {
493 if (!is_array($this->answer_status[$q_id])
494 || $this->answer_status[$q_id]["try"] == 0) {
495 if ($status != ilLMTracker::NOT_ATTEMPTED) {
496 $status = ilLMTracker::IN_PROGRESS;
497 }
498 }
499 }
500 $unlocked = false;
501 }
502 }
503 $this->tree_arr["nodes"][$a_obj_id]["unlocked"] = $unlocked;
504 $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_answers"] = $a_has_pred_incorrect_answers;
505 $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_not_unlocked_answers"] = $a_has_pred_incorrect_not_unlocked_answers;
506 }
507 } else { // free pages (currently not called, since only walking through tree structure)
508 }
509 $this->tree_arr["nodes"][$a_obj_id]["status"] = $status;
510
511 return $status;
512 }
513
514
522 public function getIconForLMObject($a_node, $a_highlighted_node = 0)
523 {
524 $this->loadLMTrackingData();
526
527 if ($a_node["child"] == $a_highlighted_node) {
528 return $icons->getImagePathRunning();
529 }
530 if (isset($this->tree_arr["nodes"][$a_node["child"]])) {
531 switch ($this->tree_arr["nodes"][$a_node["child"]]["status"]) {
533 return $icons->getImagePathInProgress();
534
536 return $icons->getImagePathFailed();
537
539 return $icons->getImagePathCompleted();
540 }
541 }
542 return $icons->getImagePathNotAttempted();
543 }
544
551 public function hasPredIncorrectAnswers($a_obj_id, $a_ignore_unlock = false)
552 {
553 $this->loadLMTrackingData();
554 $ret = false;
555 if (is_array($this->tree_arr["nodes"][$a_obj_id])) {
556 if ($a_ignore_unlock) {
557 $ret = $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_answers"];
558 } else {
559 $ret = $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_not_unlocked_answers"];
560 }
561 }
562 return $ret;
563 }
564
568
576 {
580 $ilPluginAdmin = $this->plugin_admin;
582
583 $blocked_users = array();
584
585 // load question/pages information
586 $this->page_questions = array();
587 $this->all_questions = array();
588 $page_for_question = array();
589 $q = ilLMPageObject::queryQuestionsOfLearningModule($this->lm_obj_id, "", "", 0, 0);
590 foreach ($q["set"] as $quest) {
591 $this->page_questions[$quest["page_id"]][] = $quest["question_id"];
592 $this->all_questions[] = $quest["question_id"];
593 $page_for_question[$quest["question_id"]] = $quest["page_id"];
594 }
595 // get question information
596 $qlist = new ilAssQuestionList($ilDB, $lng, $refinery, $ilPluginAdmin);
597 $qlist->setParentObjId(0);
598 $qlist->setJoinObjectData(false);
599 $qlist->addFieldFilter("question_id", $this->all_questions);
600 $qlist->load();
601 $qdata = $qlist->getQuestionDataArray();
602
603 // load question answer information
604 $this->answer_status = ilPageQuestionProcessor::getAnswerStatus($this->all_questions);
605 foreach ($this->answer_status as $as) {
606 if ($as["try"] >= $qdata[$as["qst_id"]]["nr_of_tries"] && $qdata[$as["qst_id"]]["nr_of_tries"] > 0 && !$as["passed"]) {
607 //var_dump($qdata[$as["qst_id"]]);
608 $name = ilObjUser::_lookupName($as["user_id"]);
609 $as["user_name"] = $name["lastname"] . ", " . $name["firstname"] . " [" . $name["login"] . "]";
610 $as["question_text"] = $qdata[$as["qst_id"]]["question_text"];
611 $as["page_id"] = $page_for_question[$as["qst_id"]];
612 $as["page_title"] = ilLMPageObject::_lookupTitle($as["page_id"]);
613 $blocked_users[] = $as;
614 }
615 }
616
617 return $blocked_users;
618 }
619
626 public static function _isNodeVisible($a_node)
627 {
628 if ($a_node["type"] != "pg") {
629 return true;
630 }
631
632 $lm_set = new ilSetting("lm");
634 $a_node["child"],
635 "lm",
636 $lm_set->get("time_scheduled_page_activation")
637 );
638
639 if (!$active) {
640 $act_data = ilPageObject::_lookupActivationData((int) $a_node["child"], "lm");
641 if ($act_data["show_activation_info"] &&
642 (ilUtil::now() < $act_data["activation_start"])) {
643 return true;
644 } else {
645 return false;
646 }
647 } else {
648 return true;
649 }
650 }
651}
user()
Definition: user.php:4
An exception for terminatinating execution or to throw for unit testing.
const IL_CAL_UNIX
const IL_CAL_DATETIME
static _recordReadEvent( $a_type, $a_ref_id, $obj_id, $usr_id, $isCatchupWriteEvents=true, $a_ext_rc=false, $a_ext_time=false)
Records a read event and catches up with write events.
@classDescription Date and time handling
static _getAllLMObjectsOfLM($a_lm_id, $a_type="")
Get all objects of learning module.
static _lookupTitle($a_obj_id)
Lookup title.
static queryQuestionsOfLearningModule( $a_lm_id, $a_order_field, $a_order_dir, $a_offset, $a_limit)
Get questions of learning module.
Base exception class for learning module presentation.
Track access to ILIAS learning modules.
trackAccess($a_page_id, $user_id)
Track access to lm page.
static _isNodeVisible($a_node)
Is node visible for the learner.
getCurrentPage()
Get current page.
__construct($a_id, $a_by_obj_id=false, $a_user_id)
Constructor.
getAllQuestionsCorrect()
Have all questoins been answered correctly (and questions exist)?
trackLastPageAccess($usr_id, $lm_id, $obj_id)
Track last accessed page for a learning module.
trackPageAndChapterAccess($a_page_id)
Track page and chapter access.
static getInstance($a_ref_id, $a_user_id=0)
Get instance.
determineProgressStatus($a_obj_id, &$a_has_pred_incorrect_answers, &$a_has_pred_incorrect_not_unlocked_answers)
Determine progress status of nodes.
static $instancesbyobj
getIconForLMObject($a_node, $a_highlighted_node=0)
Get icon for lm object.
static getInstanceByObjId($a_obj_id, $a_user_id=0)
Get instance.
getBlockedUsersInformation()
Get blocked users information.
hasPredIncorrectAnswers($a_obj_id, $a_ignore_unlock=false)
Has predecessing incorrect answers.
setCurrentPage($a_val)
Set current page.
loadLMTrackingData()
Load LM tracking data.
static getInstance($a_tree_id)
Get Instance.
static getInstance(int $variant=ilLPStatusIcons::ICON_VARIANT_DEFAULT, ?\ILIAS\UI\Renderer $renderer=null, ?\ILIAS\UI\Factory $factory=null)
static _updateStatus($a_obj_id, $a_usr_id, $a_obj=null, $a_percentage=false, $a_force_raise=false)
Update status.
static _tracProgress($a_user_id, $a_obj_id, $a_ref_id, $a_obj_type='')
static _lookupName($a_user_id)
lookup user name
static _lookupObjId($a_id)
static _lookupActive($a_id, $a_parent_type, $a_check_scheduled_activation=false, $a_lang="-")
lookup activation status
static _lookupActivationData($a_id, $a_parent_type, $a_lang="-")
Lookup activation data.
static getAnswerStatus($a_q_id, $a_user_id=0)
Get statistics for question.
ILIAS Setting Class.
static sortArray( $array, $a_array_sortby, $a_array_sortorder=0, $a_numeric=false, $a_keep_keys=false)
sortArray
static now()
Return current timestamp in Y-m-d H:i:s format.
$c
Definition: cli.php:37
const ANONYMOUS_USER_ID
Definition: constants.php:25
global $DIC
Definition: goto.php:24
$ilUser
Definition: imgupload.php:18
if($format !==null) $name
Definition: metadata.php:230
$ret
Definition: parser.php:6
foreach($_POST as $key=> $value) $res
global $ilDB
$lm_set