ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
class.ilLMTracker.php
Go to the documentation of this file.
1<?php
2
19use ILIAS\Refinery\Factory as Refinery;
20
27{
28 public const NOT_ATTEMPTED = 0;
29 public const IN_PROGRESS = 1;
30 public const COMPLETED = 2;
31 public const FAILED = 3;
32 public const CURRENT = 99;
33 protected int $user_id;
34
35 protected ilDBInterface $db;
36 protected ilLanguage $lng;
37 protected Refinery $refinery;
39 protected ilObjUser $user;
40 protected int $lm_ref_id;
41 protected int $lm_obj_id;
42 protected ilLMTree $lm_tree;
43 protected array $lm_obj_ids = array();
44 protected array $tree_arr = array(); // tree array
45 protected array $re_arr = array(); // read event data array
46 protected bool $loaded_for_node = false; // current node for that the tracking data has been loaded
47 protected bool $dirty = false;
48 protected array $page_questions = array();
49 protected array $all_questions = array();
50 protected array $answer_status = array();
51 protected bool $has_incorrect_answers = false;
52 protected int $current_page_id = 0;
53
54 public static array $instances = array();
55 public static array $instancesbyobj = array();
56
57 private function __construct(
58 int $a_id,
59 bool $a_by_obj_id = false,
60 int $a_user_id = 0
61 ) {
62 global $DIC;
63
64 $this->db = $DIC->database();
65 $this->lng = $DIC->language();
66 $this->user = $DIC->user();
67 $this->user_id = $a_user_id;
68 $this->refinery = $DIC['refinery'];
69 $this->component_repository = $DIC['component.repository'];
70
71 if ($a_by_obj_id) {
72 $this->lm_ref_id = 0;
73 $this->lm_obj_id = $a_id;
74 } else {
75 $this->lm_ref_id = $a_id;
76 $this->lm_obj_id = ilObject::_lookupObjId($a_id);
77 }
78
79 $this->lm_tree = ilLMTree::getInstance($this->lm_obj_id);
80 }
81
82 public static function getInstance(
83 int $a_ref_id,
84 int $a_user_id = 0
85 ): self {
86 global $DIC;
87
88 $ilUser = $DIC->user();
89
90 if ($a_user_id == 0) {
91 $a_user_id = $ilUser->getId();
92 }
93
94 if (!isset(self::$instances[$a_ref_id][$a_user_id])) {
95 self::$instances[$a_ref_id][$a_user_id] = new ilLMTracker($a_ref_id, false, $a_user_id);
96 }
97 return self::$instances[$a_ref_id][$a_user_id];
98 }
99
100 public static function getInstanceByObjId(
101 int $a_obj_id,
102 int $a_user_id = 0
103 ): self {
104 global $DIC;
105
106 $ilUser = $DIC->user();
107
108 if ($a_user_id == 0) {
109 $a_user_id = $ilUser->getId();
110 }
111
112 if (!isset(self::$instancesbyobj[$a_obj_id][$a_user_id])) {
113 self::$instancesbyobj[$a_obj_id][$a_user_id] = new ilLMTracker($a_obj_id, true, $a_user_id);
114 }
115 return self::$instancesbyobj[$a_obj_id][$a_user_id];
116 }
117
121
125 public function trackAccess(
126 int $a_page_id,
127 int $user_id
128 ): void {
130 ilChangeEvent::_recordReadEvent("lm", $this->lm_ref_id, $this->lm_obj_id, $user_id);
131 return;
132 }
133
134 if ($this->lm_ref_id == 0) {
135 throw new ilLMPresentationException("ilLMTracker: No Ref Id given.");
136 }
137
138 // track page and chapter access
139 $this->trackPageAndChapterAccess($a_page_id);
140
141 // track last page access (must be done after calling trackPageAndChapterAccess())
142 $this->trackLastPageAccess($this->user_id, $this->lm_ref_id, $a_page_id);
143
144 // #9483
145 // general learning module lp tracking
147 $this->user_id,
148 $this->lm_obj_id,
149 $this->lm_ref_id,
150 "lm"
151 );
152
153 // obsolete?
154 ilLPStatusWrapper::_updateStatus($this->lm_obj_id, $this->user_id);
155
156 // mark currently loaded data as dirty to force reload if necessary
157 $this->dirty = true;
158 }
159
166 public function trackLastPageAccess(
167 int $usr_id,
168 int $lm_id,
169 int $obj_id
170 ): void {
171 $title = "";
172 $db = $this->db;
173 $db->replace(
174 "lo_access",
175 [
176 "usr_id" => ["integer", $usr_id],
177 "lm_id" => ["integer", $lm_id]
178 ],
179 [
180 "timestamp" => ["timestamp", ilUtil::now()],
181 "obj_id" => ["integer", $obj_id],
182 "lm_title" => ["text", $title]
183 ]
184 );
185 }
186
187 protected function trackPageAndChapterAccess(
188 int $a_page_id
189 ): void {
190 $ilDB = $this->db;
191
192 $now = time();
193
194 //
195 // 1. Page access: current page
196 //
197 $set = $ilDB->query("SELECT obj_id FROM lm_read_event" .
198 " WHERE obj_id = " . $ilDB->quote($a_page_id, "integer") .
199 " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
200 if (!$ilDB->fetchAssoc($set)) {
201 $fields = array(
202 "obj_id" => array("integer", $a_page_id),
203 "usr_id" => array("integer", $this->user_id)
204 );
205 // $ilDB->insert("lm_read_event", $fields);
206 $ilDB->replace("lm_read_event", $fields, array()); // #15144
207 }
208
209 // update all parent chapters
210 $ilDB->manipulate("UPDATE lm_read_event SET" .
211 " read_count = read_count + 1 " .
212 " , last_access = " . $ilDB->quote($now, "integer") .
213 " WHERE obj_id = " . $ilDB->quote($a_page_id, "integer") .
214 " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
215
216
217 //
218 // 2. Chapter access: based on last page accessed
219 //
220
221 // get last accessed page
222 $set = $ilDB->query("SELECT * FROM lo_access WHERE " .
223 "usr_id = " . $ilDB->quote($this->user_id, "integer") . " AND " .
224 "lm_id = " . $ilDB->quote($this->lm_ref_id, "integer"));
225 $res = $ilDB->fetchAssoc($set);
226 if (isset($res["obj_id"])) {
227 $valid_timespan = ilObjUserTracking::_getValidTimeSpan();
228
229 $pg_ts = new ilDateTime($res["timestamp"], IL_CAL_DATETIME);
230 $pg_ts = $pg_ts->get(IL_CAL_UNIX);
231 $pg_id = $res["obj_id"];
232 if (!$this->lm_tree->isInTree($pg_id)) {
233 return;
234 }
235
236 $time_diff = $read_diff = 0;
237
238 // spent_seconds or read_count ?
239 if (($now - $pg_ts) <= $valid_timespan) {
240 $time_diff = $now - $pg_ts;
241 } else {
242 $read_diff = 1;
243 }
244
245 // find parent chapter(s) for that page
246 $parent_st_ids = array();
247 foreach ($this->lm_tree->getPathFull($pg_id) as $item) {
248 if ($item["type"] == "st") {
249 $parent_st_ids[] = $item["obj_id"];
250 }
251 }
252
253 if ($parent_st_ids && ($time_diff || $read_diff)) {
254 // get existing chapter entries
255 $ex_st = array();
256 $set = $ilDB->query("SELECT obj_id FROM lm_read_event" .
257 " WHERE " . $ilDB->in("obj_id", $parent_st_ids, "", "integer") .
258 " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
259 while ($row = $ilDB->fetchAssoc($set)) {
260 $ex_st[] = $row["obj_id"];
261 }
262
263 // add missing chapter entries
264 $missing_st = array_diff($parent_st_ids, $ex_st);
265 if (sizeof($missing_st)) {
266 foreach ($missing_st as $st_id) {
267 $fields = array(
268 "obj_id" => array("integer", $st_id),
269 "usr_id" => array("integer", $this->user_id)
270 );
271 // $ilDB->insert("lm_read_event", $fields);
272 $ilDB->replace("lm_read_event", $fields, array()); // #15144
273 }
274 }
275
276 // update all parent chapters
277 $ilDB->manipulate("UPDATE lm_read_event SET" .
278 " read_count = read_count + " . $ilDB->quote($read_diff, "integer") .
279 " , spent_seconds = spent_seconds + " . $ilDB->quote($time_diff, "integer") .
280 " , last_access = " . $ilDB->quote($now, "integer") .
281 " WHERE " . $ilDB->in("obj_id", $parent_st_ids, "", "integer") .
282 " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
283 }
284 }
285 }
286
287
291
292 public function setCurrentPage(
293 int $a_val
294 ): void {
295 $this->current_page_id = $a_val;
296 }
297
298 public function getCurrentPage(): int
299 {
300 return $this->current_page_id;
301 }
302
306 protected function loadLMTrackingData(): void
307 {
308 $ilDB = $this->db;
309
310 // we must prevent loading tracking data multiple times during a request where possible
311 // please note that the dirty flag works only to a certain limit
312 // e.g. if questions are answered the flag is not set (yet)
313 // or if pages/chapter are added/deleted the flag is not set
314 if ((int) $this->loaded_for_node === $this->getCurrentPage() && $this->getCurrentPage() > 0 && !$this->dirty) {
315 return;
316 }
317
318 $this->loaded_for_node = $this->getCurrentPage();
319 $this->dirty = false;
320
321 // load lm tree in array
322 $this->tree_arr = array();
323 $nodes = $this->lm_tree->getCompleteTree();
324 foreach ($nodes as $node) {
325 $this->tree_arr["childs"][$node["parent"]][] = $node;
326 $this->tree_arr["parent"][$node["child"]] = $node["parent"];
327 $this->tree_arr["nodes"][$node["child"]] = $node;
328 }
329
330 // load all lm obj ids of learning module
331 $this->lm_obj_ids = ilLMObject::_getAllLMObjectsOfLM($this->lm_obj_id);
332
333 // load read event data
334 $this->re_arr = array();
335 $set = $ilDB->query("SELECT * FROM lm_read_event " .
336 " WHERE " . $ilDB->in("obj_id", $this->lm_obj_ids, false, "integer") .
337 " AND usr_id = " . $ilDB->quote($this->user_id, "integer"));
338 while ($rec = $ilDB->fetchAssoc($set)) {
339 $this->re_arr[$rec["obj_id"]] = $rec;
340 }
341
342 // load question/pages information
343 $this->page_questions = array();
344 $this->all_questions = array();
345 $q = ilLMPageObject::queryQuestionsOfLearningModule($this->lm_obj_id, "", "", 0, 0);
346 foreach ($q["set"] as $quest) {
347 $this->page_questions[$quest["page_id"]][] = $quest["question_id"];
348 $this->all_questions[] = $quest["question_id"];
349 }
350
351 // load question answer information
352 $this->answer_status = ilPageQuestionProcessor::getAnswerStatus($this->all_questions, $this->user_id);
353
354 $this->has_incorrect_answers = false;
355
356 $has_pred_incorrect_answers = false;
357 $has_pred_incorrect_not_unlocked_answers = false;
358 $this->determineProgressStatus($this->lm_tree->readRootId(), $has_pred_incorrect_answers, $has_pred_incorrect_not_unlocked_answers);
359
360 $this->has_incorrect_answers = $has_pred_incorrect_answers;
361 }
362
367 public function getAllQuestionsCorrect(): bool
368 {
369 $this->loadLMTrackingData();
370 if (count($this->all_questions) > 0 && !$this->has_incorrect_answers) {
371 return true;
372 }
373 return false;
374 }
375
376
381 protected function determineProgressStatus(
382 int $a_obj_id,
383 bool &$a_has_pred_incorrect_answers,
384 bool &$a_has_pred_incorrect_not_unlocked_answers
385 ): int {
386 $status = ilLMTracker::NOT_ATTEMPTED;
387
388 if (isset($this->tree_arr["nodes"][$a_obj_id])) {
389 $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_answers"] = $a_has_pred_incorrect_answers;
390 $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_not_unlocked_answers"] = $a_has_pred_incorrect_not_unlocked_answers;
391
392 if (isset($this->tree_arr["childs"][$a_obj_id])) {
393 // sort childs in correct order
394 $this->tree_arr["childs"][$a_obj_id] = ilArrayUtil::sortArray(
395 $this->tree_arr["childs"][$a_obj_id],
396 "lft",
397 "asc",
398 true
399 );
400
401 $cnt_completed = 0;
402 foreach ($this->tree_arr["childs"][$a_obj_id] as $c) {
403 // if child is not activated/displayed count child as implicitly completed
404 // rationale: everything that is visible for the learner determines the status
405 // see also bug #14642
406 if (!self::_isNodeVisible($c)) {
407 $cnt_completed++;
408 continue;
409 }
410 $c_stat = $this->determineProgressStatus(
411 $c["child"],
412 $a_has_pred_incorrect_answers,
413 $a_has_pred_incorrect_not_unlocked_answers
414 );
415 if ($status != ilLMTracker::FAILED) {
416 if ($c_stat == ilLMTracker::FAILED) {
417 $status = ilLMTracker::IN_PROGRESS;
418 } elseif ($c_stat == ilLMTracker::IN_PROGRESS) {
419 $status = ilLMTracker::IN_PROGRESS;
420 } elseif ($c_stat == ilLMTracker::COMPLETED || $c_stat == ilLMTracker::CURRENT) {
421 $status = ilLMTracker::IN_PROGRESS;
422 $cnt_completed++;
423 }
424 }
425 // if an item is failed or in progress or (not attempted and contains questions)
426 // the next item has predecessing incorrect answers
427 if ($this->tree_arr["nodes"][$c["child"]]["type"] == "pg") {
428 if ($c_stat == ilLMTracker::FAILED || $c_stat == ilLMTracker::IN_PROGRESS ||
429 ($c_stat == ilLMTracker::NOT_ATTEMPTED && isset($this->page_questions[$c["child"]]) && count($this->page_questions[$c["child"]]) > 0)) {
430 $a_has_pred_incorrect_answers = true;
431 if (!$this->tree_arr["nodes"][$c["child"]]["unlocked"]) {
432 $a_has_pred_incorrect_not_unlocked_answers = true;
433 }
434 }
435 }
436 }
437 if ($cnt_completed == count($this->tree_arr["childs"][$a_obj_id])) {
438 $status = ilLMTracker::COMPLETED;
439 }
440 } elseif ($this->tree_arr["nodes"][$a_obj_id]["type"] == "pg") {
441 // check read event data
442 if (isset($this->re_arr[$a_obj_id]) && $this->re_arr[$a_obj_id]["read_count"] > 0) {
443 $status = ilLMTracker::COMPLETED;
444 } elseif ($a_obj_id == $this->getCurrentPage()) {
445 $status = ilLMTracker::CURRENT;
446 }
447
448 $unlocked = false;
449 if (isset($this->page_questions[$a_obj_id])) {
450 // check questions, if one is failed -> failed
451 $unlocked = true;
452 foreach ($this->page_questions[$a_obj_id] as $q_id) {
453 if (isset($this->answer_status[$q_id])
454 && $this->answer_status[$q_id]["try"] > 0
455 && !$this->answer_status[$q_id]["passed"]) {
456 $status = ilLMTracker::FAILED;
457 if (!$this->answer_status[$q_id]["unlocked"]) {
458 $unlocked = false;
459 }
460 }
461 }
462
463 // check questions, if one is not answered -> in progress
464 if ($status != ilLMTracker::FAILED) {
465 foreach ($this->page_questions[$a_obj_id] as $q_id) {
466 if (!isset($this->answer_status[$q_id])
467 || $this->answer_status[$q_id]["try"] == 0) {
468 if ($status != ilLMTracker::NOT_ATTEMPTED) {
469 $status = ilLMTracker::IN_PROGRESS;
470 }
471 }
472 }
473 $unlocked = false;
474 }
475 }
476 $this->tree_arr["nodes"][$a_obj_id]["unlocked"] = $unlocked;
477 $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_answers"] = $a_has_pred_incorrect_answers;
478 $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_not_unlocked_answers"] = $a_has_pred_incorrect_not_unlocked_answers;
479 }
480 } /*else { // free pages (currently not called, since only walking through tree structure)
481 }*/
482 $this->tree_arr["nodes"][$a_obj_id]["status"] = $status;
483
484 return $status;
485 }
486
487 public function getIconForLMObject(
488 array $a_node,
489 int $a_highlighted_node = 0
490 ): string {
491 $this->loadLMTrackingData();
493
494 if ($a_node["child"] == $a_highlighted_node) {
495 return $icons->getImagePathRunning();
496 }
497 if (isset($this->tree_arr["nodes"][$a_node["child"]])) {
498 switch ($this->tree_arr["nodes"][$a_node["child"]]["status"] ?? null) {
500 return $icons->getImagePathInProgress();
501
503 return $icons->getImagePathFailed();
504
506 return $icons->getImagePathCompleted();
507 }
508 }
509 return $icons->getImagePathNotAttempted();
510 }
511
516 public function hasPredIncorrectAnswers(
517 int $a_obj_id,
518 bool $a_ignore_unlock = false
519 ) {
520 $this->loadLMTrackingData();
521 $ret = false;
522 if (isset($this->tree_arr["nodes"][$a_obj_id])) {
523 if ($a_ignore_unlock) {
524 $ret = $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_answers"] ?? false;
525 } else {
526 $ret = $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_not_unlocked_answers"] ?? false;
527 }
528 }
529 return $ret;
530 }
531
535
536 public function getBlockedUsersInformation(): array
537 {
538 $ilDB = $this->db;
541 $component_repository = $this->component_repository;
542
543 $blocked_users = array();
544
545 // load question/pages information
546 $this->page_questions = array();
547 $this->all_questions = array();
548 $page_for_question = array();
549 $q = ilLMPageObject::queryQuestionsOfLearningModule($this->lm_obj_id, "", "", 0, 0);
550 foreach ($q["set"] as $quest) {
551 $this->page_questions[$quest["page_id"]][] = $quest["question_id"];
552 $this->all_questions[] = $quest["question_id"];
553 $page_for_question[$quest["question_id"]] = $quest["page_id"];
554 }
555 // get question information
556 $qlist = new ilAssQuestionList($ilDB, $lng, $refinery, $component_repository);
557 $qlist->setParentObjId(0);
558 $qlist->setJoinObjectData(false);
559 $qlist->addFieldFilter("question_id", $this->all_questions);
560 $qlist->load();
561 $qdata = $qlist->getQuestionDataArray();
562
563 // load question answer information
564 $this->answer_status = ilPageQuestionProcessor::getAnswerStatus($this->all_questions);
565 foreach ($this->answer_status as $as) {
566 if ($as["try"] >= $qdata[$as["qst_id"]]["nr_of_tries"] && $qdata[$as["qst_id"]]["nr_of_tries"] > 0 && !$as["passed"]) {
567 //var_dump($qdata[$as["qst_id"]]);
568 $name = ilObjUser::_lookupName($as["user_id"]);
569 $as["user_name"] = $name["lastname"] . ", " . $name["firstname"] . " [" . $name["login"] . "]";
570 $as["question_text"] = $qdata[$as["qst_id"]]["question_text"];
571 $as["page_id"] = $page_for_question[$as["qst_id"]];
572 $as["page_title"] = ilLMPageObject::_lookupTitle($as["page_id"]);
573 $blocked_users[] = $as;
574 }
575 }
576
577 return $blocked_users;
578 }
579
583 public static function _isNodeVisible(
584 array $a_node
585 ): bool {
586 if ($a_node["type"] != "pg") {
587 return true;
588 }
589
590 $lm_set = new ilSetting("lm");
592 $a_node["child"],
593 "lm",
594 (bool) $lm_set->get("time_scheduled_page_activation")
595 );
596
597 if (!$active) {
598 $act_data = ilPageObject::_lookupActivationData((int) $a_node["child"], "lm");
599 if ($act_data["show_activation_info"] &&
600 (ilUtil::now() < $act_data["activation_start"])) {
601 return true;
602 } else {
603 return false;
604 }
605 } else {
606 return true;
607 }
608 }
609}
Builds data types.
Definition: Factory.php:36
const IL_CAL_UNIX
const IL_CAL_DATETIME
static sortArray(array $array, string $a_array_sortby_key, string $a_array_sortorder="asc", bool $a_numeric=false, bool $a_keep_keys=false)
static _recordReadEvent(string $a_type, int $a_ref_id, int $obj_id, int $usr_id, bool $isCatchupWriteEvents=true, $a_ext_rc=null, $a_ext_time=null)
@classDescription Date and time handling
static _getAllLMObjectsOfLM(int $a_lm_id, string $a_type="")
Get all objects of learning module.
static _lookupTitle(int $a_obj_id)
static queryQuestionsOfLearningModule(int $a_lm_id, string $a_order_field, string $a_order_dir, int $a_offset, int $a_limit)
Get questions of learning module.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
hasPredIncorrectAnswers(int $a_obj_id, bool $a_ignore_unlock=false)
Has predecessing incorrect answers.
trackPageAndChapterAccess(int $a_page_id)
trackLastPageAccess(int $usr_id, int $lm_id, int $obj_id)
Track last accessed page for a learning module.
Refinery $refinery
getAllQuestionsCorrect()
Have all questions been answered correctly (and questions exist)?
determineProgressStatus(int $a_obj_id, bool &$a_has_pred_incorrect_answers, bool &$a_has_pred_incorrect_not_unlocked_answers)
Determine progress status of nodes.
static getInstance(int $a_ref_id, int $a_user_id=0)
static array $instancesbyobj
ilLanguage $lng
trackAccess(int $a_page_id, int $user_id)
Track access to lm page.
setCurrentPage(int $a_val)
ilDBInterface $db
static array $instances
bool $has_incorrect_answers
static getInstanceByObjId(int $a_obj_id, int $a_user_id=0)
__construct(int $a_id, bool $a_by_obj_id=false, int $a_user_id=0)
ilComponentRepository $component_repository
static _isNodeVisible(array $a_node)
Is node visible for the learner.
getIconForLMObject(array $a_node, int $a_highlighted_node=0)
loadLMTrackingData()
Load LM tracking data.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static getInstance(int $a_tree_id)
static getInstance(int $variant=ilLPStatusIcons::ICON_VARIANT_DEFAULT, ?\ILIAS\UI\Renderer $renderer=null, ?\ILIAS\UI\Factory $factory=null)
static _updateStatus(int $a_obj_id, int $a_usr_id, ?object $a_obj=null, bool $a_percentage=false, bool $a_force_raise=false)
language handling
static _tracProgress(int $a_user_id, int $a_obj_id, int $a_ref_id, string $a_obj_type='')
User class.
static _lookupName(int $a_user_id)
static _lookupObjId(int $ref_id)
static _lookupActive(int $a_id, string $a_parent_type, bool $a_check_scheduled_activation=false, string $a_lang="-")
lookup activation status
static _lookupActivationData(int $a_id, string $a_parent_type, string $a_lang="-")
Lookup activation data.
static getAnswerStatus( $a_q_id, int $a_user_id=0)
ILIAS Setting Class.
static now()
Return current timestamp in Y-m-d H:i:s format.
const ANONYMOUS_USER_ID
Definition: constants.php:27
$c
Definition: deliver.php:25
Readable part of repository interface to ilComponentDataDB.
Interface ilDBInterface.
replace(string $table, array $primary_keys, array $other_columns)
Replace into method.
$res
Definition: ltiservices.php:69
global $lng
Definition: privfeed.php:31
if(!file_exists('../ilias.ini.php'))
global $DIC
Definition: shib_login.php:26
$q
Definition: shib_logout.php:23
$lm_set