From 0690a271c375af646d3cb3697579561abc80bee9 Mon Sep 17 00:00:00 2001 From: David Monllao Date: Mon, 7 Aug 2017 12:15:19 +0200 Subject: [PATCH] MDL-59067 analytics: Store indicator calculations 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 | 3 + analytics/classes/local/analyser/base.php | 46 +++---- analytics/classes/local/indicator/base.php | 17 ++- .../classes/local/time_splitting/base.php | 117 +++++++++++++++++- analytics/classes/manager.php | 26 ++++ analytics/classes/model.php | 6 + analytics/tests/prediction_test.php | 33 ++++- lib/db/install.xml | 23 +++- lib/db/upgrade.php | 31 +++++ lib/tests/indicators_test.php | 24 ++-- version.php | 2 +- 11 files changed, 277 insertions(+), 51 deletions(-) diff --git a/analytics/classes/dataset_manager.php b/analytics/classes/dataset_manager.php index 882d8dc584b..4b457e88682 100644 --- a/analytics/classes/dataset_manager.php +++ b/analytics/classes/dataset_manager.php @@ -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); diff --git a/analytics/classes/local/analyser/base.php b/analytics/classes/local/analyser/base.php index 52986a40fb7..9bf10c8c7cc 100644 --- a/analytics/classes/local/analyser/base.php +++ b/analytics/classes/local/analyser/base.php @@ -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); diff --git a/analytics/classes/local/indicator/base.php b/analytics/classes/local/indicator/base.php index 310dc55fb7c..30fdf38ddc4 100644 --- a/analytics/classes/local/indicator/base.php +++ b/analytics/classes/local/indicator/base.php @@ -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); } } diff --git a/analytics/classes/local/time_splitting/base.php b/analytics/classes/local/time_splitting/base.php index 23adfb1f6d1..82d8e571f99 100644 --- a/analytics/classes/local/time_splitting/base.php +++ b/analytics/classes/local/time_splitting/base.php @@ -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; - // Calculate the indicator for each sample in this time range. - $calculated = $rangeindicator->calculate($sampleids, $samplesorigin, $range['start'], $range['end']); + $prevcalculations = array(); + if (!empty($existingcalculations[$rangeindex][$rangeindicator->get_id()])) { + $prevcalculations = $existingcalculations[$rangeindex][$rangeindicator->get_id()]; + } - // Copy the calculated data to the dataset. - foreach ($calculated as $analysersampleid => $calculatedvalues) { + // Calculate the indicator for each sample in this time range. + list($samplesfeatures, $newindicatorcalculations) = $rangeindicator->calculate($sampleids, + $samplesorigin, $range['start'], $range['end'], $prevcalculations); + + // Copy the features data to the dataset. + foreach ($samplesfeatures as $analysersampleid => $features) { $uniquesampleid = $this->append_rangeindex($analysersampleid, $rangeindex); @@ -220,10 +259,48 @@ 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; + } } diff --git a/analytics/classes/manager.php b/analytics/classes/manager.php index 80f80cd32af..fff2c50e919 100644 --- a/analytics/classes/manager.php +++ b/analytics/classes/manager.php @@ -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. * diff --git a/analytics/classes/model.php b/analytics/classes/model.php index f1c2708c533..04a95f11088 100644 --- a/analytics/classes/model.php +++ b/analytics/classes/model.php @@ -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'); diff --git a/analytics/tests/prediction_test.php b/analytics/tests/prediction_test.php index c778a5c4aa5..1e21d8d7577 100644 --- a/analytics/tests/prediction_test.php +++ b/analytics/tests/prediction_test.php @@ -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 * diff --git a/lib/db/install.xml b/lib/db/install.xml index 768f7f2b0bd..399b6130dc0 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ - @@ -3674,5 +3674,24 @@ + + + + + + + + + + + + + + + + + + +
-
\ No newline at end of file + diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 638044ff712..95e6b27d097 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -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; } diff --git a/lib/tests/indicators_test.php b/lib/tests/indicators_test.php index 59058ede54e..79b9478341d 100644 --- a/lib/tests/indicators_test.php +++ b/lib/tests/indicators_test.php @@ -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]); diff --git a/version.php b/version.php index 6c1203dd80a..421532f8f20 100644 --- a/version.php +++ b/version.php @@ -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.