ILIAS  release_5-4 Revision v5.4.26-12-gabc799a52e6
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 $plugin_admin;
28
32 protected $user;
33
34 const NOT_ATTEMPTED = 0;
35 const IN_PROGRESS = 1;
36 const COMPLETED = 2;
37 const FAILED = 3;
38 const CURRENT = 99;
39
40 protected $lm_ref_id;
41 protected $lm_obj_id;
42 protected $lm_tree;
43 protected $lm_obj_ids = array();
44 protected $tree_arr = array(); // tree array
45 protected $re_arr = array(); // read event data array
46 protected $loaded_for_node = false; // current node for that the tracking data has been loaded
47 protected $dirty = false;
48 protected $page_questions = array();
49 protected $all_questions = array();
50 protected $answer_status = array();
51 protected $has_incorrect_answers = false;
52 protected $current_page_id = 0;
53
54 public static $instances = array();
55 public static $instancesbyobj = array();
56
60
66 private function __construct($a_id, $a_by_obj_id = false, $a_user_id)
67 {
68 global $DIC;
69
70 $this->db = $DIC->database();
71 $this->lng = $DIC->language();
72 $this->plugin_admin = $DIC["ilPluginAdmin"];
73 $this->user = $DIC->user();
74 $this->user_id = $a_user_id;
75
76 if ($a_by_obj_id) {
77 $this->lm_ref_id = 0;
78 $this->lm_obj_id = $a_id;
79 } else {
80 $this->lm_ref_id = $a_id;
81 $this->lm_obj_id = ilObject::_lookupObjId($a_id);
82 }
83
84 include_once("./Modules/LearningModule/classes/class.ilLMTree.php");
85 $this->lm_tree = ilLMTree::getInstance($this->lm_obj_id);
86 }
87
94 public static function getInstance($a_ref_id, $a_user_id = 0)
95 {
96 global $DIC;
97
98 $ilUser = $DIC->user();
99
100 if ($a_user_id == 0) {
101 $a_user_id = $ilUser->getId();
102 }
103
104 if (!isset(self::$instances[$a_ref_id][$a_user_id])) {
105 self::$instances[$a_ref_id][$a_user_id] = new ilLMTracker($a_ref_id, false, $a_user_id);
106 }
107 return self::$instances[$a_ref_id][$a_user_id];
108 }
109
116 public static function getInstanceByObjId($a_obj_id, $a_user_id = 0)
117 {
118 global $DIC;
119
120 $ilUser = $DIC->user();
121
122 if ($a_user_id == 0) {
123 $a_user_id = $ilUser->getId();
124 }
125
126 if (!isset(self::$instancesbyobj[$a_obj_id][$a_user_id])) {
127 self::$instancesbyobj[$a_obj_id][$a_user_id] = new ilLMTracker($a_obj_id, true, $a_user_id);
128 }
129 return self::$instancesbyobj[$a_obj_id][$a_user_id];
130 }
131
135
141 public function trackAccess($a_page_id, $user_id)
142 {
143 if ($user_id == ANONYMOUS_USER_ID) {
144 ilChangeEvent::_recordReadEvent("lm", $this->lm_ref_id, $this->lm_obj_id, $user_id);
145 return;
146 }
147
148 if ($this->lm_ref_id == 0) {
149 throw new ilLMPresentationException("ilLMTracker: No Ref Id given.");
150 }
151
152 // track page and chapter access
153 $this->trackPageAndChapterAccess($a_page_id);
154
155 // track last page access (must be done after calling trackPageAndChapterAccess())
156 $this->trackLastPageAccess($this->user_id, $this->lm_ref_id, $a_page_id);
157
158 // #9483
159 // general learning module lp tracking
160 include_once("./Services/Tracking/classes/class.ilLearningProgress.php");
162 $this->user_id,
163 $this->lm_obj_id,
164 $this->lm_ref_id,
165 "lm"
166 );
167
168 // obsolete?
169 include_once("./Services/Tracking/classes/class.ilLPStatusWrapper.php");
170 ilLPStatusWrapper::_updateStatus($this->lm_obj_id, $this->user_id);
171
172 // mark currently loaded data as dirty to force reload if necessary
173 $this->dirty = true;
174 }
175
183 public function trackLastPageAccess($usr_id, $lm_id, $obj_id)
184 {
185 $title = "";
186 $db = $this->db;
187 $db->replace(
188 "lo_access",
189 [
190 "usr_id" => ["integer", $usr_id],
191 "lm_id" => ["integer", $lm_id]
192 ],
193 [
194 "timestamp" => ["timestamp", ilUtil::now()],
195 "obj_id" => ["integer", $obj_id],
196 "lm_title" => ["text", $title]
197 ]
198 );
199 }
200
201
205 protected function trackPageAndChapterAccess($a_page_id)
206 {
208
209 $now = time();
210
211 //
212 // 1. Page access: current page
213 //
214 $set = $ilDB->query("SELECT obj_id FROM lm_read_event" .
215 " WHERE obj_id = " . $ilDB->quote($a_page_id, "integer") .
216 " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
217 if (!$ilDB->fetchAssoc($set)) {
218 $fields = array(
219 "obj_id" => array("integer", $a_page_id),
220 "usr_id" => array("integer", $this->user_id)
221 );
222 // $ilDB->insert("lm_read_event", $fields);
223 $ilDB->replace("lm_read_event", $fields, array()); // #15144
224 }
225
226 // update all parent chapters
227 $ilDB->manipulate("UPDATE lm_read_event SET" .
228 " read_count = read_count + 1 " .
229 " , last_access = " . $ilDB->quote($now, "integer") .
230 " WHERE obj_id = " . $ilDB->quote($a_page_id, "integer") .
231 " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
232
233
234 //
235 // 2. Chapter access: based on last page accessed
236 //
237
238 // get last accessed page
239 $set = $ilDB->query("SELECT * FROM lo_access WHERE " .
240 "usr_id = " . $ilDB->quote($this->user_id, "integer") . " AND " .
241 "lm_id = " . $ilDB->quote($this->lm_ref_id, "integer"));
242 $res = $ilDB->fetchAssoc($set);
243 if ($res["obj_id"]) {
244 include_once('Services/Tracking/classes/class.ilObjUserTracking.php');
245 $valid_timespan = ilObjUserTracking::_getValidTimeSpan();
246
247 $pg_ts = new ilDateTime($res["timestamp"], IL_CAL_DATETIME);
248 $pg_ts = $pg_ts->get(IL_CAL_UNIX);
249 $pg_id = $res["obj_id"];
250 if (!$this->lm_tree->isInTree($pg_id)) {
251 return;
252 }
253
254 $time_diff = $read_diff = 0;
255
256 // spent_seconds or read_count ?
257 if (($now - $pg_ts) <= $valid_timespan) {
258 $time_diff = $now - $pg_ts;
259 } else {
260 $read_diff = 1;
261 }
262
263 // find parent chapter(s) for that page
264 $parent_st_ids = array();
265 foreach ($this->lm_tree->getPathFull($pg_id) as $item) {
266 if ($item["type"] == "st") {
267 $parent_st_ids[] = $item["obj_id"];
268 }
269 }
270
271 if ($parent_st_ids && ($time_diff || $read_diff)) {
272 // get existing chapter entries
273 $ex_st = array();
274 $set = $ilDB->query("SELECT obj_id FROM lm_read_event" .
275 " WHERE " . $ilDB->in("obj_id", $parent_st_ids, "", "integer") .
276 " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
277 while ($row = $ilDB->fetchAssoc($set)) {
278 $ex_st[] = $row["obj_id"];
279 }
280
281 // add missing chapter entries
282 $missing_st = array_diff($parent_st_ids, $ex_st);
283 if (sizeof($missing_st)) {
284 foreach ($missing_st as $st_id) {
285 $fields = array(
286 "obj_id" => array("integer", $st_id),
287 "usr_id" => array("integer", $this->user_id)
288 );
289 // $ilDB->insert("lm_read_event", $fields);
290 $ilDB->replace("lm_read_event", $fields, array()); // #15144
291 }
292 }
293
294 // update all parent chapters
295 $ilDB->manipulate("UPDATE lm_read_event SET" .
296 " read_count = read_count + " . $ilDB->quote($read_diff, "integer") .
297 " , spent_seconds = spent_seconds + " . $ilDB->quote($time_diff, "integer") .
298 " , last_access = " . $ilDB->quote($now, "integer") .
299 " WHERE " . $ilDB->in("obj_id", $parent_st_ids, "", "integer") .
300 " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
301 }
302 }
303 }
304
305
309
315 public function setCurrentPage($a_val)
316 {
317 $this->current_page_id = $a_val;
318 }
319
325 public function getCurrentPage()
326 {
328 }
329
336 protected function loadLMTrackingData()
337 {
339
340 // we must prevent loading tracking data multiple times during a request where possible
341 // please note that the dirty flag works only to a certain limit
342 // e.g. if questions are answered the flag is not set (yet)
343 // or if pages/chapter are added/deleted the flag is not set
344 if ($this->loaded_for_node === (int) $this->getCurrentPage() && !$this->dirty) {
345 return;
346 }
347
348 $this->loaded_for_node = (int) $this->getCurrentPage();
349 $this->dirty = false;
350
351 // load lm tree in array
352 $this->tree_arr = array();
353 $nodes = $this->lm_tree->getCompleteTree();
354 foreach ($nodes as $node) {
355 $this->tree_arr["childs"][$node["parent"]][] = $node;
356 $this->tree_arr["parent"][$node["child"]] = $node["parent"];
357 $this->tree_arr["nodes"][$node["child"]] = $node;
358 }
359
360 // load all lm obj ids of learning module
361 include_once("./Modules/LearningModule/classes/class.ilLMObject.php");
362 $this->lm_obj_ids = ilLMObject::_getAllLMObjectsOfLM($this->lm_obj_id);
363
364 // load read event data
365 $this->re_arr = array();
366 $set = $ilDB->query("SELECT * FROM lm_read_event " .
367 " WHERE " . $ilDB->in("obj_id", $this->lm_obj_ids, false, "integer") .
368 " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
369 while ($rec = $ilDB->fetchAssoc($set)) {
370 $this->re_arr[$rec["obj_id"]] = $rec;
371 }
372
373 // load question/pages information
374 $this->page_questions = array();
375 $this->all_questions = array();
376 include_once("./Modules/LearningModule/classes/class.ilLMPageObject.php");
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 include_once("./Services/COPage/classes/class.ilPageQuestionProcessor.php");
385 $this->answer_status = ilPageQuestionProcessor::getAnswerStatus($this->all_questions, $this->user_id);
386
387 $this->has_incorrect_answers = false;
388
389 $has_pred_incorrect_answers = false;
390 $has_pred_incorrect_not_unlocked_answers = false;
391 $this->determineProgressStatus($this->lm_tree->readRootId(), $has_pred_incorrect_answers, $has_pred_incorrect_not_unlocked_answers);
392
393 $this->has_incorrect_answers = $has_pred_incorrect_answers;
394 }
395
401 public function getAllQuestionsCorrect()
402 {
403 $this->loadLMTrackingData();
404 if (count($this->all_questions) > 0 && !$this->has_incorrect_answers) {
405 return true;
406 }
407 return false;
408 }
409
410
417 protected function determineProgressStatus($a_obj_id, &$a_has_pred_incorrect_answers, &$a_has_pred_incorrect_not_unlocked_answers)
418 {
420
421 if (isset($this->tree_arr["nodes"][$a_obj_id])) {
422 $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_answers"] = $a_has_pred_incorrect_answers;
423 $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_not_unlocked_answers"] = $a_has_pred_incorrect_not_unlocked_answers;
424
425 if (is_array($this->tree_arr["childs"][$a_obj_id])) {
426 // sort childs in correct order
427 $this->tree_arr["childs"][$a_obj_id] = ilUtil::sortArray($this->tree_arr["childs"][$a_obj_id], "lft", "asc", true);
428
429 $cnt_completed = 0;
430 foreach ($this->tree_arr["childs"][$a_obj_id] as $c) {
431 // if child is not activated/displayed count child as implicitly completed
432 // rationale: everything that is visible for the learner determines the status
433 // see also bug #14642
434 if (!self::_isNodeVisible($c)) {
435 $cnt_completed++;
436 continue;
437 }
438 $c_stat = $this->determineProgressStatus(
439 $c["child"],
440 $a_has_pred_incorrect_answers,
441 $a_has_pred_incorrect_not_unlocked_answers
442 );
443 if ($status != ilLMTracker::FAILED) {
444 if ($c_stat == ilLMTracker::FAILED) {
445 $status = ilLMTracker::IN_PROGRESS;
446 } elseif ($c_stat == ilLMTracker::IN_PROGRESS) {
447 $status = ilLMTracker::IN_PROGRESS;
448 } elseif ($c_stat == ilLMTracker::COMPLETED || $c_stat == ilLMTracker::CURRENT) {
449 $status = ilLMTracker::IN_PROGRESS;
450 $cnt_completed++;
451 }
452 }
453 // if an item is failed or in progress or (not attempted and contains questions)
454 // the next item has predecessing incorrect answers
455 if ($this->tree_arr["nodes"][$c["child"]]["type"] == "pg") {
456 if ($c_stat == ilLMTracker::FAILED || $c_stat == ilLMTracker::IN_PROGRESS ||
457 ($c_stat == ilLMTracker::NOT_ATTEMPTED && is_array($this->page_questions[$c["child"]]) && count($this->page_questions[$c["child"]]) > 0)) {
458 $a_has_pred_incorrect_answers = true;
459 if (!$this->tree_arr["nodes"][$c["child"]]["unlocked"]) {
460 $a_has_pred_incorrect_not_unlocked_answers = true;
461 }
462 }
463 }
464 }
465 if ($cnt_completed == count($this->tree_arr["childs"][$a_obj_id])) {
466 $status = ilLMTracker::COMPLETED;
467 }
468 } elseif ($this->tree_arr["nodes"][$a_obj_id]["type"] == "pg") {
469 // check read event data
470 if (isset($this->re_arr[$a_obj_id]) && $this->re_arr[$a_obj_id]["read_count"] > 0) {
471 $status = ilLMTracker::COMPLETED;
472 } elseif ($a_obj_id == $this->getCurrentPage()) {
473 $status = ilLMTracker::CURRENT;
474 }
475
476 $unlocked = false;
477 if (is_array($this->page_questions[$a_obj_id])) {
478 // check questions, if one is failed -> failed
479 $unlocked = true;
480 foreach ($this->page_questions[$a_obj_id] as $q_id) {
481 if (is_array($this->answer_status[$q_id])
482 && $this->answer_status[$q_id]["try"] > 0
483 && !$this->answer_status[$q_id]["passed"]) {
484 $status = ilLMTracker::FAILED;
485 if (!$this->answer_status[$q_id]["unlocked"]) {
486 $unlocked = false;
487 }
488 }
489 }
490
491 // check questions, if one is not answered -> in progress
492 if ($status != ilLMTracker::FAILED) {
493 foreach ($this->page_questions[$a_obj_id] as $q_id) {
494 if (!is_array($this->answer_status[$q_id])
495 || $this->answer_status[$q_id]["try"] == 0) {
496 if ($status != ilLMTracker::NOT_ATTEMPTED) {
497 $status = ilLMTracker::IN_PROGRESS;
498 }
499 }
500 }
501 $unlocked = false;
502 }
503 }
504 $this->tree_arr["nodes"][$a_obj_id]["unlocked"] = $unlocked;
505 $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_answers"] = $a_has_pred_incorrect_answers;
506 $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_not_unlocked_answers"] = $a_has_pred_incorrect_not_unlocked_answers;
507 }
508 } else { // free pages (currently not called, since only walking through tree structure)
509 }
510 $this->tree_arr["nodes"][$a_obj_id]["status"] = $status;
511
512 return $status;
513 }
514
515
523 public function getIconForLMObject($a_node, $a_highlighted_node = 0)
524 {
525 $this->loadLMTrackingData();
526 if ($a_node["child"] == $a_highlighted_node) {
527 return ilUtil::getImagePath('scorm/running.svg');
528 }
529 if (isset($this->tree_arr["nodes"][$a_node["child"]])) {
530 switch ($this->tree_arr["nodes"][$a_node["child"]]["status"]) {
532 return ilUtil::getImagePath('scorm/incomplete.svg');
533
535 return ilUtil::getImagePath('scorm/failed.svg');
536
538 return ilUtil::getImagePath('scorm/completed.svg');
539 }
540 }
541 return ilUtil::getImagePath('scorm/not_attempted.svg');
542 }
543
550 public function hasPredIncorrectAnswers($a_obj_id, $a_ignore_unlock = false)
551 {
552 $this->loadLMTrackingData();
553 $ret = false;
554 if (is_array($this->tree_arr["nodes"][$a_obj_id])) {
555 if ($a_ignore_unlock) {
556 $ret = $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_answers"];
557 } else {
558 $ret = $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_not_unlocked_answers"];
559 }
560 }
561 return $ret;
562 }
563
567
575 {
578 $ilPluginAdmin = $this->plugin_admin;
580
581 $blocked_users = array();
582
583 // load question/pages information
584 $this->page_questions = array();
585 $this->all_questions = array();
586 $page_for_question = array();
587 include_once("./Modules/LearningModule/classes/class.ilLMPageObject.php");
588 $q = ilLMPageObject::queryQuestionsOfLearningModule($this->lm_obj_id, "", "", 0, 0);
589 foreach ($q["set"] as $quest) {
590 $this->page_questions[$quest["page_id"]][] = $quest["question_id"];
591 $this->all_questions[] = $quest["question_id"];
592 $page_for_question[$quest["question_id"]] = $quest["page_id"];
593 }
594 // get question information
595 include_once("./Modules/TestQuestionPool/classes/class.ilAssQuestionList.php");
596 $qlist = new ilAssQuestionList($ilDB, $lng, $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 include_once("./Services/COPage/classes/class.ilPageQuestionProcessor.php");
605 $this->answer_status = ilPageQuestionProcessor::getAnswerStatus($this->all_questions);
606 include_once("./Modules/LearningModule/classes/class.ilLMPageObject.php");
607 foreach ($this->answer_status as $as) {
608 if ($as["try"] >= $qdata[$as["qst_id"]]["nr_of_tries"] && $qdata[$as["qst_id"]]["nr_of_tries"] > 0 && !$as["passed"]) {
609 //var_dump($qdata[$as["qst_id"]]);
610 $name = ilObjUser::_lookupName($as["user_id"]);
611 $as["user_name"] = $name["lastname"] . ", " . $name["firstname"] . " [" . $name["login"] . "]";
612 $as["question_text"] = $qdata[$as["qst_id"]]["question_text"];
613 $as["page_id"] = $page_for_question[$as["qst_id"]];
614 $as["page_title"] = ilLMPageObject::_lookupTitle($as["page_id"]);
615 $blocked_users[] = $as;
616 }
617 }
618
619 return $blocked_users;
620 }
621
628 public static function _isNodeVisible($a_node)
629 {
630 include_once("./Services/COPage/classes/class.ilPageObject.php");
631
632 if ($a_node["type"] != "pg") {
633 return true;
634 }
635
636 $lm_set = new ilSetting("lm");
638 $a_node["child"],
639 "lm",
640 $lm_set->get("time_scheduled_page_activation")
641 );
642
643 if (!$active) {
644 $act_data = ilPageObject::_lookupActivationData((int) $a_node["child"], "lm");
645 if ($act_data["show_activation_info"] &&
646 (ilUtil::now() < $act_data["activation_start"])) {
647 return true;
648 } else {
649 return false;
650 }
651 } else {
652 return true;
653 }
654 }
655}
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 _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.
static getImagePath($img, $module_path="", $mode="output", $offline=false)
get image path (for images located in a template directory)
$as
$time_diff
Definition: langcheck.php:760
$row
$ret
Definition: parser.php:6
global $DIC
Definition: saml.php:7
foreach($_POST as $key=> $value) $res
global $ilDB
$lm_set
$ilUser
Definition: imgupload.php:18