Merge branch '44315-27' of git://github.com/samhemelryk/moodle
[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    core
20  * @subpackage stats
21  * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
27 /** THESE CONSTANTS ARE USED FOR THE REPORTING PAGE. */
29 define('STATS_REPORT_LOGINS',1); // double impose logins and unique logins on a line graph. site course only.
30 define('STATS_REPORT_READS',2); // double impose student reads and teacher reads on a line graph.
31 define('STATS_REPORT_WRITES',3); // double impose student writes and teacher writes on a line graph.
32 define('STATS_REPORT_ACTIVITY',4); // 2+3 added up, teacher vs student.
33 define('STATS_REPORT_ACTIVITYBYROLE',5); // all activity, reads vs writes, selected by role.
35 // user level stats reports.
36 define('STATS_REPORT_USER_ACTIVITY',7);
37 define('STATS_REPORT_USER_ALLACTIVITY',8);
38 define('STATS_REPORT_USER_LOGINS',9);
39 define('STATS_REPORT_USER_VIEW',10);  // this is the report you see on the user profile.
41 // admin only ranking stats reports
42 define('STATS_REPORT_ACTIVE_COURSES',11);
43 define('STATS_REPORT_ACTIVE_COURSES_WEIGHTED',12);
44 define('STATS_REPORT_PARTICIPATORY_COURSES',13);
45 define('STATS_REPORT_PARTICIPATORY_COURSES_RW',14);
47 // start after 0 = show dailies.
48 define('STATS_TIME_LASTWEEK',1);
49 define('STATS_TIME_LAST2WEEKS',2);
50 define('STATS_TIME_LAST3WEEKS',3);
51 define('STATS_TIME_LAST4WEEKS',4);
53 // start after 10 = show weeklies
54 define('STATS_TIME_LAST2MONTHS',12);
56 define('STATS_TIME_LAST3MONTHS',13);
57 define('STATS_TIME_LAST4MONTHS',14);
58 define('STATS_TIME_LAST5MONTHS',15);
59 define('STATS_TIME_LAST6MONTHS',16);
61 // start after 20 = show monthlies
62 define('STATS_TIME_LAST7MONTHS',27);
63 define('STATS_TIME_LAST8MONTHS',28);
64 define('STATS_TIME_LAST9MONTHS',29);
65 define('STATS_TIME_LAST10MONTHS',30);
66 define('STATS_TIME_LAST11MONTHS',31);
67 define('STATS_TIME_LASTYEAR',32);
69 // different modes for what reports to offer
70 define('STATS_MODE_GENERAL',1);
71 define('STATS_MODE_DETAILED',2);
72 define('STATS_MODE_RANKED',3); // admins only - ranks courses
74 // Output string when nodebug is on
75 define('STATS_PLACEHOLDER_OUTPUT', '.');
77 /**
78  * Print daily cron progress
79  * @param string $ident
80  */
81 function stats_progress($ident) {
82     static $start = 0;
83     static $init  = 0;
85     if ($ident == 'init') {
86         $init = $start = microtime(true);
87         return;
88     }
90     $elapsed = round(microtime(true) - $start);
91     $start   = microtime(true);
93     if (debugging('', DEBUG_ALL)) {
94         mtrace("$ident:$elapsed ", '');
95     } else {
96         mtrace(STATS_PLACEHOLDER_OUTPUT, '');
97     }
98 }
100 /**
101  * Execute individual daily statistics queries
102  *
103  * @param string $sql The query to run
104  * @return boolean success
105  */
106 function stats_run_query($sql, $parameters = array()) {
107     global $DB;
109     try {
110         $DB->execute($sql, $parameters);
111     } catch (dml_exception $e) {
113        if (debugging('', DEBUG_ALL)) {
114            mtrace($e->getMessage());
115        }
116        return false;
117     }
118     return true;
121 /**
122  * Execute daily statistics gathering
123  *
124  * @param int $maxdays maximum number of days to be processed
125  * @return boolean success
126  */
127 function stats_cron_daily($maxdays=1) {
128     global $CFG, $DB;
130     $now = time();
132     $fpcontext = context_course::instance(SITEID, MUST_EXIST);
134     // read last execution date from db
135     if (!$timestart = get_config(NULL, 'statslastdaily')) {
136         $timestart = stats_get_base_daily(stats_get_start_from('daily'));
137         set_config('statslastdaily', $timestart);
138     }
140     // calculate scheduled time
141     $scheduledtime = stats_get_base_daily() + $CFG->statsruntimestarthour*60*60 + $CFG->statsruntimestartminute*60;
143     // Note: This will work fine for sites running cron each 4 hours or less (hopefully, 99.99% of sites). MDL-16709
144     // check to make sure we're due to run, at least 20 hours after last run
145     if (isset($CFG->statslastexecution) && ((time() - 20*60*60) < $CFG->statslastexecution)) {
146         mtrace("...preventing stats to run, last execution was less than 20 hours ago.");
147         return false;
148     // also check that we are a max of 4 hours after scheduled time, stats won't run after that
149     } else if (time() > $scheduledtime + 4*60*60) {
150         mtrace("...preventing stats to run, more than 4 hours since scheduled time.");
151         return false;
152     } else {
153         set_config('statslastexecution', time()); /// Grab this execution as last one
154     }
156     $nextmidnight = stats_get_next_day_start($timestart);
158     // are there any days that need to be processed?
159     if ($now < $nextmidnight) {
160         return true; // everything ok and up-to-date
161     }
164     $timeout = empty($CFG->statsmaxruntime) ? 60*60*24 : $CFG->statsmaxruntime;
166     if (!set_cron_lock('statsrunning', $now + $timeout)) {
167         return false;
168     }
170     // first delete entries that should not be there yet
171     $DB->delete_records_select('stats_daily',      "timeend > $timestart");
172     $DB->delete_records_select('stats_user_daily', "timeend > $timestart");
174     // Read in a few things we'll use later
175     $viewactions = stats_get_action_names('view');
176     $postactions = stats_get_action_names('post');
178     $guest           = (int)$CFG->siteguest;
179     $guestrole       = (int)$CFG->guestroleid;
180     $defaultfproleid = (int)$CFG->defaultfrontpageroleid;
182     mtrace("Running daily statistics gathering, starting at $timestart:");
183     cron_trace_time_and_memory();
185     $days  = 0;
186     $total = 0;
187     $failed  = false; // failed stats flag
188     $timeout = false;
190     if (!stats_temp_table_create()) {
191         $days = 1;
192         $failed = true;
193     }
194     mtrace('Temporary tables created');
196     if(!stats_temp_table_setup()) {
197         $days = 1;
198         $failed = true;
199     }
200     mtrace('Enrolments calculated');
202     $totalactiveusers = $DB->count_records('user', array('deleted' => '0'));
204     while (!$failed && ($now > $nextmidnight)) {
205         if ($days >= $maxdays) {
206             $timeout = true;
207             break;
208         }
210         $days++;
211         core_php_time_limit::raise($timeout - 200);
213         if ($days > 1) {
214             // move the lock
215             set_cron_lock('statsrunning', time() + $timeout, true);
216         }
218         $daystart = time();
220         stats_progress('init');
222         if (!stats_temp_table_fill($timestart, $nextmidnight)) {
223             $failed = true;
224             break;
225         }
227         // Find out if any logs available for this day
228         $sql = "SELECT 'x' FROM {temp_log1} l";
229         $logspresent = $DB->get_records_sql($sql, null, 0, 1);
231         if ($logspresent) {
232             // Insert blank record to force Query 10 to generate additional row when no logs for
233             // the site with userid 0 exist.  Added for backwards compatibility.
234             $DB->insert_record('temp_log1', array('userid' => 0, 'course' => SITEID, 'action' => ''));
235         }
237         // Calculate the number of active users today
238         $sql = 'SELECT COUNT(DISTINCT u.id)
239                   FROM {user} u
240                   JOIN {temp_log1} l ON l.userid = u.id
241                  WHERE u.deleted = 0';
242         $dailyactiveusers = $DB->count_records_sql($sql);
244         stats_progress('0');
246         // Process login info first
247         // Note: PostgreSQL doesn't like aliases in HAVING clauses
248         $sql = "INSERT INTO {temp_stats_user_daily}
249                             (stattype, timeend, courseid, userid, statsreads)
251                 SELECT 'logins', $nextmidnight AS timeend, ".SITEID." AS courseid,
252                         userid, COUNT(id) AS statsreads
253                   FROM {temp_log1} l
254                  WHERE action = 'login'
255               GROUP BY userid
256                 HAVING COUNT(id) > 0";
258         if ($logspresent && !stats_run_query($sql)) {
259             $failed = true;
260             break;
261         }
262         $DB->update_temp_table_stats();
264         stats_progress('1');
266         $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2)
268                 SELECT 'logins' AS stattype, $nextmidnight AS timeend, ".SITEID." AS courseid, 0,
269                        COALESCE(SUM(statsreads), 0) as stat1, COUNT('x') as stat2
270                   FROM {temp_stats_user_daily}
271                  WHERE stattype = 'logins' AND timeend = $nextmidnight";
273         if ($logspresent && !stats_run_query($sql)) {
274             $failed = true;
275             break;
276         }
277         stats_progress('2');
280         // Enrolments and active enrolled users
281         //
282         // Unfortunately, we do not know how many users were registered
283         // at given times in history :-(
284         // - stat1: enrolled users
285         // - stat2: enrolled users active in this period
286         // - SITEID is special case here, because it's all about default enrolment
287         //   in that case, we'll count non-deleted users.
288         //
290         $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2)
292                 SELECT 'enrolments' as stattype, $nextmidnight as timeend, courseid, roleid,
293                         COUNT(DISTINCT userid) as stat1, 0 as stat2
294                   FROM {temp_enroled}
295               GROUP BY courseid, roleid";
297         if (!stats_run_query($sql)) {
298             $failed = true;
299             break;
300         }
301         stats_progress('3');
303         // Set stat2 to the number distinct users with role assignments in the course that were active
304         // using table alias in UPDATE does not work in pg < 8.2
305         $sql = "UPDATE {temp_stats_daily}
306                    SET stat2 = (
308                     SELECT COUNT(DISTINCT userid)
309                       FROM {temp_enroled} te
310                      WHERE roleid = {temp_stats_daily}.roleid
311                        AND courseid = {temp_stats_daily}.courseid
312                        AND EXISTS (
314                         SELECT 'x'
315                           FROM {temp_log1} l
316                          WHERE l.course = {temp_stats_daily}.courseid
317                            AND l.userid = te.userid
318                                   )
319                                )
320                  WHERE {temp_stats_daily}.stattype = 'enrolments'
321                    AND {temp_stats_daily}.timeend = $nextmidnight
322                    AND {temp_stats_daily}.courseid IN (
324                     SELECT DISTINCT course FROM {temp_log2})";
326         if ($logspresent && !stats_run_query($sql, array('courselevel'=>CONTEXT_COURSE))) {
327             $failed = true;
328             break;
329         }
330         stats_progress('4');
332         // Now get course total enrolments (roleid==0) - except frontpage
333         $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2)
335                 SELECT 'enrolments', $nextmidnight AS timeend, te.courseid AS courseid, 0 AS roleid,
336                        COUNT(DISTINCT userid) AS stat1, 0 AS stat2
337                   FROM {temp_enroled} te
338               GROUP BY courseid
339                 HAVING COUNT(DISTINCT userid) > 0";
341         if ($logspresent && !stats_run_query($sql)) {
342             $failed = true;
343             break;
344         }
345         stats_progress('5');
347         // Set stat 2 to the number of enrolled users who were active in the course
348         $sql = "UPDATE {temp_stats_daily}
349                    SET stat2 = (
351                     SELECT COUNT(DISTINCT te.userid)
352                       FROM {temp_enroled} te
353                      WHERE te.courseid = {temp_stats_daily}.courseid
354                        AND EXISTS (
356                         SELECT 'x'
357                           FROM {temp_log1} l
358                          WHERE l.course = {temp_stats_daily}.courseid
359                            AND l.userid = te.userid
360                                   )
361                                )
363                  WHERE {temp_stats_daily}.stattype = 'enrolments'
364                    AND {temp_stats_daily}.timeend = $nextmidnight
365                    AND {temp_stats_daily}.roleid = 0
366                    AND {temp_stats_daily}.courseid IN (
368                     SELECT l.course
369                       FROM {temp_log2} l
370                      WHERE l.course <> ".SITEID.")";
372         if ($logspresent && !stats_run_query($sql, array())) {
373             $failed = true;
374             break;
375         }
376         stats_progress('6');
378         // Frontpage(==site) enrolments total
379         $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2)
381                 SELECT 'enrolments', $nextmidnight, ".SITEID.", 0, $totalactiveusers AS stat1,
382                        $dailyactiveusers AS stat2" .
383                 $DB->sql_null_from_clause();
385         if ($logspresent && !stats_run_query($sql)) {
386             $failed = true;
387             break;
388         }
389         // The steps up until this point, all add to {temp_stats_daily} and don't use new tables.
390         // There is no point updating statistics as they won't be used until the DELETE below.
391         $DB->update_temp_table_stats();
393         stats_progress('7');
395         // Default frontpage role enrolments are all site users (not deleted)
396         if ($defaultfproleid) {
397             // first remove default frontpage role counts if created by previous query
398             $sql = "DELETE
399                       FROM {temp_stats_daily}
400                      WHERE stattype = 'enrolments'
401                        AND courseid = ".SITEID."
402                        AND roleid = $defaultfproleid
403                        AND timeend = $nextmidnight";
405             if ($logspresent && !stats_run_query($sql)) {
406                 $failed = true;
407                 break;
408             }
409             stats_progress('8');
411             $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2)
413                     SELECT 'enrolments', $nextmidnight, ".SITEID.", $defaultfproleid,
414                            $totalactiveusers AS stat1, $dailyactiveusers AS stat2" .
415                     $DB->sql_null_from_clause();
417             if ($logspresent && !stats_run_query($sql)) {
418                 $failed = true;
419                 break;
420             }
421             stats_progress('9');
423         } else {
424             stats_progress('x');
425             stats_progress('x');
426         }
429         /// individual user stats (including not-logged-in) in each course, this is slow - reuse this data if possible
430         list($viewactionssql, $params1) = $DB->get_in_or_equal($viewactions, SQL_PARAMS_NAMED, 'view');
431         list($postactionssql, $params2) = $DB->get_in_or_equal($postactions, SQL_PARAMS_NAMED, 'post');
432         $sql = "INSERT INTO {temp_stats_user_daily} (stattype, timeend, courseid, userid, statsreads, statswrites)
434                 SELECT 'activity' AS stattype, $nextmidnight AS timeend, course AS courseid, userid,
435                        SUM(CASE WHEN action $viewactionssql THEN 1 ELSE 0 END) AS statsreads,
436                        SUM(CASE WHEN action $postactionssql THEN 1 ELSE 0 END) AS statswrites
437                   FROM {temp_log1} l
438               GROUP BY userid, course";
440         if ($logspresent && !stats_run_query($sql, array_merge($params1, $params2))) {
441             $failed = true;
442             break;
443         }
444         stats_progress('10');
447         /// How many view/post actions in each course total
448         $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2)
450                 SELECT 'activity' AS stattype, $nextmidnight AS timeend, c.id AS courseid, 0,
451                        SUM(CASE WHEN l.action $viewactionssql THEN 1 ELSE 0 END) AS stat1,
452                        SUM(CASE WHEN l.action $postactionssql THEN 1 ELSE 0 END) AS stat2
453                   FROM {course} c, {temp_log1} l
454                  WHERE l.course = c.id
455               GROUP BY c.id";
457         if ($logspresent && !stats_run_query($sql, array_merge($params1, $params2))) {
458             $failed = true;
459             break;
460         }
461         stats_progress('11');
464         /// how many view actions for each course+role - excluding guests and frontpage
466         $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2)
468                 SELECT 'activity', $nextmidnight AS timeend, courseid, roleid, SUM(statsreads), SUM(statswrites)
469                   FROM (
471                     SELECT pl.courseid, pl.roleid, sud.statsreads, sud.statswrites
472                       FROM {temp_stats_user_daily} sud, (
474                         SELECT DISTINCT te.userid, te.roleid, te.courseid
475                           FROM {temp_enroled} te
476                          WHERE te.roleid <> $guestrole
477                            AND te.userid <> $guest
478                                                         ) pl
480                      WHERE sud.userid = pl.userid
481                        AND sud.courseid = pl.courseid
482                        AND sud.timeend = $nextmidnight
483                        AND sud.stattype='activity'
484                        ) inline_view
486               GROUP BY courseid, roleid
487                 HAVING SUM(statsreads) > 0 OR SUM(statswrites) > 0";
489         if ($logspresent && !stats_run_query($sql, array('courselevel'=>CONTEXT_COURSE))) {
490             $failed = true;
491             break;
492         }
493         stats_progress('12');
495         /// how many view actions from guests only in each course - excluding frontpage
496         /// normal users may enter course with temporary guest access too
498         $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2)
500                 SELECT 'activity', $nextmidnight AS timeend, courseid, $guestrole AS roleid,
501                        SUM(statsreads), SUM(statswrites)
502                   FROM (
504                     SELECT sud.courseid, sud.statsreads, sud.statswrites
505                       FROM {temp_stats_user_daily} sud
506                      WHERE sud.timeend = $nextmidnight
507                        AND sud.courseid <> ".SITEID."
508                        AND sud.stattype='activity'
509                        AND (sud.userid = $guest OR sud.userid NOT IN (
511                         SELECT userid
512                           FROM {temp_enroled} te
513                          WHERE te.courseid = sud.courseid
514                                                                      ))
515                        ) inline_view
517               GROUP BY courseid
518                 HAVING SUM(statsreads) > 0 OR SUM(statswrites) > 0";
520         if ($logspresent && !stats_run_query($sql, array())) {
521             $failed = true;
522             break;
523         }
524         stats_progress('13');
527         /// How many view actions for each role on frontpage - excluding guests, not-logged-in and default frontpage role
528         $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2)
530                 SELECT 'activity', $nextmidnight AS timeend, courseid, roleid,
531                        SUM(statsreads), SUM(statswrites)
532                   FROM (
533                     SELECT pl.courseid, pl.roleid, sud.statsreads, sud.statswrites
534                       FROM {temp_stats_user_daily} sud, (
536                         SELECT DISTINCT ra.userid, ra.roleid, c.instanceid AS courseid
537                           FROM {role_assignments} ra
538                           JOIN {context} c ON c.id = ra.contextid
539                          WHERE ra.contextid = :fpcontext
540                            AND ra.roleid <> $defaultfproleid
541                            AND ra.roleid <> $guestrole
542                            AND ra.userid <> $guest
543                                                    ) pl
544                      WHERE sud.userid = pl.userid
545                        AND sud.courseid = pl.courseid
546                        AND sud.timeend = $nextmidnight
547                        AND sud.stattype='activity'
548                        ) inline_view
550               GROUP BY courseid, roleid
551                 HAVING SUM(statsreads) > 0 OR SUM(statswrites) > 0";
553         if ($logspresent && !stats_run_query($sql, array('fpcontext'=>$fpcontext->id))) {
554             $failed = true;
555             break;
556         }
557         stats_progress('14');
560         // How many view actions for default frontpage role on frontpage only
561         $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2)
563                 SELECT 'activity', timeend, courseid, $defaultfproleid AS roleid,
564                        SUM(statsreads), SUM(statswrites)
565                   FROM (
566                     SELECT sud.timeend AS timeend, sud.courseid, sud.statsreads, sud.statswrites
567                       FROM {temp_stats_user_daily} sud
568                      WHERE sud.timeend = :nextm
569                        AND sud.courseid = :siteid
570                        AND sud.stattype='activity'
571                        AND sud.userid <> $guest
572                        AND sud.userid <> 0
573                        AND sud.userid NOT IN (
575                         SELECT ra.userid
576                           FROM {role_assignments} ra
577                          WHERE ra.roleid <> $guestrole
578                            AND ra.roleid <> $defaultfproleid
579                            AND ra.contextid = :fpcontext)
580                        ) inline_view
582               GROUP BY timeend, courseid
583                 HAVING SUM(statsreads) > 0 OR SUM(statswrites) > 0";
585         if ($logspresent && !stats_run_query($sql, array('fpcontext'=>$fpcontext->id, 'siteid'=>SITEID, 'nextm'=>$nextmidnight))) {
586             $failed = true;
587             break;
588         }
589         $DB->update_temp_table_stats();
590         stats_progress('15');
592         // How many view actions for guests or not-logged-in on frontpage
593         $sql = "INSERT INTO {temp_stats_daily} (stattype, timeend, courseid, roleid, stat1, stat2)
595                 SELECT stattype, timeend, courseid, $guestrole AS roleid,
596                        SUM(statsreads) AS stat1, SUM(statswrites) AS stat2
597                   FROM (
598                     SELECT sud.stattype, sud.timeend, sud.courseid,
599                            sud.statsreads, sud.statswrites
600                       FROM {temp_stats_user_daily} sud
601                      WHERE (sud.userid = $guest OR sud.userid = 0)
602                        AND sud.timeend = $nextmidnight
603                        AND sud.courseid = ".SITEID."
604                        AND sud.stattype='activity'
605                        ) inline_view
606                  GROUP BY stattype, timeend, courseid
607                  HAVING SUM(statsreads) > 0 OR SUM(statswrites) > 0";
609         if ($logspresent && !stats_run_query($sql)) {
610             $failed = true;
611             break;
612         }
613         stats_progress('16');
615         stats_temp_table_clean();
617         stats_progress('out');
619         // remember processed days
620         set_config('statslastdaily', $nextmidnight);
621         $elapsed = time()-$daystart;
622         mtrace("  finished until $nextmidnight: ".userdate($nextmidnight)." (in $elapsed s)");
623         $total += $elapsed;
625         $timestart    = $nextmidnight;
626         $nextmidnight = stats_get_next_day_start($nextmidnight);
627     }
629     stats_temp_table_drop();
631     set_cron_lock('statsrunning', null);
633     if ($failed) {
634         $days--;
635         mtrace("...error occurred, completed $days days of statistics in {$total} s.");
636         return false;
638     } else if ($timeout) {
639         mtrace("...stopping early, reached maximum number of $maxdays days ({$total} s) - will continue next time.");
640         return false;
642     } else {
643         mtrace("...completed $days days of statistics in {$total} s.");
644         return true;
645     }
649 /**
650  * Execute weekly statistics gathering
651  * @return boolean success
652  */
653 function stats_cron_weekly() {
654     global $CFG, $DB;
656     $now = time();
658     // read last execution date from db
659     if (!$timestart = get_config(NULL, 'statslastweekly')) {
660         $timestart = stats_get_base_daily(stats_get_start_from('weekly'));
661         set_config('statslastweekly', $timestart);
662     }
664     $nextstartweek = stats_get_next_week_start($timestart);
666     // are there any weeks that need to be processed?
667     if ($now < $nextstartweek) {
668         return true; // everything ok and up-to-date
669     }
671     $timeout = empty($CFG->statsmaxruntime) ? 60*60*24 : $CFG->statsmaxruntime;
673     if (!set_cron_lock('statsrunning', $now + $timeout)) {
674         return false;
675     }
677     // fisrt delete entries that should not be there yet
678     $DB->delete_records_select('stats_weekly',      "timeend > $timestart");
679     $DB->delete_records_select('stats_user_weekly', "timeend > $timestart");
681     mtrace("Running weekly statistics gathering, starting at $timestart:");
682     cron_trace_time_and_memory();
684     $weeks = 0;
685     while ($now > $nextstartweek) {
686         core_php_time_limit::raise($timeout - 200);
687         $weeks++;
689         if ($weeks > 1) {
690             // move the lock
691             set_cron_lock('statsrunning', time() + $timeout, true);
692         }
694         $logtimesql  = "l.time >= $timestart AND l.time < $nextstartweek";
695         $stattimesql = "timeend > $timestart AND timeend <= $nextstartweek";
697         $weekstart = time();
698         stats_progress('init');
700     /// process login info first
701         $sql = "INSERT INTO {stats_user_weekly} (stattype, timeend, courseid, userid, statsreads)
703                 SELECT 'logins', timeend, courseid, userid, COUNT(statsreads)
704                   FROM (
705                            SELECT $nextstartweek AS timeend, ".SITEID." as courseid, l.userid, l.id AS statsreads
706                              FROM {log} l
707                             WHERE action = 'login' AND $logtimesql
708                        ) inline_view
709               GROUP BY timeend, courseid, userid
710                 HAVING COUNT(statsreads) > 0";
712         $DB->execute($sql);
714         stats_progress('1');
716         $sql = "INSERT INTO {stats_weekly} (stattype, timeend, courseid, roleid, stat1, stat2)
718                 SELECT 'logins' AS stattype, $nextstartweek AS timeend, ".SITEID." as courseid, 0,
719                        COALESCE((SELECT SUM(statsreads)
720                                    FROM {stats_user_weekly} s1
721                                   WHERE s1.stattype = 'logins' AND timeend = $nextstartweek), 0) AS nstat1,
722                        (SELECT COUNT('x')
723                           FROM {stats_user_weekly} s2
724                          WHERE s2.stattype = 'logins' AND timeend = $nextstartweek) AS nstat2" .
725                 $DB->sql_null_from_clause();
727         $DB->execute($sql);
729         stats_progress('2');
731     /// now enrolments averages
732         $sql = "INSERT INTO {stats_weekly} (stattype, timeend, courseid, roleid, stat1, stat2)
734                 SELECT 'enrolments', ntimeend, courseid, roleid, " . $DB->sql_ceil('AVG(stat1)') . ", " . $DB->sql_ceil('AVG(stat2)') . "
735                   FROM (
736                            SELECT $nextstartweek AS ntimeend, courseid, roleid, stat1, stat2
737                              FROM {stats_daily} sd
738                             WHERE stattype = 'enrolments' AND $stattimesql
739                        ) inline_view
740               GROUP BY ntimeend, courseid, roleid";
742         $DB->execute($sql);
744         stats_progress('3');
746     /// activity read/write averages
747         $sql = "INSERT INTO {stats_weekly} (stattype, timeend, courseid, roleid, stat1, stat2)
749                 SELECT 'activity', ntimeend, courseid, roleid, SUM(stat1), SUM(stat2)
750                   FROM (
751                            SELECT $nextstartweek AS ntimeend, courseid, roleid, stat1, stat2
752                              FROM {stats_daily}
753                             WHERE stattype = 'activity' AND $stattimesql
754                        ) inline_view
755               GROUP BY ntimeend, courseid, roleid";
757         $DB->execute($sql);
759         stats_progress('4');
761     /// user read/write averages
762         $sql = "INSERT INTO {stats_user_weekly} (stattype, timeend, courseid, userid, statsreads, statswrites)
764                 SELECT 'activity', ntimeend, courseid, userid, SUM(statsreads), SUM(statswrites)
765                   FROM (
766                            SELECT $nextstartweek AS ntimeend, courseid, userid, statsreads, statswrites
767                              FROM {stats_user_daily}
768                             WHERE stattype = 'activity' AND $stattimesql
769                        ) inline_view
770               GROUP BY ntimeend, courseid, userid";
772         $DB->execute($sql);
774         stats_progress('5');
776         set_config('statslastweekly', $nextstartweek);
777         $elapsed = time()-$weekstart;
778         mtrace(" finished until $nextstartweek: ".userdate($nextstartweek) ." (in $elapsed s)");
780         $timestart     = $nextstartweek;
781         $nextstartweek = stats_get_next_week_start($nextstartweek);
782     }
784     set_cron_lock('statsrunning', null);
785     mtrace("...completed $weeks weeks of statistics.");
786     return true;
789 /**
790  * Execute monthly statistics gathering
791  * @return boolean success
792  */
793 function stats_cron_monthly() {
794     global $CFG, $DB;
796     $now = time();
798     // read last execution date from db
799     if (!$timestart = get_config(NULL, 'statslastmonthly')) {
800         $timestart = stats_get_base_monthly(stats_get_start_from('monthly'));
801         set_config('statslastmonthly', $timestart);
802     }
804     $nextstartmonth = stats_get_next_month_start($timestart);
806     // are there any months that need to be processed?
807     if ($now < $nextstartmonth) {
808         return true; // everything ok and up-to-date
809     }
811     $timeout = empty($CFG->statsmaxruntime) ? 60*60*24 : $CFG->statsmaxruntime;
813     if (!set_cron_lock('statsrunning', $now + $timeout)) {
814         return false;
815     }
817     // fisr delete entries that should not be there yet
818     $DB->delete_records_select('stats_monthly', "timeend > $timestart");
819     $DB->delete_records_select('stats_user_monthly', "timeend > $timestart");
821     $startmonth = stats_get_base_monthly($now);
824     mtrace("Running monthly statistics gathering, starting at $timestart:");
825     cron_trace_time_and_memory();
827     $months = 0;
828     while ($now > $nextstartmonth) {
829         core_php_time_limit::raise($timeout - 200);
830         $months++;
832         if ($months > 1) {
833             // move the lock
834             set_cron_lock('statsrunning', time() + $timeout, true);
835         }
837         $logtimesql  = "l.time >= $timestart AND l.time < $nextstartmonth";
838         $stattimesql = "timeend > $timestart AND timeend <= $nextstartmonth";
840         $monthstart = time();
841         stats_progress('init');
843     /// process login info first
844         $sql = "INSERT INTO {stats_user_monthly} (stattype, timeend, courseid, userid, statsreads)
846                 SELECT 'logins', timeend, courseid, userid, COUNT(statsreads)
847                   FROM (
848                            SELECT $nextstartmonth AS timeend, ".SITEID." as courseid, l.userid, l.id AS statsreads
849                              FROM {log} l
850                             WHERE action = 'login' AND $logtimesql
851                        ) inline_view
852               GROUP BY timeend, courseid, userid";
854         $DB->execute($sql);
856         stats_progress('1');
858         $sql = "INSERT INTO {stats_monthly} (stattype, timeend, courseid, roleid, stat1, stat2)
860                 SELECT 'logins' AS stattype, $nextstartmonth AS timeend, ".SITEID." as courseid, 0,
861                        COALESCE((SELECT SUM(statsreads)
862                                    FROM {stats_user_monthly} s1
863                                   WHERE s1.stattype = 'logins' AND timeend = $nextstartmonth), 0) AS nstat1,
864                        (SELECT COUNT('x')
865                           FROM {stats_user_monthly} s2
866                          WHERE s2.stattype = 'logins' AND timeend = $nextstartmonth) AS nstat2" .
867                 $DB->sql_null_from_clause();
869         $DB->execute($sql);
871         stats_progress('2');
873     /// now enrolments averages
874         $sql = "INSERT INTO {stats_monthly} (stattype, timeend, courseid, roleid, stat1, stat2)
876                 SELECT 'enrolments', ntimeend, courseid, roleid, " . $DB->sql_ceil('AVG(stat1)') . ", " . $DB->sql_ceil('AVG(stat2)') . "
877                   FROM (
878                            SELECT $nextstartmonth AS ntimeend, courseid, roleid, stat1, stat2
879                              FROM {stats_daily} sd
880                             WHERE stattype = 'enrolments' AND $stattimesql
881                        ) inline_view
882               GROUP BY ntimeend, courseid, roleid";
884         $DB->execute($sql);
886         stats_progress('3');
888     /// activity read/write averages
889         $sql = "INSERT INTO {stats_monthly} (stattype, timeend, courseid, roleid, stat1, stat2)
891                 SELECT 'activity', ntimeend, courseid, roleid, SUM(stat1), SUM(stat2)
892                   FROM (
893                            SELECT $nextstartmonth AS ntimeend, courseid, roleid, stat1, stat2
894                              FROM {stats_daily}
895                             WHERE stattype = 'activity' AND $stattimesql
896                        ) inline_view
897               GROUP BY ntimeend, courseid, roleid";
899         $DB->execute($sql);
901         stats_progress('4');
903     /// user read/write averages
904         $sql = "INSERT INTO {stats_user_monthly} (stattype, timeend, courseid, userid, statsreads, statswrites)
906                 SELECT 'activity', ntimeend, courseid, userid, SUM(statsreads), SUM(statswrites)
907                   FROM (
908                            SELECT $nextstartmonth AS ntimeend, courseid, userid, statsreads, statswrites
909                              FROM {stats_user_daily}
910                             WHERE stattype = 'activity' AND $stattimesql
911                        ) inline_view
912               GROUP BY ntimeend, courseid, userid";
914         $DB->execute($sql);
916         stats_progress('5');
918         set_config('statslastmonthly', $nextstartmonth);
919         $elapsed = time() - $monthstart;
920         mtrace(" finished until $nextstartmonth: ".userdate($nextstartmonth) ." (in $elapsed s)");
922         $timestart      = $nextstartmonth;
923         $nextstartmonth = stats_get_next_month_start($nextstartmonth);
924     }
926     set_cron_lock('statsrunning', null);
927     mtrace("...completed $months months of statistics.");
928     return true;
931 /**
932  * Return starting date of stats processing
933  * @param string $str name of table - daily, weekly or monthly
934  * @return int timestamp
935  */
936 function stats_get_start_from($str) {
937     global $CFG, $DB;
939     // are there any data in stats table? Should not be...
940     if ($timeend = $DB->get_field_sql('SELECT MAX(timeend) FROM {stats_'.$str.'}')) {
941         return $timeend;
942     }
943     // decide what to do based on our config setting (either all or none or a timestamp)
944     switch ($CFG->statsfirstrun) {
945         case 'all':
946             if ($firstlog = $DB->get_field_sql('SELECT MIN(time) FROM {log}')) {
947                 return $firstlog;
948             }
949         default:
950             if (is_numeric($CFG->statsfirstrun)) {
951                 return time() - $CFG->statsfirstrun;
952             }
953             // not a number? use next instead
954         case 'none':
955             return strtotime('-3 day', time());
956     }
959 /**
960  * Start of day
961  * @param int $time timestamp
962  * @return start of day
963  */
964 function stats_get_base_daily($time=0) {
965     global $CFG;
967     if (empty($time)) {
968         $time = time();
969     }
970     if ($CFG->timezone == 99) {
971         $time = strtotime(date('d-M-Y', $time));
972         return $time;
973     } else {
974         $offset = get_timezone_offset($CFG->timezone);
975         $gtime = $time + $offset;
976         $gtime = intval($gtime / (60*60*24)) * 60*60*24;
977         return $gtime - $offset;
978     }
981 /**
982  * Start of week
983  * @param int $time timestamp
984  * @return start of week
985  */
986 function stats_get_base_weekly($time=0) {
987     global $CFG;
989     $time = stats_get_base_daily($time);
990     $startday = $CFG->calendar_startwday;
991     if ($CFG->timezone == 99) {
992         $thisday = date('w', $time);
993     } else {
994         $offset = get_timezone_offset($CFG->timezone);
995         $gtime = $time + $offset;
996         $thisday = gmdate('w', $gtime);
997     }
998     if ($thisday > $startday) {
999         $time = $time - (($thisday - $startday) * 60*60*24);
1000     } else if ($thisday < $startday) {
1001         $time = $time - ((7 + $thisday - $startday) * 60*60*24);
1002     }
1003     return $time;
1006 /**
1007  * Start of month
1008  * @param int $time timestamp
1009  * @return start of month
1010  */
1011 function stats_get_base_monthly($time=0) {
1012     global $CFG;
1014     if (empty($time)) {
1015         $time = time();
1016     }
1017     if ($CFG->timezone == 99) {
1018         return strtotime(date('1-M-Y', $time));
1020     } else {
1021         $time = stats_get_base_daily($time);
1022         $offset = get_timezone_offset($CFG->timezone);
1023         $gtime = $time + $offset;
1024         $day = gmdate('d', $gtime);
1025         if ($day == 1) {
1026             return $time;
1027         }
1028         return $gtime - (($day-1) * 60*60*24);
1029     }
1032 /**
1033  * Start of next day
1034  * @param int $time timestamp
1035  * @return start of next day
1036  */
1037 function stats_get_next_day_start($time) {
1038     $next = stats_get_base_daily($time);
1039     $next = $next + 60*60*26;
1040     $next = stats_get_base_daily($next);
1041     if ($next <= $time) {
1042         //DST trouble - prevent infinite loops
1043         $next = $next + 60*60*24;
1044     }
1045     return $next;
1048 /**
1049  * Start of next week
1050  * @param int $time timestamp
1051  * @return start of next week
1052  */
1053 function stats_get_next_week_start($time) {
1054     $next = stats_get_base_weekly($time);
1055     $next = $next + 60*60*24*9;
1056     $next = stats_get_base_weekly($next);
1057     if ($next <= $time) {
1058         //DST trouble - prevent infinite loops
1059         $next = $next + 60*60*24*7;
1060     }
1061     return $next;
1064 /**
1065  * Start of next month
1066  * @param int $time timestamp
1067  * @return start of next month
1068  */
1069 function stats_get_next_month_start($time) {
1070     $next = stats_get_base_monthly($time);
1071     $next = $next + 60*60*24*33;
1072     $next = stats_get_base_monthly($next);
1073     if ($next <= $time) {
1074         //DST trouble - prevent infinite loops
1075         $next = $next + 60*60*24*31;
1076     }
1077     return $next;
1080 /**
1081  * Remove old stats data
1082  */
1083 function stats_clean_old() {
1084     global $DB;
1085     mtrace("Running stats cleanup tasks...");
1086     cron_trace_time_and_memory();
1087     $deletebefore =  stats_get_base_monthly();
1089     // delete dailies older than 3 months (to be safe)
1090     $deletebefore = strtotime('-3 months', $deletebefore);
1091     $DB->delete_records_select('stats_daily',      "timeend < $deletebefore");
1092     $DB->delete_records_select('stats_user_daily', "timeend < $deletebefore");
1094     // delete weeklies older than 9  months (to be safe)
1095     $deletebefore = strtotime('-6 months', $deletebefore);
1096     $DB->delete_records_select('stats_weekly',      "timeend < $deletebefore");
1097     $DB->delete_records_select('stats_user_weekly', "timeend < $deletebefore");
1099     // don't delete monthlies
1101     mtrace("...stats cleanup finished");
1104 function stats_get_parameters($time,$report,$courseid,$mode,$roleid=0) {
1105     global $CFG, $DB;
1107     $param = new stdClass();
1108     $param->params = array();
1110     if ($time < 10) { // dailies
1111         // number of days to go back = 7* time
1112         $param->table = 'daily';
1113         $param->timeafter = strtotime("-".($time*7)." days",stats_get_base_daily());
1114     } elseif ($time < 20) { // weeklies
1115         // number of weeks to go back = time - 10 * 4 (weeks) + base week
1116         $param->table = 'weekly';
1117         $param->timeafter = strtotime("-".(($time - 10)*4)." weeks",stats_get_base_weekly());
1118     } else { // monthlies.
1119         // number of months to go back = time - 20 * months + base month
1120         $param->table = 'monthly';
1121         $param->timeafter = strtotime("-".($time - 20)." months",stats_get_base_monthly());
1122     }
1124     $param->extras = '';
1126     switch ($report) {
1127     // ******************** STATS_MODE_GENERAL ******************** //
1128     case STATS_REPORT_LOGINS:
1129         $param->fields = 'timeend,sum(stat1) as line1,sum(stat2) as line2';
1130         $param->fieldscomplete = true;
1131         $param->stattype = 'logins';
1132         $param->line1 = get_string('statslogins');
1133         $param->line2 = get_string('statsuniquelogins');
1134         if ($courseid == SITEID) {
1135             $param->extras = 'GROUP BY timeend';
1136         }
1137         break;
1139     case STATS_REPORT_READS:
1140         $param->fields = $DB->sql_concat('timeend','roleid').' AS uniqueid, timeend, roleid, stat1 as line1';
1141         $param->fieldscomplete = true; // set this to true to avoid anything adding stuff to the list and breaking complex queries.
1142         $param->aggregategroupby = 'roleid';
1143         $param->stattype = 'activity';
1144         $param->crosstab = true;
1145         $param->extras = 'GROUP BY timeend,roleid,stat1';
1146         if ($courseid == SITEID) {
1147             $param->fields = $DB->sql_concat('timeend','roleid').' AS uniqueid, timeend, roleid, sum(stat1) as line1';
1148             $param->extras = 'GROUP BY timeend,roleid';
1149         }
1150         break;
1152     case STATS_REPORT_WRITES:
1153         $param->fields = $DB->sql_concat('timeend','roleid').' AS uniqueid, timeend, roleid, stat2 as line1';
1154         $param->fieldscomplete = true; // set this to true to avoid anything adding stuff to the list and breaking complex queries.
1155         $param->aggregategroupby = 'roleid';
1156         $param->stattype = 'activity';
1157         $param->crosstab = true;
1158         $param->extras = 'GROUP BY timeend,roleid,stat2';
1159         if ($courseid == SITEID) {
1160             $param->fields = $DB->sql_concat('timeend','roleid').' AS uniqueid, timeend, roleid, sum(stat2) as line1';
1161             $param->extras = 'GROUP BY timeend,roleid';
1162         }
1163         break;
1165     case STATS_REPORT_ACTIVITY:
1166         $param->fields = $DB->sql_concat('timeend','roleid').' AS uniqueid, timeend, roleid, sum(stat1+stat2) as line1';
1167         $param->fieldscomplete = true; // set this to true to avoid anything adding stuff to the list and breaking complex queries.
1168         $param->aggregategroupby = 'roleid';
1169         $param->stattype = 'activity';
1170         $param->crosstab = true;
1171         $param->extras = 'GROUP BY timeend,roleid';
1172         if ($courseid == SITEID) {
1173             $param->extras = 'GROUP BY timeend,roleid';
1174         }
1175         break;
1177     case STATS_REPORT_ACTIVITYBYROLE;
1178         $param->fields = 'stat1 AS line1, stat2 AS line2';
1179         $param->stattype = 'activity';
1180         $rolename = $DB->get_field('role','name', array('id'=>$roleid));
1181         $param->line1 = $rolename . get_string('statsreads');
1182         $param->line2 = $rolename . get_string('statswrites');
1183         if ($courseid == SITEID) {
1184             $param->extras = 'GROUP BY timeend';
1185         }
1186         break;
1188     // ******************** STATS_MODE_DETAILED ******************** //
1189     case STATS_REPORT_USER_ACTIVITY:
1190         $param->fields = 'statsreads as line1, statswrites as line2';
1191         $param->line1 = get_string('statsuserreads');
1192         $param->line2 = get_string('statsuserwrites');
1193         $param->stattype = 'activity';
1194         break;
1196     case STATS_REPORT_USER_ALLACTIVITY:
1197         $param->fields = 'statsreads+statswrites as line1';
1198         $param->line1 = get_string('statsuseractivity');
1199         $param->stattype = 'activity';
1200         break;
1202     case STATS_REPORT_USER_LOGINS:
1203         $param->fields = 'statsreads as line1';
1204         $param->line1 = get_string('statsuserlogins');
1205         $param->stattype = 'logins';
1206         break;
1208     case STATS_REPORT_USER_VIEW:
1209         $param->fields = 'statsreads as line1, statswrites as line2, statsreads+statswrites as line3';
1210         $param->line1 = get_string('statsuserreads');
1211         $param->line2 = get_string('statsuserwrites');
1212         $param->line3 = get_string('statsuseractivity');
1213         $param->stattype = 'activity';
1214         break;
1216     // ******************** STATS_MODE_RANKED ******************** //
1217     case STATS_REPORT_ACTIVE_COURSES:
1218         $param->fields = 'sum(stat1+stat2) AS line1';
1219         $param->stattype = 'activity';
1220         $param->orderby = 'line1 DESC';
1221         $param->line1 = get_string('activity');
1222         $param->graphline = 'line1';
1223         break;
1225     case STATS_REPORT_ACTIVE_COURSES_WEIGHTED:
1226         $threshold = 0;
1227         if (!empty($CFG->statsuserthreshold) && is_numeric($CFG->statsuserthreshold)) {
1228             $threshold = $CFG->statsuserthreshold;
1229         }
1230         $param->fields = '';
1231         $param->sql = 'SELECT activity.courseid, activity.all_activity AS line1, enrolments.highest_enrolments AS line2,
1232                         activity.all_activity / enrolments.highest_enrolments as line3
1233                        FROM (
1234                             SELECT courseid, sum(stat1+stat2) AS all_activity
1235                               FROM {stats_'.$param->table.'}
1236                              WHERE stattype=\'activity\' AND timeend >= '.(int)$param->timeafter.' AND roleid = 0 GROUP BY courseid
1237                        ) activity
1238                        INNER JOIN
1239                             (
1240                             SELECT courseid, max(stat1) AS highest_enrolments
1241                               FROM {stats_'.$param->table.'}
1242                              WHERE stattype=\'enrolments\' AND timeend >= '.(int)$param->timeafter.' AND stat1 > '.(int)$threshold.'
1243                           GROUP BY courseid
1244                       ) enrolments
1245                       ON (activity.courseid = enrolments.courseid)
1246                       ORDER BY line3 DESC';
1247         $param->line1 = get_string('activity');
1248         $param->line2 = get_string('users');
1249         $param->line3 = get_string('activityweighted');
1250         $param->graphline = 'line3';
1251         break;
1253     case STATS_REPORT_PARTICIPATORY_COURSES:
1254         $threshold = 0;
1255         if (!empty($CFG->statsuserthreshold) && is_numeric($CFG->statsuserthreshold)) {
1256             $threshold = $CFG->statsuserthreshold;
1257         }
1258         $param->fields = '';
1259         $param->sql = 'SELECT courseid, ' . $DB->sql_ceil('avg(all_enrolments)') . ' as line1, ' .
1260                          $DB->sql_ceil('avg(active_enrolments)') . ' as line2, avg(proportion_active) AS line3
1261                        FROM (
1262                            SELECT courseid, timeend, stat2 as active_enrolments,
1263                                   stat1 as all_enrolments, '.$DB->sql_cast_char2real('stat2').'/'.$DB->sql_cast_char2real('stat1').' AS proportion_active
1264                              FROM {stats_'.$param->table.'}
1265                             WHERE stattype=\'enrolments\' AND roleid = 0 AND stat1 > '.(int)$threshold.'
1266                        ) aq
1267                        WHERE timeend >= '.(int)$param->timeafter.'
1268                        GROUP BY courseid
1269                        ORDER BY line3 DESC';
1271         $param->line1 = get_string('users');
1272         $param->line2 = get_string('activeusers');
1273         $param->line3 = get_string('participationratio');
1274         $param->graphline = 'line3';
1275         break;
1277     case STATS_REPORT_PARTICIPATORY_COURSES_RW:
1278         $param->fields = '';
1279         $param->sql =  'SELECT courseid, sum(views) AS line1, sum(posts) AS line2,
1280                            avg(proportion_active) AS line3
1281                          FROM (
1282                            SELECT courseid, timeend, stat1 as views, stat2 AS posts,
1283                                   '.$DB->sql_cast_char2real('stat2').'/'.$DB->sql_cast_char2real('stat1').' as proportion_active
1284                              FROM {stats_'.$param->table.'}
1285                             WHERE stattype=\'activity\' AND roleid = 0 AND stat1 > 0
1286                        ) aq
1287                        WHERE timeend >= '.(int)$param->timeafter.'
1288                        GROUP BY courseid
1289                        ORDER BY line3 DESC';
1290         $param->line1 = get_string('views');
1291         $param->line2 = get_string('posts');
1292         $param->line3 = get_string('participationratio');
1293         $param->graphline = 'line3';
1294         break;
1295     }
1297     /*
1298     if ($courseid == SITEID && $mode != STATS_MODE_RANKED) { // just aggregate all courses.
1299         $param->fields = preg_replace('/(?:sum)([a-zA-Z0-9+_]*)\W+as\W+([a-zA-Z0-9_]*)/i','sum($1) as $2',$param->fields);
1300         $param->extras = ' GROUP BY timeend'.((!empty($param->aggregategroupby)) ? ','.$param->aggregategroupby : '');
1301     }
1302     */
1303     //TODO must add the SITEID reports to the rest of the reports.
1304     return $param;
1307 function stats_get_view_actions() {
1308     return array('view','view all','history');
1311 function stats_get_post_actions() {
1312     return array('add','delete','edit','add mod','delete mod','edit section'.'enrol','loginas','new','unenrol','update','update mod');
1315 function stats_get_action_names($str) {
1316     global $CFG, $DB;
1318     $mods = $DB->get_records('modules');
1319     $function = 'stats_get_'.$str.'_actions';
1320     $actions = $function();
1321     foreach ($mods as $mod) {
1322         $file = $CFG->dirroot.'/mod/'.$mod->name.'/lib.php';
1323         if (!is_readable($file)) {
1324             continue;
1325         }
1326         require_once($file);
1327         $function = $mod->name.'_get_'.$str.'_actions';
1328         if (function_exists($function)) {
1329             $mod_actions = $function();
1330             if (is_array($mod_actions)) {
1331                 $actions = array_merge($actions, $mod_actions);
1332             }
1333         }
1334     }
1336     // The array_values() forces a stack-like array
1337     // so we can later loop over safely...
1338     $actions =  array_values(array_unique($actions));
1339     $c = count($actions);
1340     for ($n=0;$n<$c;$n++) {
1341         $actions[$n] = $actions[$n];
1342     }
1343     return $actions;
1346 function stats_get_time_options($now,$lastweekend,$lastmonthend,$earliestday,$earliestweek,$earliestmonth) {
1348     $now = stats_get_base_daily(time());
1349     // it's really important that it's TIMEEND in the table. ie, tuesday 00:00:00 is monday night.
1350     // so we need to take a day off here (essentially add a day to $now
1351     $now += 60*60*24;
1353     $timeoptions = array();
1355     if ($now - (60*60*24*7) >= $earliestday) {
1356         $timeoptions[STATS_TIME_LASTWEEK] = get_string('numweeks','moodle',1);
1357     }
1358     if ($now - (60*60*24*14) >= $earliestday) {
1359         $timeoptions[STATS_TIME_LAST2WEEKS] = get_string('numweeks','moodle',2);
1360     }
1361     if ($now - (60*60*24*21) >= $earliestday) {
1362         $timeoptions[STATS_TIME_LAST3WEEKS] = get_string('numweeks','moodle',3);
1363     }
1364     if ($now - (60*60*24*28) >= $earliestday) {
1365         $timeoptions[STATS_TIME_LAST4WEEKS] = get_string('numweeks','moodle',4);// show dailies up to (including) here.
1366     }
1367     if ($lastweekend - (60*60*24*56) >= $earliestweek) {
1368         $timeoptions[STATS_TIME_LAST2MONTHS] = get_string('nummonths','moodle',2);
1369     }
1370     if ($lastweekend - (60*60*24*84) >= $earliestweek) {
1371         $timeoptions[STATS_TIME_LAST3MONTHS] = get_string('nummonths','moodle',3);
1372     }
1373     if ($lastweekend - (60*60*24*112) >= $earliestweek) {
1374         $timeoptions[STATS_TIME_LAST4MONTHS] = get_string('nummonths','moodle',4);
1375     }
1376     if ($lastweekend - (60*60*24*140) >= $earliestweek) {
1377         $timeoptions[STATS_TIME_LAST5MONTHS] = get_string('nummonths','moodle',5);
1378     }
1379     if ($lastweekend - (60*60*24*168) >= $earliestweek) {
1380         $timeoptions[STATS_TIME_LAST6MONTHS] = get_string('nummonths','moodle',6); // show weeklies up to (including) here
1381     }
1382     if (strtotime('-7 months',$lastmonthend) >= $earliestmonth) {
1383         $timeoptions[STATS_TIME_LAST7MONTHS] = get_string('nummonths','moodle',7);
1384     }
1385     if (strtotime('-8 months',$lastmonthend) >= $earliestmonth) {
1386         $timeoptions[STATS_TIME_LAST8MONTHS] = get_string('nummonths','moodle',8);
1387     }
1388     if (strtotime('-9 months',$lastmonthend) >= $earliestmonth) {
1389         $timeoptions[STATS_TIME_LAST9MONTHS] = get_string('nummonths','moodle',9);
1390     }
1391     if (strtotime('-10 months',$lastmonthend) >= $earliestmonth) {
1392         $timeoptions[STATS_TIME_LAST10MONTHS] = get_string('nummonths','moodle',10);
1393     }
1394     if (strtotime('-11 months',$lastmonthend) >= $earliestmonth) {
1395         $timeoptions[STATS_TIME_LAST11MONTHS] = get_string('nummonths','moodle',11);
1396     }
1397     if (strtotime('-1 year',$lastmonthend) >= $earliestmonth) {
1398         $timeoptions[STATS_TIME_LASTYEAR] = get_string('lastyear');
1399     }
1401     $years = (int)date('y', $now) - (int)date('y', $earliestmonth);
1402     if ($years > 1) {
1403         for($i = 2; $i <= $years; $i++) {
1404             $timeoptions[$i*12+20] = get_string('numyears', 'moodle', $i);
1405         }
1406     }
1408     return $timeoptions;
1411 function stats_get_report_options($courseid,$mode) {
1412     global $CFG, $DB;
1414     $reportoptions = array();
1416     switch ($mode) {
1417     case STATS_MODE_GENERAL:
1418         $reportoptions[STATS_REPORT_ACTIVITY] = get_string('statsreport'.STATS_REPORT_ACTIVITY);
1419         if ($courseid != SITEID && $context = context_course::instance($courseid)) {
1420             $sql = 'SELECT r.id, r.name FROM {role} r JOIN {stats_daily} s ON s.roleid = r.id WHERE s.courseid = :courseid GROUP BY r.id, r.name';
1421             if ($roles = $DB->get_records_sql($sql, array('courseid' => $courseid))) {
1422                 foreach ($roles as $role) {
1423                     $reportoptions[STATS_REPORT_ACTIVITYBYROLE.$role->id] = get_string('statsreport'.STATS_REPORT_ACTIVITYBYROLE). ' '.$role->name;
1424                 }
1425             }
1426         }
1427         $reportoptions[STATS_REPORT_READS] = get_string('statsreport'.STATS_REPORT_READS);
1428         $reportoptions[STATS_REPORT_WRITES] = get_string('statsreport'.STATS_REPORT_WRITES);
1429         if ($courseid == SITEID) {
1430             $reportoptions[STATS_REPORT_LOGINS] = get_string('statsreport'.STATS_REPORT_LOGINS);
1431         }
1433         break;
1434     case STATS_MODE_DETAILED:
1435         $reportoptions[STATS_REPORT_USER_ACTIVITY] = get_string('statsreport'.STATS_REPORT_USER_ACTIVITY);
1436         $reportoptions[STATS_REPORT_USER_ALLACTIVITY] = get_string('statsreport'.STATS_REPORT_USER_ALLACTIVITY);
1437         if (has_capability('report/stats:view', context_system::instance())) {
1438             $site = get_site();
1439             $reportoptions[STATS_REPORT_USER_LOGINS] = get_string('statsreport'.STATS_REPORT_USER_LOGINS);
1440         }
1441         break;
1442     case STATS_MODE_RANKED:
1443         if (has_capability('report/stats:view', context_system::instance())) {
1444             $reportoptions[STATS_REPORT_ACTIVE_COURSES] = get_string('statsreport'.STATS_REPORT_ACTIVE_COURSES);
1445             $reportoptions[STATS_REPORT_ACTIVE_COURSES_WEIGHTED] = get_string('statsreport'.STATS_REPORT_ACTIVE_COURSES_WEIGHTED);
1446             $reportoptions[STATS_REPORT_PARTICIPATORY_COURSES] = get_string('statsreport'.STATS_REPORT_PARTICIPATORY_COURSES);
1447             $reportoptions[STATS_REPORT_PARTICIPATORY_COURSES_RW] = get_string('statsreport'.STATS_REPORT_PARTICIPATORY_COURSES_RW);
1448         }
1449         break;
1450     }
1452     return $reportoptions;
1455 /**
1456  * Fix missing entries in the statistics.
1457  *
1458  * This creates a dummy stat when nothing happened during a day/week/month.
1459  *
1460  * @param array $stats array of statistics.
1461  * @param int $timeafter unused.
1462  * @param string $timestr type of statistics to generate (dayly, weekly, monthly).
1463  * @param boolean $line2
1464  * @param boolean $line3
1465  * @return array of fixed statistics.
1466  */
1467 function stats_fix_zeros($stats,$timeafter,$timestr,$line2=true,$line3=false) {
1469     if (empty($stats)) {
1470         return;
1471     }
1473     $timestr = str_replace('user_','',$timestr); // just in case.
1475     // Gets the current user base time.
1476     $fun = 'stats_get_base_'.$timestr;
1477     $now = $fun();
1479     // Extract the ending time of the statistics.
1480     $actualtimes = array();
1481     $actualtimeshour = null;
1482     foreach ($stats as $statid => $s) {
1483         // Normalise the month date to the 1st if for any reason it's set to later. But we ignore
1484         // anything above or equal to 29 because sometimes we get the end of the month. Also, we will
1485         // set the hours of the result to all of them, that way we prevent DST differences.
1486         if ($timestr == 'monthly') {
1487             $day = date('d', $s->timeend);
1488             if (date('d', $s->timeend) > 1 && date('d', $s->timeend) < 29) {
1489                 $day = 1;
1490             }
1491             if (is_null($actualtimeshour)) {
1492                 $actualtimeshour = date('H', $s->timeend);
1493             }
1494             $s->timeend = mktime($actualtimeshour, 0, 0, date('m', $s->timeend), $day, date('Y', $s->timeend));
1495         }
1496         $stats[$statid] = $s;
1497         $actualtimes[] = $s->timeend;
1498     }
1500     $actualtimesvalues = array_values($actualtimes);
1501     $timeafter = array_pop($actualtimesvalues);
1503     // Generate a base timestamp for each possible month/week/day.
1504     $times = array();
1505     while ($timeafter < $now) {
1506         $times[] = $timeafter;
1507         if ($timestr == 'daily') {
1508             $timeafter = stats_get_next_day_start($timeafter);
1509         } else if ($timestr == 'weekly') {
1510             $timeafter = stats_get_next_week_start($timeafter);
1511         } else if ($timestr == 'monthly') {
1512             // We can't just simply +1 month because the 31st Jan + 1 month = 2nd of March.
1513             $year = date('Y', $timeafter);
1514             $month = date('m', $timeafter);
1515             $day = date('d', $timeafter);
1516             $dayofnextmonth = $day;
1517             if ($day >= 29) {
1518                 $daysinmonth = date('n', mktime(0, 0, 0, $month+1, 1, $year));
1519                 if ($day > $daysinmonth) {
1520                     $dayofnextmonth = $daysinmonth;
1521                 }
1522             }
1523             $timeafter = mktime($actualtimeshour, 0, 0, $month+1, $dayofnextmonth, $year);
1524         } else {
1525             // This will put us in a never ending loop.
1526             return $stats;
1527         }
1528     }
1530     // Add the base timestamp to the statistics if not present.
1531     foreach ($times as $count => $time) {
1532         if (!in_array($time,$actualtimes) && $count != count($times) -1) {
1533             $newobj = new StdClass;
1534             $newobj->timeend = $time;
1535             $newobj->id = 0;
1536             $newobj->roleid = 0;
1537             $newobj->line1 = 0;
1538             if (!empty($line2)) {
1539                 $newobj->line2 = 0;
1540             }
1541             if (!empty($line3)) {
1542                 $newobj->line3 = 0;
1543             }
1544             $newobj->zerofixed = true;
1545             $stats[] = $newobj;
1546         }
1547     }
1549     usort($stats,"stats_compare_times");
1550     return $stats;
1553 // helper function to sort arrays by $obj->timeend
1554 function stats_compare_times($a,$b) {
1555    if ($a->timeend == $b->timeend) {
1556        return 0;
1557    }
1558    return ($a->timeend > $b->timeend) ? -1 : 1;
1561 function stats_check_uptodate($courseid=0) {
1562     global $CFG, $DB;
1564     if (empty($courseid)) {
1565         $courseid = SITEID;
1566     }
1568     $latestday = stats_get_start_from('daily');
1570     if ((time() - 60*60*24*2) < $latestday) { // we're ok
1571         return NULL;
1572     }
1574     $a = new stdClass();
1575     $a->daysdone = $DB->get_field_sql("SELECT COUNT(DISTINCT(timeend)) FROM {stats_daily}");
1577     // how many days between the last day and now?
1578     $a->dayspending = ceil((stats_get_base_daily() - $latestday)/(60*60*24));
1580     if ($a->dayspending == 0 && $a->daysdone != 0) {
1581         return NULL; // we've only just started...
1582     }
1584     //return error as string
1585     return get_string('statscatchupmode','error',$a);
1588 /**
1589  * Create temporary tables to speed up log generation
1590  */
1591 function stats_temp_table_create() {
1592     global $CFG, $DB;
1594     $dbman = $DB->get_manager(); // We are going to use database_manager services
1596     stats_temp_table_drop();
1598     $tables = array();
1600     /// Define tables user to be created
1601     $table = new xmldb_table('temp_stats_daily');
1602     $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
1603     $table->add_field('courseid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, '0');
1604     $table->add_field('timeend', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, '0');
1605     $table->add_field('roleid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, '0');
1606     $table->add_field('stattype', XMLDB_TYPE_CHAR, 20, null, XMLDB_NOTNULL, null, 'activity');
1607     $table->add_field('stat1', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, '0');
1608     $table->add_field('stat2', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, '0');
1609     $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
1610     $table->add_index('courseid', XMLDB_INDEX_NOTUNIQUE, array('courseid'));
1611     $table->add_index('timeend', XMLDB_INDEX_NOTUNIQUE, array('timeend'));
1612     $table->add_index('roleid', XMLDB_INDEX_NOTUNIQUE, array('roleid'));
1613     $tables['temp_stats_daily'] = $table;
1615     $table = new xmldb_table('temp_stats_user_daily');
1616     $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
1617     $table->add_field('courseid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, '0');
1618     $table->add_field('userid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, '0');
1619     $table->add_field('roleid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, '0');
1620     $table->add_field('timeend', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, '0');
1621     $table->add_field('statsreads', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, '0');
1622     $table->add_field('statswrites', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, '0');
1623     $table->add_field('stattype', XMLDB_TYPE_CHAR, 30, null, XMLDB_NOTNULL, null, null);
1624     $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
1625     $table->add_index('courseid', XMLDB_INDEX_NOTUNIQUE, array('courseid'));
1626     $table->add_index('userid', XMLDB_INDEX_NOTUNIQUE, array('userid'));
1627     $table->add_index('timeend', XMLDB_INDEX_NOTUNIQUE, array('timeend'));
1628     $table->add_index('roleid', XMLDB_INDEX_NOTUNIQUE, array('roleid'));
1629     $tables['temp_stats_user_daily'] = $table;
1631     $table = new xmldb_table('temp_enroled');
1632     $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
1633     $table->add_field('userid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, '0');
1634     $table->add_field('courseid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, '0');
1635     $table->add_field('roleid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, null);
1636     $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
1637     $table->add_index('userid', XMLDB_INDEX_NOTUNIQUE, array('userid'));
1638     $table->add_index('courseid', XMLDB_INDEX_NOTUNIQUE, array('courseid'));
1639     $table->add_index('roleid', XMLDB_INDEX_NOTUNIQUE, array('roleid'));
1640     $tables['temp_enroled'] = $table;
1643     $table = new xmldb_table('temp_log1');
1644     $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
1645     $table->add_field('userid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, '0');
1646     $table->add_field('course', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, '0');
1647     $table->add_field('action', XMLDB_TYPE_CHAR, 40, null, XMLDB_NOTNULL, null, null);
1648     $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
1649     $table->add_index('action', XMLDB_INDEX_NOTUNIQUE, array('action'));
1650     $table->add_index('course', XMLDB_INDEX_NOTUNIQUE, array('course'));
1651     $table->add_index('user', XMLDB_INDEX_NOTUNIQUE, array('userid'));
1652     $table->add_index('usercourseaction', XMLDB_INDEX_NOTUNIQUE, array('userid','course','action'));
1653     $tables['temp_log1'] = $table;
1655     /// temp_log2 is exactly the same as temp_log1.
1656     $tables['temp_log2'] = clone $tables['temp_log1'];
1657     $tables['temp_log2']->setName('temp_log2');
1659     try {
1661         foreach ($tables as $table) {
1662             $dbman->create_temp_table($table);
1663         }
1665     } catch (Exception $e) {
1666         mtrace('Temporary table creation failed: '. $e->getMessage());
1667         return false;
1668     }
1670     return true;
1673 /**
1674  * Deletes summary logs table for stats calculation
1675  */
1676 function stats_temp_table_drop() {
1677     global $DB;
1679     $dbman = $DB->get_manager();
1681     $tables = array('temp_log1', 'temp_log2', 'temp_stats_daily', 'temp_stats_user_daily', 'temp_enroled');
1683     foreach ($tables as $name) {
1685         if ($dbman->table_exists($name)) {
1686             $table = new xmldb_table($name);
1688             try {
1689                 $dbman->drop_table($table);
1690             } catch (Exception $e) {
1691                 mtrace("Error occured while dropping temporary tables!");
1692             }
1693         }
1694     }
1697 /**
1698  * Fills the temporary stats tables with new data
1699  *
1700  * This function is meant to be called once at the start of stats generation
1701  *
1702  * @param timestart timestamp of the start time of logs view
1703  * @param timeend timestamp of the end time of logs view
1704  * @return boolen success (true) or failure(false)
1705  */
1706 function stats_temp_table_setup() {
1707     global $DB;
1709     $sql = "INSERT INTO {temp_enroled} (userid, courseid, roleid)
1711                SELECT ue.userid, e.courseid, ra.roleid
1712                 FROM {role_assignments} ra
1713                 JOIN {context} c ON (c.id = ra.contextid AND c.contextlevel = :courselevel)
1714                 JOIN {enrol} e ON e.courseid = c.instanceid
1715                 JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = ra.userid)";
1717     return stats_run_query($sql, array('courselevel' => CONTEXT_COURSE));
1720 /**
1721  * Fills the temporary stats tables with new data
1722  *
1723  * This function is meant to be called to get a new day of data
1724  *
1725  * @param timestart timestamp of the start time of logs view
1726  * @param timeend timestamp of the end time of logs view
1727  * @return boolen success (true) or failure(false)
1728  */
1729 function stats_temp_table_fill($timestart, $timeend) {
1730     global $DB;
1732     $sql = 'INSERT INTO {temp_log1} (userid, course, action)
1734             SELECT userid, course, action FROM {log}
1735              WHERE time >= ? AND time < ?';
1737     $DB->execute($sql, array($timestart, $timeend));
1739     $sql = 'INSERT INTO {temp_log2} (userid, course, action)
1741             SELECT userid, course, action FROM {temp_log1}';
1743     $DB->execute($sql);
1745     // We have just loaded all the temp tables, collect statistics for that.
1746     $DB->update_temp_table_stats();
1748     return true;
1752 /**
1753  * Deletes summary logs table for stats calculation
1754  *
1755  * @return boolen success (true) or failure(false)
1756  */
1757 function stats_temp_table_clean() {
1758     global $DB;
1760     $sql = array();
1762     $sql['up1'] = 'INSERT INTO {stats_daily} (courseid, roleid, stattype, timeend, stat1, stat2)
1764                    SELECT courseid, roleid, stattype, timeend, stat1, stat2 FROM {temp_stats_daily}';
1766     $sql['up2'] = 'INSERT INTO {stats_user_daily}
1767                                (courseid, userid, roleid, timeend, statsreads, statswrites, stattype)
1769                    SELECT courseid, userid, roleid, timeend, statsreads, statswrites, stattype
1770                      FROM {temp_stats_user_daily}';
1772     foreach ($sql as $id => $query) {
1773         if (! stats_run_query($query)) {
1774             mtrace("Error during table cleanup!");
1775             return false;
1776         }
1777     }
1779     $tables = array('temp_log1', 'temp_log2', 'temp_stats_daily', 'temp_stats_user_daily');
1781     foreach ($tables as $name) {
1782         $DB->delete_records($name);
1783     }
1785     return true;