966e64af08dba981479e5718bb036ed5627cf0a2
[moodle.git] / lib / statslib.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * @package   moodlecore
20  * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
21  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22  */
24     /** THESE CONSTANTS ARE USED FOR THE REPORTING PAGE. */
26     define('STATS_REPORT_LOGINS',1); // double impose logins and unqiue logins on a line graph. site course only.
27     define('STATS_REPORT_READS',2); // double impose student reads and teacher reads on a line graph.
28     define('STATS_REPORT_WRITES',3); // double impose student writes and teacher writes on a line graph.
29     define('STATS_REPORT_ACTIVITY',4); // 2+3 added up, teacher vs student.
30     define('STATS_REPORT_ACTIVITYBYROLE',5); // all activity, reads vs writes, seleted by role.
32     // user level stats reports.
33     define('STATS_REPORT_USER_ACTIVITY',7);
34     define('STATS_REPORT_USER_ALLACTIVITY',8);
35     define('STATS_REPORT_USER_LOGINS',9);
36     define('STATS_REPORT_USER_VIEW',10);  // this is the report you see on the user profile.
38     // admin only ranking stats reports
39     define('STATS_REPORT_ACTIVE_COURSES',11);
40     define('STATS_REPORT_ACTIVE_COURSES_WEIGHTED',12);
41     define('STATS_REPORT_PARTICIPATORY_COURSES',13);
42     define('STATS_REPORT_PARTICIPATORY_COURSES_RW',14);
44     // start after 0 = show dailies.
45     define('STATS_TIME_LASTWEEK',1);
46     define('STATS_TIME_LAST2WEEKS',2);
47     define('STATS_TIME_LAST3WEEKS',3);
48     define('STATS_TIME_LAST4WEEKS',4);
50     // start after 10 = show weeklies
51     define('STATS_TIME_LAST2MONTHS',12);
53     define('STATS_TIME_LAST3MONTHS',13);
54     define('STATS_TIME_LAST4MONTHS',14);
55     define('STATS_TIME_LAST5MONTHS',15);
56     define('STATS_TIME_LAST6MONTHS',16);
58     // start after 20 = show monthlies
59     define('STATS_TIME_LAST7MONTHS',27);
60     define('STATS_TIME_LAST8MONTHS',28);
61     define('STATS_TIME_LAST9MONTHS',29);
62     define('STATS_TIME_LAST10MONTHS',30);
63     define('STATS_TIME_LAST11MONTHS',31);
64     define('STATS_TIME_LASTYEAR',32);
66     // different modes for what reports to offer
67     define('STATS_MODE_GENERAL',1);
68     define('STATS_MODE_DETAILED',2);
69     define('STATS_MODE_RANKED',3); // admins only - ranks courses
71 /**
72  * Print daily cron progress
73  * @param string $ident
74  */
75 function stats_daily_progress($ident) {
76     static $start = 0;
77     static $init  = 0;
79     if ($ident == 'init') {
80         $init = $start = time();
81         return;
82     }
84     $elapsed = time() - $start;
85     $start   = time();
87     if (debugging('', DEBUG_ALL)) {
88         mtrace("$ident:$elapsed ", '');
89     } else {
90         mtrace('.', '');
91     }
92 }
94 /**
95  * Execute daily statistics gathering
96  * @param int $maxdays maximum number of days to be processed
97  * @return boolean success
98  */
99 function stats_cron_daily($maxdays=1) {
100     global $CFG, $DB;
102     $now = time();
104     // read last execution date from db
105     if (!$timestart = get_config(NULL, 'statslastdaily')) {
106         $timestart = stats_get_base_daily(stats_get_start_from('daily'));
107         set_config('statslastdaily', $timestart);
108     }
110     // calculate scheduled time
111     $scheduledtime = stats_get_base_daily() + $CFG->statsruntimestarthour*60*60 + $CFG->statsruntimestartminute*60;
113     // Note: This will work fine for sites running cron each 4 hours or less (hopefully, 99.99% of sites). MDL-16709
114     // check to make sure we're due to run, at least 20 hours after last run
115     if (isset($CFG->statslastexecution) and ((time() - 20*60*60) < $CFG->statslastexecution)) {
116         mtrace("...preventing stats to run, last execution was less than 20 hours ago.");
117         return false;
118     // also check that we are a max of 4 hours after scheduled time, stats won't run after that
119     } else if (time() > $scheduledtime + 4*60*60) {
120         mtrace("...preventing stats to run, more than 4 hours since scheduled time.");
121         return false;
122     } else {
123         set_config('statslastexecution', time()); /// Grab this execution as last one
124     }
126     $nextmidnight = stats_get_next_day_start($timestart);
128     // are there any days that need to be processed?
129     if ($now < $nextmidnight) {
130         return true; // everything ok and up-to-date
131     }
134     $timeout = empty($CFG->statsmaxruntime) ? 60*60*24 : $CFG->statsmaxruntime;
136     if (!set_cron_lock('statsrunning', $now + $timeout)) {
137         return false;
138     }
140     // first delete entries that should not be there yet
141     $DB->delete_records_select('stats_daily',      "timeend > $timestart");
142     $DB->delete_records_select('stats_user_daily', "timeend > $timestart");
144     // Read in a few things we'll use later
145     $viewactions = implode(',', stats_get_action_names('view'));
146     $postactions = implode(',', stats_get_action_names('post'));
148     $guest     = get_complete_user_data('username', 'guest');
149     $guestrole = get_guest_role();
151     list($enroljoin, $enrolwhere, $enrolparams)          = stats_get_enrolled_sql($CFG->statscatdepth, true);
152     list($enroljoin_na, $enrolwhere_na, $enrolparams_na) = stats_get_enrolled_sql($CFG->statscatdepth, false);
153     list($fpjoin, $fpwhere, $fpparams)                   = stats_get_enrolled_sql(0, true);
155     mtrace("Running daily statistics gathering, starting at $timestart:");
157     $days = 0;
158     $failed = false; // failed stats flag
160     while ($now > $nextmidnight) {
161         if ($days >= $maxdays) {
162             mtrace("...stopping early, reached maximum number of $maxdays days - will continue next time.");
163             set_cron_lock('statsrunning', null);
164             return false;
165         }
167         $days++;
168         @set_time_limit($timeout - 200);
170         if ($days > 1) {
171             // move the lock
172             set_cron_lock('statsrunning', time() + $timeout, true);
173         }
175         $daystart = time();
177         $timesql  = "l.time >= $timestart  AND l.time  < $nextmidnight";
178         $timesql1 = "l1.time >= $timestart AND l1.time < $nextmidnight";
179         $timesql2 = "l2.time >= $timestart AND l2.time < $nextmidnight";
181         stats_daily_progress('init');
184     /// find out if any logs available for this day
185         $sql = "SELECT 'x'
186                   FROM {log} l
187                  WHERE $timesql";
188         $logspresent = $DB->get_records_sql($sql, null, 0, 1);
190     /// process login info first
191         $sql = "INSERT INTO {stats_user_daily} (stattype, timeend, courseid, userid, statsreads)
193                 SELECT 'logins', timeend, courseid, userid, count(statsreads)
194                  FROM (
195                           SELECT $nextmidnight AS timeend, ".SITEID." AS courseid, l.userid, l.id AS statsreads
196                             FROM {log} l
197                            WHERE action = 'login' AND $timesql
198                        ) inline_view
199               GROUP BY timeend, courseid, userid
200                 HAVING count(statsreads) > 0";
202         if ($logspresent and !$DB->execute($sql)) {
203             $failed = true;
204             break;
205         }
206         stats_daily_progress('1');
208         $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2)
210                 SELECT 'logins' AS stattype, $nextmidnight AS timeend, ".SITEID." as courseid, 0,
211                        COALESCE((SELECT SUM(statsreads)
212                                        FROM {stats_user_daily} s1
213                                       WHERE s1.stattype = 'logins' AND timeend = $nextmidnight), 0) AS stat1,
214                        (SELECT COUNT('x')
215                           FROM {stats_user_daily} s2
216                          WHERE s2.stattype = 'logins' AND timeend = $nextmidnight) AS stat2" .
217                 $DB->sql_null_from_clause();
219         if ($logspresent and !$DB->execute($sql)) {
220             $failed = true;
221             break;
222         }
223         stats_daily_progress('2');
226         // Enrolments and active enrolled users
227         //
228         // Unfortunately, we do not know how many users were registered
229         // at given times in history :-(
230         // - stat1: enrolled users
231         // - stat2: enrolled users active in this period
232         // - enrolment is defined now as having course:view capability in
233         //   course context or above, we look 3 cats upwards only and ignore prevent
234         //   and prohibit caps to simplify it
235         // - SITEID is special case here, because it's all about default enrolment
236         //   in that case, we'll count non-deleted users.
237         //
239         $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2)
241                 SELECT 'enrolments', timeend, courseid, roleid, COUNT(DISTINCT userid), 0
242                   FROM (
243                            SELECT $nextmidnight AS timeend, pl.courseid, pl.roleid, pl.userid
244                              FROM (
245                                       SELECT DISTINCT ra.roleid, ra.userid, c.id as courseid
246                                         FROM {role_assignments} ra $enroljoin_na
247                                        WHERE $enrolwhere_na
248                                   ) pl
249                        ) inline_view
250               GROUP BY timeend, courseid, roleid";
252         if (!$DB->execute($sql, $enrolparams_na)) {
253             $failed = true;
254             break;
255         }
256         stats_daily_progress('3');
258         // using table alias in UPDATE does not work in pg < 8.2
259         $sql = "UPDATE {stats_daily}
260                    SET stat2 = (SELECT COUNT(DISTINCT ra.userid)
261                                   FROM {role_assignments} ra $enroljoin_na
262                                  WHERE ra.roleid = {stats_daily}.roleid AND
263                                        c.id = {stats_daily}.courseid AND
264                                        $enrolwhere_na AND
265                                        EXISTS (SELECT 'x'
266                                                  FROM {log} l
267                                                 WHERE l.course = {stats_daily}.courseid AND
268                                                       l.userid = ra.userid AND $timesql))
269                  WHERE {stats_daily}.stattype = 'enrolments' AND
270                        {stats_daily}.timeend = $nextmidnight AND
271                        {stats_daily}.courseid IN
272                           (SELECT DISTINCT l.course
273                              FROM {log} l
274                             WHERE $timesql)";
276         if (!$DB->execute($sql, $enrolparams_na)) {
277             $failed = true;
278             break;
279         }
280         stats_daily_progress('4');
282     /// now get course total enrolments (roleid==0) - except frontpage
283         $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2)
285                 SELECT 'enrolments', timeend, id, nroleid, COUNT(DISTINCT userid), 0
286                   FROM (
287                            SELECT $nextmidnight AS timeend, c.id, 0 AS nroleid, ra.userid
288                              FROM {role_assignments} ra $enroljoin_na
289                             WHERE c.id <> ".SITEID." AND $enrolwhere_na
290                        ) inline_view
291               GROUP BY timeend, id, nroleid
292                 HAVING COUNT(DISTINCT userid) > 0";
294         if ($logspresent and !$DB->execute($sql, $enrolparams_na)) {
295             $failed = true;
296             break;
297         }
298         stats_daily_progress('5');
300         $sql = "UPDATE {stats_daily}
301                    SET stat2 = (SELECT COUNT(DISTINCT ra.userid)
302                                   FROM {role_assignments} ra $enroljoin_na
303                                  WHERE c.id = {stats_daily}.courseid AND
304                                        $enrolwhere_na AND
305                                        EXISTS (SELECT 'x'
306                                                  FROM {log} l
307                                                 WHERE l.course = {stats_daily}.courseid AND
308                                                       l.userid = ra.userid AND $timesql))
309                  WHERE {stats_daily}.stattype = 'enrolments' AND
310                        {stats_daily}.timeend = $nextmidnight AND
311                        {stats_daily}.roleid = 0 AND
312                        {stats_daily}.courseid IN
313                           (SELECT l.course
314                              FROM {log} l
315                             WHERE $timesql AND l.course <> ".SITEID.")";
317         if ($logspresent and !$DB->execute($sql, $enrolparams_na)) {
318             $failed = true;
319             break;
320         }
321         stats_daily_progress('6');
323     /// frontapge(==site) enrolments total
324         $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2)
326                 SELECT 'enrolments', $nextmidnight, ".SITEID.", 0,
327                        (SELECT COUNT('x')
328                           FROM {user} u
329                          WHERE u.deleted = 0) AS stat1,
330                        (SELECT COUNT(DISTINCT u.id)
331                           FROM {user} u
332                                JOIN {log} l ON l.userid = u.id
333                          WHERE u.deleted = 0 AND $timesql) AS stat2" .
334                 $DB->sql_null_from_clause();
336         if ($logspresent and !$DB->execute($sql)) {
337             $failed = true;
338             break;
339         }
340         stats_daily_progress('7');
342         if (empty($CFG->defaultfrontpageroleid)) { // 1.9 only, so far
343             $defaultfproleid = 0;
344         } else {
345             $defaultfproleid = $CFG->defaultfrontpageroleid;
346         }
348     /// Default frontpage role enrolments are all site users (not deleted)
349         if ($defaultfproleid) {
350             // first remove default frontpage role counts if created by previous query
351             $sql = "DELETE
352                       FROM {stats_daily}
353                      WHERE stattype = 'enrolments' AND courseid = ".SITEID." AND
354                            roleid = $defaultfproleid AND timeend = $nextmidnight";
355             if ($logspresent and !$DB->execute($sql)) {
356                 $failed = true;
357                 break;
358             }
359             stats_daily_progress('8');
361             $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2)
363                     SELECT 'enrolments', $nextmidnight, ".SITEID.", $defaultfproleid,
364                            (SELECT COUNT('x')
365                               FROM {user} u
366                              WHERE u.deleted = 0) AS stat1,
367                            (SELECT COUNT(DISTINCT u.id)
368                               FROM {user} u
369                                    JOIN {log} l ON l.userid = u.id
370                              WHERE u.deleted = 0 AND $timesql) AS stat2" .
371                     $DB->sql_null_from_clause();;
373             if ($logspresent and !$DB->execute($sql)) {
374                 $failed = true;
375                 break;
376             }
377             stats_daily_progress('9');
379         } else {
380             stats_daily_progress('x');
381             stats_daily_progress('x');
382         }
386     /// individual user stats (including not-logged-in) in each course, this is slow - reuse this data if possible
387         $sql = "INSERT INTO {stats_user_daily} (stattype, timeend, courseid, userid, statsreads, statswrites)
389                 SELECT 'activity' AS stattype, $nextmidnight AS timeend, d.courseid, d.userid,
390                        (SELECT COUNT('x')
391                           FROM {log} l
392                          WHERE l.userid = d.userid AND
393                                l.course = d.courseid AND $timesql AND
394                                l.action IN ($viewactions)) AS statsreads,
395                        (SELECT COUNT('x')
396                           FROM {log} l
397                          WHERE l.userid = d.userid AND
398                                l.course = d.courseid AND $timesql AND
399                                l.action IN ($postactions)) AS statswrites
400                   FROM (SELECT DISTINCT u.id AS userid, l.course AS courseid
401                           FROM {user} u, {log} l
402                          WHERE u.id = l.userid AND $timesql
403                        UNION
404                         SELECT 0 AS userid, ".SITEID." AS courseid" . $DB->sql_null_from_clause() . ") d";
405                         // can not use group by here because pg can not handle it :-(
407         if ($logspresent and !$DB->execute($sql)) {
408             $failed = true;
409             break;
410         }
411         stats_daily_progress('10');
414     /// how many view/post actions in each course total
415         $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2)
417                 SELECT 'activity' AS stattype, $nextmidnight AS timeend, c.id AS courseid, 0,
418                        (SELECT COUNT('x')
419                           FROM {log} l1
420                          WHERE l1.course = c.id AND l1.action IN ($viewactions) AND
421                                $timesql1) AS stat1,
422                        (SELECT COUNT('x')
423                           FROM {log} l2
424                          WHERE l2.course = c.id AND l2.action IN ($postactions) AND
425                                $timesql2) AS stat2
426                   FROM {course} c
427                  WHERE EXISTS (SELECT 'x'
428                                  FROM {log} l
429                                 WHERE l.course = c.id and $timesql)";
431         if ($logspresent and !$DB->execute($sql)) {
432             $failed = true;
433             break;
434         }
435         stats_daily_progress('11');
438     /// how many view actions for each course+role - excluding guests and frontpage
440         $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2)
442                 SELECT 'activity', timeend, courseid, roleid, SUM(statsreads), SUM(statswrites)
443                   FROM (
444                            SELECT $nextmidnight AS timeend, pl.courseid, pl.roleid, sud.statsreads, sud.statswrites
445                              FROM {stats_user_daily} sud,
446                                       (SELECT DISTINCT ra.userid, ra.roleid, c.id AS courseid
447                                          FROM {role_assignments} ra $enroljoin
448                                         WHERE c.id <> ".SITEID." AND
449                                               ra.roleid <> $guestrole->id AND
450                                               ra.userid <> $guest->id AND
451                                               $enrolwhere
452                                       ) pl
453                             WHERE sud.userid = pl.userid AND
454                                   sud.courseid = pl.courseid AND
455                                   sud.timeend = $nextmidnight AND
456                                   sud.stattype='activity'
457                        ) inline_view
458               GROUP BY timeend, courseid, roleid
459                 HAVING SUM(statsreads) > 0 OR SUM(statswrites) > 0";
461         if ($logspresent and !$DB->execute($sql, $enrolparams)) {
462             $failed = true;
463             break;
464         }
465         stats_daily_progress('12');
467     /// how many view actions from guests only in each course - excluding frontpage
468     /// (guest is anybody with guest role or no role with course:view in course - this may not work properly if category limit too low)
469     /// normal users may enter course with temporary guest access too
471         $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2)
473                 SELECT 'activity', timeend, courseid, nroleid, SUM(statsreads), SUM(statswrites)
474                   FROM (
475                            SELECT $nextmidnight AS timeend, sud.courseid, $guestrole->id AS nroleid, sud.statsreads, sud.statswrites
476                              FROM {stats_user_daily} sud
477                             WHERE sud.timeend = $nextmidnight AND sud.courseid <> ".SITEID." AND
478                                   sud.stattype='activity' AND
479                                   (sud.userid = $guest->id OR sud.userid
480                                     NOT IN (SELECT ra.userid
481                                               FROM {role_assignments} ra $enroljoin
482                                              WHERE c.id <> ".SITEID." AND  ra.roleid <> $guestrole->id AND
483                                                    $enrolwhere))
484                        ) inline_view
485               GROUP BY timeend, courseid, nroleid
486                 HAVING SUM(statsreads) > 0 OR SUM(statswrites) > 0";
488         if ($logspresent and !$DB->execute($sql, $enrolparams)) {
489             $failed = true;
490             break;
491         }
492         stats_daily_progress('13');
495     /// how many view actions for each role on frontpage - excluding guests, not-logged-in and default frontpage role
496         $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2)
498                 SELECT 'activity', timeend, courseid, roleid, SUM(statsreads), SUM(statswrites)
499                   FROM (
500                            SELECT $nextmidnight AS timeend, pl.courseid, pl.roleid, sud.statsreads, sud.statswrites
501                              FROM {stats_user_daily} sud,
502                                       (SELECT DISTINCT ra.userid, ra.roleid, c.id AS courseid
503                                          FROM {role_assignments} ra $enroljoin
504                                         WHERE c.id = ".SITEID." AND
505                                               ra.roleid <> $defaultfproleid AND
506                                               ra.roleid <> $guestrole->id AND
507                                               ra.userid <> $guest->id AND
508                                               $enrolwhere
509                                       ) pl
510                             WHERE sud.userid = pl.userid AND
511                                   sud.courseid = pl.courseid AND
512                                   sud.timeend = $nextmidnight AND
513                                   sud.stattype='activity'
514                        ) inline_view
515               GROUP BY timeend, courseid, roleid
516                 HAVING SUM(statsreads) > 0 OR SUM(statswrites) > 0";
518         if ($logspresent and !$DB->execute($sql, $enrolparams)) {
519             $failed = true;
520             break;
521         }
522         stats_daily_progress('14');
525     /// how many view actions for default frontpage role on frontpage only
526         $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2)
528                 SELECT 'activity', timeend, courseid, nroleid, SUM(statsreads), SUM(statswrites)
529                   FROM (
530                            SELECT $nextmidnight AS timeend, sud.courseid, $defaultfproleid AS nroleid, sud.statsreads, sud.statswrites
531                              FROM {stats_user_daily} sud
532                             WHERE sud.timeend = $nextmidnight AND sud.courseid = ".SITEID." AND
533                                   sud.stattype='activity' AND
534                                   sud.userid <> $guest->id AND sud.userid <> 0 AND sud.userid
535                                   NOT IN (SELECT ra.userid
536                                             FROM {role_assignments} ra $fpjoin
537                                            WHERE c.id = ".SITEID." AND  ra.roleid <> $guestrole->id AND
538                                                  ra.roleid <> $defaultfproleid AND $fpwhere)
539                        ) inline_view
540               GROUP BY timeend, courseid, nroleid
541                 HAVING SUM(statsreads) > 0 OR SUM(statswrites) > 0";
543         if ($logspresent and !$DB->execute($sql, $fpparams)) {
544             $failed = true;
545             break;
546         }
547         stats_daily_progress('15');
549     /// how many view actions for guests or not-logged-in on frontpage
550         $sql = "INSERT INTO {stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2)
552                 SELECT 'activity', timeend, courseid, nroleid, SUM(statsreads), SUM(statswrites)
553                   FROM (
554                            SELECT $nextmidnight AS timeend, ".SITEID." AS courseid, $guestrole->id AS nroleid, pl.statsreads, pl.statswrites
555                              FROM (
556                                       SELECT sud.statsreads, sud.statswrites
557                                         FROM {stats_user_daily} sud
558                                        WHERE (sud.userid = $guest->id OR sud.userid = 0) AND
559                                              sud.timeend = $nextmidnight AND sud.courseid = ".SITEID." AND
560                                              sud.stattype='activity'
561                                   ) pl
562                        ) inline_view
563               GROUP BY timeend, courseid, nroleid
564                 HAVING SUM(statsreads) > 0 OR SUM(statswrites) > 0";
566         if ($logspresent and !$DB->execute($sql)) {
567             $failed = true;
568             break;
569         }
570         stats_daily_progress('16');
572         // remember processed days
573         set_config('statslastdaily', $nextmidnight);
574         mtrace("  finished until $nextmidnight: ".userdate($nextmidnight)." (in ".(time()-$daystart)." s)");
576         $timestart    = $nextmidnight;
577         $nextmidnight = stats_get_next_day_start($nextmidnight);
578     }
580     set_cron_lock('statsrunning', null);
582     if ($failed) {
583         $days--;
584         mtrace("...error occurred, completed $days days of statistics.");
585         return false;
587     } else {
588         mtrace("...completed $days days of statistics.");
589         return true;
590     }
594 /**
595  * Execute weekly statistics gathering
596  * @return boolean success
597  */
598 function stats_cron_weekly() {
599     global $CFG, $DB;
601     $now = time();
603     // read last execution date from db
604     if (!$timestart = get_config(NULL, 'statslastweekly')) {
605         $timestart = stats_get_base_daily(stats_get_start_from('weekly'));
606         set_config('statslastweekly', $timestart);
607     }
609     $nextstartweek = stats_get_next_week_start($timestart);
611     // are there any weeks that need to be processed?
612     if ($now < $nextstartweek) {
613         return true; // everything ok and up-to-date
614     }
616     $timeout = empty($CFG->statsmaxruntime) ? 60*60*24 : $CFG->statsmaxruntime;
618     if (!set_cron_lock('statsrunning', $now + $timeout)) {
619         return false;
620     }
622     // fisrt delete entries that should not be there yet
623     $DB->delete_records_select('stats_weekly',      "timeend > $timestart");
624     $DB->delete_records_select('stats_user_weekly', "timeend > $timestart");
626     mtrace("Running weekly statistics gathering, starting at $timestart:");
628     $weeks = 0;
629     while ($now > $nextstartweek) {
630         @set_time_limit($timeout - 200);
631         $weeks++;
633         if ($weeks > 1) {
634             // move the lock
635             set_cron_lock('statsrunning', time() + $timeout, true);
636         }
638         $logtimesql  = "l.time >= $timestart AND l.time < $nextstartweek";
639         $stattimesql = "timeend > $timestart AND timeend <= $nextstartweek";
641     /// process login info first
642         $sql = "INSERT INTO {stats_user_weekly} (stattype, timeend, courseid, userid, statsreads)
644                 SELECT 'logins', timeend, courseid, userid, COUNT(statsreads)
645                   FROM (
646                            SELECT $nextstartweek AS timeend, ".SITEID." as courseid, l.userid, l.id AS statsreads
647                              FROM {log} l
648                             WHERE action = 'login' AND $logtimesql
649                        ) inline_view
650               GROUP BY timeend, courseid, userid
651                 HAVING count(statsreads) > 0";
653         $DB->execute($sql);
655         $sql = "INSERT INTO {stats_weekly} (stattype, timeend, courseid, roleid, stat1, stat2)
657                 SELECT 'logins' AS stattype, $nextstartweek AS timeend, ".SITEID." as courseid, 0,
658                        COALESCE((SELECT SUM(statsreads)
659                                    FROM {stats_user_weekly} s1
660                                   WHERE s1.stattype = 'logins' AND timeend = $nextstartweek), 0) AS nstat1,
661                        (SELECT COUNT('x')
662                           FROM {stats_user_weekly} s2
663                          WHERE s2.stattype = 'logins' AND timeend = $nextstartweek) AS nstat2" .
664                 $DB->sql_null_from_clause();
666         $DB->execute($sql);
669     /// now enrolments averages
670         $sql = "INSERT INTO {stats_weekly} (stattype, timeend, courseid, roleid, stat1, stat2)
672                 SELECT 'enrolments', ntimeend, courseid, roleid, " . $DB->sql_ceil('AVG(stat1)') . ", " . $DB->sql_ceil('AVG(stat2)') . "
673                   FROM (
674                            SELECT $nextstartweek AS ntimeend, courseid, roleid, stat1, stat2
675                              FROM {stats_daily} sd
676                             WHERE stattype = 'enrolments' AND $stattimesql
677                        ) inline_view
678               GROUP BY ntimeend, courseid, roleid";
680         $DB->execute($sql);
683     /// activity read/write averages
684         $sql = "INSERT INTO {stats_weekly} (stattype, timeend, courseid, roleid, stat1, stat2)
686                 SELECT 'activity', ntimeend, courseid, roleid, SUM(stat1), SUM(stat2)
687                   FROM (
688                            SELECT $nextstartweek AS ntimeend, courseid, roleid, stat1, stat2
689                              FROM {stats_daily}
690                             WHERE stattype = 'activity' AND $stattimesql
691                        ) inline_view
692               GROUP BY ntimeend, courseid, roleid";
694         $DB->execute($sql);
697     /// user read/write averages
698         $sql = "INSERT INTO {stats_user_weekly} (stattype, timeend, courseid, userid, statsreads, statswrites)
700                 SELECT 'activity', ntimeend, courseid, userid, SUM(statsreads), SUM(statswrites)
701                   FROM (
702                            SELECT $nextstartweek AS ntimeend, courseid, userid, statsreads, statswrites
703                              FROM {stats_user_daily}
704                             WHERE stattype = 'activity' AND $stattimesql
705                        ) inline_view
706               GROUP BY ntimeend, courseid, userid";
708         $DB->execute($sql);
710         set_config('statslastweekly', $nextstartweek);
711         mtrace(" finished until $nextstartweek: ".userdate($nextstartweek));
713         $timestart     = $nextstartweek;
714         $nextstartweek = stats_get_next_week_start($nextstartweek);
715     }
717     set_cron_lock('statsrunning', null);
718     mtrace("...completed $weeks weeks of statistics.");
719     return true;
722 /**
723  * Execute monthly statistics gathering
724  * @return boolean success
725  */
726 function stats_cron_monthly() {
727     global $CFG, $DB;
729     $now = time();
731     // read last execution date from db
732     if (!$timestart = get_config(NULL, 'statslastmonthly')) {
733         $timestart = stats_get_base_monthly(stats_get_start_from('monthly'));
734         set_config('statslastmonthly', $timestart);
735     }
737     $nextstartmonth = stats_get_next_month_start($timestart);
739     // are there any months that need to be processed?
740     if ($now < $nextstartmonth) {
741         return true; // everything ok and up-to-date
742     }
744     $timeout = empty($CFG->statsmaxruntime) ? 60*60*24 : $CFG->statsmaxruntime;
746     if (!set_cron_lock('statsrunning', $now + $timeout)) {
747         return false;
748     }
750     // fisr delete entries that should not be there yet
751     $DB->delete_records_select('stats_monthly', "timeend > $timestart");
752     $DB->delete_records_select('stats_user_monthly', "timeend > $timestart");
754     $startmonth = stats_get_base_monthly($now);
757     mtrace("Running monthly statistics gathering, starting at $timestart:");
759     $months = 0;
760     while ($now > $nextstartmonth) {
761         @set_time_limit($timeout - 200);
762         $months++;
764         if ($months > 1) {
765             // move the lock
766             set_cron_lock('statsrunning', time() + $timeout, true);
767         }
769         $logtimesql  = "l.time >= $timestart AND l.time < $nextstartmonth";
770         $stattimesql = "timeend > $timestart AND timeend <= $nextstartmonth";
772     /// process login info first
773         $sql = "INSERT INTO {stats_user_monthly} (stattype, timeend, courseid, userid, statsreads)
775                 SELECT 'logins', timeend, courseid, userid, COUNT(statsreads)
776                   FROM (
777                            SELECT $nextstartmonth AS timeend, ".SITEID." as courseid, l.userid, l.id AS statsreads
778                              FROM {log} l
779                             WHERE action = 'login' AND $logtimesql
780                        ) inline_view
781               GROUP BY timeend, courseid, userid";
783         $DB->execute($sql);
785         $sql = "INSERT INTO {stats_monthly} (stattype, timeend, courseid, roleid, stat1, stat2)
787                 SELECT 'logins' AS stattype, $nextstartmonth AS timeend, ".SITEID." as courseid, 0,
788                        COALESCE((SELECT SUM(statsreads)
789                                    FROM {stats_user_monthly} s1
790                                   WHERE s1.stattype = 'logins' AND timeend = $nextstartmonth), 0) AS nstat1,
791                        (SELECT COUNT('x')
792                           FROM {stats_user_monthly} s2
793                          WHERE s2.stattype = 'logins' AND timeend = $nextstartmonth) AS nstat2" .
794                 $DB->sql_null_from_clause();
796         $DB->execute($sql);
799     /// now enrolments averages
800         $sql = "INSERT INTO {stats_monthly} (stattype, timeend, courseid, roleid, stat1, stat2)
802                 SELECT 'enrolments', ntimeend, courseid, roleid, " . $DB->sql_ceil('AVG(stat1)') . ", " . $DB->sql_ceil('AVG(stat2)') . "
803                   FROM (
804                            SELECT $nextstartmonth AS ntimeend, courseid, roleid, stat1, stat2
805                              FROM {stats_daily} sd
806                             WHERE stattype = 'enrolments' AND $stattimesql
807                        ) inline_view
808               GROUP BY ntimeend, courseid, roleid";
810         $DB->execute($sql);
813     /// activity read/write averages
814         $sql = "INSERT INTO {stats_monthly} (stattype, timeend, courseid, roleid, stat1, stat2)
816                 SELECT 'activity', ntimeend, courseid, roleid, SUM(stat1), SUM(stat2)
817                   FROM (
818                            SELECT $nextstartmonth AS ntimeend, courseid, roleid, stat1, stat2
819                              FROM {stats_daily}
820                             WHERE stattype = 'activity' AND $stattimesql
821                        ) inline_view
822               GROUP BY ntimeend, courseid, roleid";
824         $DB->execute($sql);
827     /// user read/write averages
828         $sql = "INSERT INTO {stats_user_monthly} (stattype, timeend, courseid, userid, statsreads, statswrites)
830                 SELECT 'activity', ntimeend, courseid, userid, SUM(statsreads), SUM(statswrites)
831                   FROM (
832                            SELECT $nextstartmonth AS ntimeend, courseid, userid, statsreads, statswrites
833                              FROM {stats_user_daily}
834                             WHERE stattype = 'activity' AND $stattimesql
835                        ) inline_view
836               GROUP BY ntimeend, courseid, userid";
838         $DB->execute($sql);
840         set_config('statslastmonthly', $nextstartmonth);
841         mtrace(" finished until $nextstartmonth: ".userdate($nextstartmonth));
843         $timestart      = $nextstartmonth;
844         $nextstartmonth = stats_get_next_month_start($nextstartmonth);
845     }
847     set_cron_lock('statsrunning', null);
848     mtrace("...completed $months months of statistics.");
849     return true;
852 /**
853  * Returns simplified enrolment sql join data
854  * @param int $limit number of max parent course categories
855  * @param bool $includedoanything include also admins
856  * @return array ra join and where string
857  */
858 function stats_get_enrolled_sql($limit, $ignored) {
859     global $CFG;
860     static $n = 0;
862     $params = array();
863     $n++;
865     $join = "JOIN {context} ctx ON ctx.id = ra.contextid
866             CROSS JOIN {course} c
867              JOIN {role_capabilities} rc ON rc.roleid = ra.roleid";
868     $where = "((rc.capability = :sge_courseview_$n)
869                AND rc.permission = 1 AND rc.contextid = ".SYSCONTEXTID."
870                AND (ctx.contextlevel = ".CONTEXT_SYSTEM."
871                     OR (c.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_COURSE.")";
872     $params['sge_courseview_'.$n] = 'moodle/course:participate';
874     for($i=1; $i<=$limit; $i++) {
875         if ($i == 1) {
876             $join .= " LEFT JOIN {course_categories} cc1 ON cc1.id = c.category";
877             $where .= " OR (cc1.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_COURSECAT.")";
878         } else {
879             $j = $i-1;
880             $join .= " LEFT JOIN {course_categories} cc$i ON cc$i.id = cc$j.parent";
881             $where .= " OR (cc$i.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_COURSECAT.")";
882         }
883     }
885     $where .= "))";
887     return array($join, $where, $params);
890 /**
891  * Return starting date of stats processing
892  * @param string $str name of table - daily, weekly or monthly
893  * @return int timestamp
894  */
895 function stats_get_start_from($str) {
896     global $CFG, $DB;
898     // are there any data in stats table? Should not be...
899     if ($timeend = $DB->get_field_sql('SELECT timeend FROM {stats_'.$str.'} ORDER BY timeend DESC')) {
900         return $timeend;
901     }
902     // decide what to do based on our config setting (either all or none or a timestamp)
903     switch ($CFG->statsfirstrun) {
904         case 'all':
905             if ($firstlog = $DB->get_field_sql('SELECT time FROM {log} ORDER BY time ASC')) {
906                 return $firstlog;
907             }
908         default:
909             if (is_numeric($CFG->statsfirstrun)) {
910                 return time() - $CFG->statsfirstrun;
911             }
912             // not a number? use next instead
913         case 'none':
914             return strtotime('-3 day', time());
915     }
918 /**
919  * Start of day
920  * @param int $time timestamp
921  * @return start of day
922  */
923 function stats_get_base_daily($time=0) {
924     global $CFG;
926     if (empty($time)) {
927         $time = time();
928     }
929     if ($CFG->timezone == 99) {
930         $time = strtotime(date('d-M-Y', $time));
931         return $time;
932     } else {
933         $offset = get_timezone_offset($CFG->timezone);
934         $gtime = $time + $offset;
935         $gtime = intval($gtime / (60*60*24)) * 60*60*24;
936         return $gtime - $offset;
937     }
940 /**
941  * Start of week
942  * @param int $time timestamp
943  * @return start of week
944  */
945 function stats_get_base_weekly($time=0) {
946     global $CFG;
948     $time = stats_get_base_daily($time);
949     $startday = $CFG->calendar_startwday;
950     if ($CFG->timezone == 99) {
951         $thisday = date('w', $time);
952     } else {
953         $offset = get_timezone_offset($CFG->timezone);
954         $gtime = $time + $offset;
955         $thisday = gmdate('w', $gtime);
956     }
957     if ($thisday > $startday) {
958         $time = $time - (($thisday - $startday) * 60*60*24);
959     } else if ($thisday < $startday) {
960         $time = $time - ((7 + $thisday - $startday) * 60*60*24);
961     }
962     return $time;
965 /**
966  * Start of month
967  * @param int $time timestamp
968  * @return start of month
969  */
970 function stats_get_base_monthly($time=0) {
971     global $CFG;
973     if (empty($time)) {
974         $time = time();
975     }
976     if ($CFG->timezone == 99) {
977         return strtotime(date('1-M-Y', $time));
979     } else {
980         $time = stats_get_base_daily($time);
981         $offset = get_timezone_offset($CFG->timezone);
982         $gtime = $time + $offset;
983         $day = gmdate('d', $gtime);
984         if ($day == 1) {
985             return $time;
986         }
987         return $gtime - (($day-1) * 60*60*24);
988     }
991 /**
992  * Start of next day
993  * @param int $time timestamp
994  * @return start of next day
995  */
996 function stats_get_next_day_start($time) {
997     $next = stats_get_base_daily($time);
998     $next = $next + 60*60*26;
999     $next = stats_get_base_daily($next);
1000     if ($next <= $time) {
1001         //DST trouble - prevent infinite loops
1002         $next = $next + 60*60*24;
1003     }
1004     return $next;
1007 /**
1008  * Start of next week
1009  * @param int $time timestamp
1010  * @return start of next week
1011  */
1012 function stats_get_next_week_start($time) {
1013     $next = stats_get_base_weekly($time);
1014     $next = $next + 60*60*24*9;
1015     $next = stats_get_base_weekly($next);
1016     if ($next <= $time) {
1017         //DST trouble - prevent infinite loops
1018         $next = $next + 60*60*24*7;
1019     }
1020     return $next;
1023 /**
1024  * Start of next month
1025  * @param int $time timestamp
1026  * @return start of next month
1027  */
1028 function stats_get_next_month_start($time) {
1029     $next = stats_get_base_monthly($time);
1030     $next = $next + 60*60*24*33;
1031     $next = stats_get_base_monthly($next);
1032     if ($next <= $time) {
1033         //DST trouble - prevent infinite loops
1034         $next = $next + 60*60*24*31;
1035     }
1036     return $next;
1039 /**
1040  * Remove old stats data
1041  */
1042 function stats_clean_old() {
1043     global $DB;
1044     mtrace("Running stats cleanup tasks...");
1045     $deletebefore =  stats_get_base_monthly();
1047     // delete dailies older than 3 months (to be safe)
1048     $deletebefore = strtotime('-3 months', $deletebefore);
1049     $DB->delete_records_select('stats_daily',      "timeend < $deletebefore");
1050     $DB->delete_records_select('stats_user_daily', "timeend < $deletebefore");
1052     // delete weeklies older than 9  months (to be safe)
1053     $deletebefore = strtotime('-6 months', $deletebefore);
1054     $DB->delete_records_select('stats_weekly',      "timeend < $deletebefore");
1055     $DB->delete_records_select('stats_user_weekly', "timeend < $deletebefore");
1057     // don't delete monthlies
1059     mtrace("...stats cleanup finished");
1062 function stats_get_parameters($time,$report,$courseid,$mode,$roleid=0) {
1063     global $CFG, $DB;
1065     $param = new object();
1066     $params->params = array();
1068     if ($time < 10) { // dailies
1069         // number of days to go back = 7* time
1070         $param->table = 'daily';
1071         $param->timeafter = strtotime("-".($time*7)." days",stats_get_base_daily());
1072     } elseif ($time < 20) { // weeklies
1073         // number of weeks to go back = time - 10 * 4 (weeks) + base week
1074         $param->table = 'weekly';
1075         $param->timeafter = strtotime("-".(($time - 10)*4)." weeks",stats_get_base_weekly());
1076     } else { // monthlies.
1077         // number of months to go back = time - 20 * months + base month
1078         $param->table = 'monthly';
1079         $param->timeafter = strtotime("-".($time - 20)." months",stats_get_base_monthly());
1080     }
1082     $param->extras = '';
1084     switch ($report) {
1085     // ******************** STATS_MODE_GENERAL ******************** //
1086     case STATS_REPORT_LOGINS:
1087         $param->fields = 'timeend,sum(stat1) as line1,sum(stat2) as line2';
1088         $param->fieldscomplete = true;
1089         $param->stattype = 'logins';
1090         $param->line1 = get_string('statslogins');
1091         $param->line2 = get_string('statsuniquelogins');
1092         if ($courseid == SITEID) {
1093             $param->extras = 'GROUP BY timeend';
1094         }
1095         break;
1097     case STATS_REPORT_READS:
1098         $param->fields = $DB->sql_concat('timeend','roleid').' AS uniqueid, timeend, roleid, stat1 as line1';
1099         $param->fieldscomplete = true; // set this to true to avoid anything adding stuff to the list and breaking complex queries.
1100         $param->aggregategroupby = 'roleid';
1101         $param->stattype = 'activity';
1102         $param->crosstab = true;
1103         $param->extras = 'GROUP BY timeend,roleid,stat1';
1104         if ($courseid == SITEID) {
1105             $param->fields = $DB->sql_concat('timeend','roleid').' AS uniqueid, timeend, roleid, sum(stat1) as line1';
1106             $param->extras = 'GROUP BY timeend,roleid';
1107         }
1108         break;
1110     case STATS_REPORT_WRITES:
1111         $param->fields = $DB->sql_concat('timeend','roleid').' AS uniqueid, timeend, roleid, stat2 as line1';
1112         $param->fieldscomplete = true; // set this to true to avoid anything adding stuff to the list and breaking complex queries.
1113         $param->aggregategroupby = 'roleid';
1114         $param->stattype = 'activity';
1115         $param->crosstab = true;
1116         $param->extras = 'GROUP BY timeend,roleid,stat2';
1117         if ($courseid == SITEID) {
1118             $param->fields = $DB->sql_concat('timeend','roleid').' AS uniqueid, timeend, roleid, sum(stat2) as line1';
1119             $param->extras = 'GROUP BY timeend,roleid';
1120         }
1121         break;
1123     case STATS_REPORT_ACTIVITY:
1124         $param->fields = $DB->sql_concat('timeend','roleid').' AS uniqueid, timeend, roleid, sum(stat1+stat2) as line1';
1125         $param->fieldscomplete = true; // set this to true to avoid anything adding stuff to the list and breaking complex queries.
1126         $param->aggregategroupby = 'roleid';
1127         $param->stattype = 'activity';
1128         $param->crosstab = true;
1129         $param->extras = 'GROUP BY timeend,roleid';
1130         if ($courseid == SITEID) {
1131             $param->extras = 'GROUP BY timeend,roleid';
1132         }
1133         break;
1135     case STATS_REPORT_ACTIVITYBYROLE;
1136         $param->fields = 'stat1 AS line1, stat2 AS line2';
1137         $param->stattype = 'activity';
1138         $rolename = $DB->get_field('role','name', array('id'=>$roleid));
1139         $param->line1 = $rolename . get_string('statsreads');
1140         $param->line2 = $rolename . get_string('statswrites');
1141         if ($courseid == SITEID) {
1142             $param->extras = 'GROUP BY timeend';
1143         }
1144         break;
1146     // ******************** STATS_MODE_DETAILED ******************** //
1147     case STATS_REPORT_USER_ACTIVITY:
1148         $param->fields = 'statsreads as line1, statswrites as line2';
1149         $param->line1 = get_string('statsuserreads');
1150         $param->line2 = get_string('statsuserwrites');
1151         $param->stattype = 'activity';
1152         break;
1154     case STATS_REPORT_USER_ALLACTIVITY:
1155         $param->fields = 'statsreads+statswrites as line1';
1156         $param->line1 = get_string('statsuseractivity');
1157         $param->stattype = 'activity';
1158         break;
1160     case STATS_REPORT_USER_LOGINS:
1161         $param->fields = 'statsreads as line1';
1162         $param->line1 = get_string('statsuserlogins');
1163         $param->stattype = 'logins';
1164         break;
1166     case STATS_REPORT_USER_VIEW:
1167         $param->fields = 'statsreads as line1, statswrites as line2, statsreads+statswrites as line3';
1168         $param->line1 = get_string('statsuserreads');
1169         $param->line2 = get_string('statsuserwrites');
1170         $param->line3 = get_string('statsuseractivity');
1171         $param->stattype = 'activity';
1172         break;
1174     // ******************** STATS_MODE_RANKED ******************** //
1175     case STATS_REPORT_ACTIVE_COURSES:
1176         $param->fields = 'sum(stat1+stat2) AS line1';
1177         $param->stattype = 'activity';
1178         $param->orderby = 'line1 DESC';
1179         $param->line1 = get_string('activity');
1180         $param->graphline = 'line1';
1181         break;
1183     case STATS_REPORT_ACTIVE_COURSES_WEIGHTED:
1184         $threshold = 0;
1185         if (!empty($CFG->statsuserthreshold) && is_numeric($CFG->statsuserthreshold)) {
1186             $threshold = $CFG->statsuserthreshold;
1187         }
1188         $param->fields = '';
1189         $param->sql = 'SELECT activity.courseid, activity.all_activity AS line1, enrolments.highest_enrolments AS line2,
1190                         activity.all_activity / enrolments.highest_enrolments as line3
1191                        FROM (
1192                             SELECT courseid, sum(stat1+stat2) AS all_activity
1193                               FROM {stats_'.$param->table.'}
1194                              WHERE stattype=\'activity\' AND timeend >= '.$param->timeafter.' AND roleid = 0 GROUP BY courseid
1195                        ) activity
1196                        INNER JOIN
1197                             (
1198                             SELECT courseid, max(stat1) AS highest_enrolments
1199                               FROM {stats_'.$param->table.'}
1200                              WHERE stattype=\'enrolments\' AND timeend >= '.$param->timeafter.' AND stat1 > '.$threshold.'
1201                           GROUP BY courseid
1202                       ) enrolments
1203                       ON (activity.courseid = enrolments.courseid)
1204                       ORDER BY line3 DESC';
1205         $param->line1 = get_string('activity');
1206         $param->line2 = get_string('users');
1207         $param->line3 = get_string('activityweighted');
1208         $param->graphline = 'line3';
1209         break;
1211     case STATS_REPORT_PARTICIPATORY_COURSES:
1212         $threshold = 0;
1213         if (!empty($CFG->statsuserthreshold) && is_numeric($CFG->statsuserthreshold)) {
1214             $threshold = $CFG->statsuserthreshold;
1215         }
1216         $param->fields = '';
1217         $param->sql = 'SELECT courseid, ' . $DB->sql_ceil('avg(all_enrolments)') . ' as line1, ' .
1218                          $DB->sql_ceil('avg(active_enrolments)') . ' as line2, avg(proportion_active) AS line3
1219                        FROM (
1220                            SELECT courseid, timeend, stat2 as active_enrolments,
1221                                   stat1 as all_enrolments, '.$DB->sql_cast_char2real('stat2').'/'.$DB->sql_cast_char2real('stat1').' AS proportion_active
1222                              FROM {stats_'.$param->table.'}
1223                             WHERE stattype=\'enrolments\' AND roleid = 0 AND stat1 > '.$threshold.'
1224                        ) aq
1225                        WHERE timeend >= '.$param->timeafter.'
1226                        GROUP BY courseid
1227                        ORDER BY line3 DESC';
1229         $param->line1 = get_string('users');
1230         $param->line2 = get_string('activeusers');
1231         $param->line3 = get_string('participationratio');
1232         $param->graphline = 'line3';
1233         break;
1235     case STATS_REPORT_PARTICIPATORY_COURSES_RW:
1236         $param->fields = '';
1237         $param->sql =  'SELECT courseid, sum(views) AS line1, sum(posts) AS line2,
1238                            avg(proportion_active) AS line3
1239                          FROM (
1240                            SELECT courseid, timeend, stat1 as views, stat2 AS posts,
1241                                   '.$DB->sql_cast_char2real('stat2').'/'.$DB->sql_cast_char2real('stat1').' as proportion_active
1242                              FROM {stats_'.$param->table.'}
1243                             WHERE stattype=\'activity\' AND roleid = 0 AND stat1 > 0
1244                        ) aq
1245                        WHERE timeend >= '.$param->timeafter.'
1246                        GROUP BY courseid
1247                        ORDER BY line3 DESC';
1248         $param->line1 = get_string('views');
1249         $param->line2 = get_string('posts');
1250         $param->line3 = get_string('participationratio');
1251         $param->graphline = 'line3';
1252         break;
1253     }
1255     /*
1256     if ($courseid == SITEID && $mode != STATS_MODE_RANKED) { // just aggregate all courses.
1257         $param->fields = preg_replace('/(?:sum)([a-zA-Z0-9+_]*)\W+as\W+([a-zA-Z0-9_]*)/i','sum($1) as $2',$param->fields);
1258         $param->extras = ' GROUP BY timeend'.((!empty($param->aggregategroupby)) ? ','.$param->aggregategroupby : '');
1259     }
1260     */
1261     //TODO must add the SITEID reports to the rest of the reports.
1262     return $param;
1265 function stats_get_view_actions() {
1266     return array('view','view all','history');
1269 function stats_get_post_actions() {
1270     return array('add','delete','edit','add mod','delete mod','edit section'.'enrol','loginas','new','unenrol','update','update mod');
1273 function stats_get_action_names($str) {
1274     global $CFG, $DB;
1276     $mods = $DB->get_records('modules');
1277     $function = 'stats_get_'.$str.'_actions';
1278     $actions = $function();
1279     foreach ($mods as $mod) {
1280         $file = $CFG->dirroot.'/mod/'.$mod->name.'/lib.php';
1281         if (!is_readable($file)) {
1282             continue;
1283         }
1284         require_once($file);
1285         $function = $mod->name.'_get_'.$str.'_actions';
1286         if (function_exists($function)) {
1287             $mod_actions = $function();
1288             if (is_array($mod_actions)) {
1289                 $actions = array_merge($actions, $mod_actions);
1290             }
1291         }
1292     }
1294     // The array_values() forces a stack-like array
1295     // so we can later loop over safely...
1296     $actions =  array_values(array_unique($actions));
1297     $c = count($actions);
1298     for ($n=0;$n<$c;$n++) {
1299         $actions[$n] = "'" . $actions[$n] . "'"; // quote them for SQL
1300     }
1301     return $actions;
1304 function stats_get_time_options($now,$lastweekend,$lastmonthend,$earliestday,$earliestweek,$earliestmonth) {
1306     $now = stats_get_base_daily(time());
1307     // it's really important that it's TIMEEND in the table. ie, tuesday 00:00:00 is monday night.
1308     // so we need to take a day off here (essentially add a day to $now
1309     $now += 60*60*24;
1311     $timeoptions = array();
1313     if ($now - (60*60*24*7) >= $earliestday) {
1314         $timeoptions[STATS_TIME_LASTWEEK] = get_string('numweeks','moodle',1);
1315     }
1316     if ($now - (60*60*24*14) >= $earliestday) {
1317         $timeoptions[STATS_TIME_LAST2WEEKS] = get_string('numweeks','moodle',2);
1318     }
1319     if ($now - (60*60*24*21) >= $earliestday) {
1320         $timeoptions[STATS_TIME_LAST3WEEKS] = get_string('numweeks','moodle',3);
1321     }
1322     if ($now - (60*60*24*28) >= $earliestday) {
1323         $timeoptions[STATS_TIME_LAST4WEEKS] = get_string('numweeks','moodle',4);// show dailies up to (including) here.
1324     }
1325     if ($lastweekend - (60*60*24*56) >= $earliestweek) {
1326         $timeoptions[STATS_TIME_LAST2MONTHS] = get_string('nummonths','moodle',2);
1327     }
1328     if ($lastweekend - (60*60*24*84) >= $earliestweek) {
1329         $timeoptions[STATS_TIME_LAST3MONTHS] = get_string('nummonths','moodle',3);
1330     }
1331     if ($lastweekend - (60*60*24*112) >= $earliestweek) {
1332         $timeoptions[STATS_TIME_LAST4MONTHS] = get_string('nummonths','moodle',4);
1333     }
1334     if ($lastweekend - (60*60*24*140) >= $earliestweek) {
1335         $timeoptions[STATS_TIME_LAST5MONTHS] = get_string('nummonths','moodle',5);
1336     }
1337     if ($lastweekend - (60*60*24*168) >= $earliestweek) {
1338         $timeoptions[STATS_TIME_LAST6MONTHS] = get_string('nummonths','moodle',6); // show weeklies up to (including) here
1339     }
1340     if (strtotime('-7 months',$lastmonthend) >= $earliestmonth) {
1341         $timeoptions[STATS_TIME_LAST7MONTHS] = get_string('nummonths','moodle',7);
1342     }
1343     if (strtotime('-8 months',$lastmonthend) >= $earliestmonth) {
1344         $timeoptions[STATS_TIME_LAST8MONTHS] = get_string('nummonths','moodle',8);
1345     }
1346     if (strtotime('-9 months',$lastmonthend) >= $earliestmonth) {
1347         $timeoptions[STATS_TIME_LAST9MONTHS] = get_string('nummonths','moodle',9);
1348     }
1349     if (strtotime('-10 months',$lastmonthend) >= $earliestmonth) {
1350         $timeoptions[STATS_TIME_LAST10MONTHS] = get_string('nummonths','moodle',10);
1351     }
1352     if (strtotime('-11 months',$lastmonthend) >= $earliestmonth) {
1353         $timeoptions[STATS_TIME_LAST11MONTHS] = get_string('nummonths','moodle',11);
1354     }
1355     if (strtotime('-1 year',$lastmonthend) >= $earliestmonth) {
1356         $timeoptions[STATS_TIME_LASTYEAR] = get_string('lastyear');
1357     }
1359     $years = (int)date('y', $now) - (int)date('y', $earliestmonth);
1360     if ($years > 1) {
1361         for($i = 2; $i <= $years; $i++) {
1362             $timeoptions[$i*12+20] = get_string('numyears', 'moodle', $i);
1363         }
1364     }
1366     return $timeoptions;
1369 function stats_get_report_options($courseid,$mode) {
1370     global $CFG, $DB;
1372     $reportoptions = array();
1374     switch ($mode) {
1375     case STATS_MODE_GENERAL:
1376         $reportoptions[STATS_REPORT_ACTIVITY] = get_string('statsreport'.STATS_REPORT_ACTIVITY);
1377         if ($courseid != SITEID && $context = get_context_instance(CONTEXT_COURSE, $courseid)) {
1378             $sql = 'SELECT r.id,r.name FROM {role} r JOIN {stats_daily} s ON s.roleid = r.id WHERE s.courseid = '.$courseid;
1379             if ($roles = $DB->get_records_sql($sql)) {
1380                 foreach ($roles as $role) {
1381                     $reportoptions[STATS_REPORT_ACTIVITYBYROLE.$role->id] = get_string('statsreport'.STATS_REPORT_ACTIVITYBYROLE). ' '.$role->name;
1382                 }
1383             }
1384         }
1385         $reportoptions[STATS_REPORT_READS] = get_string('statsreport'.STATS_REPORT_READS);
1386         $reportoptions[STATS_REPORT_WRITES] = get_string('statsreport'.STATS_REPORT_WRITES);
1387         if ($courseid == SITEID) {
1388             $reportoptions[STATS_REPORT_LOGINS] = get_string('statsreport'.STATS_REPORT_LOGINS);
1389         }
1391         break;
1392     case STATS_MODE_DETAILED:
1393         $reportoptions[STATS_REPORT_USER_ACTIVITY] = get_string('statsreport'.STATS_REPORT_USER_ACTIVITY);
1394         $reportoptions[STATS_REPORT_USER_ALLACTIVITY] = get_string('statsreport'.STATS_REPORT_USER_ALLACTIVITY);
1395         if (has_capability('coursereport/stats:view', get_context_instance(CONTEXT_SYSTEM))) {
1396             $site = get_site();
1397             $reportoptions[STATS_REPORT_USER_LOGINS] = get_string('statsreport'.STATS_REPORT_USER_LOGINS);
1398         }
1399         break;
1400     case STATS_MODE_RANKED:
1401         if (has_capability('coursereport/stats:view', get_context_instance(CONTEXT_SYSTEM))) {
1402             $reportoptions[STATS_REPORT_ACTIVE_COURSES] = get_string('statsreport'.STATS_REPORT_ACTIVE_COURSES);
1403             $reportoptions[STATS_REPORT_ACTIVE_COURSES_WEIGHTED] = get_string('statsreport'.STATS_REPORT_ACTIVE_COURSES_WEIGHTED);
1404             $reportoptions[STATS_REPORT_PARTICIPATORY_COURSES] = get_string('statsreport'.STATS_REPORT_PARTICIPATORY_COURSES);
1405             $reportoptions[STATS_REPORT_PARTICIPATORY_COURSES_RW] = get_string('statsreport'.STATS_REPORT_PARTICIPATORY_COURSES_RW);
1406         }
1407      break;
1408     }
1410     return $reportoptions;
1413 function stats_fix_zeros($stats,$timeafter,$timestr,$line2=true,$line3=false) {
1415     if (empty($stats)) {
1416         return;
1417     }
1419     $timestr = str_replace('user_','',$timestr); // just in case.
1420     $fun = 'stats_get_base_'.$timestr;
1422     $now = $fun();
1424     $times = array();
1425     // add something to timeafter since it is our absolute base
1426     $actualtimes = array();
1427     foreach ($stats as $statid=>$s) {
1428         //normalize the times in stats - those might have been created in different timezone, DST etc.
1429         $s->timeend = $fun($s->timeend + 60*60*5);
1430         $stats[$statid] = $s;
1432         $actualtimes[] = $s->timeend;
1433     }
1435     $timeafter = array_pop(array_values($actualtimes));
1437     while ($timeafter < $now) {
1438         $times[] = $timeafter;
1439         if ($timestr == 'daily') {
1440             $timeafter = stats_get_next_day_start($timeafter);
1441         } else if ($timestr == 'weekly') {
1442             $timeafter = stats_get_next_week_start($timeafter);
1443         } else if ($timestr == 'monthly') {
1444             $timeafter = stats_get_next_month_start($timeafter);
1445         } else {
1446             return $stats; // this will put us in a never ending loop.
1447         }
1448     }
1450     foreach ($times as $count => $time) {
1451         if (!in_array($time,$actualtimes) && $count != count($times) -1) {
1452             $newobj = new StdClass;
1453             $newobj->timeend = $time;
1454             $newobj->id = 0;
1455             $newobj->roleid = 0;
1456             $newobj->line1 = 0;
1457             if (!empty($line2)) {
1458                 $newobj->line2 = 0;
1459             }
1460             if (!empty($line3)) {
1461                 $newobj->line3 = 0;
1462             }
1463             $newobj->zerofixed = true;
1464             $stats[] = $newobj;
1465         }
1466     }
1468     usort($stats,"stats_compare_times");
1469     return $stats;
1473 // helper function to sort arrays by $obj->timeend
1474 function stats_compare_times($a,$b) {
1475    if ($a->timeend == $b->timeend) {
1476        return 0;
1477    }
1478    return ($a->timeend > $b->timeend) ? -1 : 1;
1481 function stats_check_uptodate($courseid=0) {
1482     global $CFG, $DB;
1484     if (empty($courseid)) {
1485         $courseid = SITEID;
1486     }
1488     $latestday = stats_get_start_from('daily');
1490     if ((time() - 60*60*24*2) < $latestday) { // we're ok
1491         return NULL;
1492     }
1494     $a = new object();
1495     $a->daysdone = $DB->get_field_sql("SELECT COUNT(DISTINCT(timeend)) FROM {stats_daily}");
1497     // how many days between the last day and now?
1498     $a->dayspending = ceil((stats_get_base_daily() - $latestday)/(60*60*24));
1500     if ($a->dayspending == 0 && $a->daysdone != 0) {
1501         return NULL; // we've only just started...
1502     }
1504     //return error as string
1505     return get_string('statscatchupmode','error',$a);