Updated the 19 build version to 20090123
[moodle.git] / lib / statslib.php
blob2318a9435de470cda83ec650a90d061edb554b64
1 <?php
3 // THESE CONSTANTS ARE USED FOR THE REPORTING PAGE.
5 define('STATS_REPORT_LOGINS',1); // double impose logins and unqiue logins on a line graph. site course only.
6 define('STATS_REPORT_READS',2); // double impose student reads and teacher reads on a line graph.
7 define('STATS_REPORT_WRITES',3); // double impose student writes and teacher writes on a line graph.
8 define('STATS_REPORT_ACTIVITY',4); // 2+3 added up, teacher vs student.
9 define('STATS_REPORT_ACTIVITYBYROLE',5); // all activity, reads vs writes, seleted by role.
11 // user level stats reports.
12 define('STATS_REPORT_USER_ACTIVITY',7);
13 define('STATS_REPORT_USER_ALLACTIVITY',8);
14 define('STATS_REPORT_USER_LOGINS',9);
15 define('STATS_REPORT_USER_VIEW',10); // this is the report you see on the user profile.
17 // admin only ranking stats reports
18 define('STATS_REPORT_ACTIVE_COURSES',11);
19 define('STATS_REPORT_ACTIVE_COURSES_WEIGHTED',12);
20 define('STATS_REPORT_PARTICIPATORY_COURSES',13);
21 define('STATS_REPORT_PARTICIPATORY_COURSES_RW',14);
23 // start after 0 = show dailies.
24 define('STATS_TIME_LASTWEEK',1);
25 define('STATS_TIME_LAST2WEEKS',2);
26 define('STATS_TIME_LAST3WEEKS',3);
27 define('STATS_TIME_LAST4WEEKS',4);
29 // start after 10 = show weeklies
30 define('STATS_TIME_LAST2MONTHS',12);
32 define('STATS_TIME_LAST3MONTHS',13);
33 define('STATS_TIME_LAST4MONTHS',14);
34 define('STATS_TIME_LAST5MONTHS',15);
35 define('STATS_TIME_LAST6MONTHS',16);
37 // start after 20 = show monthlies
38 define('STATS_TIME_LAST7MONTHS',27);
39 define('STATS_TIME_LAST8MONTHS',28);
40 define('STATS_TIME_LAST9MONTHS',29);
41 define('STATS_TIME_LAST10MONTHS',30);
42 define('STATS_TIME_LAST11MONTHS',31);
43 define('STATS_TIME_LASTYEAR',32);
45 // different modes for what reports to offer
46 define('STATS_MODE_GENERAL',1);
47 define('STATS_MODE_DETAILED',2);
48 define('STATS_MODE_RANKED',3); // admins only - ranks courses
50 /**
51 * Print daily cron progress
52 * @param string $ident
54 function stats_daily_progress($ident) {
55 static $start = 0;
56 static $init = 0;
58 if ($ident == 'init') {
59 $init = $start = time();
60 return;
63 $elapsed = time() - $start;
64 $start = time();
66 if (debugging('', DEBUG_ALL)) {
67 mtrace("$ident:$elapsed ", '');
68 } else {
69 mtrace('.', '');
73 /**
74 * Execute daily statistics gathering
75 * @param int $maxdays maximum number of days to be processed
76 * @return boolean success
78 function stats_cron_daily($maxdays=1) {
79 global $CFG;
81 $now = time();
83 // read last execution date from db
84 if (!$timestart = get_config(NULL, 'statslastdaily')) {
85 $timestart = stats_get_base_daily(stats_get_start_from('daily'));
86 set_config('statslastdaily', $timestart);
89 // calculate scheduled time
90 $scheduledtime = stats_get_base_daily() + $CFG->statsruntimestarthour*60*60 + $CFG->statsruntimestartminute*60;
92 // Note: This will work fine for sites running cron each 4 hours or less (hoppefully, 99.99% of sites). MDL-16709
93 // check to make sure we're due to run, at least 20 hours after last run
94 if (isset($CFG->statslastexecution) and ((time() - 20*60*60) < $CFG->statslastexecution)) {
95 mtrace("...preventing stats to run, last execution was less than 20 hours ago.");
96 return false;
97 // also check that we are a max of 4 hours after scheduled time, stats won't run after that
98 } else if (time() > $scheduledtime + 4*60*60) {
99 mtrace("...preventing stats to run, more than 4 hours since scheduled time.");
100 return false;
101 } else {
102 set_config('statslastexecution', time()); /// Grab this execution as last one
105 $nextmidnight = stats_get_next_day_start($timestart);
107 // are there any days that need to be processed?
108 if ($now < $nextmidnight) {
109 return true; // everything ok and up-to-date
113 $timeout = empty($CFG->statsmaxruntime) ? 60*60*24 : $CFG->statsmaxruntime;
115 if (!set_cron_lock('statsrunning', $now + $timeout)) {
116 return false;
119 // fisrt delete entries that should not be there yet
120 delete_records_select('stats_daily', "timeend > $timestart");
121 delete_records_select('stats_user_daily', "timeend > $timestart");
123 // Read in a few things we'll use later
124 $viewactions = implode(',', stats_get_action_names('view'));
125 $postactions = implode(',', stats_get_action_names('post'));
127 $guest = get_guest();
128 $guestrole = get_guest_role();
130 list($enroljoin, $enrolwhere) = stats_get_enrolled_sql($CFG->statscatdepth, true);
131 list($enroljoin_na, $enrolwhere_na) = stats_get_enrolled_sql($CFG->statscatdepth, false);
132 list($fpjoin, $fpwhere) = stats_get_enrolled_sql(0, true);
134 mtrace("Running daily statistics gathering, starting at $timestart:");
136 $days = 0;
137 $failed = false; // failed stats flag
139 while ($now > $nextmidnight) {
140 if ($days >= $maxdays) {
141 mtrace("...stopping early, reached maximum number of $maxdays days - will continue next time.");
142 set_cron_lock('statsrunning', null);
143 return false;
146 $days++;
147 @set_time_limit($timeout - 200);
149 if ($days > 1) {
150 // move the lock
151 set_cron_lock('statsrunning', time() + $timeout, true);
154 $daystart = time();
156 $timesql = "l.time >= $timestart AND l.time < $nextmidnight";
157 $timesql1 = "l1.time >= $timestart AND l1.time < $nextmidnight";
158 $timesql2 = "l2.time >= $timestart AND l2.time < $nextmidnight";
160 stats_daily_progress('init');
163 /// find out if any logs available for this day
164 $sql = "SELECT 'x'
165 FROM {$CFG->prefix}log l
166 WHERE $timesql";
167 $logspresent = get_records_sql($sql, 0, 1);
169 /// process login info first
170 $sql = "INSERT INTO {$CFG->prefix}stats_user_daily (stattype, timeend, courseid, userid, statsreads)
172 SELECT 'logins', timeend, courseid, userid, count(statsreads)
173 FROM (
174 SELECT $nextmidnight AS timeend, ".SITEID." AS courseid, l.userid, l.id AS statsreads
175 FROM {$CFG->prefix}log l
176 WHERE action = 'login' AND $timesql
177 ) inline_view
178 GROUP BY timeend, courseid, userid
179 HAVING count(statsreads) > 0";
181 if ($logspresent and !execute_sql($sql, false)) {
182 $failed = true;
183 break;
185 stats_daily_progress('1');
187 $sql = "INSERT INTO {$CFG->prefix}stats_daily (stattype, timeend, courseid, roleid, stat1, stat2)
189 SELECT 'logins' AS stattype, $nextmidnight AS timeend, ".SITEID." as courseid, 0,
190 COALESCE((SELECT SUM(statsreads)
191 FROM {$CFG->prefix}stats_user_daily s1
192 WHERE s1.stattype = 'logins' AND timeend = $nextmidnight), 0) AS stat1,
193 (SELECT COUNT('x')
194 FROM {$CFG->prefix}stats_user_daily s2
195 WHERE s2.stattype = 'logins' AND timeend = $nextmidnight) AS stat2" .
196 sql_null_from_clause();
198 if ($logspresent and !execute_sql($sql, false)) {
199 $failed = true;
200 break;
202 stats_daily_progress('2');
205 // Enrolments and active enrolled users
207 // Unfortunately, we do not know how many users were registered
208 // at given times in history :-(
209 // - stat1: enrolled users
210 // - stat2: enrolled users active in this period
211 // - enrolment is defined now as having course:view capability in
212 // course context or above, we look 3 cats upwards only and ignore prevent
213 // and prohibit caps to simplify it
214 // - SITEID is specialcased here, because it's all about default enrolment
215 // in that case, we'll count non-deleted users.
218 $sql = "INSERT INTO {$CFG->prefix}stats_daily (stattype, timeend, courseid, roleid, stat1, stat2)
220 SELECT 'enrolments', timeend, courseid, roleid, COUNT(DISTINCT userid), 0
221 FROM (
222 SELECT $nextmidnight AS timeend, pl.courseid, pl.roleid, pl.userid
223 FROM (
224 SELECT DISTINCT ra.roleid, ra.userid, c.id as courseid
225 FROM {$CFG->prefix}role_assignments ra $enroljoin_na
226 WHERE $enrolwhere_na
227 ) pl
228 ) inline_view
229 GROUP BY timeend, courseid, roleid";
231 if (!execute_sql($sql, false)) {
232 $failed = true;
233 break;
235 stats_daily_progress('3');
237 // using table alias in UPDATE does not work in pg < 8.2
238 $sql = "UPDATE {$CFG->prefix}stats_daily
239 SET stat2 = (SELECT COUNT(DISTINCT ra.userid)
240 FROM {$CFG->prefix}role_assignments ra $enroljoin_na
241 WHERE ra.roleid = {$CFG->prefix}stats_daily.roleid AND
242 c.id = {$CFG->prefix}stats_daily.courseid AND
243 $enrolwhere_na AND
244 EXISTS (SELECT 'x'
245 FROM {$CFG->prefix}log l
246 WHERE l.course = {$CFG->prefix}stats_daily.courseid AND
247 l.userid = ra.userid AND $timesql))
248 WHERE {$CFG->prefix}stats_daily.stattype = 'enrolments' AND
249 {$CFG->prefix}stats_daily.timeend = $nextmidnight AND
250 {$CFG->prefix}stats_daily.courseid IN
251 (SELECT DISTINCT l.course
252 FROM {$CFG->prefix}log l
253 WHERE $timesql)";
255 if ($logspresent and !execute_sql($sql, false)) {
256 $failed = true;
257 break;
259 stats_daily_progress('4');
261 /// now get course total enrolments (roleid==0) - except frontpage
262 $sql = "INSERT INTO {$CFG->prefix}stats_daily (stattype, timeend, courseid, roleid, stat1, stat2)
264 SELECT 'enrolments', timeend, id, nroleid, COUNT(DISTINCT userid), 0
265 FROM (
266 SELECT $nextmidnight AS timeend, c.id, 0 AS nroleid, ra.userid
267 FROM {$CFG->prefix}role_assignments ra $enroljoin_na
268 WHERE c.id <> ".SITEID." AND $enrolwhere_na
269 ) inline_view
270 GROUP BY timeend, id, nroleid
271 HAVING COUNT(DISTINCT userid) > 0";
273 if ($logspresent and !execute_sql($sql, false)) {
274 $failed = true;
275 break;
277 stats_daily_progress('5');
279 $sql = "UPDATE {$CFG->prefix}stats_daily
280 SET stat2 = (SELECT COUNT(DISTINCT ra.userid)
281 FROM {$CFG->prefix}role_assignments ra $enroljoin_na
282 WHERE c.id = {$CFG->prefix}stats_daily.courseid AND
283 $enrolwhere_na AND
284 EXISTS (SELECT 'x'
285 FROM {$CFG->prefix}log l
286 WHERE l.course = {$CFG->prefix}stats_daily.courseid AND
287 l.userid = ra.userid AND $timesql))
288 WHERE {$CFG->prefix}stats_daily.stattype = 'enrolments' AND
289 {$CFG->prefix}stats_daily.timeend = $nextmidnight AND
290 {$CFG->prefix}stats_daily.roleid = 0 AND
291 {$CFG->prefix}stats_daily.courseid IN
292 (SELECT l.course
293 FROM {$CFG->prefix}log l
294 WHERE $timesql AND l.course <> ".SITEID.")";
296 if ($logspresent and !execute_sql($sql, false)) {
297 $failed = true;
298 break;
300 stats_daily_progress('6');
302 /// frontapge(==site) enrolments total
303 $sql = "INSERT INTO {$CFG->prefix}stats_daily (stattype, timeend, courseid, roleid, stat1, stat2)
305 SELECT 'enrolments', $nextmidnight, ".SITEID.", 0,
306 (SELECT COUNT('x')
307 FROM {$CFG->prefix}user u
308 WHERE u.deleted = 0) AS stat1,
309 (SELECT COUNT(DISTINCT u.id)
310 FROM {$CFG->prefix}user u
311 JOIN {$CFG->prefix}log l ON l.userid = u.id
312 WHERE u.deleted = 0 AND $timesql) AS stat2" .
313 sql_null_from_clause();
315 if ($logspresent and !execute_sql($sql, false)) {
316 $failed = true;
317 break;
319 stats_daily_progress('7');
321 if (empty($CFG->defaultfrontpageroleid)) { // 1.9 only, so far
322 $defaultfproleid = 0;
323 } else {
324 $defaultfproleid = $CFG->defaultfrontpageroleid;
327 /// Default frontpage role enrolments are all site users (not deleted)
328 if ($defaultfproleid) {
329 // first remove default frontpage role counts if created by previous query
330 $sql = "DELETE
331 FROM {$CFG->prefix}stats_daily
332 WHERE stattype = 'enrolments' AND courseid = ".SITEID." AND
333 roleid = $defaultfproleid AND timeend = $nextmidnight";
334 if ($logspresent and !execute_sql($sql, false)) {
335 $failed = true;
336 break;
338 stats_daily_progress('8');
340 $sql = "INSERT INTO {$CFG->prefix}stats_daily (stattype, timeend, courseid, roleid, stat1, stat2)
342 SELECT 'enrolments', $nextmidnight, ".SITEID.", $defaultfproleid,
343 (SELECT COUNT('x')
344 FROM {$CFG->prefix}user u
345 WHERE u.deleted = 0) AS stat1,
346 (SELECT COUNT(DISTINCT u.id)
347 FROM {$CFG->prefix}user u
348 JOIN {$CFG->prefix}log l ON l.userid = u.id
349 WHERE u.deleted = 0 AND $timesql) AS stat2" .
350 sql_null_from_clause();
352 if ($logspresent and !execute_sql($sql, false)) {
353 $failed = true;
354 break;
356 stats_daily_progress('9');
358 } else {
359 stats_daily_progress('x');
360 stats_daily_progress('x');
365 /// individual user stats (including not-logged-in) in each course, this is slow - reuse this data if possible
366 $sql = "INSERT INTO {$CFG->prefix}stats_user_daily (stattype, timeend, courseid, userid, statsreads, statswrites)
368 SELECT 'activity' AS stattype, $nextmidnight AS timeend, d.courseid, d.userid,
369 (SELECT COUNT('x')
370 FROM {$CFG->prefix}log l
371 WHERE l.userid = d.userid AND
372 l.course = d.courseid AND $timesql AND
373 l.action IN ($viewactions)) AS statsreads,
374 (SELECT COUNT('x')
375 FROM {$CFG->prefix}log l
376 WHERE l.userid = d.userid AND
377 l.course = d.courseid AND $timesql AND
378 l.action IN ($postactions)) AS statswrites
379 FROM (SELECT DISTINCT u.id AS userid, l.course AS courseid
380 FROM {$CFG->prefix}user u, {$CFG->prefix}log l
381 WHERE u.id = l.userid AND $timesql
382 UNION
383 SELECT 0 AS userid, ".SITEID." AS courseid" . sql_null_from_clause() . ") d";
384 // can not use group by here because pg can not handle it :-(
386 if ($logspresent and !execute_sql($sql, false)) {
387 $failed = true;
388 break;
390 stats_daily_progress('10');
393 /// how many view/post actions in each course total
394 $sql = "INSERT INTO {$CFG->prefix}stats_daily (stattype, timeend, courseid, roleid, stat1, stat2)
396 SELECT 'activity' AS stattype, $nextmidnight AS timeend, c.id AS courseid, 0,
397 (SELECT COUNT('x')
398 FROM {$CFG->prefix}log l1
399 WHERE l1.course = c.id AND l1.action IN ($viewactions) AND
400 $timesql1) AS stat1,
401 (SELECT COUNT('x')
402 FROM {$CFG->prefix}log l2
403 WHERE l2.course = c.id AND l2.action IN ($postactions) AND
404 $timesql2) AS stat2
405 FROM {$CFG->prefix}course c
406 WHERE EXISTS (SELECT 'x'
407 FROM {$CFG->prefix}log l
408 WHERE l.course = c.id and $timesql)";
410 if ($logspresent and !execute_sql($sql, false)) {
411 $failed = true;
412 break;
414 stats_daily_progress('11');
417 /// how many view actions for each course+role - excluding guests and frontpage
419 $sql = "INSERT INTO {$CFG->prefix}stats_daily (stattype, timeend, courseid, roleid, stat1, stat2)
421 SELECT 'activity', timeend, courseid, roleid, SUM(statsreads), SUM(statswrites)
422 FROM (
423 SELECT $nextmidnight AS timeend, pl.courseid, pl.roleid, sud.statsreads, sud.statswrites
424 FROM {$CFG->prefix}stats_user_daily sud,
425 (SELECT DISTINCT ra.userid, ra.roleid, c.id AS courseid
426 FROM {$CFG->prefix}role_assignments ra $enroljoin
427 WHERE c.id <> ".SITEID." AND
428 ra.roleid <> $guestrole->id AND
429 ra.userid <> $guest->id AND
430 $enrolwhere
431 ) pl
432 WHERE sud.userid = pl.userid AND
433 sud.courseid = pl.courseid AND
434 sud.timeend = $nextmidnight AND
435 sud.stattype='activity'
436 ) inline_view
437 GROUP BY timeend, courseid, roleid
438 HAVING SUM(statsreads) > 0 OR SUM(statswrites) > 0";
440 if ($logspresent and !execute_sql($sql, false)) {
441 $failed = true;
442 break;
444 stats_daily_progress('12');
446 /// how many view actions from guests only in each course - excluding frontpage
447 /// (guest is anybody with guest role or no role with course:view in course - this may not work properly if category limit too low)
448 /// normal users may enter course with temporary guest acces too
450 $sql = "INSERT INTO {$CFG->prefix}stats_daily (stattype, timeend, courseid, roleid, stat1, stat2)
452 SELECT 'activity', timeend, courseid, nroleid, SUM(statsreads), SUM(statswrites)
453 FROM (
454 SELECT $nextmidnight AS timeend, sud.courseid, $guestrole->id AS nroleid, sud.statsreads, sud.statswrites
455 FROM {$CFG->prefix}stats_user_daily sud
456 WHERE sud.timeend = $nextmidnight AND sud.courseid <> ".SITEID." AND
457 sud.stattype='activity' AND
458 (sud.userid = $guest->id OR sud.userid
459 NOT IN (SELECT ra.userid
460 FROM {$CFG->prefix}role_assignments ra $enroljoin
461 WHERE c.id <> ".SITEID." AND ra.roleid <> $guestrole->id AND
462 $enrolwhere))
463 ) inline_view
464 GROUP BY timeend, courseid, nroleid
465 HAVING SUM(statsreads) > 0 OR SUM(statswrites) > 0";
467 if ($logspresent and !execute_sql($sql, false)) {
468 $failed = true;
469 break;
471 stats_daily_progress('13');
474 /// how many view actions for each role on frontpage - excluding guests, not-logged-in and default frontpage role
475 $sql = "INSERT INTO {$CFG->prefix}stats_daily (stattype, timeend, courseid, roleid, stat1, stat2)
477 SELECT 'activity', timeend, courseid, roleid, SUM(statsreads), SUM(statswrites)
478 FROM (
479 SELECT $nextmidnight AS timeend, pl.courseid, pl.roleid, sud.statsreads, sud.statswrites
480 FROM {$CFG->prefix}stats_user_daily sud,
481 (SELECT DISTINCT ra.userid, ra.roleid, c.id AS courseid
482 FROM {$CFG->prefix}role_assignments ra $enroljoin
483 WHERE c.id = ".SITEID." AND
484 ra.roleid <> $defaultfproleid AND
485 ra.roleid <> $guestrole->id AND
486 ra.userid <> $guest->id AND
487 $enrolwhere
488 ) pl
489 WHERE sud.userid = pl.userid AND
490 sud.courseid = pl.courseid AND
491 sud.timeend = $nextmidnight AND
492 sud.stattype='activity'
493 ) inline_view
494 GROUP BY timeend, courseid, roleid
495 HAVING SUM(statsreads) > 0 OR SUM(statswrites) > 0";
497 if ($logspresent and !execute_sql($sql, false)) {
498 $failed = true;
499 break;
501 stats_daily_progress('14');
504 /// how many view actions for default frontpage role on frontpage only
505 $sql = "INSERT INTO {$CFG->prefix}stats_daily (stattype, timeend, courseid, roleid, stat1, stat2)
507 SELECT 'activity', timeend, courseid, nroleid, SUM(statsreads), SUM(statswrites)
508 FROM (
509 SELECT $nextmidnight AS timeend, sud.courseid, $defaultfproleid AS nroleid, sud.statsreads, sud.statswrites
510 FROM {$CFG->prefix}stats_user_daily sud
511 WHERE sud.timeend = $nextmidnight AND sud.courseid = ".SITEID." AND
512 sud.stattype='activity' AND
513 sud.userid <> $guest->id AND sud.userid <> 0 AND sud.userid
514 NOT IN (SELECT ra.userid
515 FROM {$CFG->prefix}role_assignments ra $fpjoin
516 WHERE c.id = ".SITEID." AND ra.roleid <> $guestrole->id AND
517 ra.roleid <> $defaultfproleid AND $fpwhere)
518 ) inline_view
519 GROUP BY timeend, courseid, nroleid
520 HAVING SUM(statsreads) > 0 OR SUM(statswrites) > 0";
522 if ($logspresent and !execute_sql($sql, false)) {
523 $failed = true;
524 break;
526 stats_daily_progress('15');
528 /// how many view actions for guests or not-logged-in on frontpage
529 $sql = "INSERT INTO {$CFG->prefix}stats_daily (stattype, timeend, courseid, roleid, stat1, stat2)
531 SELECT 'activity', timeend, courseid, nroleid, SUM(statsreads), SUM(statswrites)
532 FROM (
533 SELECT $nextmidnight AS timeend, ".SITEID." AS courseid, $guestrole->id AS nroleid, pl.statsreads, pl.statswrites
534 FROM (
535 SELECT sud.statsreads, sud.statswrites
536 FROM {$CFG->prefix}stats_user_daily sud
537 WHERE (sud.userid = $guest->id OR sud.userid = 0) AND
538 sud.timeend = $nextmidnight AND sud.courseid = ".SITEID." AND
539 sud.stattype='activity'
540 ) pl
541 ) inline_view
542 GROUP BY timeend, courseid, nroleid
543 HAVING SUM(statsreads) > 0 OR SUM(statswrites) > 0";
545 if ($logspresent and !execute_sql($sql, false)) {
546 $failed = true;
547 break;
549 stats_daily_progress('16');
551 // remember processed days
552 set_config('statslastdaily', $nextmidnight);
553 mtrace(" finished until $nextmidnight: ".userdate($nextmidnight)." (in ".(time()-$daystart)." s)");
555 $timestart = $nextmidnight;
556 $nextmidnight = stats_get_next_day_start($nextmidnight);
559 set_cron_lock('statsrunning', null);
561 if ($failed) {
562 $days--;
563 mtrace("...error occured, completed $days days of statistics.");
564 return false;
566 } else {
567 mtrace("...completed $days days of statistics.");
568 return true;
574 * Execute weekly statistics gathering
575 * @return boolean success
577 function stats_cron_weekly() {
578 global $CFG;
580 $now = time();
582 // read last execution date from db
583 if (!$timestart = get_config(NULL, 'statslastweekly')) {
584 $timestart = stats_get_base_daily(stats_get_start_from('weekly'));
585 set_config('statslastweekly', $timestart);
588 $nextstartweek = stats_get_next_week_start($timestart);
590 // are there any weeks that need to be processed?
591 if ($now < $nextstartweek) {
592 return true; // everything ok and up-to-date
595 $timeout = empty($CFG->statsmaxruntime) ? 60*60*24 : $CFG->statsmaxruntime;
597 if (!set_cron_lock('statsrunning', $now + $timeout)) {
598 return false;
601 // fisrt delete entries that should not be there yet
602 delete_records_select('stats_weekly', "timeend > $timestart");
603 delete_records_select('stats_user_weekly', "timeend > $timestart");
605 mtrace("Running weekly statistics gathering, starting at $timestart:");
607 $weeks = 0;
608 while ($now > $nextstartweek) {
609 @set_time_limit($timeout - 200);
610 $weeks++;
612 if ($weeks > 1) {
613 // move the lock
614 set_cron_lock('statsrunning', time() + $timeout, true);
617 $logtimesql = "l.time >= $timestart AND l.time < $nextstartweek";
618 $stattimesql = "timeend > $timestart AND timeend <= $nextstartweek";
620 /// process login info first
621 $sql = "INSERT INTO {$CFG->prefix}stats_user_weekly (stattype, timeend, courseid, userid, statsreads)
623 SELECT 'logins', timeend, courseid, userid, COUNT(statsreads)
624 FROM (
625 SELECT $nextstartweek AS timeend, ".SITEID." as courseid, l.userid, l.id AS statsreads
626 FROM {$CFG->prefix}log l
627 WHERE action = 'login' AND $logtimesql
628 ) inline_view
629 GROUP BY timeend, courseid, userid
630 HAVING COUNT(statsreads) > 0";
632 execute_sql($sql, false);
635 $sql = "INSERT INTO {$CFG->prefix}stats_weekly (stattype, timeend, courseid, roleid, stat1, stat2)
637 SELECT 'logins' AS stattype, $nextstartweek AS timeend, ".SITEID." as courseid, 0,
638 COALESCE((SELECT SUM(statsreads)
639 FROM {$CFG->prefix}stats_user_weekly s1
640 WHERE s1.stattype = 'logins' AND timeend = $nextstartweek), 0) AS nstat1,
641 (SELECT COUNT('x')
642 FROM {$CFG->prefix}stats_user_weekly s2
643 WHERE s2.stattype = 'logins' AND timeend = $nextstartweek) AS nstat2" .
644 sql_null_from_clause();
646 execute_sql($sql, false);
649 /// now enrolments averages
650 $sql = "INSERT INTO {$CFG->prefix}stats_weekly (stattype, timeend, courseid, roleid, stat1, stat2)
652 SELECT 'enrolments', ntimeend, courseid, roleid, " . sql_ceil('AVG(stat1)') . ", " . sql_ceil('AVG(stat2)') . "
653 FROM (
654 SELECT $nextstartweek AS ntimeend, courseid, roleid, stat1, stat2
655 FROM {$CFG->prefix}stats_daily sd
656 WHERE stattype = 'enrolments' AND $stattimesql
657 ) inline_view
658 GROUP BY ntimeend, courseid, roleid";
660 execute_sql($sql, false);
663 /// activity read/write averages
664 $sql = "INSERT INTO {$CFG->prefix}stats_weekly (stattype, timeend, courseid, roleid, stat1, stat2)
666 SELECT 'activity', ntimeend, courseid, roleid, SUM(stat1), SUM(stat2)
667 FROM (
668 SELECT $nextstartweek AS ntimeend, courseid, roleid, stat1, stat2
669 FROM {$CFG->prefix}stats_daily
670 WHERE stattype = 'activity' AND $stattimesql
671 ) inline_view
672 GROUP BY ntimeend, courseid, roleid";
674 execute_sql($sql, false);
677 /// user read/write averages
678 $sql = "INSERT INTO {$CFG->prefix}stats_user_weekly (stattype, timeend, courseid, userid, statsreads, statswrites)
680 SELECT 'activity', ntimeend, courseid, userid, SUM(statsreads), SUM(statswrites)
681 FROM (
682 SELECT $nextstartweek AS ntimeend, courseid, userid, statsreads, statswrites
683 FROM {$CFG->prefix}stats_user_daily
684 WHERE stattype = 'activity' AND $stattimesql
685 ) inline_view
686 GROUP BY ntimeend, courseid, userid";
688 execute_sql($sql, false);
690 set_config('statslastweekly', $nextstartweek);
691 mtrace(" finished until $nextstartweek: ".userdate($nextstartweek));
693 $timestart = $nextstartweek;
694 $nextstartweek = stats_get_next_week_start($nextstartweek);
697 set_cron_lock('statsrunning', null);
698 mtrace("...completed $weeks weeks of statistics.");
699 return true;
703 * Execute monthly statistics gathering
704 * @return boolean success
706 function stats_cron_monthly() {
707 global $CFG;
709 $now = time();
711 // read last execution date from db
712 if (!$timestart = get_config(NULL, 'statslastmonthly')) {
713 $timestart = stats_get_base_monthly(stats_get_start_from('monthly'));
714 set_config('statslastmonthly', $timestart);
717 $nextstartmonth = stats_get_next_month_start($timestart);
719 // are there any months that need to be processed?
720 if ($now < $nextstartmonth) {
721 return true; // everything ok and up-to-date
724 $timeout = empty($CFG->statsmaxruntime) ? 60*60*24 : $CFG->statsmaxruntime;
726 if (!set_cron_lock('statsrunning', $now + $timeout)) {
727 return false;
730 // fisr delete entries that should not be there yet
731 delete_records_select('stats_monthly', "timeend > $timestart");
732 delete_records_select('stats_user_monthly', "timeend > $timestart");
734 $startmonth = stats_get_base_monthly($now);
737 mtrace("Running monthly statistics gathering, starting at $timestart:");
739 $months = 0;
740 while ($now > $nextstartmonth) {
741 @set_time_limit($timeout - 200);
742 $months++;
744 if ($months > 1) {
745 // move the lock
746 set_cron_lock('statsrunning', time() + $timeout, true);
749 $logtimesql = "l.time >= $timestart AND l.time < $nextstartmonth";
750 $stattimesql = "timeend > $timestart AND timeend <= $nextstartmonth";
752 /// process login info first
753 $sql = "INSERT INTO {$CFG->prefix}stats_user_monthly (stattype, timeend, courseid, userid, statsreads)
755 SELECT 'logins', timeend, courseid, userid, COUNT(statsreads)
756 FROM (
757 SELECT $nextstartmonth AS timeend, ".SITEID." as courseid, l.userid, l.id AS statsreads
758 FROM {$CFG->prefix}log l
759 WHERE action = 'login' AND $logtimesql
760 ) inline_view
761 GROUP BY timeend, courseid, userid";
763 execute_sql($sql, false);
766 $sql = "INSERT INTO {$CFG->prefix}stats_monthly (stattype, timeend, courseid, roleid, stat1, stat2)
768 SELECT 'logins' AS stattype, $nextstartmonth AS timeend, ".SITEID." as courseid, 0,
769 COALESCE((SELECT SUM(statsreads)
770 FROM {$CFG->prefix}stats_user_monthly s1
771 WHERE s1.stattype = 'logins' AND timeend = $nextstartmonth), 0) AS nstat1,
772 (SELECT COUNT('x')
773 FROM {$CFG->prefix}stats_user_monthly s2
774 WHERE s2.stattype = 'logins' AND timeend = $nextstartmonth) AS nstat2" .
775 sql_null_from_clause();
777 execute_sql($sql, false);
780 /// now enrolments averages
781 $sql = "INSERT INTO {$CFG->prefix}stats_monthly (stattype, timeend, courseid, roleid, stat1, stat2)
783 SELECT 'enrolments', ntimeend, courseid, roleid, " . sql_ceil('AVG(stat1)') . ", " . sql_ceil('AVG(stat2)') . "
784 FROM (
785 SELECT $nextstartmonth AS ntimeend, courseid, roleid, stat1, stat2
786 FROM {$CFG->prefix}stats_daily sd
787 WHERE stattype = 'enrolments' AND $stattimesql
788 ) inline_view
789 GROUP BY ntimeend, courseid, roleid";
791 execute_sql($sql, false);
794 /// activity read/write averages
795 $sql = "INSERT INTO {$CFG->prefix}stats_monthly (stattype, timeend, courseid, roleid, stat1, stat2)
797 SELECT 'activity', ntimeend, courseid, roleid, SUM(stat1), SUM(stat2)
798 FROM (
799 SELECT $nextstartmonth AS ntimeend, courseid, roleid, stat1, stat2
800 FROM {$CFG->prefix}stats_daily
801 WHERE stattype = 'activity' AND $stattimesql
802 ) inline_view
803 GROUP BY ntimeend, courseid, roleid";
805 execute_sql($sql, false);
808 /// user read/write averages
809 $sql = "INSERT INTO {$CFG->prefix}stats_user_monthly (stattype, timeend, courseid, userid, statsreads, statswrites)
811 SELECT 'activity', ntimeend, courseid, userid, SUM(statsreads), SUM(statswrites)
812 FROM (
813 SELECT $nextstartmonth AS ntimeend, courseid, userid, statsreads, statswrites
814 FROM {$CFG->prefix}stats_user_daily
815 WHERE stattype = 'activity' AND $stattimesql
816 ) inline_view
817 GROUP BY ntimeend, courseid, userid";
819 execute_sql($sql, false);
821 set_config('statslastmonthly', $nextstartmonth);
822 mtrace(" finished until $nextstartmonth: ".userdate($nextstartmonth));
824 $timestart = $nextstartmonth;
825 $nextstartmonth = stats_get_next_month_start($nextstartmonth);
828 set_cron_lock('statsrunning', null);
829 mtrace("...completed $months months of statistics.");
830 return true;
834 * Returns simplified enrolment sql join data
835 * @param int $limit number of max parent course categories
836 * @param bool $includedoanything include also admins
837 * @return array ra join and where string
839 function stats_get_enrolled_sql($limit, $includedoanything) {
840 global $CFG;
842 $adm = $includedoanything ? " OR rc.capability = 'moodle/site:doanything'" : "";
844 $join = "JOIN {$CFG->prefix}context ctx
845 ON ctx.id = ra.contextid
846 CROSS JOIN {$CFG->prefix}course c
847 JOIN {$CFG->prefix}role_capabilities rc
848 ON rc.roleid = ra.roleid";
849 $where = "((rc.capability = 'moodle/course:view' $adm)
850 AND rc.permission = 1 AND rc.contextid = ".SYSCONTEXTID."
851 AND (ctx.contextlevel = ".CONTEXT_SYSTEM."
852 OR (c.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_COURSE.")";
854 for($i=1; $i<=$limit; $i++) {
855 if ($i == 1) {
856 $join .= " LEFT OUTER JOIN {$CFG->prefix}course_categories cc1
857 ON cc1.id = c.category";
858 $where .= " OR (cc1.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_COURSECAT.")";
859 } else {
860 $j = $i-1;
861 $join .= " LEFT OUTER JOIN {$CFG->prefix}course_categories cc$i
862 ON cc$i.id = cc$j.parent";
863 $where .= " OR (cc$i.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_COURSECAT.")";
867 $where .= "))";
869 return array($join, $where);
873 * Return starting date of stats processing
874 * @param string $str name of table - daily, weekly or monthly
875 * @return int timestamp
877 function stats_get_start_from($str) {
878 global $CFG;
880 // are there any data in stats table? Should not be...
881 if ($timeend = get_field_sql('SELECT timeend FROM '.$CFG->prefix.'stats_'.$str.' ORDER BY timeend DESC')) {
882 return $timeend;
884 // decide what to do based on our config setting (either all or none or a timestamp)
885 switch ($CFG->statsfirstrun) {
886 case 'all':
887 if ($firstlog = get_field_sql('SELECT time FROM '.$CFG->prefix.'log ORDER BY time ASC')) {
888 return $firstlog;
890 default:
891 if (is_numeric($CFG->statsfirstrun)) {
892 return time() - $CFG->statsfirstrun;
894 // not a number? use next instead
895 case 'none':
896 return strtotime('-3 day', time());
901 * Start of day
902 * @param int $time timestamp
903 * @return start of day
905 function stats_get_base_daily($time=0) {
906 global $CFG;
908 if (empty($time)) {
909 $time = time();
911 if ($CFG->timezone == 99) {
912 $time = strtotime(date('d-M-Y', $time));
913 return $time;
914 } else {
915 $offset = get_timezone_offset($CFG->timezone);
916 $gtime = $time + $offset;
917 $gtime = intval($gtime / (60*60*24)) * 60*60*24;
918 return $gtime - $offset;
923 * Start of week
924 * @param int $time timestamp
925 * @return start of week
927 function stats_get_base_weekly($time=0) {
928 global $CFG;
930 $time = stats_get_base_daily($time);
931 $startday = $CFG->calendar_startwday;
932 if ($CFG->timezone == 99) {
933 $thisday = date('w', $time);
934 } else {
935 $offset = get_timezone_offset($CFG->timezone);
936 $gtime = $time + $offset;
937 $thisday = gmdate('w', $gtime);
939 if ($thisday > $startday) {
940 $time = $time - (($thisday - $startday) * 60*60*24);
941 } else if ($thisday < $startday) {
942 $time = $time - ((7 + $thisday - $startday) * 60*60*24);
944 return $time;
948 * Start of month
949 * @param int $time timestamp
950 * @return start of month
952 function stats_get_base_monthly($time=0) {
953 global $CFG;
955 if (empty($time)) {
956 $time = time();
958 if ($CFG->timezone == 99) {
959 return strtotime(date('1-M-Y', $time));
961 } else {
962 $time = stats_get_base_daily($time);
963 $offset = get_timezone_offset($CFG->timezone);
964 $gtime = $time + $offset;
965 $day = gmdate('d', $gtime);
966 if ($day == 1) {
967 return $time;
969 return $gtime - (($day-1) * 60*60*24);
974 * Start of next day
975 * @param int $time timestamp
976 * @return start of next day
978 function stats_get_next_day_start($time) {
979 $next = stats_get_base_daily($time);
980 $next = $next + 60*60*26;
981 $next = stats_get_base_daily($next);
982 if ($next <= $time) {
983 //DST trouble - prevent infinite loops
984 $next = $next + 60*60*24;
986 return $next;
990 * Start of next week
991 * @param int $time timestamp
992 * @return start of next week
994 function stats_get_next_week_start($time) {
995 $next = stats_get_base_weekly($time);
996 $next = $next + 60*60*24*9;
997 $next = stats_get_base_weekly($next);
998 if ($next <= $time) {
999 //DST trouble - prevent infinite loops
1000 $next = $next + 60*60*24*7;
1002 return $next;
1006 * Start of next month
1007 * @param int $time timestamp
1008 * @return start of next month
1010 function stats_get_next_month_start($time) {
1011 $next = stats_get_base_monthly($time);
1012 $next = $next + 60*60*24*33;
1013 $next = stats_get_base_monthly($next);
1014 if ($next <= $time) {
1015 //DST trouble - prevent infinite loops
1016 $next = $next + 60*60*24*31;
1018 return $next;
1022 * Remove old stats data
1024 function stats_clean_old() {
1025 mtrace("Running stats cleanup tasks...");
1026 $deletebefore = stats_get_base_monthly();
1028 // delete dailies older than 3 months (to be safe)
1029 $deletebefore = strtotime('-3 months', $deletebefore);
1030 delete_records_select('stats_daily', "timeend < $deletebefore");
1031 delete_records_select('stats_user_daily', "timeend < $deletebefore");
1033 // delete weeklies older than 9 months (to be safe)
1034 $deletebefore = strtotime('-6 months', $deletebefore);
1035 delete_records_select('stats_weekly', "timeend < $deletebefore");
1036 delete_records_select('stats_user_weekly', "timeend < $deletebefore");
1038 // don't delete monthlies
1040 mtrace("...stats cleanup finished");
1043 function stats_get_parameters($time,$report,$courseid,$mode,$roleid=0) {
1044 global $CFG,$db;
1046 $param = new object();
1048 if ($time < 10) { // dailies
1049 // number of days to go back = 7* time
1050 $param->table = 'daily';
1051 $param->timeafter = strtotime("-".($time*7)." days",stats_get_base_daily());
1052 } elseif ($time < 20) { // weeklies
1053 // number of weeks to go back = time - 10 * 4 (weeks) + base week
1054 $param->table = 'weekly';
1055 $param->timeafter = strtotime("-".(($time - 10)*4)." weeks",stats_get_base_weekly());
1056 } else { // monthlies.
1057 // number of months to go back = time - 20 * months + base month
1058 $param->table = 'monthly';
1059 $param->timeafter = strtotime("-".($time - 20)." months",stats_get_base_monthly());
1062 $param->extras = '';
1064 // compatibility - if we're in postgres, cast to real for some reports.
1065 $real = '';
1066 if ($CFG->dbfamily == 'postgres') {
1067 $real = '::real';
1070 switch ($report) {
1071 // ******************** STATS_MODE_GENERAL ******************** //
1072 case STATS_REPORT_LOGINS:
1073 $param->fields = 'timeend,sum(stat1) as line1,sum(stat2) as line2';
1074 $param->fieldscomplete = true;
1075 $param->stattype = 'logins';
1076 $param->line1 = get_string('statslogins');
1077 $param->line2 = get_string('statsuniquelogins');
1078 if ($courseid == SITEID) {
1079 $param->extras = 'GROUP BY timeend';
1081 break;
1083 case STATS_REPORT_READS:
1084 $param->fields = sql_concat('timeend','roleid').' AS uniqueid, timeend, roleid, stat1 as line1';
1085 $param->fieldscomplete = true; // set this to true to avoid anything adding stuff to the list and breaking complex queries.
1086 $param->aggregategroupby = 'roleid';
1087 $param->stattype = 'activity';
1088 $param->crosstab = true;
1089 $param->extras = 'GROUP BY timeend,roleid,stat1';
1090 if ($courseid == SITEID) {
1091 $param->fields = sql_concat('timeend','roleid').' AS uniqueid, timeend, roleid, sum(stat1) as line1';
1092 $param->extras = 'GROUP BY timeend,roleid';
1094 break;
1096 case STATS_REPORT_WRITES:
1097 $param->fields = sql_concat('timeend','roleid').' AS uniqueid, timeend, roleid, stat2 as line1';
1098 $param->fieldscomplete = true; // set this to true to avoid anything adding stuff to the list and breaking complex queries.
1099 $param->aggregategroupby = 'roleid';
1100 $param->stattype = 'activity';
1101 $param->crosstab = true;
1102 $param->extras = 'GROUP BY timeend,roleid,stat2';
1103 if ($courseid == SITEID) {
1104 $param->fields = sql_concat('timeend','roleid').' AS uniqueid, timeend, roleid, sum(stat2) as line1';
1105 $param->extras = 'GROUP BY timeend,roleid';
1107 break;
1109 case STATS_REPORT_ACTIVITY:
1110 $param->fields = sql_concat('timeend','roleid').' AS uniqueid, timeend, roleid, sum(stat1+stat2) as line1';
1111 $param->fieldscomplete = true; // set this to true to avoid anything adding stuff to the list and breaking complex queries.
1112 $param->aggregategroupby = 'roleid';
1113 $param->stattype = 'activity';
1114 $param->crosstab = true;
1115 $param->extras = 'GROUP BY timeend,roleid';
1116 if ($courseid == SITEID) {
1117 $param->extras = 'GROUP BY timeend,roleid';
1119 break;
1121 case STATS_REPORT_ACTIVITYBYROLE;
1122 $param->fields = 'stat1 AS line1, stat2 AS line2';
1123 $param->stattype = 'activity';
1124 $rolename = get_field('role','name','id',$roleid);
1125 $param->line1 = $rolename . get_string('statsreads');
1126 $param->line2 = $rolename . get_string('statswrites');
1127 if ($courseid == SITEID) {
1128 $param->extras = 'GROUP BY timeend';
1130 break;
1132 // ******************** STATS_MODE_DETAILED ******************** //
1133 case STATS_REPORT_USER_ACTIVITY:
1134 $param->fields = 'statsreads as line1, statswrites as line2';
1135 $param->line1 = get_string('statsuserreads');
1136 $param->line2 = get_string('statsuserwrites');
1137 $param->stattype = 'activity';
1138 break;
1140 case STATS_REPORT_USER_ALLACTIVITY:
1141 $param->fields = 'statsreads+statswrites as line1';
1142 $param->line1 = get_string('statsuseractivity');
1143 $param->stattype = 'activity';
1144 break;
1146 case STATS_REPORT_USER_LOGINS:
1147 $param->fields = 'statsreads as line1';
1148 $param->line1 = get_string('statsuserlogins');
1149 $param->stattype = 'logins';
1150 break;
1152 case STATS_REPORT_USER_VIEW:
1153 $param->fields = 'statsreads as line1, statswrites as line2, statsreads+statswrites as line3';
1154 $param->line1 = get_string('statsuserreads');
1155 $param->line2 = get_string('statsuserwrites');
1156 $param->line3 = get_string('statsuseractivity');
1157 $param->stattype = 'activity';
1158 break;
1160 // ******************** STATS_MODE_RANKED ******************** //
1161 case STATS_REPORT_ACTIVE_COURSES:
1162 $param->fields = 'sum(stat1+stat2) AS line1';
1163 $param->stattype = 'activity';
1164 $param->orderby = 'line1 DESC';
1165 $param->line1 = get_string('activity');
1166 $param->graphline = 'line1';
1167 break;
1169 case STATS_REPORT_ACTIVE_COURSES_WEIGHTED:
1170 $threshold = 0;
1171 if (!empty($CFG->statsuserthreshold) && is_numeric($CFG->statsuserthreshold)) {
1172 $threshold = $CFG->statsuserthreshold;
1174 $param->fields = '';
1175 $param->sql = 'SELECT activity.courseid, activity.all_activity AS line1, enrolments.highest_enrolments AS line2,
1176 activity.all_activity / enrolments.highest_enrolments as line3
1177 FROM (
1178 SELECT courseid, (stat1+stat2) AS all_activity
1179 FROM '.$CFG->prefix.'stats_'.$param->table.'
1180 WHERE stattype=\'activity\' AND timeend >= '.$param->timeafter.' AND roleid = 0
1181 ) activity
1182 INNER JOIN
1184 SELECT courseid, max(stat1) AS highest_enrolments
1185 FROM '.$CFG->prefix.'stats_'.$param->table.'
1186 WHERE stattype=\'enrolments\' AND timeend >= '.$param->timeafter.' AND stat1 > '.$threshold.'
1187 GROUP BY courseid
1188 ) enrolments
1189 ON (activity.courseid = enrolments.courseid)
1190 ORDER BY line3 DESC';
1191 $param->line1 = get_string('activity');
1192 $param->line2 = get_string('users');
1193 $param->line3 = get_string('activityweighted');
1194 $param->graphline = 'line3';
1195 break;
1197 case STATS_REPORT_PARTICIPATORY_COURSES:
1198 $threshold = 0;
1199 if (!empty($CFG->statsuserthreshold) && is_numeric($CFG->statsuserthreshold)) {
1200 $threshold = $CFG->statsuserthreshold;
1202 $param->fields = '';
1203 $param->sql = 'SELECT courseid, ' . sql_ceil('avg(all_enrolments)') . ' as line1, ' .
1204 sql_ceil('avg(active_enrolments)') . ' as line2, avg(proportion_active) AS line3
1205 FROM (
1206 SELECT courseid, timeend, stat2 as active_enrolments,
1207 stat1 as all_enrolments, stat2'.$real.'/stat1'.$real.' as proportion_active
1208 FROM '.$CFG->prefix.'stats_'.$param->table.'
1209 WHERE stattype=\'enrolments\' AND roleid = 0 AND stat1 > '.$threshold.'
1210 ) aq
1211 WHERE timeend >= '.$param->timeafter.'
1212 GROUP BY courseid
1213 ORDER BY line3 DESC';
1215 $param->line1 = get_string('users');
1216 $param->line2 = get_string('activeusers');
1217 $param->line3 = get_string('participationratio');
1218 $param->graphline = 'line3';
1219 break;
1221 case STATS_REPORT_PARTICIPATORY_COURSES_RW:
1222 $param->fields = '';
1223 $param->sql = 'SELECT courseid, sum(views) AS line1, sum(posts) AS line2,
1224 avg(proportion_active) AS line3
1225 FROM (
1226 SELECT courseid, timeend, stat1 as views, stat2 AS posts,
1227 stat2'.$real.'/stat1'.$real.' as proportion_active
1228 FROM '.$CFG->prefix.'stats_'.$param->table.'
1229 WHERE stattype=\'activity\' AND roleid = 0 AND stat1 > 0
1230 ) aq
1231 WHERE timeend >= '.$param->timeafter.'
1232 GROUP BY courseid
1233 ORDER BY line3 DESC';
1234 $param->line1 = get_string('views');
1235 $param->line2 = get_string('posts');
1236 $param->line3 = get_string('participationratio');
1237 $param->graphline = 'line3';
1238 break;
1242 if ($courseid == SITEID && $mode != STATS_MODE_RANKED) { // just aggregate all courses.
1243 $param->fields = preg_replace('/(?:sum)([a-zA-Z0-9+_]*)\W+as\W+([a-zA-Z0-9_]*)/i','sum($1) as $2',$param->fields);
1244 $param->extras = ' GROUP BY timeend'.((!empty($param->aggregategroupby)) ? ','.$param->aggregategroupby : '');
1247 //TODO must add the SITEID reports to the rest of the reports.
1248 return $param;
1251 function stats_get_view_actions() {
1252 return array('view','view all','history');
1255 function stats_get_post_actions() {
1256 return array('add','delete','edit','add mod','delete mod','edit section'.'enrol','loginas','new','unenrol','update','update mod');
1259 function stats_get_action_names($str) {
1260 global $CFG;
1262 $mods = get_records('modules');
1263 $function = 'stats_get_'.$str.'_actions';
1264 $actions = $function();
1265 foreach ($mods as $mod) {
1266 $file = $CFG->dirroot.'/mod/'.$mod->name.'/lib.php';
1267 if (!is_readable($file)) {
1268 continue;
1270 require_once($file);
1271 $function = $mod->name.'_get_'.$str.'_actions';
1272 if (function_exists($function)) {
1273 $actions = array_merge($actions,$function());
1277 // The array_values() forces a stack-like array
1278 // so we can later loop over safely...
1279 $actions = array_values(array_unique($actions));
1280 $c = count($actions);
1281 for ($n=0;$n<$c;$n++) {
1282 $actions[$n] = "'" . $actions[$n] . "'"; // quote them for SQL
1284 return $actions;
1287 function stats_get_time_options($now,$lastweekend,$lastmonthend,$earliestday,$earliestweek,$earliestmonth) {
1289 $now = stats_get_base_daily(time());
1290 // it's really important that it's TIMEEND in the table. ie, tuesday 00:00:00 is monday night.
1291 // so we need to take a day off here (essentially add a day to $now
1292 $now += 60*60*24;
1294 $timeoptions = array();
1296 if ($now - (60*60*24*7) >= $earliestday) {
1297 $timeoptions[STATS_TIME_LASTWEEK] = get_string('numweeks','moodle',1);
1299 if ($now - (60*60*24*14) >= $earliestday) {
1300 $timeoptions[STATS_TIME_LAST2WEEKS] = get_string('numweeks','moodle',2);
1302 if ($now - (60*60*24*21) >= $earliestday) {
1303 $timeoptions[STATS_TIME_LAST3WEEKS] = get_string('numweeks','moodle',3);
1305 if ($now - (60*60*24*28) >= $earliestday) {
1306 $timeoptions[STATS_TIME_LAST4WEEKS] = get_string('numweeks','moodle',4);// show dailies up to (including) here.
1308 if ($lastweekend - (60*60*24*56) >= $earliestweek) {
1309 $timeoptions[STATS_TIME_LAST2MONTHS] = get_string('nummonths','moodle',2);
1311 if ($lastweekend - (60*60*24*84) >= $earliestweek) {
1312 $timeoptions[STATS_TIME_LAST3MONTHS] = get_string('nummonths','moodle',3);
1314 if ($lastweekend - (60*60*24*112) >= $earliestweek) {
1315 $timeoptions[STATS_TIME_LAST4MONTHS] = get_string('nummonths','moodle',4);
1317 if ($lastweekend - (60*60*24*140) >= $earliestweek) {
1318 $timeoptions[STATS_TIME_LAST5MONTHS] = get_string('nummonths','moodle',5);
1320 if ($lastweekend - (60*60*24*168) >= $earliestweek) {
1321 $timeoptions[STATS_TIME_LAST6MONTHS] = get_string('nummonths','moodle',6); // show weeklies up to (including) here
1323 if (strtotime('-7 months',$lastmonthend) >= $earliestmonth) {
1324 $timeoptions[STATS_TIME_LAST7MONTHS] = get_string('nummonths','moodle',7);
1326 if (strtotime('-8 months',$lastmonthend) >= $earliestmonth) {
1327 $timeoptions[STATS_TIME_LAST8MONTHS] = get_string('nummonths','moodle',8);
1329 if (strtotime('-9 months',$lastmonthend) >= $earliestmonth) {
1330 $timeoptions[STATS_TIME_LAST9MONTHS] = get_string('nummonths','moodle',9);
1332 if (strtotime('-10 months',$lastmonthend) >= $earliestmonth) {
1333 $timeoptions[STATS_TIME_LAST10MONTHS] = get_string('nummonths','moodle',10);
1335 if (strtotime('-11 months',$lastmonthend) >= $earliestmonth) {
1336 $timeoptions[STATS_TIME_LAST11MONTHS] = get_string('nummonths','moodle',11);
1338 if (strtotime('-1 year',$lastmonthend) >= $earliestmonth) {
1339 $timeoptions[STATS_TIME_LASTYEAR] = get_string('lastyear');
1342 $years = (int)date('y', $now) - (int)date('y', $earliestmonth);
1343 if ($years > 1) {
1344 for($i = 2; $i <= $years; $i++) {
1345 $timeoptions[$i*12+20] = get_string('numyears', 'moodle', $i);
1349 return $timeoptions;
1352 function stats_get_report_options($courseid,$mode) {
1353 global $CFG;
1355 $reportoptions = array();
1357 switch ($mode) {
1358 case STATS_MODE_GENERAL:
1359 $reportoptions[STATS_REPORT_ACTIVITY] = get_string('statsreport'.STATS_REPORT_ACTIVITY);
1360 if ($courseid != SITEID && $context = get_context_instance(CONTEXT_COURSE, $courseid)) {
1361 $sql = 'SELECT r.id,r.name FROM '.$CFG->prefix.'role r JOIN '.$CFG->prefix.'stats_daily s ON s.roleid = r.id WHERE s.courseid = '.$courseid;
1362 if ($roles = get_records_sql($sql)) {
1363 foreach ($roles as $role) {
1364 $reportoptions[STATS_REPORT_ACTIVITYBYROLE.$role->id] = get_string('statsreport'.STATS_REPORT_ACTIVITYBYROLE). ' '.$role->name;
1368 $reportoptions[STATS_REPORT_READS] = get_string('statsreport'.STATS_REPORT_READS);
1369 $reportoptions[STATS_REPORT_WRITES] = get_string('statsreport'.STATS_REPORT_WRITES);
1370 if ($courseid == SITEID) {
1371 $reportoptions[STATS_REPORT_LOGINS] = get_string('statsreport'.STATS_REPORT_LOGINS);
1374 break;
1375 case STATS_MODE_DETAILED:
1376 $reportoptions[STATS_REPORT_USER_ACTIVITY] = get_string('statsreport'.STATS_REPORT_USER_ACTIVITY);
1377 $reportoptions[STATS_REPORT_USER_ALLACTIVITY] = get_string('statsreport'.STATS_REPORT_USER_ALLACTIVITY);
1378 if (has_capability('coursereport/stats:view', get_context_instance(CONTEXT_SYSTEM))) {
1379 $site = get_site();
1380 $reportoptions[STATS_REPORT_USER_LOGINS] = get_string('statsreport'.STATS_REPORT_USER_LOGINS);
1382 break;
1383 case STATS_MODE_RANKED:
1384 if (has_capability('coursereport/stats:view', get_context_instance(CONTEXT_SYSTEM))) {
1385 $reportoptions[STATS_REPORT_ACTIVE_COURSES] = get_string('statsreport'.STATS_REPORT_ACTIVE_COURSES);
1386 $reportoptions[STATS_REPORT_ACTIVE_COURSES_WEIGHTED] = get_string('statsreport'.STATS_REPORT_ACTIVE_COURSES_WEIGHTED);
1387 $reportoptions[STATS_REPORT_PARTICIPATORY_COURSES] = get_string('statsreport'.STATS_REPORT_PARTICIPATORY_COURSES);
1388 $reportoptions[STATS_REPORT_PARTICIPATORY_COURSES_RW] = get_string('statsreport'.STATS_REPORT_PARTICIPATORY_COURSES_RW);
1390 break;
1393 return $reportoptions;
1396 function stats_fix_zeros($stats,$timeafter,$timestr,$line2=true,$line3=false) {
1398 if (empty($stats)) {
1399 return;
1402 $timestr = str_replace('user_','',$timestr); // just in case.
1403 $fun = 'stats_get_base_'.$timestr;
1405 $now = $fun();
1407 $times = array();
1408 // add something to timeafter since it is our absolute base
1409 $actualtimes = array();
1410 foreach ($stats as $statid=>$s) {
1411 //normalize the times in stats - those might have been created in different timezone, DST etc.
1412 $s->timeend = $fun($s->timeend + 60*60*5);
1413 $stats[$statid] = $s;
1415 $actualtimes[] = $s->timeend;
1418 $timeafter = array_pop(array_values($actualtimes));
1420 while ($timeafter < $now) {
1421 $times[] = $timeafter;
1422 if ($timestr == 'daily') {
1423 $timeafter = stats_get_next_day_start($timeafter);
1424 } else if ($timestr == 'weekly') {
1425 $timeafter = stats_get_next_week_start($timeafter);
1426 } else if ($timestr == 'monthly') {
1427 $timeafter = stats_get_next_month_start($timeafter);
1428 } else {
1429 return $stats; // this will put us in a never ending loop.
1433 foreach ($times as $count => $time) {
1434 if (!in_array($time,$actualtimes) && $count != count($times) -1) {
1435 $newobj = new StdClass;
1436 $newobj->timeend = $time;
1437 $newobj->id = 0;
1438 $newobj->roleid = 0;
1439 $newobj->line1 = 0;
1440 if (!empty($line2)) {
1441 $newobj->line2 = 0;
1443 if (!empty($line3)) {
1444 $newobj->line3 = 0;
1446 $newobj->zerofixed = true;
1447 $stats[] = $newobj;
1451 usort($stats,"stats_compare_times");
1452 return $stats;
1456 // helper function to sort arrays by $obj->timeend
1457 function stats_compare_times($a,$b) {
1458 if ($a->timeend == $b->timeend) {
1459 return 0;
1461 return ($a->timeend > $b->timeend) ? -1 : 1;
1464 function stats_check_uptodate($courseid=0) {
1465 global $CFG;
1467 if (empty($courseid)) {
1468 $courseid = SITEID;
1471 $latestday = stats_get_start_from('daily');
1473 if ((time() - 60*60*24*2) < $latestday) { // we're ok
1474 return NULL;
1477 $a = new object();
1478 $a->daysdone = get_field_sql("SELECT count(distinct(timeend)) from {$CFG->prefix}stats_daily");
1480 // how many days between the last day and now?
1481 $a->dayspending = ceil((stats_get_base_daily() - $latestday)/(60*60*24));
1483 if ($a->dayspending == 0 && $a->daysdone != 0) {
1484 return NULL; // we've only just started...
1487 //return error as string
1488 return get_string('statscatchupmode','error',$a);
1492 * Calculate missing course totals in stats
1494 function stats_upgrade_totals() {
1495 global $CFG;
1497 if (empty($CFG->statsrolesupgraded)) {
1498 // stats not yet upgraded to cope with roles...
1499 return;
1502 $types = array('daily', 'weekly', 'monthly');
1504 $now = time();
1505 $y30 = 60*60*24*365*30; // 30 years ago :-O
1506 $y20 = 60*60*24*365*20; // 20 years ago :-O
1507 $limit = $now - $y20;
1509 foreach ($types as $i => $type) {
1510 $type2 = $types[($i+1) % count($types)];
1512 // delete previous incomplete data
1513 $sql = "DELETE FROM {$CFG->prefix}stats_$type2
1514 WHERE timeend < $limit";
1515 execute_sql($sql);
1517 // clear the totals if already exist
1518 $sql = "DELETE FROM {$CFG->prefix}stats_$type
1519 WHERE (stattype = 'enrolments' OR stattype = 'activity') AND
1520 roleid = 0";
1521 execute_sql($sql);
1523 $sql = "INSERT INTO {$CFG->prefix}stats_$type2 (stattype, timeend, courseid, roleid, stat1, stat2)
1525 SELECT stattype, (timeend - $y30), courseid, 0, SUM(stat1), SUM(stat2)
1526 FROM {$CFG->prefix}stats_$type
1527 WHERE (stattype = 'enrolments' OR stattype = 'activity') AND
1528 roleid <> 0
1529 GROUP BY stattype, timeend, courseid";
1530 execute_sql($sql);
1532 $sql = "INSERT INTO {$CFG->prefix}stats_$type (stattype, timeend, courseid, roleid, stat1, stat2)
1534 SELECT stattype, (timeend + $y30), courseid, roleid, stat1, stat2
1535 FROM {$CFG->prefix}stats_$type2
1536 WHERE (stattype = 'enrolments' OR stattype = 'activity') AND
1537 roleid = 0 AND timeend < $y20";
1538 execute_sql($sql);
1540 $sql = "DELETE FROM {$CFG->prefix}stats_$type2
1541 WHERE timeend < $limit";
1542 execute_sql($sql);
1547 function stats_upgrade_for_roles_wrapper() {
1548 global $CFG;
1549 if (!empty($CFG->statsrolesupgraded)) {
1550 return true;
1553 $result = begin_sql();
1555 $result = $result && stats_upgrade_user_table_for_roles('daily');
1556 $result = $result && stats_upgrade_user_table_for_roles('weekly');
1557 $result = $result && stats_upgrade_user_table_for_roles('monthly');
1559 $result = $result && stats_upgrade_table_for_roles('daily');
1560 $result = $result && stats_upgrade_table_for_roles('weekly');
1561 $result = $result && stats_upgrade_table_for_roles('monthly');
1564 $result = $result && commit_sql();
1566 if (!empty($result)) {
1567 set_config('statsrolesupgraded',time());
1570 // finally upgade totals, no big deal if it fails
1571 stats_upgrade_totals();
1573 return $result;
1577 * Upgrades a prefix_stats_user_* table for the new role based permission
1578 * system.
1580 * @param string $period daily, weekly or monthly: the stat period to upgrade
1581 * @return boolean @todo maybe something else (error message) depending on
1582 * how this will be called.
1584 function stats_upgrade_user_table_for_roles($period) {
1585 global $CFG;
1586 static $teacher_role_id, $student_role_id;
1588 if (!in_array($period, array('daily', 'weekly', 'monthly'))) {
1589 error_log('stats upgrade: invalid period: ' . $period);
1590 return false;
1593 if (!$teacher_role_id) {
1594 $role = get_roles_with_capability('moodle/legacy:editingteacher', CAP_ALLOW);
1595 $role = array_keys($role);
1596 $teacher_role_id = $role[0];
1597 $role = get_roles_with_capability('moodle/legacy:student', CAP_ALLOW);
1598 $role = array_keys($role);
1599 $student_role_id = $role[0];
1602 if (empty($teacher_role_id) || empty($student_role_id)) {
1603 error_log("Couldn't find legacy roles for teacher or student");
1604 return false;
1607 $status = true;
1609 $status = $status && execute_sql("UPDATE {$CFG->prefix}stats_user_{$period}
1610 SET roleid = $teacher_role_id
1611 WHERE roleid = 1");
1612 $status = $status && execute_sql("UPDATE {$CFG->prefix}stats_user_{$period}
1613 SET roleid = $student_role_id
1614 WHERE roleid = 2");
1616 return $status;
1620 * Upgrades a prefix_stats_* table for the new role based permission system.
1622 * @param string $period daily, weekly or monthly: the stat period to upgrade
1623 * @return boolean @todo depends on how this will be called
1625 function stats_upgrade_table_for_roles ($period) {
1626 global $CFG;
1627 static $teacher_role_id, $student_role_id;
1629 if (!in_array($period, array('daily', 'weekly', 'monthly'))) {
1630 return false;
1633 if (!$teacher_role_id) {
1634 $role = get_roles_with_capability('moodle/legacy:editingteacher', CAP_ALLOW);
1635 $role = array_keys($role);
1636 $teacher_role_id = $role[0];
1637 $role = get_roles_with_capability('moodle/legacy:student', CAP_ALLOW);
1638 $role = array_keys($role);
1639 $student_role_id = $role[0];
1642 if (empty($teacher_role_id) || empty($student_role_id)) {
1643 error_log("Couldn't find legacy roles for teacher or student");
1644 return false;
1647 execute_sql("CREATE TABLE {$CFG->prefix}stats_{$period}_tmp AS
1648 SELECT * FROM {$CFG->prefix}stats_{$period}");
1650 $table = new XMLDBTable('stats_' . $period);
1651 if (!drop_table($table)) {
1652 return false;
1655 // Create a new stats table
1656 // @todo this definition I have made blindly by looking at how definitions are
1657 // made, it needs work to make sure it works properly
1658 require_once("$CFG->libdir/xmldb/classes/XMLDBTable.class.php");
1660 $table = new XMLDBTable('stats_' . $period);
1661 $table->addFieldInfo('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED,
1662 XMLDB_NOTNULL, XMLDB_SEQUENCE, null, null, null);
1664 $table->addFieldInfo('courseid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED,
1665 XMLDB_NOTNULL, null, null, null, null);
1667 $table->addFieldInfo('roleid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED,
1668 XMLDB_NOTNULL, null, null, null, null);
1669 $table->addFieldInfo('timeend', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED,
1670 XMLDB_NOTNULL, null, null, null, null);
1671 $table->addFieldInfo('stattype', XMLDB_TYPE_CHAR, '20', null, XMLDB_NOTNULL,
1672 null, XMLDB_ENUM, array('enrolments', 'activity', 'logins'), 'activity');
1673 $table->addFieldInfo('stat1', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED,
1674 XMLDB_NOTNULL, null, null, null, null);
1675 $table->addFieldInfo('stat2', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED,
1676 XMLDB_NOTNULL, null, null, null, null);
1678 /// Adding keys to table stats_daily
1679 $table->addKeyInfo('primary', XMLDB_KEY_PRIMARY, array('id'));
1681 /// Adding indexes to table stats_daily
1682 $table->addIndexInfo('courseid', XMLDB_INDEX_NOTUNIQUE, array('courseid'));
1683 $table->addIndexInfo('timeend', XMLDB_INDEX_NOTUNIQUE, array('timeend'));
1684 $table->addIndexInfo('roleid', XMLDB_INDEX_NOTUNIQUE, array('roleid'));
1686 if (!create_table($table)) {
1687 return false;
1691 // Now insert the data from the temporary table into the new one
1694 // Student enrolments
1695 execute_sql("INSERT INTO {$CFG->prefix}stats_{$period}
1696 (courseid, roleid, timeend, stattype, stat1, stat2)
1697 SELECT courseid, $student_role_id, timeend, 'enrolments', students, activestudents
1698 FROM {$CFG->prefix}stats_{$period}_tmp");
1700 // Teacher enrolments
1701 execute_sql("INSERT INTO {$CFG->prefix}stats_{$period}
1702 (courseid, roleid, timeend, stattype, stat1, stat2)
1703 SELECT courseid, $teacher_role_id, timeend, 'enrolments', teachers, activeteachers
1704 FROM {$CFG->prefix}stats_{$period}_tmp");
1706 // Student activity
1707 execute_sql("INSERT INTO {$CFG->prefix}stats_{$period}
1708 (courseid, roleid, timeend, stattype, stat1, stat2)
1709 SELECT courseid, $student_role_id, timeend, 'activity', studentreads, studentwrites
1710 FROM {$CFG->prefix}stats_{$period}_tmp");
1712 // Teacher activity
1713 execute_sql("INSERT INTO {$CFG->prefix}stats_{$period}
1714 (courseid, roleid, timeend, stattype, stat1, stat2)
1715 SELECT courseid, $teacher_role_id, timeend, 'activity', teacherreads, teacherwrites
1716 FROM {$CFG->prefix}stats_{$period}_tmp");
1718 // Logins
1719 execute_sql("INSERT INTO {$CFG->prefix}stats_{$period}
1720 (courseid, roleid, timeend, stattype, stat1, stat2)
1721 SELECT courseid, 0, timeend, 'logins', logins, uniquelogins
1722 FROM {$CFG->prefix}stats_{$period}_tmp WHERE courseid = ".SITEID);
1724 // Drop the temporary table
1725 $table = new XMLDBTable('stats_' . $period . '_tmp');
1726 if (!drop_table($table)) {
1727 return false;
1730 return true;