ILIAS  Release_5_0_x_branch Revision 61816
 All Data Structures Namespaces Files Functions Variables Groups Pages
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 {
14  const NOT_ATTEMPTED = 0;
15  const IN_PROGRESS = 1;
16  const COMPLETED = 2;
17  const FAILED = 3;
18  const CURRENT = 99;
19 
20  protected $lm_ref_id;
21  protected $lm_obj_id;
22  protected $lm_tree;
23  protected $lm_obj_ids = array();
24  protected $tree_arr = array(); // tree array
25  protected $re_arr = array(); // read event data array
26  protected $loaded_for_node = false; // current node for that the tracking data has been loaded
27  protected $dirty = false;
28  protected $page_questions = array();
29  protected $all_questions = array();
30  protected $answer_status = array();
31  protected $has_incorrect_answers = false;
32  protected $current_page_id = 0;
33 
34  static $instances = array();
35  static $instancesbyobj = array();
36 
40 
46  private function __construct($a_id, $a_by_obj_id = false, $a_user_id)
47  {
48  $this->user_id = $a_user_id;
49 
50  if ($a_by_obj_id)
51  {
52  $this->lm_ref_id = 0;
53  $this->lm_obj_id = $a_id;
54  }
55  else
56  {
57  $this->lm_ref_id = $a_id;
58  $this->lm_obj_id = ilObject::_lookupObjId($a_id);
59  }
60 
61  include_once("./Modules/LearningModule/classes/class.ilLMTree.php");
62  $this->lm_tree = ilLMTree::getInstance($this->lm_obj_id);
63  }
64 
71  static function getInstance($a_ref_id, $a_user_id = 0)
72  {
73  global $ilUser;
74 
75  if ($a_user_id == 0)
76  {
77  $a_user_id = $ilUser->getId();
78  }
79 
80  if (!isset(self::$instances[$a_ref_id][$a_user_id]))
81  {
82  self::$instances[$a_ref_id][$a_user_id] = new ilLMTracker($a_ref_id, false, $a_user_id);
83  }
84  return self::$instances[$a_ref_id][$a_user_id];
85  }
86 
93  static function getInstanceByObjId($a_obj_id, $a_user_id = 0)
94  {
95  global $ilUser;
96 
97  if ($a_user_id == 0)
98  {
99  $a_user_id = $ilUser->getId();
100  }
101 
102  if (!isset(self::$instancesbyobj[$a_obj_id][$a_user_id]))
103  {
104  self::$instancesbyobj[$a_obj_id][$a_user_id] = new ilLMTracker($a_obj_id, true, $a_user_id);
105  }
106  return self::$instancesbyobj[$a_obj_id][$a_user_id];
107  }
108 
112 
118  function trackAccess($a_page_id)
119  {
120  if ($this->lm_ref_id == 0)
121  {
122  die("ilLMTracker: No Ref Id given.");
123  }
124 
125  // track page and chapter access
126  $this->trackPageAndChapterAccess($a_page_id);
127 
128  // track last page access (must be done after calling trackPageAndChapterAccess())
129  $this->trackLastPageAccess($this->user_id, $this->lm_ref_id, $a_page_id);
130 
131  // #9483
132  // general learning module lp tracking
133  include_once("./Services/Tracking/classes/class.ilLearningProgress.php");
134  ilLearningProgress::_tracProgress($this->user_id, $this->lm_obj_id,
135  $this->lm_ref_id, "lm");
136 
137  // obsolete?
138  include_once("./Services/Tracking/classes/class.ilLPStatusWrapper.php");
139  ilLPStatusWrapper::_updateStatus($this->lm_obj_id, $this->user_id);
140 
141  // mark currently loaded data as dirty to force reload if necessary
142  $this->dirty = true;
143  }
144 
152  function trackLastPageAccess($usr_id, $lm_id, $obj_id)
153  {
154  global $ilDB;
155 
156  // first check if an entry for this user and this lm already exist, when so, delete
157  $q = "DELETE FROM lo_access ".
158  "WHERE usr_id = ".$ilDB->quote((int) $usr_id, "integer")." ".
159  "AND lm_id = ".$ilDB->quote((int) $lm_id, "integer");
160  $ilDB->manipulate($q);
161 
162  $title = "";
163 
164  $q = "INSERT INTO lo_access ".
165  "(timestamp,usr_id,lm_id,obj_id,lm_title) ".
166  "VALUES ".
167  "(".$ilDB->now().",".
168  $ilDB->quote((int) $usr_id, "integer").",".
169  $ilDB->quote((int) $lm_id, "integer").",".
170  $ilDB->quote((int) $obj_id, "integer").",".
171  $ilDB->quote($title, "text").")";
172  $ilDB->manipulate($q);
173  }
174 
175 
179  protected function trackPageAndChapterAccess($a_page_id)
180  {
181  global $ilDB;
182 
183  $now = time();
184 
185  //
186  // 1. Page access: current page
187  //
188  $set = $ilDB->query("SELECT obj_id FROM lm_read_event".
189  " WHERE obj_id = ".$ilDB->quote($a_page_id, "integer").
190  " AND usr_id = ".$ilDB->quote($this->user_id, "integer"));
191  if (!$ilDB->fetchAssoc($set))
192  {
193  $fields = array(
194  "obj_id" => array("integer", $a_page_id),
195  "usr_id" => array("integer", $this->user_id)
196  );
197  // $ilDB->insert("lm_read_event", $fields);
198  $ilDB->replace("lm_read_event", $fields, array()); // #15144
199  }
200 
201  // update all parent chapters
202  $ilDB->manipulate("UPDATE lm_read_event SET".
203  " read_count = read_count + 1 ".
204  " , last_access = ".$ilDB->quote($now, "integer").
205  " WHERE obj_id = ".$ilDB->quote($a_page_id, "integer").
206  " AND usr_id = ".$ilDB->quote($this->user_id, "integer"));
207 
208 
209  //
210  // 2. Chapter access: based on last page accessed
211  //
212 
213  // get last accessed page
214  $set = $ilDB->query("SELECT * FROM lo_access WHERE ".
215  "usr_id = ".$ilDB->quote($this->user_id, "integer")." AND ".
216  "lm_id = ".$ilDB->quote($this->lm_ref_id, "integer"));
217  $res = $ilDB->fetchAssoc($set);
218  if($res["obj_id"])
219  {
220  include_once('Services/Tracking/classes/class.ilObjUserTracking.php');
221  $valid_timespan = ilObjUserTracking::_getValidTimeSpan();
222 
223  $pg_ts = new ilDateTime($res["timestamp"], IL_CAL_DATETIME);
224  $pg_ts = $pg_ts->get(IL_CAL_UNIX);
225  $pg_id = $res["obj_id"];
226  if(!$this->lm_tree->isInTree($pg_id))
227  {
228  return;
229  }
230 
231  $time_diff = $read_diff = 0;
232 
233  // spent_seconds or read_count ?
234  if (($now-$pg_ts) <= $valid_timespan)
235  {
236  $time_diff = $now-$pg_ts;
237  }
238  else
239  {
240  $read_diff = 1;
241  }
242 
243  // find parent chapter(s) for that page
244  $parent_st_ids = array();
245  foreach($this->lm_tree->getPathFull($pg_id) as $item)
246  {
247  if($item["type"] == "st")
248  {
249  $parent_st_ids[] = $item["obj_id"];
250  }
251  }
252 
253  if($parent_st_ids && ($time_diff || $read_diff))
254  {
255  // get existing chapter entries
256  $ex_st = array();
257  $set = $ilDB->query("SELECT obj_id FROM lm_read_event".
258  " WHERE ".$ilDB->in("obj_id", $parent_st_ids, "", "integer").
259  " AND usr_id = ".$ilDB->quote($this->user_id, "integer"));
260  while($row = $ilDB->fetchAssoc($set))
261  {
262  $ex_st[] = $row["obj_id"];
263  }
264 
265  // add missing chapter entries
266  $missing_st = array_diff($parent_st_ids, $ex_st);
267  if(sizeof($missing_st))
268  {
269  foreach($missing_st as $st_id)
270  {
271  $fields = array(
272  "obj_id" => array("integer", $st_id),
273  "usr_id" => array("integer", $this->user_id)
274  );
275  // $ilDB->insert("lm_read_event", $fields);
276  $ilDB->replace("lm_read_event", $fields, array()); // #15144
277  }
278  }
279 
280  // update all parent chapters
281  $ilDB->manipulate("UPDATE lm_read_event SET".
282  " read_count = read_count + ".$ilDB->quote($read_diff, "integer").
283  " , spent_seconds = spent_seconds + ".$ilDB->quote($time_diff, "integer").
284  " , last_access = ".$ilDB->quote($now, "integer").
285  " WHERE ".$ilDB->in("obj_id", $parent_st_ids, "", "integer").
286  " AND usr_id = ".$ilDB->quote($this->user_id, "integer"));
287  }
288  }
289  }
290 
291 
295 
301  public function setCurrentPage($a_val)
302  {
303  $this->current_page_id = $a_val;
304  }
305 
311  public function getCurrentPage()
312  {
313  return $this->current_page_id;
314  }
315 
322  protected function loadLMTrackingData()
323  {
324  global $ilDB;
325 
326  // we must prevent loading tracking data multiple times during a request where possible
327  // please note that the dirty flag works only to a certain limit
328  // e.g. if questions are answered the flag is not set (yet)
329  // or if pages/chapter are added/deleted the flag is not set
330  if ($this->loaded_for_node === (int) $this->getCurrentPage() && !$this->dirty)
331  {
332  return;
333  }
334 
335  $this->loaded_for_node = (int) $this->getCurrentPage();
336  $this->dirty = false;
337 
338  // load lm tree in array
339  $this->tree_arr = array();
340  $nodes = $this->lm_tree->getSubTree($this->lm_tree->getNodeData($this->lm_tree->readRootId()));
341  foreach ($nodes as $node)
342  {
343  $this->tree_arr["childs"][$node["parent"]][] = $node;
344  $this->tree_arr["parent"][$node["child"]] = $node["parent"];
345  $this->tree_arr["nodes"][$node["child"]] = $node;
346  }
347 
348  // load all lm obj ids of learning module
349  include_once("./Modules/LearningModule/classes/class.ilLMObject.php");
350  $this->lm_obj_ids = ilLMObject::_getAllLMObjectsOfLM($this->lm_obj_id);
351 
352  // load read event data
353  $this->re_arr = array();
354  $set = $ilDB->query("SELECT * FROM lm_read_event ".
355  " WHERE ".$ilDB->in("obj_id", $this->lm_obj_ids, false, "integer").
356  " AND usr_id = ".$ilDB->quote($this->user_id, "integer"));
357  while ($rec = $ilDB->fetchAssoc($set))
358  {
359  $this->re_arr[$rec["obj_id"]] = $rec;
360  }
361 
362  // load question/pages information
363  $this->page_questions = array();
364  $this->all_questions = array();
365  include_once("./Modules/LearningModule/classes/class.ilLMPageObject.php");
366  $q = ilLMPageObject::queryQuestionsOfLearningModule($this->lm_obj_id, "", "", 0, 0);
367  foreach ($q["set"] as $quest)
368  {
369  $this->page_questions[$quest["page_id"]][] = $quest["question_id"];
370  $this->all_questions[] = $quest["question_id"];
371  }
372 
373  // load question answer information
374  include_once("./Services/COPage/classes/class.ilPageQuestionProcessor.php");
375  $this->answer_status = ilPageQuestionProcessor::getAnswerStatus($this->all_questions, $this->user_id);
376 
377  $this->has_incorrect_answers = false;
378 
379  $has_pred_incorrect_answers = false;
380  $has_pred_incorrect_not_unlocked_answers = false;
381  $this->determineProgressStatus($this->lm_tree->readRootId(), $has_pred_incorrect_answers, $has_pred_incorrect_not_unlocked_answers);
382 
383  $this->has_incorrect_answers = $has_pred_incorrect_answers;
384  }
385 
392  {
393  $this->loadLMTrackingData();
394  if (count($this->all_questions) > 0 && !$this->has_incorrect_answers)
395  {
396  return true;
397  }
398  return false;
399  }
400 
401 
408  protected function determineProgressStatus($a_obj_id, &$a_has_pred_incorrect_answers, $a_has_pred_incorrect_not_unlocked_answers)
409  {
410  $status = ilLMTracker::NOT_ATTEMPTED;
411 
412  if (isset($this->tree_arr["nodes"][$a_obj_id]))
413  {
414  $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_answers"] = $a_has_pred_incorrect_answers;
415  $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_not_unlocked_answers"] = $a_has_pred_incorrect_not_unlocked_answers;
416 
417  if (is_array($this->tree_arr["childs"][$a_obj_id]))
418  {
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  {
425  // if child is not activated/displayed count child as implicitly completed
426  // rationale: everything that is visible for the learner determines the status
427  // see also bug #14642
428  if (!self::_isNodeVisible($c))
429  {
430  $cnt_completed++;
431  continue;
432  }
433  $c_stat = $this->determineProgressStatus($c["child"], $a_has_pred_incorrect_answers,
434  $a_has_pred_incorrect_not_unlocked_answers);
435  if ($status != ilLMTracker::FAILED)
436  {
437  if ($c_stat == ilLMTracker::FAILED)
438  {
439  $status = ilLMTracker::IN_PROGRESS;
440  }
441  else if ($c_stat == ilLMTracker::IN_PROGRESS)
442  {
443  $status = ilLMTracker::IN_PROGRESS;
444  }
445  else if ($c_stat == ilLMTracker::COMPLETED || $c_stat == ilLMTracker::CURRENT)
446  {
447  $status = ilLMTracker::IN_PROGRESS;
448  $cnt_completed++;
449  }
450  }
451  // if an item is failed or in progress or (not attempted and contains questions)
452  // the next item has predecessing incorrect answers
453  if ($c_stat == ilLMTracker::FAILED || $c_stat == ilLMTracker::IN_PROGRESS ||
454  ($c_stat == ilLMTracker::NOT_ATTEMPTED && is_array($this->page_questions[$c["child"]]) && count($this->page_questions[$c["child"]]) > 0))
455  {
456  $a_has_pred_incorrect_answers = true;
457  if (!$this->tree_arr["nodes"][$c["child"]]["unlocked"])
458  {
459  $a_has_pred_incorrect_not_unlocked_answers = true;
460  }
461  }
462  }
463  if ($cnt_completed == count($this->tree_arr["childs"][$a_obj_id]))
464  {
465  $status = ilLMTracker::COMPLETED;
466  }
467  }
468  else if ($this->tree_arr["nodes"][$a_obj_id]["type"] == "pg")
469  {
470  // check read event data
471  if (isset($this->re_arr[$a_obj_id]) && $this->re_arr[$a_obj_id]["read_count"] > 0)
472  {
473  $status = ilLMTracker::COMPLETED;
474  }
475  else if ($a_obj_id == $this->getCurrentPage())
476  {
477  $status = ilLMTracker::CURRENT;
478  }
479 
480  $unlocked = false;
481  if (is_array($this->page_questions[$a_obj_id]))
482  {
483  // check questions, if one is failed -> failed
484  $unlocked = true;
485  foreach ($this->page_questions[$a_obj_id] as $q_id)
486  {
487  if (is_array($this->answer_status[$q_id])
488  && $this->answer_status[$q_id]["try"] > 0
489  && !$this->answer_status[$q_id]["passed"])
490  {
491  $status = ilLMTracker::FAILED;
492  if (!$this->answer_status[$q_id]["unlocked"])
493  {
494  $unlocked = false;
495  }
496  }
497  }
498 
499  // check questions, if one is not answered -> in progress
500  if ($status != ilLMTracker::FAILED)
501  {
502  foreach ($this->page_questions[$a_obj_id] as $q_id)
503  {
504  if (!is_array($this->answer_status[$q_id])
505  || $this->answer_status[$q_id]["try"] == 0)
506  {
507  if ($status != ilLMTracker::NOT_ATTEMPTED)
508  {
509  $status = ilLMTracker::IN_PROGRESS;
510  }
511  }
512  }
513  $unlocked = false;
514  }
515  }
516  $this->tree_arr["nodes"][$a_obj_id]["unlocked"] = $unlocked;
517  $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_answers"] = $a_has_pred_incorrect_answers;
518  $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_not_unlocked_answers"] = $a_has_pred_incorrect_not_unlocked_answers;
519  }
520  }
521  else // free pages (currently not called, since only walking through tree structure)
522  {
523 
524  }
525  $this->tree_arr["nodes"][$a_obj_id]["status"] = $status;
526 
527  return $status;
528  }
529 
530 
538  public function getIconForLMObject($a_node, $a_highlighted_node = 0)
539  {
540  $this->loadLMTrackingData();
541  if ($a_node["child"] == $a_highlighted_node)
542  {
543  return ilUtil::getImagePath('scorm/running.svg');
544  }
545  if (isset($this->tree_arr["nodes"][$a_node["child"]]))
546  {
547  switch ($this->tree_arr["nodes"][$a_node["child"]]["status"])
548  {
550  return ilUtil::getImagePath('scorm/incomplete.svg');
551 
552  case ilLMTracker::FAILED:
553  return ilUtil::getImagePath('scorm/failed.svg');
554 
556  return ilUtil::getImagePath('scorm/completed.svg');
557  }
558  }
559  return ilUtil::getImagePath('scorm/not_attempted.svg');
560  }
561 
568  function hasPredIncorrectAnswers($a_obj_id, $a_ignore_unlock = false)
569  {
570  $this->loadLMTrackingData();
571  $ret = false;
572  if (is_array($this->tree_arr["nodes"][$a_obj_id]))
573  {
574  if ($a_ignore_unlock)
575  {
576  $ret = $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_answers"];
577  }
578  else
579  {
580  $ret = $this->tree_arr["nodes"][$a_obj_id]["has_pred_incorrect_not_unlocked_answers"];
581  }
582  }
583 
584  return $ret;
585  }
586 
590 
598  {
599  global $ilDB, $lng, $ilPluginAdmin, $ilUser;
600 
601  $blocked_users = array();
602 
603  // load question/pages information
604  $this->page_questions = array();
605  $this->all_questions = array();
606  $page_for_question = array();
607  include_once("./Modules/LearningModule/classes/class.ilLMPageObject.php");
608  $q = ilLMPageObject::queryQuestionsOfLearningModule($this->lm_obj_id, "", "", 0, 0);
609  foreach ($q["set"] as $quest)
610  {
611  $this->page_questions[$quest["page_id"]][] = $quest["question_id"];
612  $this->all_questions[] = $quest["question_id"];
613  $page_for_question[$quest["question_id"]] = $quest["page_id"];
614  }
615 
616  // get question information
617  include_once("./Modules/TestQuestionPool/classes/class.ilAssQuestionList.php");
618  $qlist = new ilAssQuestionList($ilDB, $lng, $ilPluginAdmin, 0);
619  $qlist->addFieldFilter("question_id", $this->all_questions);
620  $qlist->load();
621  $qdata = $qlist->getQuestionDataArray();
622 
623  // load question answer information
624  include_once("./Services/COPage/classes/class.ilPageQuestionProcessor.php");
625  $this->answer_status = ilPageQuestionProcessor::getAnswerStatus($this->all_questions);
626 
627  include_once("./Modules/LearningModule/classes/class.ilLMPageObject.php");
628  foreach ($this->answer_status as $as)
629  {
630  if ($as["try"] >= $qdata[$as["qst_id"]]["nr_of_tries"] && $qdata[$as["qst_id"]]["nr_of_tries"] > 0 && !$as["passed"])
631  {
632  //var_dump($qdata[$as["qst_id"]]);
633  $name = ilObjUser::_lookupName($as["user_id"]);
634  $as["user_name"] = $name["lastname"].", ".$name["firstname"]." [".$name["login"]."]";
635  $as["question_text"] = $qdata[$as["qst_id"]]["question_text"];
636  $as["page_id"] = $page_for_question[$as["qst_id"]];
637  $as["page_title"] = ilLMPageObject::_lookupTitle($as["page_id"]);
638  $blocked_users[] = $as;
639  }
640  }
641 
642  return $blocked_users;
643  }
644 
651  static function _isNodeVisible($a_node)
652  {
653  include_once("./Services/COPage/classes/class.ilPageObject.php");
654 
655  if ($a_node["type"] != "pg")
656  {
657  return true;
658  }
659 
660  $lm_set = new ilSetting("lm");
661  $active = ilPageObject::_lookupActive($a_node["child"], "lm",
662  $lm_set->get("time_scheduled_page_activation"));
663 
664  if(!$active)
665  {
666  $act_data = ilPageObject::_lookupActivationData((int) $a_node["child"], "lm");
667  if ($act_data["show_activation_info"] &&
668  (ilUtil::now() < $act_data["activation_start"]))
669  {
670  return true;
671  }
672  else
673  {
674  return false;
675  }
676  }
677  else
678  {
679  return true;
680  }
681  }
682 
683 
684 }
685 
686 ?>