ILIAS  release_6 Revision v6.24-5-g0c8bfefb3b8
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 $this->lm_tree = ilLMTree::getInstance($this->lm_obj_id);
85 }
86
93 public static function getInstance($a_ref_id, $a_user_id = 0)
94 {
95 global $DIC;
96
97 $ilUser = $DIC->user();
98
99 if ($a_user_id == 0) {
100 $a_user_id = $ilUser->getId();
101 }
102
103 if (!isset(self::$instances[$a_ref_id][$a_user_id])) {
104 self::$instances[$a_ref_id][$a_user_id] = new ilLMTracker($a_ref_id, false, $a_user_id);
105 }
106 return self::$instances[$a_ref_id][$a_user_id];
107 }
108
115 public static function getInstanceByObjId($a_obj_id, $a_user_id = 0)
116 {
117 global $DIC;
118
119 $ilUser = $DIC->user();
120
121 if ($a_user_id == 0) {
122 $a_user_id = $ilUser->getId();
123 }
124
125 if (!isset(self::$instancesbyobj[$a_obj_id][$a_user_id])) {
126 self::$instancesbyobj[$a_obj_id][$a_user_id] = new ilLMTracker($a_obj_id, true, $a_user_id);
127 }
128 return self::$instancesbyobj[$a_obj_id][$a_user_id];
129 }
130
134
140 public function trackAccess($a_page_id, $user_id)
141 {
142 if ($user_id == ANONYMOUS_USER_ID) {
143 ilChangeEvent::_recordReadEvent("lm", $this->lm_ref_id, $this->lm_obj_id, $user_id);
144 return;
145 }
146
147 if ($this->lm_ref_id == 0) {
148 throw new ilLMPresentationException("ilLMTracker: No Ref Id given.");
149 }
150
151 // track page and chapter access
152 $this->trackPageAndChapterAccess($a_page_id);
153
154 // track last page access (must be done after calling trackPageAndChapterAccess())
155 $this->trackLastPageAccess($this->user_id, $this->lm_ref_id, $a_page_id);
156
157 // #9483
158 // general learning module lp tracking
160 $this->user_id,
161 $this->lm_obj_id,
162 $this->lm_ref_id,
163 "lm"
164 );
165
166 // obsolete?
167 ilLPStatusWrapper::_updateStatus($this->lm_obj_id, $this->user_id);
168
169 // mark currently loaded data as dirty to force reload if necessary
170 $this->dirty = true;
171 }
172
180 public function trackLastPageAccess($usr_id, $lm_id, $obj_id)
181 {
182 $title = "";
183 $db = $this->db;
184 $db->replace(
185 "lo_access",
186 [
187 "usr_id" => ["integer", $usr_id],
188 "lm_id" => ["integer", $lm_id]
189 ],
190 [
191 "timestamp" => ["timestamp", ilUtil::now()],
192 "obj_id" => ["integer", $obj_id],
193 "lm_title" => ["text", $title]
194 ]
195 );
196 }
197
198
202 protected function trackPageAndChapterAccess($a_page_id)
203 {
205
206 $now = time();
207
208 //
209 // 1. Page access: current page
210 //
211 $set = $ilDB->query("SELECT obj_id FROM lm_read_event" .
212 " WHERE obj_id = " . $ilDB->quote($a_page_id, "integer") .
213 " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
214 if (!$ilDB->fetchAssoc($set)) {
215 $fields = array(
216 "obj_id" => array("integer", $a_page_id),
217 "usr_id" => array("integer", $this->user_id)
218 );
219 // $ilDB->insert("lm_read_event", $fields);
220 $ilDB->replace("lm_read_event", $fields, array()); // #15144
221 }
222
223 // update all parent chapters
224 $ilDB->manipulate("UPDATE lm_read_event SET" .
225 " read_count = read_count + 1 " .
226 " , last_access = " . $ilDB->quote($now, "integer") .
227 " WHERE obj_id = " . $ilDB->quote($a_page_id, "integer") .
228 " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
229
230
231 //
232 // 2. Chapter access: based on last page accessed
233 //
234
235 // get last accessed page
236 $set = $ilDB->query("SELECT * FROM lo_access WHERE " .
237 "usr_id = " . $ilDB->quote($this->user_id, "integer") . " AND " .
238 "lm_id = " . $ilDB->quote($this->lm_ref_id, "integer"));
239 $res = $ilDB->fetchAssoc($set);
240 if ($res["obj_id"]) {
241 $valid_timespan = ilObjUserTracking::_getValidTimeSpan();
242
243 $pg_ts = new ilDateTime($res["timestamp"], IL_CAL_DATETIME);
244 $pg_ts = $pg_ts->get(IL_CAL_UNIX);
245 $pg_id = $res["obj_id"];
246 if (!$this->lm_tree->isInTree($pg_id)) {
247 return;
248 }
249
250 $time_diff = $read_diff = 0;
251
252 // spent_seconds or read_count ?
253 if (($now - $pg_ts) <= $valid_timespan) {
254 $time_diff = $now - $pg_ts;
255 } else {
256 $read_diff = 1;
257 }
258
259 // find parent chapter(s) for that page
260 $parent_st_ids = array();
261 foreach ($this->lm_tree->getPathFull($pg_id) as $item) {
262 if ($item["type"] == "st") {
263 $parent_st_ids[] = $item["obj_id"];
264 }
265 }
266
267 if ($parent_st_ids && ($time_diff || $read_diff)) {
268 // get existing chapter entries
269 $ex_st = array();
270 $set = $ilDB->query("SELECT obj_id FROM lm_read_event" .
271 " WHERE " . $ilDB->in("obj_id", $parent_st_ids, "", "integer") .
272 " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
273 while ($row = $ilDB->fetchAssoc($set)) {
274 $ex_st[] = $row["obj_id"];
275 }
276
277 // add missing chapter entries
278 $missing_st = array_diff($parent_st_ids, $ex_st);
279 if (sizeof($missing_st)) {
280 foreach ($missing_st as $st_id) {
281 $fields = array(
282 "obj_id" => array("integer", $st_id),
283 "usr_id" => array("integer", $this->user_id)
284 );
285 // $ilDB->insert("lm_read_event", $fields);
286 $ilDB->replace("lm_read_event", $fields, array()); // #15144
287 }
288 }
289
290 // update all parent chapters
291 $ilDB->manipulate("UPDATE lm_read_event SET" .
292 " read_count = read_count + " . $ilDB->quote($read_diff, "integer") .
293 " , spent_seconds = spent_seconds + " . $ilDB->quote($time_diff, "integer") .
294 " , last_access = " . $ilDB->quote($now, "integer") .
295 " WHERE " . $ilDB->in("obj_id", $parent_st_ids, "", "integer") .
296 " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
297 }
298 }
299 }
300
301
305
311 public function setCurrentPage($a_val)
312 {
313 $this->current_page_id = $a_val;
314 }
315
321 public function getCurrentPage()
322 {
324 }
325
332 protected function loadLMTrackingData()
333 {
335
336 // we must prevent loading tracking data multiple times during a request where possible
337 // please note that the dirty flag works only to a certain limit
338 // e.g. if questions are answered the flag is not set (yet)
339 // or if pages/chapter are added/deleted the flag is not set
340 if ($this->loaded_for_node === (int) $this->getCurrentPage() && !$this->dirty) {
341 return;
342 }
343
344 $this->loaded_for_node = (int) $this->getCurrentPage();
345 $this->dirty = false;
346
347 // load lm tree in array
348 $this->tree_arr = array();
349 $nodes = $this->lm_tree->getCompleteTree();
350 foreach ($nodes as $node) {
351 $this->tree_arr["childs"][$node["parent"]][] = $node;
352 $this->tree_arr["parent"][$node["child"]] = $node["parent"];
353 $this->tree_arr["nodes"][$node["child"]] = $node;
354 }
355
356 // load all lm obj ids of learning module
357 $this->lm_obj_ids = ilLMObject::_getAllLMObjectsOfLM($this->lm_obj_id);
358
359 // load read event data
360 $this->re_arr = array();
361 $set = $ilDB->query("SELECT * FROM lm_read_event " .
362 " WHERE " . $ilDB->in("obj_id", $this->lm_obj_ids, false, "integer") .
363 " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
364 while ($rec = $ilDB->fetchAssoc($set)) {
365 $this->re_arr[$rec["obj_id"]] = $rec;
366 }
367
368 // load question/pages information
369 $this->page_questions = array();
370 $this->all_questions = array();
371 $q = ilLMPageObject::queryQuestionsOfLearningModule($this->lm_obj_id, "", "", 0, 0);
372 foreach ($q["set"] as $quest) {
373 $this->page_questions[$quest["page_id"]][] = $quest["question_id"];
374 $this->all_questions[] = $quest["question_id"];
375 }
376
377 // load question answer information
378 $this->answer_status = ilPageQuestionProcessor::getAnswerStatus($this->all_questions, $this->user_id);
379
380 $this->has_incorrect_answers = false;
381
382 $has_pred_incorrect_answers = false;
383 $has_pred_incorrect_not_unlocked_answers = false;
384 $this->determineProgressStatus($this->lm_tree->readRootId(), $has_pred_incorrect_answers, $has_pred_incorrect_not_unlocked_answers);
385
386 $this->has_incorrect_answers = $has_pred_incorrect_answers;
387 }
388
394 public function getAllQuestionsCorrect()
395 {
396 $this->loadLMTrackingData();
397 if (count($this->all_questions) > 0 && !$this->has_incorrect_answers) {
398 return true;
399 }
400 return false;
401 }
402
403
410 protected function determineProgressStatus($a_obj_id, &$a_has_pred_incorrect_answers, &$a_has_pred_incorrect_not_unlocked_answers)
411 {
413
414 if (isset($this->tree_arr["nodes"][$a_obj_id])) {
415 $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_answers"] = $a_has_pred_incorrect_answers;
416 $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_not_unlocked_answers"] = $a_has_pred_incorrect_not_unlocked_answers;
417
418 if (is_array($this->tree_arr["childs"][$a_obj_id])) {
419 // sort childs in correct order
420 $this->tree_arr["childs"][$a_obj_id] = ilUtil::sortArray($this->tree_arr["childs"][$a_obj_id], "lft", "asc", true);
421
422 $cnt_completed = 0;
423 foreach ($this->tree_arr["childs"][$a_obj_id] as $c) {
424 // if child is not activated/displayed count child as implicitly completed
425 // rationale: everything that is visible for the learner determines the status
426 // see also bug #14642
427 if (!self::_isNodeVisible($c)) {
428 $cnt_completed++;
429 continue;
430 }
431 $c_stat = $this->determineProgressStatus(
432 $c["child"],
433 $a_has_pred_incorrect_answers,
434 $a_has_pred_incorrect_not_unlocked_answers
435 );
436 if ($status != ilLMTracker::FAILED) {
437 if ($c_stat == ilLMTracker::FAILED) {
438 $status = ilLMTracker::IN_PROGRESS;
439 } elseif ($c_stat == ilLMTracker::IN_PROGRESS) {
440 $status = ilLMTracker::IN_PROGRESS;
441 } elseif ($c_stat == ilLMTracker::COMPLETED || $c_stat == ilLMTracker::CURRENT) {
442 $status = ilLMTracker::IN_PROGRESS;
443 $cnt_completed++;
444 }
445 }
446 // if an item is failed or in progress or (not attempted and contains questions)
447 // the next item has predecessing incorrect answers
448 if ($this->tree_arr["nodes"][$c["child"]]["type"] == "pg") {
449 if ($c_stat == ilLMTracker::FAILED || $c_stat == ilLMTracker::IN_PROGRESS ||
450 ($c_stat == ilLMTracker::NOT_ATTEMPTED && is_array($this->page_questions[$c["child"]]) && count($this->page_questions[$c["child"]]) > 0)) {
451 $a_has_pred_incorrect_answers = true;
452 if (!$this->tree_arr["nodes"][$c["child"]]["unlocked"]) {
453 $a_has_pred_incorrect_not_unlocked_answers = true;
454 }
455 }
456 }
457 }
458 if ($cnt_completed == count($this->tree_arr["childs"][$a_obj_id])) {
459 $status = ilLMTracker::COMPLETED;
460 }
461 } elseif ($this->tree_arr["nodes"][$a_obj_id]["type"] == "pg") {
462 // check read event data
463 if (isset($this->re_arr[$a_obj_id]) && $this->re_arr[$a_obj_id]["read_count"] > 0) {
464 $status = ilLMTracker::COMPLETED;
465 } elseif ($a_obj_id == $this->getCurrentPage()) {
466 $status = ilLMTracker::CURRENT;
467 }
468
469 $unlocked = false;
470 if (is_array($this->page_questions[$a_obj_id])) {
471 // check questions, if one is failed -> failed
472 $unlocked = true;
473 foreach ($this->page_questions[$a_obj_id] as $q_id) {
474 if (is_array($this->answer_status[$q_id])
475 && $this->answer_status[$q_id]["try"] > 0
476 && !$this->answer_status[$q_id]["passed"]) {
477 $status = ilLMTracker::FAILED;
478 if (!$this->answer_status[$q_id]["unlocked"]) {
479 $unlocked = false;
480 }
481 }
482 }
483
484 // check questions, if one is not answered -> in progress
485 if ($status != ilLMTracker::FAILED) {
486 foreach ($this->page_questions[$a_obj_id] as $q_id) {
487 if (!is_array($this->answer_status[$q_id])
488 || $this->answer_status[$q_id]["try"] == 0) {
489 if ($status != ilLMTracker::NOT_ATTEMPTED) {
490 $status = ilLMTracker::IN_PROGRESS;
491 }
492 }
493 }
494 $unlocked = false;
495 }
496 }
497 $this->tree_arr["nodes"][$a_obj_id]["unlocked"] = $unlocked;
498 $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_answers"] = $a_has_pred_incorrect_answers;
499 $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_not_unlocked_answers"] = $a_has_pred_incorrect_not_unlocked_answers;
500 }
501 } else { // free pages (currently not called, since only walking through tree structure)
502 }
503 $this->tree_arr["nodes"][$a_obj_id]["status"] = $status;
504
505 return $status;
506 }
507
508
516 public function getIconForLMObject($a_node, $a_highlighted_node = 0)
517 {
518 $this->loadLMTrackingData();
519 if ($a_node["child"] == $a_highlighted_node) {
520 return ilUtil::getImagePath('scorm/running.svg');
521 }
522 if (isset($this->tree_arr["nodes"][$a_node["child"]])) {
523 switch ($this->tree_arr["nodes"][$a_node["child"]]["status"]) {
525 return ilUtil::getImagePath('scorm/incomplete.svg');
526
528 return ilUtil::getImagePath('scorm/failed.svg');
529
531 return ilUtil::getImagePath('scorm/completed.svg');
532 }
533 }
534 return ilUtil::getImagePath('scorm/not_attempted.svg');
535 }
536
543 public function hasPredIncorrectAnswers($a_obj_id, $a_ignore_unlock = false)
544 {
545 $this->loadLMTrackingData();
546 $ret = false;
547 if (is_array($this->tree_arr["nodes"][$a_obj_id])) {
548 if ($a_ignore_unlock) {
549 $ret = $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_answers"];
550 } else {
551 $ret = $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_not_unlocked_answers"];
552 }
553 }
554 return $ret;
555 }
556
560
568 {
571 $ilPluginAdmin = $this->plugin_admin;
573
574 $blocked_users = array();
575
576 // load question/pages information
577 $this->page_questions = array();
578 $this->all_questions = array();
579 $page_for_question = array();
580 $q = ilLMPageObject::queryQuestionsOfLearningModule($this->lm_obj_id, "", "", 0, 0);
581 foreach ($q["set"] as $quest) {
582 $this->page_questions[$quest["page_id"]][] = $quest["question_id"];
583 $this->all_questions[] = $quest["question_id"];
584 $page_for_question[$quest["question_id"]] = $quest["page_id"];
585 }
586 // get question information
587 $qlist = new ilAssQuestionList($ilDB, $lng, $ilPluginAdmin);
588 $qlist->setParentObjId(0);
589 $qlist->setJoinObjectData(false);
590 $qlist->addFieldFilter("question_id", $this->all_questions);
591 $qlist->load();
592 $qdata = $qlist->getQuestionDataArray();
593
594 // load question answer information
595 $this->answer_status = ilPageQuestionProcessor::getAnswerStatus($this->all_questions);
596 foreach ($this->answer_status as $as) {
597 if ($as["try"] >= $qdata[$as["qst_id"]]["nr_of_tries"] && $qdata[$as["qst_id"]]["nr_of_tries"] > 0 && !$as["passed"]) {
598 //var_dump($qdata[$as["qst_id"]]);
599 $name = ilObjUser::_lookupName($as["user_id"]);
600 $as["user_name"] = $name["lastname"] . ", " . $name["firstname"] . " [" . $name["login"] . "]";
601 $as["question_text"] = $qdata[$as["qst_id"]]["question_text"];
602 $as["page_id"] = $page_for_question[$as["qst_id"]];
603 $as["page_title"] = ilLMPageObject::_lookupTitle($as["page_id"]);
604 $blocked_users[] = $as;
605 }
606 }
607
608 return $blocked_users;
609 }
610
617 public static function _isNodeVisible($a_node)
618 {
619 if ($a_node["type"] != "pg") {
620 return true;
621 }
622
623 $lm_set = new ilSetting("lm");
625 $a_node["child"],
626 "lm",
627 $lm_set->get("time_scheduled_page_activation")
628 );
629
630 if (!$active) {
631 $act_data = ilPageObject::_lookupActivationData((int) $a_node["child"], "lm");
632 if ($act_data["show_activation_info"] &&
633 (ilUtil::now() < $act_data["activation_start"])) {
634 return true;
635 } else {
636 return false;
637 }
638 } else {
639 return true;
640 }
641 }
642}
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)
if($format !==null) $name
Definition: metadata.php:230
$ret
Definition: parser.php:6
foreach($_POST as $key=> $value) $res
global $ilDB
$lm_set
$ilUser
Definition: imgupload.php:18
$DIC
Definition: xapitoken.php:46