MDL-59067 analytics: Store indicator calculations
authorDavid Monllao <davidm@moodle.com>
Mon, 7 Aug 2017 10:15:19 +0000 (12:15 +0200)
committerDavid Monllao <davidm@moodle.com>
Tue, 22 Aug 2017 20:28:04 +0000 (22:28 +0200)
This was supposed to be split into multiple commits to make it easier to understand
but I failed to do it properly. So this is the list of changes:

- New analytics_indicator_calc db table to store indicators calculations
- Reuse previous calculations during prediction/training; other models
  previous calculations should also be reused as long as they belong to
  the same sample (sampleid depends on sampleorigin), time range and indicator
- Allow bulk inserting of these calculations as this can hurt database performance
- Block the same analysable to be analysed for training and for prediction
- Use a new instance of the target and use it for is_valid_* functions
  as using ::is_valid_sample can lead to problems if people
  uses it to cache stuff

analytics/classes/dataset_manager.php
analytics/classes/local/analyser/base.php
analytics/classes/local/indicator/base.php
analytics/classes/local/time_splitting/base.php
analytics/classes/manager.php
analytics/classes/model.php
analytics/tests/prediction_test.php
lib/db/install.xml
lib/db/upgrade.php
lib/tests/indicators_test.php
version.php

index 882d8dc..4b457e8 100644 (file)
@@ -119,6 +119,9 @@ class dataset_manager {
      * @return bool Could we get the lock or not.
      */
     public function init_process() {
+
+        // Do not include $this->includetarget as we don't want the same analysable to be analysed for training
+        // and prediction at the same time.
         $lockkey = 'modelid:' . $this->modelid . '-analysableid:' . $this->analysableid .
             '-timesplitting:' . self::clean_time_splitting_id($this->timesplittingid);
 
index 52986a4..9bf10c8 100644 (file)
@@ -47,6 +47,13 @@ abstract class base {
      */
     protected $target;
 
+    /**
+     * A $this->$target copy loaded with the ongoing analysis analysable.
+     *
+     * @var \core_analytics\local\target\base
+     */
+    protected $analysabletarget;
+
     /**
      * The model indicators.
      *
@@ -255,11 +262,11 @@ abstract class base {
 
         // Target instances scope is per-analysable (it can't be lower as calculations run once per
         // analysable, not time splitting method nor time range).
-        $target = call_user_func(array($this->target, 'instance'));
+        $this->analysabletarget = call_user_func(array($this->target, 'instance'));
 
         // We need to check that the analysable is valid for the target even if we don't include targets
         // as we still need to discard invalid analysables for the target.
-        $result = $target->is_valid_analysable($analysable, $includetarget);
+        $result = $this->analysabletarget->is_valid_analysable($analysable, $includetarget);
         if ($result !== true) {
             $a = new \stdClass();
             $a->analysableid = $analysable->get_id();
@@ -291,11 +298,7 @@ abstract class base {
                 }
             }
 
-            if ($includetarget) {
-                $result = $this->process_time_splitting($timesplitting, $analysable, $target);
-            } else {
-                $result = $this->process_time_splitting($timesplitting, $analysable);
-            }
+            $result = $this->process_time_splitting($timesplitting, $analysable, $includetarget);
 
             if (!empty($result->file)) {
                 $files[$timesplitting->get_id()] = $result->file;
@@ -342,10 +345,10 @@ abstract class base {
      *
      * @param \core_analytics\local\time_splitting\base $timesplitting
      * @param \core_analytics\analysable $analysable
-     * @param \core_analytics\local\target\base|false $target
+     * @param bool $includetarget
      * @return \stdClass Results object.
      */
-    protected function process_time_splitting($timesplitting, $analysable, $target = false) {
+    protected function process_time_splitting($timesplitting, $analysable, $includetarget = false) {
 
         $result = new \stdClass();
 
@@ -372,7 +375,7 @@ abstract class base {
             return $result;
         }
 
-        if ($target) {
+        if ($includetarget) {
             // All ranges are used when we are calculating data for training.
             $ranges = $timesplitting->get_all_ranges();
         } else {
@@ -399,7 +402,7 @@ abstract class base {
             }
 
             // Only when processing data for predictions.
-            if ($target === false) {
+            if (!$includetarget) {
                 // We also filter out samples and ranges that have already been used for predictions.
                 $this->filter_out_prediction_samples_and_ranges($sampleids, $ranges, $timesplitting);
             }
@@ -433,10 +436,9 @@ abstract class base {
             return $result;
         }
 
-        // Remove samples the target consider invalid. Note that we use $this->target, $target will be false
-        // during prediction, but we still need to discard samples the target considers invalid.
-        $this->target->add_sample_data($samplesdata);
-        $this->target->filter_out_invalid_samples($sampleids, $analysable, $target);
+        // Remove samples the target consider invalid.
+        $this->analysabletarget->add_sample_data($samplesdata);
+        $this->analysabletarget->filter_out_invalid_samples($sampleids, $analysable, $includetarget);
 
         if (!$sampleids) {
             $result->status = \core_analytics\model::NO_DATASET;
@@ -450,15 +452,15 @@ abstract class base {
             // indicator to calculate the sample.
             $this->indicators[$key]->add_sample_data($samplesdata);
         }
-        // Provide samples to the target instance (different than $this->target) $target is the new instance we get
-        // for each analysis in progress.
-        if ($target) {
-            $target->add_sample_data($samplesdata);
-        }
 
         // Here we start the memory intensive process that will last until $data var is
         // unset (until the method is finished basically).
-        $data = $timesplitting->calculate($sampleids, $this->get_samples_origin(), $this->indicators, $ranges, $target);
+        if ($includetarget) {
+            $data = $timesplitting->calculate($sampleids, $this->get_samples_origin(), $this->indicators, $ranges,
+                $this->analysabletarget);
+        } else {
+            $data = $timesplitting->calculate($sampleids, $this->get_samples_origin(), $this->indicators, $ranges);
+        }
 
         if (!$data) {
             $result->status = \core_analytics\model::ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD;
@@ -477,7 +479,7 @@ abstract class base {
         if ($this->options['evaluation'] === false) {
             // Save the samples that have been already analysed so they are not analysed again in future.
 
-            if ($target) {
+            if ($includetarget) {
                 $this->save_train_samples($sampleids, $timesplitting, $file);
             } else {
                 $this->save_prediction_samples($sampleids, $ranges, $timesplitting);
index 310dc55..30fdf38 100644 (file)
@@ -130,18 +130,25 @@ abstract class base extends \core_analytics\calculable {
      * @param string $samplesorigin
      * @param integer $starttime Limit the calculation to this timestart
      * @param integer $endtime Limit the calculation to this timeend
-     * @return array The format to follow is [userid] = int|float[]
+     * @param array $existingcalculations Existing calculations of this indicator, indexed by sampleid.
+     * @return array array[0] with format [sampleid] = int[]|float[], array[1] with format [sampleid] = int|float
      */
-    public function calculate($sampleids, $samplesorigin, $starttime = false, $endtime = false) {
+    public function calculate($sampleids, $samplesorigin, $starttime = false, $endtime = false, $existingcalculations = array()) {
 
         if (!PHPUNIT_TEST && CLI_SCRIPT) {
             echo '.';
         }
 
         $calculations = array();
+        $newcalculations = array();
         foreach ($sampleids as $sampleid => $unusedsampleid) {
 
-            $calculatedvalue = $this->calculate_sample($sampleid, $samplesorigin, $starttime, $endtime);
+            if (isset($existingcalculations[$sampleid])) {
+                $calculatedvalue = $existingcalculations[$sampleid];
+            } else {
+                $calculatedvalue = $this->calculate_sample($sampleid, $samplesorigin, $starttime, $endtime);
+                $newcalculations[$sampleid] = $calculatedvalue;
+            }
 
             if (!is_null($calculatedvalue) && ($calculatedvalue > self::MAX_VALUE || $calculatedvalue < self::MIN_VALUE)) {
                 throw new \coding_exception('Calculated values should be higher than ' . self::MIN_VALUE .
@@ -151,8 +158,8 @@ abstract class base extends \core_analytics\calculable {
             $calculations[$sampleid] = $calculatedvalue;
         }
 
-        $calculations = $this->to_features($calculations);
+        $features = $this->to_features($calculations);
 
-        return $calculations;
+        return array($features, $newcalculations);
     }
 }
index 23adfb1..82d8e57 100644 (file)
@@ -66,6 +66,11 @@ abstract class base {
      */
     protected static $indicators = [];
 
+    /**
+     * @var bool
+     */
+    protected $evaluation = false;
+
     /**
      * Define the time splitting methods ranges.
      *
@@ -95,6 +100,24 @@ abstract class base {
         return '\\' . get_class($this);
     }
 
+    /**
+     * Returns current evaluation value.
+     *
+     * @return bool
+     */
+    public function is_evaluating() {
+        return $this->evaluation;
+    }
+
+    /**
+     * Sets the evaluation flag.
+     *
+     * @param bool $evaluation
+     */
+    public function set_evaluating($evaluation) {
+        $this->evaluation = (bool)$evaluation;
+    }
+
     /**
      * Assigns the analysable and updates the time ranges according to the analysable start and end dates.
      *
@@ -195,10 +218,20 @@ abstract class base {
      * @return array
      */
     protected function calculate_indicators($sampleids, $samplesorigin, $indicators, $ranges) {
+        global $DB;
 
         $dataset = array();
 
+        // Faster to run 1 db query per range.
+        $existingcalculations = array();
+        foreach ($ranges as $rangeindex => $range) {
+            // Load existing calculations.
+            $existingcalculations[$rangeindex] = \core_analytics\manager::get_indicator_calculations($this->analysable,
+                $range['start'], $range['end'], $samplesorigin);
+        }
+
         // Fill the dataset samples with indicators data.
+        $newcalculations = array();
         foreach ($indicators as $indicator) {
 
             // Per-range calculations.
@@ -207,11 +240,17 @@ abstract class base {
                 // Indicator instances are per-range.
                 $rangeindicator = clone $indicator;
 
+                $prevcalculations = array();
+                if (!empty($existingcalculations[$rangeindex][$rangeindicator->get_id()])) {
+                    $prevcalculations = $existingcalculations[$rangeindex][$rangeindicator->get_id()];
+                }
+
                 // Calculate the indicator for each sample in this time range.
-                $calculated = $rangeindicator->calculate($sampleids, $samplesorigin, $range['start'], $range['end']);
+                list($samplesfeatures, $newindicatorcalculations) = $rangeindicator->calculate($sampleids,
+                    $samplesorigin, $range['start'], $range['end'], $prevcalculations);
 
-                // Copy the calculated data to the dataset.
-                foreach ($calculated as $analysersampleid => $calculatedvalues) {
+                // Copy the features data to the dataset.
+                foreach ($samplesfeatures as $analysersampleid => $features) {
 
                     $uniquesampleid = $this->append_rangeindex($analysersampleid, $rangeindex);
 
@@ -220,12 +259,50 @@ abstract class base {
                         $dataset[$uniquesampleid] = array();
                     }
 
-                    // Append the calculated indicator features at the end of the sample.
-                    $dataset[$uniquesampleid] = array_merge($dataset[$uniquesampleid], $calculatedvalues);
+                    // Append the features indicator features at the end of the sample.
+                    $dataset[$uniquesampleid] = array_merge($dataset[$uniquesampleid], $features);
+                }
+
+                if (!$this->is_evaluating()) {
+                    $timecreated = time();
+                    foreach ($newindicatorcalculations as $sampleid => $calculatedvalue) {
+                        // Prepare the new calculations to be stored into DB.
+
+                        $indcalc = new \stdClass();
+                        $indcalc->contextid = $this->analysable->get_context()->id;
+                        $indcalc->starttime = $range['start'];
+                        $indcalc->endtime = $range['end'];
+                        $indcalc->sampleid = $sampleid;
+                        $indcalc->sampleorigin = $samplesorigin;
+                        $indcalc->indicator = $rangeindicator->get_id();
+                        $indcalc->value = $calculatedvalue;
+                        $indcalc->timecreated = $timecreated;
+                        $newcalculations[] = $indcalc;
+                    }
+                }
+            }
+
+            if (!$this->is_evaluating()) {
+                $batchsize = self::get_insert_batch_size();
+                if (count($newcalculations) > $batchsize) {
+                    // We don't want newcalculations array to grow too much as we already keep the
+                    // system memory busy storing $dataset contents.
+
+                    // Insert from the beginning.
+                    $remaining = array_splice($newcalculations, $batchsize);
+
+                    // Sorry mssql and oracle, this will be slow.
+                    $DB->insert_records('analytics_indicator_calc', $newcalculations);
+                    $newcalculations = $remaining;
                 }
             }
         }
 
+        if (!$this->is_evaluating() && $newcalculations) {
+            // Insert the remaining records.
+            $DB->insert_records('analytics_indicator_calc', $newcalculations);
+        }
+
         return $dataset;
     }
 
@@ -410,4 +487,32 @@ abstract class base {
             }
         }
     }
+
+    /**
+     * Returns the batch size used for insert_records.
+     *
+     * This method tries to find the best batch size without getting
+     * into dml internals. Maximum 1000 records to save memory.
+     *
+     * @return int
+     */
+    private static function get_insert_batch_size() {
+        global $DB;
+
+        // 500 is pgsql default so using 1000 is fine, no other db driver uses a hardcoded value.
+        if (empty($DB->dboptions['bulkinsertsize'])) {
+            return 1000;
+        }
+
+        $bulkinsert = $DB->dboptions['bulkinsertsize'];
+        if ($bulkinsert < 1000) {
+            return $bulkinsert;
+        }
+
+        while ($bulkinsert > 1000) {
+            $bulkinsert = round($bulkinsert / 2, 0);
+        }
+
+        return (int)$bulkinsert;
+    }
 }
index 80f80cd..fff2c50 100644 (file)
@@ -337,6 +337,32 @@ class manager {
         return $logstore;
     }
 
+    /**
+     * Returns this analysable calculations during the provided period.
+     *
+     * @param \core_analytics\analysable $analysable
+     * @param int $starttime
+     * @param int $endtime
+     * @param string $samplesorigin The samples origin as sampleid is not unique across models.
+     * @return array
+     */
+    public static function get_indicator_calculations($analysable, $starttime, $endtime, $samplesorigin) {
+        global $DB;
+
+        $params = array('starttime' => $starttime, 'endtime' => $endtime, 'contextid' => $analysable->get_context()->id,
+            'sampleorigin' => $samplesorigin);
+        $calculations = $DB->get_recordset('analytics_indicator_calc', $params, '', 'indicator, sampleid, value');
+
+        $existingcalculations = array();
+        foreach ($calculations as $calculation) {
+            if (empty($existingcalculations[$calculation->indicator])) {
+                $existingcalculations[$calculation->indicator] = array();
+            }
+            $existingcalculations[$calculation->indicator][$calculation->sampleid] = $calculation->value;
+        }
+        return $existingcalculations;
+    }
+
     /**
      * Returns the models with insights at the provided context.
      *
index f1c2708..04a95f1 100644 (file)
@@ -293,6 +293,12 @@ class model {
             throw new \moodle_exception('errornotimesplittings', 'analytics');
         }
 
+        if (!empty($options['evaluation'])) {
+            foreach ($timesplittings as $timesplitting) {
+                $timesplitting->set_evaluating(true);
+            }
+        }
+
         $classname = $target->get_analyser_class();
         if (!class_exists($classname)) {
             throw new \coding_exception($classname . ' class does not exists');
index c778a5c..1e21d8d 100644 (file)
@@ -107,10 +107,11 @@ class core_analytics_prediction_testcase extends advanced_testcase {
      * @dataProvider provider_ml_training_and_prediction
      * @param string $timesplittingid
      * @param int $predictedrangeindex
+     * @param int $nranges
      * @param string $predictionsprocessorclass
      * @return void
      */
-    public function test_ml_training_and_prediction($timesplittingid, $predictedrangeindex, $predictionsprocessorclass) {
+    public function test_ml_training_and_prediction($timesplittingid, $predictedrangeindex, $nranges, $predictionsprocessorclass) {
         global $DB;
 
         $this->resetAfterTest(true);
@@ -153,6 +154,10 @@ class core_analytics_prediction_testcase extends advanced_testcase {
         $this->assertEquals(1, $model->is_enabled());
         $this->assertEquals(1, $model->is_trained());
 
+        // 20 courses * the 3 model indicators * the number of time ranges of this time splitting method.
+        $indicatorcalc = 20 * 3 * $nranges;
+        $this->assertEquals($indicatorcalc, $DB->count_records('analytics_indicator_calc'));
+
         // 1 training file was created.
         $trainedsamples = $DB->get_records('analytics_train_samples', array('modelid' => $model->get_id()));
         $this->assertCount(1, $trainedsamples);
@@ -260,8 +265,8 @@ class core_analytics_prediction_testcase extends advanced_testcase {
      */
     public function provider_ml_training_and_prediction() {
         $cases = array(
-            'no_splitting' => array('\core\analytics\time_splitting\no_splitting', 0),
-            'quarters' => array('\core\analytics\time_splitting\quarters', 3)
+            'no_splitting' => array('\core\analytics\time_splitting\no_splitting', 0, 1),
+            'quarters' => array('\core\analytics\time_splitting\quarters', 3, 4)
         );
 
         // We need to test all system prediction processors.
@@ -334,6 +339,28 @@ class core_analytics_prediction_testcase extends advanced_testcase {
         get_log_manager(true);
     }
 
+    /**
+     * test_read_indicator_calculations
+     *
+     * @return void
+     */
+    public function test_read_indicator_calculations() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $starttime = 123;
+        $endtime = 321;
+        $sampleorigin = 'whatever';
+
+        $indicator = $this->getMockBuilder('test_indicator_max')->setMethods(['calculate_sample'])->getMock();
+        $indicator->expects($this->never())->method('calculate_sample');
+
+        $existingcalcs = array(111 => 1, 222 => 0.5);
+        $sampleids = array(111 => 111, 222 => 222);
+        list($values, $unused) = $indicator->calculate($sampleids, $sampleorigin, $starttime, $endtime, $existingcalcs);
+    }
+
     /**
      * provider_ml_test_evaluation
      *
index 768f7f2..399b613 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20170721" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20170801" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <INDEX NAME="modelidandfileidandaction" UNIQUE="false" FIELDS="modelid, fileid, action" COMMENT="Index on modelid and fileid and action"/>
       </INDEXES>
     </TABLE>
+    <TABLE NAME="analytics_indicator_calc" COMMENT="Stored indicator calculations">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="starttime" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="endtime" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="sampleorigin" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="sampleid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="indicator" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="value" TYPE="number" LENGTH="10" NOTNULL="false" SEQUENCE="false" DECIMALS="2" COMMENT="The calculated value, it can be null."/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="starttime-endtime-contextid" UNIQUE="false" FIELDS="starttime, endtime, contextid"/>
+      </INDEXES>
+    </TABLE>
   </TABLES>
-</XMLDB>
\ No newline at end of file
+</XMLDB>
index 638044f..95e6b27 100644 (file)
@@ -2308,5 +2308,36 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2017080700.01);
     }
 
+    if ($oldversion < 2017081700.01) {
+
+        // Define table analytics_indicator_calc to be created.
+        $table = new xmldb_table('analytics_indicator_calc');
+
+        // Adding fields to table analytics_indicator_calc.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('starttime', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('endtime', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('contextid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('sampleorigin', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('sampleid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('indicator', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('value', XMLDB_TYPE_NUMBER, '10, 2', null, null, null, null);
+        $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+
+        // Adding keys to table analytics_indicator_calc.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+
+        // Adding indexes to table analytics_indicator_calc.
+        $table->add_index('starttime-endtime-contextid', XMLDB_INDEX_NOTUNIQUE, array('starttime', 'endtime', 'contextid'));
+
+        // Conditionally launch create table for analytics_indicator_calc.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2017081700.01);
+    }
+
     return true;
 }
index 59058ed..79b9478 100644 (file)
@@ -80,13 +80,13 @@ class core_analytics_indicators_testcase extends advanced_testcase {
         $data[$user2->id]['user'] = $user2;
         $indicator->add_sample_data($data);
 
-        $values = $indicator->calculate($sampleids, 'notrelevanthere');
+        list($values, $unused) = $indicator->calculate($sampleids, 'notrelevanthere');
         $this->assertEquals($indicator::get_min_value(), $values[$user1->id][0]);
         $this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
 
         \logstore_standard\event\unittest_executed::create(
             array('context' => $coursecontext, 'userid' => $user1->id))->trigger();
-        $values = $indicator->calculate($sampleids, 'notrelevanthere');
+        list($values, $unused) = $indicator->calculate($sampleids, 'notrelevanthere');
         $this->assertEquals($indicator::get_max_value(), $values[$user1->id][0]);
         $this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
 
@@ -112,13 +112,13 @@ class core_analytics_indicators_testcase extends advanced_testcase {
         $data[$user2->id]['user'] = $user2;
         $indicator->add_sample_data($data);
 
-        $values = $indicator->calculate($sampleids, 'notrelevanthere');
+        list($values, $unused) = $indicator->calculate($sampleids, 'notrelevanthere');
         $this->assertEquals($indicator::get_min_value(), $values[$user1->id][0]);
         $this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
 
         \logstore_standard\event\unittest_executed::create(
             array('context' => $coursecontext, 'userid' => $user1->id))->trigger();
-        $values = $indicator->calculate($sampleids, 'notrelevanthere');
+        list($values, $unused) = $indicator->calculate($sampleids, 'notrelevanthere');
         $this->assertEquals($indicator::get_max_value(), $values[$user1->id][0]);
         $this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
 
@@ -141,7 +141,7 @@ class core_analytics_indicators_testcase extends advanced_testcase {
         $data[$user2->id]['user'] = $user2;
         $indicator->add_sample_data($data);
 
-        $values = $indicator->calculate($sampleids, 'user');
+        list($values, $unused) = $indicator->calculate($sampleids, 'user');
         $this->assertEquals($indicator::get_min_value(), $values[$user1->id][0]);
         $this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
 
@@ -150,7 +150,7 @@ class core_analytics_indicators_testcase extends advanced_testcase {
 
         \logstore_standard\event\unittest_executed::create(
             array('context' => $coursecontext1, 'userid' => $user1->id))->trigger();
-        $values = $indicator->calculate($sampleids, 'user');
+        list($values, $unused) = $indicator->calculate($sampleids, 'user');
         $this->assertEquals($indicator::get_max_value(), $values[$user1->id][0]);
         $this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
 
@@ -171,7 +171,7 @@ class core_analytics_indicators_testcase extends advanced_testcase {
 
         // Limited by time to avoid previous logs interfering as other logs
         // have been generated by the system.
-        $values = $indicator->calculate($sampleids, 'course', $beforecourseeventcreate);
+        list($values, $unused) = $indicator->calculate($sampleids, 'course', $beforecourseeventcreate);
         $this->assertEquals($indicator::get_max_value(), $values[$course1->id][0]);
         $this->assertEquals($indicator::get_min_value(), $values[$course2->id][0]);
 
@@ -197,21 +197,21 @@ class core_analytics_indicators_testcase extends advanced_testcase {
         $enddate = time() + (WEEKSECS * 2);
 
         $this->setAdminUser();
-        $values = $indicator->calculate($sampleids, 'user');
+        list($values, $unused) = $indicator->calculate($sampleids, 'user');
         $this->assertNull($values[$user1->id][0]);
         $this->assertNull($values[$user1->id][1]);
         $this->assertNull($values[$user1->id][0]);
         $this->assertNull($values[$user2->id][1]);
 
         // Zero score for 0 accesses.
-        $values = $indicator->calculate($sampleids, 'user', $startdate, $enddate);
+        list($values, $unused) = $indicator->calculate($sampleids, 'user', $startdate, $enddate);
         $this->assertEquals($indicator::get_min_value(), $values[$user1->id][0]);
         $this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
 
         // 1/3 score for more than 0 accesses.
         \core\event\course_viewed::create(
             array('context' => $coursecontext, 'userid' => $user1->id))->trigger();
-        $values = $indicator->calculate($sampleids, 'user', $startdate, $enddate);
+        list($values, $unused) = $indicator->calculate($sampleids, 'user', $startdate, $enddate);
         $this->assertEquals(-0.33, $values[$user1->id][0]);
         $this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
 
@@ -220,7 +220,7 @@ class core_analytics_indicators_testcase extends advanced_testcase {
             \core\event\course_viewed::create(
                 array('context' => $coursecontext, 'userid' => $user1->id))->trigger();
         }
-        $values = $indicator->calculate($sampleids, 'user', $startdate, $enddate);
+        list($values, $unused) = $indicator->calculate($sampleids, 'user', $startdate, $enddate);
         $this->assertEquals(0.33, $values[$user1->id][0]);
         $this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
 
@@ -229,7 +229,7 @@ class core_analytics_indicators_testcase extends advanced_testcase {
             \core\event\course_viewed::create(
                 array('context' => $coursecontext, 'userid' => $user1->id))->trigger();
         }
-        $values = $indicator->calculate($sampleids, 'user', $startdate, $enddate);
+        list($values, $unused) = $indicator->calculate($sampleids, 'user', $startdate, $enddate);
         $this->assertEquals($indicator::get_max_value(), $values[$user1->id][0]);
         $this->assertEquals($indicator::get_min_value(), $values[$user2->id][0]);
 
index 6c1203d..421532f 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2017081700.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2017081700.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.