diff --git a/admin/tool/analytics/amd/build/potential-contexts.min.js b/admin/tool/analytics/amd/build/potential-contexts.min.js
new file mode 100644
index 00000000000..c93ee47896f
--- /dev/null
+++ b/admin/tool/analytics/amd/build/potential-contexts.min.js
@@ -0,0 +1,2 @@
+define ("tool_analytics/potential-contexts",["jquery","core/ajax"],function(a,b){return{processResults:function processResults(b,c){var d=[];if(a.isArray(c)){a.each(c,function(a,b){d.push({value:b.id,label:b.name})});return d}else{return c}},transport:function transport(c,d,e,f){var g,h=a(c).attr("modelid")||null;g=b.call([{methodname:"tool_analytics_potential_contexts",args:{query:d,modelid:h}}]);g[0].then(e).fail(f)}}});
+//# sourceMappingURL=potential-contexts.min.js.map
diff --git a/admin/tool/analytics/amd/build/potential-contexts.min.js.map b/admin/tool/analytics/amd/build/potential-contexts.min.js.map
new file mode 100644
index 00000000000..03b6fb56a01
--- /dev/null
+++ b/admin/tool/analytics/amd/build/potential-contexts.min.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["../src/potential-contexts.js"],"names":["define","$","Ajax","processResults","selector","results","contexts","isArray","each","index","context","push","value","id","label","name","transport","query","success","failure","promise","modelid","attr","call","methodname","args","then","fail"],"mappings":"AAyBAA,OAAM,qCAAC,CAAC,QAAD,CAAW,WAAX,CAAD,CAA0B,SAASC,CAAT,CAAYC,CAAZ,CAAkB,CAE9C,MAA8D,CAE1DC,cAAc,CAAE,wBAASC,CAAT,CAAmBC,CAAnB,CAA4B,CACxC,GAAIC,CAAAA,CAAQ,CAAG,EAAf,CACA,GAAIL,CAAC,CAACM,OAAF,CAAUF,CAAV,CAAJ,CAAwB,CACpBJ,CAAC,CAACO,IAAF,CAAOH,CAAP,CAAgB,SAASI,CAAT,CAAgBC,CAAhB,CAAyB,CACrCJ,CAAQ,CAACK,IAAT,CAAc,CACVC,KAAK,CAAEF,CAAO,CAACG,EADL,CAEVC,KAAK,CAAEJ,CAAO,CAACK,IAFL,CAAd,CAIH,CALD,EAMA,MAAOT,CAAAA,CAEV,CATD,IASO,CACH,MAAOD,CAAAA,CACV,CACJ,CAhByD,CAkB1DW,SAAS,CAAE,mBAASZ,CAAT,CAAmBa,CAAnB,CAA0BC,CAA1B,CAAmCC,CAAnC,CAA4C,IAC/CC,CAAAA,CAD+C,CAG/CC,CAAO,CAAGpB,CAAC,CAACG,CAAD,CAAD,CAAYkB,IAAZ,CAAiB,SAAjB,GAA+B,IAHM,CAInDF,CAAO,CAAGlB,CAAI,CAACqB,IAAL,CAAU,CAAC,CACjBC,UAAU,CAAE,mCADK,CAEjBC,IAAI,CAAE,CACFR,KAAK,CAAEA,CADL,CAEFI,OAAO,CAAEA,CAFP,CAFW,CAAD,CAAV,CAAV,CAQAD,CAAO,CAAC,CAAD,CAAP,CAAWM,IAAX,CAAgBR,CAAhB,EAAyBS,IAAzB,CAA8BR,CAA9B,CACH,CA/ByD,CAmCjE,CArCK,CAAN","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Potential contexts selector module.\n *\n * @module tool_analytics/potential-contexts\n * @class potential-contexts\n * @package tool_analytics\n * @copyright 2019 David Monllao\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'core/ajax'], function($, Ajax) {\n\n return /** @alias module:tool_analytics/potential-contexts */ {\n\n processResults: function(selector, results) {\n var contexts = [];\n if ($.isArray(results)) {\n $.each(results, function(index, context) {\n contexts.push({\n value: context.id,\n label: context.name\n });\n });\n return contexts;\n\n } else {\n return results;\n }\n },\n\n transport: function(selector, query, success, failure) {\n var promise;\n\n let modelid = $(selector).attr('modelid') || null;\n promise = Ajax.call([{\n methodname: 'tool_analytics_potential_contexts',\n args: {\n query: query,\n modelid: modelid\n }\n }]);\n\n promise[0].then(success).fail(failure);\n }\n\n };\n\n});\n"],"file":"potential-contexts.min.js"}
\ No newline at end of file
diff --git a/admin/tool/analytics/amd/src/potential-contexts.js b/admin/tool/analytics/amd/src/potential-contexts.js
new file mode 100644
index 00000000000..eb353675e6c
--- /dev/null
+++ b/admin/tool/analytics/amd/src/potential-contexts.js
@@ -0,0 +1,63 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * Potential contexts selector module.
+ *
+ * @module tool_analytics/potential-contexts
+ * @class potential-contexts
+ * @package tool_analytics
+ * @copyright 2019 David Monllao
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['jquery', 'core/ajax'], function($, Ajax) {
+
+ return /** @alias module:tool_analytics/potential-contexts */ {
+
+ processResults: function(selector, results) {
+ var contexts = [];
+ if ($.isArray(results)) {
+ $.each(results, function(index, context) {
+ contexts.push({
+ value: context.id,
+ label: context.name
+ });
+ });
+ return contexts;
+
+ } else {
+ return results;
+ }
+ },
+
+ transport: function(selector, query, success, failure) {
+ var promise;
+
+ let modelid = $(selector).attr('modelid') || null;
+ promise = Ajax.call([{
+ methodname: 'tool_analytics_potential_contexts',
+ args: {
+ query: query,
+ modelid: modelid
+ }
+ }]);
+
+ promise[0].then(success).fail(failure);
+ }
+
+ };
+
+});
diff --git a/admin/tool/analytics/classes/external.php b/admin/tool/analytics/classes/external.php
new file mode 100644
index 00000000000..515c08a63dd
--- /dev/null
+++ b/admin/tool/analytics/classes/external.php
@@ -0,0 +1,113 @@
+.
+
+/**
+ * This is the external API for this component.
+ *
+ * @package tool_analytics
+ * @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_analytics;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once("$CFG->libdir/externallib.php");
+
+use external_api;
+use external_function_parameters;
+use external_value;
+use external_single_structure;
+use external_multiple_structure;
+
+/**
+ * This is the external API for this component.
+ *
+ * @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class external extends external_api {
+
+ const MAX_CONTEXTS_RETURNED = 100;
+
+ /**
+ * potential_contexts parameters.
+ *
+ * @since Moodle 3.8
+ * @return external_function_parameters
+ */
+ public static function potential_contexts_parameters() {
+ return new external_function_parameters(
+ array(
+ 'query' => new external_value(PARAM_NOTAGS, 'The model id', VALUE_DEFAULT),
+ 'modelid' => new external_value(PARAM_INT, 'The model id', VALUE_DEFAULT)
+ )
+ );
+ }
+
+ /**
+ * Return the contexts that match the provided query.
+ *
+ * @since Moodle 3.8
+ * @param string|null $query
+ * @param int|null $modelid
+ * @return array an array of contexts
+ */
+ public static function potential_contexts(?string $query = null, ?int $modelid = null) {
+
+ $params = self::validate_parameters(self::potential_contexts_parameters(), ['modelid' => $modelid, 'query' => $query]);
+
+ \core_analytics\manager::check_can_manage_models();
+
+ if ($params['modelid']) {
+ $model = new \core_analytics\model($params['modelid']);
+ $contexts = ($model->get_analyser(['notimesplitting' => true]))::potential_context_restrictions($params['query']);
+ } else {
+ $contexts = \core_analytics\manager::get_potential_context_restrictions(null, $params['query']);
+ }
+
+ $contextoptions = [];
+ $i = 0;
+ foreach ($contexts as $contextid => $contextname) {
+
+ if ($i === self::MAX_CONTEXTS_RETURNED) {
+ // Limited to MAX_CONTEXTS_RETURNED items.
+ break;
+ }
+
+ $contextoptions[] = ['id' => $contextid, 'name' => $contextname];
+ $i++;
+ }
+
+ return $contextoptions;
+ }
+
+ /**
+ * potential_contexts return
+ *
+ * @since Moodle 3.8
+ * @return external_description
+ */
+ public static function potential_contexts_returns() {
+ return new external_multiple_structure(
+ new external_single_structure([
+ 'id' => new external_value(PARAM_INT, 'ID of the context'),
+ 'name' => new external_value(PARAM_NOTAGS, 'The context name')
+ ])
+ );
+ }
+}
diff --git a/admin/tool/analytics/classes/output/form/edit_model.php b/admin/tool/analytics/classes/output/form/edit_model.php
index c897b5c81bc..b9515cdda2f 100644
--- a/admin/tool/analytics/classes/output/form/edit_model.php
+++ b/admin/tool/analytics/classes/output/form/edit_model.php
@@ -105,6 +105,28 @@ class edit_model extends \moodleform {
$mform->addElement('select', 'timesplitting', get_string('timesplittingmethod', 'analytics'), $timesplittings);
$mform->addHelpButton('timesplitting', 'timesplittingmethod', 'analytics');
+ // Contexts restriction.
+ if (!empty($this->_customdata['supportscontexts'])) {
+
+ $options = [
+ 'ajax' => 'tool_analytics/potential-contexts',
+ 'multiple' => true,
+ 'noselectionstring' => get_string('all')
+ ];
+
+ if (!empty($this->_customdata['id'])) {
+ $options['modelid'] = $this->_customdata['id'];
+ $contexts = $this->load_current_contexts();
+ } else {
+ // No need to preload any selected contexts.
+ $contexts = [];
+ }
+
+ $mform->addElement('autocomplete', 'contexts', get_string('contexts', 'tool_analytics'), $contexts, $options);
+ $mform->setType('contexts', PARAM_INT);
+ $mform->addHelpButton('contexts', 'contexts', 'tool_analytics');
+ }
+
// Predictions processor.
if (!$this->_customdata['staticmodel']) {
$defaultprocessor = \core_analytics\manager::get_predictions_processor_name(
@@ -146,20 +168,37 @@ class edit_model extends \moodleform {
public function validation($data, $files) {
$errors = parent::validation($data, $files);
+ $targetclass = \tool_analytics\output\helper::option_to_class($data['target']);
+ $target = \core_analytics\manager::get_target($targetclass);
+
if (!empty($data['timesplitting'])) {
$timesplittingclass = \tool_analytics\output\helper::option_to_class($data['timesplitting']);
if (\core_analytics\manager::is_valid($timesplittingclass, '\core_analytics\local\time_splitting\base') === false) {
$errors['timesplitting'] = get_string('errorinvalidtimesplitting', 'analytics');
}
- $targetclass = \tool_analytics\output\helper::option_to_class($data['target']);
$timesplitting = \core_analytics\manager::get_time_splitting($timesplittingclass);
- $target = \core_analytics\manager::get_target($targetclass);
if (!$target->can_use_timesplitting($timesplitting)) {
$errors['timesplitting'] = get_string('invalidtimesplitting', 'tool_analytics');
}
}
+ if (!empty($data['contexts'])) {
+
+ $analyserclass = $target->get_analyser_class();
+ if (!$potentialcontexts = $analyserclass::potential_context_restrictions()) {
+ $errors['contexts'] = get_string('errornocontextrestrictions', 'analytics');
+ } else {
+
+ // Flip the contexts array so we can just diff by key.
+ $selectedcontexts = array_flip($data['contexts']);
+ $invalidcontexts = array_diff_key($selectedcontexts, $potentialcontexts);
+ if (!empty($invalidcontexts)) {
+ $errors['contexts'] = get_string('errorinvalidcontexts', 'analytics');
+ }
+ }
+ }
+
if (!$this->_customdata['staticmodel']) {
if (empty($data['indicators'])) {
$errors['indicators'] = get_string('errornoindicators', 'analytics');
@@ -179,4 +218,18 @@ class edit_model extends \moodleform {
return $errors;
}
+
+ /**
+ * Load the currently selected context options.
+ *
+ * @return array
+ */
+ protected function load_current_contexts() {
+ $contexts = [];
+ foreach ($this->_customdata['contexts'] as $context) {
+ $contexts[$context->id] = $context->get_context_name(true, true);
+ }
+
+ return $contexts;
+ }
}
diff --git a/admin/tool/analytics/classes/output/invalid_analysables.php b/admin/tool/analytics/classes/output/invalid_analysables.php
index fd205f1dd8e..201175493a3 100644
--- a/admin/tool/analytics/classes/output/invalid_analysables.php
+++ b/admin/tool/analytics/classes/output/invalid_analysables.php
@@ -76,7 +76,8 @@ class invalid_analysables implements \renderable, \templatable {
$offset = $this->page * $this->perpage;
- $analysables = $this->model->get_analyser(['notimesplitting' => true])->get_analysables_iterator();
+ $contexts = $this->model->get_contexts();
+ $analysables = $this->model->get_analyser(['notimesplitting' => true])->get_analysables_iterator(null, $contexts);
$skipped = 0;
$enoughresults = false;
diff --git a/admin/tool/analytics/cli/evaluate_model.php b/admin/tool/analytics/cli/evaluate_model.php
index 3094ffaaec2..7d07b6ffe05 100644
--- a/admin/tool/analytics/cli/evaluate_model.php
+++ b/admin/tool/analytics/cli/evaluate_model.php
@@ -34,7 +34,6 @@ Options:
--list List models
--non-interactive Not interactive questions
--analysisinterval Restrict the evaluation to 1 single analysis interval (Optional)
---filter Analyser dependant. e.g. A courseid would evaluate the model using a single course (Optional)
--mode 'configuration' or 'trainedmodel'. You can only use mode=trainedmodel when the trained" .
" model was imported" . "
--reuse-prev-analysed Reuse recently analysed courses instead of analysing the whole site. Set it to false while" .
@@ -42,7 +41,7 @@ Options:
-h, --help Print out this help
Example:
-\$ php admin/tool/analytics/cli/evaluate_model.php --modelid=1 --analysisinterval='\\core\\analytics\\time_splitting\\quarters' --filter=123,321
+\$ php admin/tool/analytics/cli/evaluate_model.php --modelid=1 --analysisinterval='\\core\\analytics\\time_splitting\\quarters'
";
// Now get cli options.
@@ -55,7 +54,6 @@ list($options, $unrecognized) = cli_get_params(
'mode' => 'configuration',
'reuse-prev-analysed' => true,
'non-interactive' => false,
- 'filter' => false
),
array(
'h' => 'help',
@@ -83,11 +81,6 @@ if ($options['modelid'] === false) {
exit(0);
}
-// Reformat them as an array.
-if ($options['filter'] !== false) {
- $options['filter'] = explode(',', $options['filter']);
-}
-
if ($options['mode'] !== 'configuration' && $options['mode'] !== 'trainedmodel') {
cli_error('Error: The provided mode is not supported');
}
@@ -110,7 +103,6 @@ if ($options['reuse-prev-analysed']) {
$renderer = $PAGE->get_renderer('tool_analytics');
$analyseroptions = array(
- 'filter' => $options['filter'],
'timesplitting' => $options['analysisinterval'],
'reuseprevanalysed' => $options['reuse-prev-analysed'],
'mode' => $options['mode'],
diff --git a/admin/tool/analytics/createmodel.php b/admin/tool/analytics/createmodel.php
index ad4fc836e32..678eec8201b 100644
--- a/admin/tool/analytics/createmodel.php
+++ b/admin/tool/analytics/createmodel.php
@@ -45,6 +45,8 @@ $targets = array_filter(\core_analytics\manager::get_all_targets(), function($ta
return (!$target->based_on_assumptions());
});
+// Set 'supportscontexts' to true as at this stage we don't know if the contexts are supported by
+// the selected target.
$customdata = array(
'trainedmodel' => false,
'staticmodel' => false,
@@ -52,6 +54,7 @@ $customdata = array(
'indicators' => \core_analytics\manager::get_all_indicators(),
'timesplittings' => \core_analytics\manager::get_all_time_splittings(),
'predictionprocessors' => \core_analytics\manager::get_all_prediction_processors(),
+ 'supportscontexts' => true,
);
$mform = new \tool_analytics\output\form\edit_model(null, $customdata);
@@ -86,8 +89,8 @@ if ($mform->is_cancelled()) {
$indicators = array_diff_key($indicators, $invalidindicators);
}
- // Update the model with the valid list of indicators.
- $model->update($data->enabled, $indicators, $timesplitting, $predictionsprocessor);
+ // Update the model with the rest of the data provided in the form.
+ $model->update($data->enabled, $indicators, $timesplitting, $predictionsprocessor, $data->contexts);
$message = '';
$messagetype = \core\output\notification::NOTIFY_SUCCESS;
diff --git a/admin/tool/analytics/db/services.php b/admin/tool/analytics/db/services.php
new file mode 100644
index 00000000000..fefbfb8df19
--- /dev/null
+++ b/admin/tool/analytics/db/services.php
@@ -0,0 +1,37 @@
+.
+
+/**
+ * Tool analytics webservice definitions.
+ *
+ * @package tool_analytics
+ * @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$functions = array(
+
+ 'tool_analytics_potential_contexts' => array(
+ 'classname' => 'tool_analytics\external',
+ 'methodname' => 'potential_contexts',
+ 'description' => 'Retrieve the list of potential contexts for a model.',
+ 'type' => 'read',
+ 'ajax' => true,
+ 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+ ),
+);
diff --git a/admin/tool/analytics/lang/en/tool_analytics.php b/admin/tool/analytics/lang/en/tool_analytics.php
index ca3d1a271a1..acace09273b 100644
--- a/admin/tool/analytics/lang/en/tool_analytics.php
+++ b/admin/tool/analytics/lang/en/tool_analytics.php
@@ -45,6 +45,8 @@ $string['component'] = 'Component';
$string['componentcore'] = 'Core';
$string['componentselect'] = 'Select all models provided by the component \'{$a}\'';
$string['componentselectnone'] = 'Unselect all';
+$string['contexts'] = 'Contexts';
+$string['contexts_help'] = 'The model will be limited to this set of contexts. No context restrictions will be applied if no contexts are selected.';
$string['createmodel'] = 'Create model';
$string['currenttimesplitting'] = 'Current analysis interval';
$string['delete'] = 'Delete';
diff --git a/admin/tool/analytics/model.php b/admin/tool/analytics/model.php
index 4886ff5ffb0..3788f150731 100644
--- a/admin/tool/analytics/model.php
+++ b/admin/tool/analytics/model.php
@@ -122,6 +122,7 @@ switch ($action) {
$invalidcurrenttimesplitting = $model->invalid_timesplitting_selected();
$potentialtimesplittings = $model->get_potential_timesplittings();
+ $analyser = $model->get_analyser();
$customdata = array(
'id' => $model->get_id(),
@@ -132,7 +133,9 @@ switch ($action) {
'targetname' => $model->get_target()->get_name(),
'indicators' => $model->get_potential_indicators(),
'timesplittings' => $potentialtimesplittings,
- 'predictionprocessors' => \core_analytics\manager::get_all_prediction_processors()
+ 'predictionprocessors' => \core_analytics\manager::get_all_prediction_processors(),
+ 'supportscontexts' => ($analyser)::context_restriction_support(),
+ 'contexts' => $model->get_contexts(),
);
$mform = new \tool_analytics\output\form\edit_model(null, $customdata);
@@ -157,7 +160,7 @@ switch ($action) {
$predictionsprocessor = false;
}
- $model->update($data->enabled, $indicators, $timesplitting, $predictionsprocessor);
+ $model->update($data->enabled, $indicators, $timesplitting, $predictionsprocessor, $data->contexts);
redirect($returnurl);
}
@@ -168,6 +171,9 @@ switch ($action) {
$callable = array('\tool_analytics\output\helper', 'class_to_option');
$modelobj->indicators = array_map($callable, json_decode($modelobj->indicators));
$modelobj->timesplitting = \tool_analytics\output\helper::class_to_option($modelobj->timesplitting);
+ if ($modelobj->contextids) {
+ $modelobj->contexts = array_map($callable, json_decode($modelobj->contextids));
+ }
$modelobj->predictionsprocessor = \tool_analytics\output\helper::class_to_option($modelobj->predictionsprocessor);
$mform->set_data($modelobj);
$mform->display();
diff --git a/admin/tool/analytics/tests/external_test.php b/admin/tool/analytics/tests/external_test.php
new file mode 100644
index 00000000000..ff26c646b8c
--- /dev/null
+++ b/admin/tool/analytics/tests/external_test.php
@@ -0,0 +1,80 @@
+.
+
+/**
+ * Tool analytics external functions tests.
+ *
+ * @package tool_analytics
+ * @category external
+ * @copyright 2019 David MonllaĆ³ {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 3.8
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+require_once($CFG->dirroot . '/webservice/tests/helpers.php');
+require_once(__DIR__ . '/../../../../analytics/tests/fixtures/test_indicator_max.php');
+require_once(__DIR__ . '/../../../../analytics/tests/fixtures/test_target_course_level_shortname.php');
+
+/**
+ * Tool analytics external functions tests
+ *
+ * @package tool_analytics
+ * @category external
+ * @copyright 2019 David MonllaĆ³ {@link http://www.davidmonllao.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 3.8
+ */
+class tool_analytics_external_testcase extends externallib_advanced_testcase {
+
+ /**
+ * test_potential_contexts description
+ */
+ public function test_potential_contexts() {
+ $this->resetAfterTest();
+
+ $this->setAdminUser();
+
+ // Include the all context levels so the misc. category get included.
+ $this->assertCount(1, \tool_analytics\external::potential_contexts());
+
+ // The frontpage is not included.
+ $this->assertCount(0, \tool_analytics\external::potential_contexts('PHPUnit'));
+
+ $target = \core_analytics\manager::get_target('test_target_course_level_shortname');
+ $indicators = ['test_indicator_max' => \core_analytics\manager::get_indicator('test_indicator_max')];
+ $model = \core_analytics\model::create($target, $indicators);
+
+ $this->assertCount(1, \tool_analytics\external::potential_contexts(null, $model->get_id()));
+ }
+
+ /**
+ * test_potential_contexts description
+ *
+ * @expectedException required_capability_exception
+ */
+ public function test_potential_contexts_no_manager() {
+ $this->resetAfterTest();
+
+ $user = $this->getDataGenerator()->create_user();
+ $this->setUser($user);
+
+ $this->assertCount(2, \tool_analytics\external::potential_contexts());
+ }
+}
diff --git a/admin/tool/analytics/version.php b/admin/tool/analytics/version.php
index 03c5187f185..c2e2b1c791d 100644
--- a/admin/tool/analytics/version.php
+++ b/admin/tool/analytics/version.php
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2019052000; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version = 2019052002; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2019051100; // Requires this Moodle version.
$plugin->component = 'tool_analytics'; // Full name of the plugin (used for diagnostics).
diff --git a/analytics/classes/analysis.php b/analytics/classes/analysis.php
index ad39b48d0ac..f11bfae03d7 100644
--- a/analytics/classes/analysis.php
+++ b/analytics/classes/analysis.php
@@ -75,9 +75,10 @@ class analysis {
/**
* Runs the analysis.
*
+ * @param \context[] $contexts Restrict the analysis to these contexts. No context restrictions if null.
* @return null
*/
- public function run() {
+ public function run(array $contexts = []) {
$options = $this->analyser->get_options();
@@ -89,7 +90,7 @@ class analysis {
} else {
$action = 'prediction';
}
- $analysables = $this->analyser->get_analysables_iterator($action);
+ $analysables = $this->analyser->get_analysables_iterator($action, $contexts);
$processedanalysables = $this->get_processed_analysables();
diff --git a/analytics/classes/local/analyser/base.php b/analytics/classes/local/analyser/base.php
index 0065f15ab85..99735bd2c45 100644
--- a/analytics/classes/local/analyser/base.php
+++ b/analytics/classes/local/analyser/base.php
@@ -131,9 +131,10 @@ abstract class base {
* to ease to implementation of get_analysables_iterator: get_iterator_sql and order_sql.
*
* @param string|null $action 'prediction', 'training' or null if no specific action needed.
+ * @param \context[] $contexts Only analysables that depend on the provided contexts. All analysables in the system if empty.
* @return \Iterator
*/
- public function get_analysables_iterator(?string $action = null) {
+ public function get_analysables_iterator(?string $action = null, array $contexts = []) {
debugging('Please overwrite get_analysables_iterator with your own implementation, we only keep this default
implementation for backwards compatibility purposes with get_analysables(). note that $action param will
@@ -266,38 +267,42 @@ abstract class base {
/**
* Returns labelled data (training and evaluation).
*
+ * @param \context[] $contexts Restrict the analysis to these contexts. No context restrictions if null.
* @return \stored_file[]
*/
- public function get_labelled_data() {
+ public function get_labelled_data(array $contexts = []) {
// Delegates all processing to the analysis.
$result = new \core_analytics\local\analysis\result_file($this->get_modelid(), true, $this->get_options());
$analysis = new \core_analytics\analysis($this, true, $result);
- $analysis->run();
+ $analysis->run($contexts);
return $result->get();
}
/**
* Returns unlabelled data (prediction).
*
+ * @param \context[] $contexts Restrict the analysis to these contexts. No context restrictions if null.
* @return \stored_file[]
*/
- public function get_unlabelled_data() {
+ public function get_unlabelled_data(array $contexts = []) {
// Delegates all processing to the analysis.
$result = new \core_analytics\local\analysis\result_file($this->get_modelid(), false, $this->get_options());
$analysis = new \core_analytics\analysis($this, false, $result);
- $analysis->run();
+ $analysis->run($contexts);
return $result->get();
}
/**
* Returns indicator calculations as an array.
+ *
+ * @param \context[] $contexts Restrict the analysis to these contexts. No context restrictions if null.
* @return array
*/
- public function get_static_data() {
+ public function get_static_data(array $contexts = []) {
// Delegates all processing to the analysis.
$result = new \core_analytics\local\analysis\result_array($this->get_modelid(), false, $this->get_options());
$analysis = new \core_analytics\analysis($this, false, $result);
- $analysis->run();
+ $analysis->run($contexts);
return $result->get();
}
@@ -422,6 +427,34 @@ abstract class base {
return false;
}
+ /**
+ * Returns an array of context levels that can be used to restrict the contexts used during analysis.
+ *
+ * The contexts provided to self::get_analysables_iterator will match these contextlevels.
+ *
+ * @return array Array of context levels or an empty array if context restriction is not supported.
+ */
+ public static function context_restriction_support(): array {
+ return [];
+ }
+
+ /**
+ * Returns the possible contexts used by the analyser.
+ *
+ * This method uses separate logic for each context level because to iterate through
+ * the list of contexts calling get_context_name for each of them would be expensive
+ * in performance terms.
+ *
+ * This generic implementation returns all the contexts in the site for the provided context level.
+ * Overwrite it for specific restrictions in your analyser.
+ *
+ * @param string|null $query Context name filter.
+ * @return int[]
+ */
+ public static function potential_context_restrictions(string $query = null) {
+ return \core_analytics\manager::get_potential_context_restrictions(static::context_restriction_support(), $query);
+ }
+
/**
* Get the sql of a default implementation of the iterator.
*
@@ -431,9 +464,12 @@ abstract class base {
* @param int $contextlevel The context level of the analysable
* @param string|null $action
* @param string|null $tablealias The table alias
+ * @param \context[] $contexts Only analysables that depend on the provided contexts. All analysables if empty.
* @return array [0] => sql and [1] => params array
*/
- protected function get_iterator_sql(string $tablename, int $contextlevel, ?string $action = null, ?string $tablealias = null) {
+ protected function get_iterator_sql(string $tablename, int $contextlevel, ?string $action = null, ?string $tablealias = null,
+ array $contexts = []) {
+ global $DB;
if (!$tablealias) {
$tablealias = 'analysable';
@@ -452,13 +488,30 @@ abstract class base {
$params = $params + ['action' => $action];
}
- // Adding the 1 = 1 just to have the WHERE part so that all further conditions added by callers can be
- // appended to $sql with and ' AND'.
$sql = 'SELECT ' . $select . '
FROM {' . $tablename . '} ' . $tablealias . '
' . $usedanalysablesjoin . '
- JOIN {context} ctx ON (ctx.contextlevel = :contextlevel AND ctx.instanceid = ' . $tablealias . '.id)
- WHERE 1 = 1';
+ JOIN {context} ctx ON (ctx.contextlevel = :contextlevel AND ctx.instanceid = ' . $tablealias . '.id) ';
+
+ if (!$contexts) {
+ // Adding the 1 = 1 just to have the WHERE part so that all further conditions
+ // added by callers can be appended to $sql with and ' AND'.
+ $sql .= 'WHERE 1 = 1';
+ } else {
+
+ $contextsqls = [];
+ foreach ($contexts as $context) {
+ $paramkey1 = 'paramctxlike' . $context->id;
+ $paramkey2 = 'paramctxeq' . $context->id;
+ $contextsqls[] = $DB->sql_like('ctx.path', ':' . $paramkey1);
+ $contextsqls[] = 'ctx.path = :' . $paramkey2;
+
+ // This includes the context itself.
+ $params[$paramkey1] = $context->path . '/%';
+ $params[$paramkey2] = $context->path;
+ }
+ $sql .= 'WHERE (' . implode(' OR ', $contextsqls) . ')';
+ }
return [$sql, $params];
}
diff --git a/analytics/classes/local/analyser/by_course.php b/analytics/classes/local/analyser/by_course.php
index c0d308b0d70..59b66273605 100644
--- a/analytics/classes/local/analyser/by_course.php
+++ b/analytics/classes/local/analyser/by_course.php
@@ -39,24 +39,13 @@ abstract class by_course extends base {
* Return the list of courses to analyse.
*
* @param string|null $action 'prediction', 'training' or null if no specific action needed.
+ * @param \context[] $contexts Only analysables that depend on the provided contexts. All analysables in the system if empty.
* @return \Iterator
*/
- public function get_analysables_iterator(?string $action = null) {
+ public function get_analysables_iterator(?string $action = null, array $contexts = []) {
global $DB;
- list($sql, $params) = $this->get_iterator_sql('course', CONTEXT_COURSE, $action, 'c');
-
- // This will be updated to filter by context as part of MDL-64739.
- if (!empty($this->options['filter'])) {
- $courses = array();
- foreach ($this->options['filter'] as $courseid) {
- $courses[$courseid] = intval($courseid);
- }
-
- list($coursesql, $courseparams) = $DB->get_in_or_equal($courses, SQL_PARAMS_NAMED);
- $sql .= " AND c.id $coursesql";
- $params = $params + $courseparams;
- }
+ list($sql, $params) = $this->get_iterator_sql('course', CONTEXT_COURSE, $action, 'c', $contexts);
$ordersql = $this->order_sql('sortorder', 'ASC', 'c');
@@ -76,4 +65,13 @@ abstract class by_course extends base {
return \core_analytics\course::instance($record, $context);
});
}
-}
\ No newline at end of file
+
+ /**
+ * Can be limited to course categories or specific courses.
+ *
+ * @return array
+ */
+ public static function context_restriction_support(): array {
+ return [CONTEXT_COURSE, CONTEXT_COURSECAT];
+ }
+}
diff --git a/analytics/classes/local/analyser/sitewide.php b/analytics/classes/local/analyser/sitewide.php
index da6ea71db82..28a4db3a4e9 100644
--- a/analytics/classes/local/analyser/sitewide.php
+++ b/analytics/classes/local/analyser/sitewide.php
@@ -39,9 +39,10 @@ abstract class sitewide extends base {
* Return the list of analysables to analyse.
*
* @param string|null $action 'prediction', 'training' or null if no specific action needed.
+ * @param \context[] $contexts Ignored here.
* @return \Iterator
*/
- public function get_analysables_iterator(?string $action = null) {
+ public function get_analysables_iterator(?string $action = null, array $contexts = []) {
// We can safely ignore $action as we have 1 single analysable element in this analyser.
return new \ArrayIterator([new \core_analytics\site()]);
}
diff --git a/analytics/classes/manager.php b/analytics/classes/manager.php
index 60f1f4597fd..131c782d582 100644
--- a/analytics/classes/manager.php
+++ b/analytics/classes/manager.php
@@ -646,6 +646,8 @@ class manager {
$usedanalysablesanalysableids = array_flip($usedanalysablesanalysableids);
$analyser = $model->get_analyser(array('notimesplitting' => true));
+
+ // We do not honour the list of contexts in this model as it can contain stale records.
$analysables = $analyser->get_analysables_iterator();
$analysableids = [];
@@ -913,4 +915,79 @@ class manager {
return [$target, $indicators];
}
+
+ /**
+ * Return the context restrictions that can be applied to the provided context levels.
+ *
+ * @throws \coding_exception
+ * @param array|null $contextlevels The list of context levels provided by the analyser. Null if all of them.
+ * @param string|null $query
+ * @return array Associative array with contextid as key and the short version of the context name as value.
+ */
+ public static function get_potential_context_restrictions(?array $contextlevels = null, string $query = null) {
+ global $DB;
+
+ if (empty($contextlevels) && !is_null($contextlevels)) {
+ return false;
+ }
+
+ if (!is_null($contextlevels)) {
+ foreach ($contextlevels as $contextlevel) {
+ if ($contextlevel !== CONTEXT_COURSE && $contextlevel !== CONTEXT_COURSECAT) {
+ throw new \coding_exception('Only CONTEXT_COURSE and CONTEXT_COURSECAT are supported at the moment.');
+ }
+ }
+ }
+
+ $contexts = [];
+
+ // We have a separate process for each context level for performance reasons (to iterate through mdl_context calling
+ // get_context_name() would be too slow).
+ $contextsystem = \context_system::instance();
+ if (is_null($contextlevels) || in_array(CONTEXT_COURSECAT, $contextlevels)) {
+
+ $sql = "SELECT cc.id, cc.name, ctx.id AS contextid
+ FROM {course_categories} cc
+ JOIN {context} ctx ON ctx.contextlevel = :ctxlevel AND ctx.instanceid = cc.id";
+ $params = ['ctxlevel' => CONTEXT_COURSECAT];
+
+ if ($query) {
+ $sql .= " WHERE " . $DB->sql_like('cc.name', ':query', false, false);
+ $params['query'] = '%' . $query . '%';
+ }
+
+ $coursecats = $DB->get_recordset_sql($sql, $params);
+ foreach ($coursecats as $record) {
+ $contexts[$record->contextid] = get_string('category') . ': ' .
+ format_string($record->name, true, array('context' => $contextsystem));
+ }
+ $coursecats->close();
+ }
+
+ if (is_null($contextlevels) || in_array(CONTEXT_COURSE, $contextlevels)) {
+
+ $sql = "SELECT c.id, c.shortname, ctx.id AS contextid
+ FROM {course} c
+ JOIN {context} ctx ON ctx.contextlevel = :ctxlevel AND ctx.instanceid = c.id
+ WHERE c.id != :siteid";
+ $params = ['ctxlevel' => CONTEXT_COURSE, 'siteid' => SITEID];
+
+ if ($query) {
+ $sql .= ' AND (' . $DB->sql_like('c.fullname', ':query1', false, false) . ' OR ' .
+ $DB->sql_like('c.shortname', ':query2', false, false) . ')';
+ $params['query1'] = '%' . $query . '%';
+ $params['query2'] = '%' . $query . '%';
+ }
+
+ $courses = $DB->get_recordset_sql($sql, $params);
+ foreach ($courses as $record) {
+ $contexts[$record->contextid] = get_string('course') . ': ' .
+ format_string($record->shortname, true, array('context' => $contextsystem));
+ }
+ $courses->close();
+ }
+
+ return $contexts;
+ }
+
}
diff --git a/analytics/classes/model.php b/analytics/classes/model.php
index 9a524be4c63..cc92516b297 100644
--- a/analytics/classes/model.php
+++ b/analytics/classes/model.php
@@ -120,6 +120,11 @@ class model {
*/
protected $indicators = null;
+ /**
+ * @var \context[]
+ */
+ protected $contexts = null;
+
/**
* Unique Model id created from site info and last model modification.
*
@@ -459,9 +464,11 @@ class model {
* @param \core_analytics\local\indicator\base[]|false $indicators False to respect current indicators
* @param string|false $timesplittingid False to respect current time splitting method
* @param string|false $predictionsprocessor False to respect current predictors processor value
+ * @param int[]|false $contextids List of context ids for this model. False to respect the current list of contexts.
* @return void
*/
- public function update($enabled, $indicators = false, $timesplittingid = '', $predictionsprocessor = false) {
+ public function update($enabled, $indicators = false, $timesplittingid = '', $predictionsprocessor = false,
+ $contextids = false) {
global $USER, $DB;
\core_analytics\manager::check_can_manage_models();
@@ -486,6 +493,15 @@ class model {
$predictionsprocessor = $this->model->predictionsprocessor;
}
+ if ($contextids !== false) {
+ $contextsstr = json_encode($contextids);
+
+ // Reset the internal cache.
+ $this->contexts = null;
+ } else {
+ $contextsstr = $this->model->contextids;
+ }
+
if ($this->model->timesplitting !== $timesplittingid ||
$this->model->indicators !== $indicatorsstr ||
$this->model->predictionsprocessor !== $predictionsprocessor) {
@@ -514,6 +530,7 @@ class model {
$this->model->indicators = $indicatorsstr;
$this->model->timesplitting = $timesplittingid;
$this->model->predictionsprocessor = $predictionsprocessor;
+ $this->model->contextids = $contextsstr;
$this->model->timemodified = $now;
$this->model->usermodified = $USER->id;
@@ -603,7 +620,7 @@ class model {
// Before get_labelled_data call so we get an early exception if it is not ready.
$predictor = $this->get_predictions_processor();
- $datasets = $this->get_analyser()->get_labelled_data();
+ $datasets = $this->get_analyser()->get_labelled_data($this->get_contexts());
// No datasets generated.
if (empty($datasets)) {
@@ -695,7 +712,7 @@ class model {
// Before get_labelled_data call so we get an early exception if it is not ready.
$predictor = $this->get_predictions_processor();
- $datasets = $this->get_analyser()->get_labelled_data();
+ $datasets = $this->get_analyser()->get_labelled_data($this->get_contexts());
// No training if no files have been provided.
if (empty($datasets) || empty($datasets[$this->model->timesplitting])) {
@@ -766,7 +783,7 @@ class model {
// Before get_unlabelled_data call so we get an early exception if it is not ready.
$predictor = $this->get_predictions_processor();
- $samplesdata = $this->get_analyser()->get_unlabelled_data();
+ $samplesdata = $this->get_analyser()->get_unlabelled_data($this->get_contexts());
// Get the prediction samples file.
if (empty($samplesdata) || empty($samplesdata[$this->model->timesplitting])) {
@@ -802,7 +819,7 @@ class model {
} else {
// Predictions based on assumptions.
- $indicatorcalculations = $this->get_analyser()->get_static_data();
+ $indicatorcalculations = $this->get_analyser()->get_static_data($this->get_contexts());
// Get the prediction samples file.
if (empty($indicatorcalculations) || empty($indicatorcalculations[$this->model->timesplitting])) {
@@ -1956,6 +1973,30 @@ class model {
return [$allsampleids, $allsamplesdata];
}
+ /**
+ * Contexts where this model should be active.
+ *
+ * @return \context[] Empty array if there are no context restrictions.
+ */
+ public function get_contexts() {
+ if ($this->contexts !== null) {
+ return $this->contexts;
+ }
+
+ if (!$this->model->contextids) {
+ $this->contexts = [];
+ return $this->contexts;
+ }
+ $contextids = json_decode($this->model->contextids);
+
+ // We don't expect this list to be massive as contexts need to be selected manually using the edit model form.
+ $this->contexts = array_map(function($contextid) {
+ return \context::instance_by_id($contextid, IGNORE_MISSING);
+ }, $contextids);
+
+ return $this->contexts;
+ }
+
/**
* Purges the insights cache.
*/
diff --git a/analytics/tests/manager_test.php b/analytics/tests/manager_test.php
index 02166a8f65b..582c45740a2 100644
--- a/analytics/tests/manager_test.php
+++ b/analytics/tests/manager_test.php
@@ -487,4 +487,31 @@ class analytics_manager_testcase extends advanced_testcase {
$this->assertNotEmpty($indicators);
$this->assertContainsOnlyInstancesOf(\core_analytics\local\indicator\base::class, $indicators);
}
+
+ /**
+ * test_get_potential_context_restrictions description
+ */
+ public function test_get_potential_context_restrictions() {
+ $this->resetAfterTest();
+
+ // No potential context restrictions.
+ $this->assertFalse(\core_analytics\manager::get_potential_context_restrictions([]));
+
+ // Include the all context levels so the misc. category get included.
+ $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions());
+
+ $this->getDataGenerator()->create_course();
+ $this->getDataGenerator()->create_category();
+ $this->assertCount(3, \core_analytics\manager::get_potential_context_restrictions());
+ $this->assertCount(3, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE, CONTEXT_COURSECAT]));
+
+ $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE]));
+ $this->assertCount(2, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT]));
+
+ $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'Course category'));
+ $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'Course category 1'));
+ $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSECAT], 'Miscellaneous'));
+ $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE], 'Test course 1'));
+ $this->assertCount(1, \core_analytics\manager::get_potential_context_restrictions([CONTEXT_COURSE], 'Test course'));
+ }
}
diff --git a/analytics/tests/prediction_test.php b/analytics/tests/prediction_test.php
index 2b437f6270c..1fd9ac2a724 100644
--- a/analytics/tests/prediction_test.php
+++ b/analytics/tests/prediction_test.php
@@ -126,6 +126,57 @@ class core_analytics_prediction_testcase extends advanced_testcase {
array('modelid' => $model->get_id())));
}
+ /**
+ * test_model_contexts
+ */
+ public function test_model_contexts() {
+ global $DB;
+
+ $this->resetAfterTest(true);
+ $this->setAdminuser();
+
+ $misc = $DB->get_record('course_categories', ['name' => 'Miscellaneous']);
+ $miscctx = \context_coursecat::instance($misc->id);
+
+ $category = $this->getDataGenerator()->create_category();
+ $categoryctx = \context_coursecat::instance($category->id);
+
+ // One course per category.
+ $courseparams = array('shortname' => 'aaaaaa', 'fullname' => 'aaaaaa', 'visible' => 0,
+ 'category' => $category->id);
+ $course1 = $this->getDataGenerator()->create_course($courseparams);
+ $course1ctx = \context_course::instance($course1->id);
+ $courseparams = array('shortname' => 'bbbbbb', 'fullname' => 'bbbbbb', 'visible' => 0,
+ 'category' => $misc->id);
+ $course2 = $this->getDataGenerator()->create_course($courseparams);
+
+ $model = $this->add_perfect_model('test_static_target_shortname');
+
+ // Just 1 category.
+ $model->update(true, false, '\core\analytics\time_splitting\no_splitting', false, [$categoryctx->id]);
+ $this->assertCount(1, $model->predict()->predictions);
+
+ // Now with 2 categories.
+ $model->update(true, false, false, false, [$categoryctx->id, $miscctx->id]);
+
+ // The courses in the new category are processed.
+ $this->assertCount(1, $model->predict()->predictions);
+
+ // Clear the predictions generated by the model and predict() again.
+ $model->clear();
+ $this->assertCount(2, $model->predict()->predictions);
+
+ // Course context restriction.
+ $model->update(true, false, '\core\analytics\time_splitting\no_splitting', false, [$course1ctx->id]);
+
+ // Nothing new as the course was already analysed.
+ $result = $model->predict();
+ $this->assertTrue(empty($result->predictions));
+
+ $model->clear();
+ $this->assertCount(1, $model->predict()->predictions);
+ }
+
/**
* test_ml_training_and_prediction
*
diff --git a/analytics/upgrade.txt b/analytics/upgrade.txt
index 38395c33be4..4773d4a793d 100644
--- a/analytics/upgrade.txt
+++ b/analytics/upgrade.txt
@@ -30,6 +30,10 @@ information provided here is intended especially for developers.
* Predictions flagged as "Not useful" in models whose targets use analysers that provide multiple samples
per analysable (e.g. students at risk or no teaching) have been updated to "Incorrectly flagged".
* \core_analytics\predictor::delete_output_dir has a new 2nd parameter, $uniquemodelid.
+* Analyser's get_analysables_iterator and get_iterator_sql have a new $contexts parameter to limit the returned analysables to
+ the ones that depend on the provided contexts.
+* Analysers can implement a context_restriction_support() method to restrict models to a subset of the
+ contents in the site. Only CONTEXT_COURSE and CONTEXT_COURSECAT are supported.
=== 3.7 ===
diff --git a/lang/en/analytics.php b/lang/en/analytics.php
index 1c72cb1b07a..c32225abae5 100644
--- a/lang/en/analytics.php
+++ b/lang/en/analytics.php
@@ -46,8 +46,10 @@ $string['errorimportmissingcomponents'] = 'The provided model requires the follo
$string['errorimportversionmismatches'] = 'The version of the following components differs from the version installed on this site: {$a}. You can use the option \'Ignore version mismatches\' to ignore these differences.';
$string['errorimportmissingclasses'] = 'The following analytics components are not available on this site: {$a->missingclasses}.';
$string['errorinvalidindicator'] = 'Invalid {$a} indicator';
+$string['errorinvalidcontexts'] = 'Some of the selected contexts can not be used in this target.';
$string['errorinvalidtarget'] = 'Invalid {$a} target';
$string['errorinvalidtimesplitting'] = 'Invalid analysis interval; please ensure you add the fully qualified class name.';
+$string['errornocontextrestrictions'] = 'The selected target does not support context restrictions';
$string['errornoexportconfig'] = 'There was a problem exporting the model configuration.';
$string['errornoexportconfigrequirements'] = 'Only non-static models with an analysis interval can be exported.';
$string['errornoindicators'] = 'This model does not have any indicators.';
diff --git a/lib/classes/analytics/analyser/users.php b/lib/classes/analytics/analyser/users.php
index 40ba586e762..76bf3942f0a 100644
--- a/lib/classes/analytics/analyser/users.php
+++ b/lib/classes/analytics/analyser/users.php
@@ -39,14 +39,15 @@ class users extends \core_analytics\local\analyser\base {
* The site users are the analysable elements returned by this analyser.
*
* @param string|null $action 'prediction', 'training' or null if no specific action needed.
+ * @param \context[] $contexts Only analysables that depend on the provided contexts. All analysables in the system if empty.
* @return \Iterator
*/
- public function get_analysables_iterator(?string $action = null) {
+ public function get_analysables_iterator(?string $action = null, array $contexts = []) {
global $DB, $CFG;
$siteadmins = explode(',', $CFG->siteadmins);
- list($sql, $params) = $this->get_iterator_sql('user', CONTEXT_USER, $action, 'u');
+ list($sql, $params) = $this->get_iterator_sql('user', CONTEXT_USER, $action, 'u', $contexts);
$sql .= " AND u.deleted = :deleted AND u.confirmed = :confirmed AND u.suspended = :suspended";
$params = $params + ['deleted' => 0, 'confirmed' => 1, 'suspended' => 0];
diff --git a/lib/db/install.xml b/lib/db/install.xml
index b762bfcf675..a2e970f3020 100644
--- a/lib/db/install.xml
+++ b/lib/db/install.xml
@@ -3844,6 +3844,7 @@
+
diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php
index 5f27ae17f66..d4c526e4a63 100644
--- a/lib/db/upgrade.php
+++ b/lib/db/upgrade.php
@@ -3619,5 +3619,24 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2019101600.01);
}
+ if ($oldversion < 2019101800.02) {
+
+ // Get the table by its previous name.
+ $table = new xmldb_table('analytics_models');
+ if ($dbman->table_exists($table)) {
+
+ // Define field contextids to be added to analytics_models.
+ $field = new xmldb_field('contextids', XMLDB_TYPE_TEXT, null, null, null, null, null, 'version');
+
+ // Conditionally launch add field contextids.
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+ }
+
+ // Main savepoint reached.
+ upgrade_main_savepoint(true, 2019101800.02);
+ }
+
return true;
}
diff --git a/lib/tests/analysers_test.php b/lib/tests/analysers_test.php
index f52ab0f9636..2c6160326aa 100644
--- a/lib/tests/analysers_test.php
+++ b/lib/tests/analysers_test.php
@@ -48,16 +48,16 @@ class core_analytics_analysers_testcase extends advanced_testcase {
public function test_courses_analyser() {
$this->resetAfterTest(true);
- $course = $this->getDataGenerator()->create_course();
- $coursecontext = \context_course::instance($course->id);
+ $course1 = $this->getDataGenerator()->create_course();
+ $coursecontext = \context_course::instance($course1->id);
$target = new test_target_shortname();
$analyser = new \core\analytics\analyser\courses(1, $target, [], [], []);
- $analysable = new \core_analytics\course($course);
+ $analysable = new \core_analytics\course($course1);
- $this->assertInstanceOf('\core_analytics\course', $analyser->get_sample_analysable($course->id));
+ $this->assertInstanceOf('\core_analytics\course', $analyser->get_sample_analysable($course1->id));
- $this->assertInstanceOf('\context_course', $analyser->sample_access_context($course->id));
+ $this->assertInstanceOf('\context_course', $analyser->sample_access_context($course1->id));
// Just 1 sample per course.
$class = new ReflectionClass('\core\analytics\analyser\courses');
@@ -66,8 +66,8 @@ class core_analytics_analysers_testcase extends advanced_testcase {
list($sampleids, $samplesdata) = $method->invoke($analyser, $analysable);
$this->assertCount(1, $sampleids);
$sampleid = reset($sampleids);
- $this->assertEquals($course->id, $sampleid);
- $this->assertEquals($course->fullname, $samplesdata[$sampleid]['course']->fullname);
+ $this->assertEquals($course1->id, $sampleid);
+ $this->assertEquals($course1->fullname, $samplesdata[$sampleid]['course']->fullname);
$this->assertEquals($coursecontext, $samplesdata[$sampleid]['context']);
// To compare it later.
@@ -75,6 +75,16 @@ class core_analytics_analysers_testcase extends advanced_testcase {
list($sampleids, $samplesdata) = $analyser->get_samples(array($sampleid));
$this->assertEquals($prevsampledata['context'], $samplesdata[$sampleid]['context']);
$this->assertEquals($prevsampledata['course']->shortname, $samplesdata[$sampleid]['course']->shortname);
+
+ // Context restriction.
+ $category1 = $this->getDataGenerator()->create_category();
+ $category1context = \context_coursecat::instance($category1->id);
+ $category2 = $this->getDataGenerator()->create_category();
+ $category2context = \context_coursecat::instance($category2->id);
+ $course2 = $this->getDataGenerator()->create_course(['category' => $category1->id]);
+ $course3 = $this->getDataGenerator()->create_course(['category' => $category2->id]);
+ $this->assertCount(2, $analyser->get_analysables_iterator(false, [$category1context, $category2context]));
+
}
/**
@@ -130,24 +140,24 @@ class core_analytics_analysers_testcase extends advanced_testcase {
$this->resetAfterTest(true);
- $course = $this->getDataGenerator()->create_course();
- $coursecontext = \context_course::instance($course->id);
+ $course1 = $this->getDataGenerator()->create_course();
+ $course1context = \context_course::instance($course1->id);
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$user3 = $this->getDataGenerator()->create_user();
// Checking that suspended users are also included.
- $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student');
- $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student', 'manual', 0, 0, ENROL_USER_SUSPENDED);
- $this->getDataGenerator()->enrol_user($user3->id, $course->id, 'editingteacher');
- $enrol = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'manual'));
+ $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'student');
+ $this->getDataGenerator()->enrol_user($user2->id, $course1->id, 'student', 'manual', 0, 0, ENROL_USER_SUSPENDED);
+ $this->getDataGenerator()->enrol_user($user3->id, $course1->id, 'editingteacher');
+ $enrol = $DB->get_record('enrol', array('courseid' => $course1->id, 'enrol' => 'manual'));
$ue1 = $DB->get_record('user_enrolments', array('userid' => $user1->id, 'enrolid' => $enrol->id));
$ue2 = $DB->get_record('user_enrolments', array('userid' => $user2->id, 'enrolid' => $enrol->id));
$target = new test_target_shortname();
$analyser = new \core\analytics\analyser\student_enrolments(1, $target, [], [], []);
- $analysable = new \core_analytics\course($course);
+ $analysable = new \core_analytics\course($course1);
$this->assertInstanceOf('\core_analytics\course', $analyser->get_sample_analysable($ue1->id));
$this->assertInstanceOf('\context_course', $analyser->sample_access_context($ue1->id));
@@ -165,8 +175,8 @@ class core_analytics_analysers_testcase extends advanced_testcase {
// Shouldn't matter which one we select.
$sampleid = $ue1->id;
$this->assertEquals($ue1, $samplesdata[$sampleid]['user_enrolments']);
- $this->assertEquals($course->fullname, $samplesdata[$sampleid]['course']->fullname);
- $this->assertEquals($coursecontext, $samplesdata[$sampleid]['context']);
+ $this->assertEquals($course1->fullname, $samplesdata[$sampleid]['course']->fullname);
+ $this->assertEquals($course1context, $samplesdata[$sampleid]['context']);
$this->assertEquals($user1->firstname, $samplesdata[$sampleid]['user']->firstname);
// To compare it later.
@@ -176,6 +186,15 @@ class core_analytics_analysers_testcase extends advanced_testcase {
$this->assertEquals($prevsampledata['context'], $samplesdata[$sampleid]['context']);
$this->assertEquals($prevsampledata['course']->shortname, $samplesdata[$sampleid]['course']->shortname);
$this->assertEquals($prevsampledata['user']->firstname, $samplesdata[$sampleid]['user']->firstname);
+
+ // Context restriction.
+ $category1 = $this->getDataGenerator()->create_category();
+ $category1context = \context_coursecat::instance($category1->id);
+ $category2 = $this->getDataGenerator()->create_category();
+ $category2context = \context_coursecat::instance($category2->id);
+ $course2 = $this->getDataGenerator()->create_course(['category' => $category1->id]);
+ $course3 = $this->getDataGenerator()->create_course(['category' => $category2->id]);
+ $this->assertCount(2, $analyser->get_analysables_iterator(false, [$category1context, $category2context]));
}
/**
diff --git a/version.php b/version.php
index 297ec2862dc..6ecdd39c873 100644
--- a/version.php
+++ b/version.php
@@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
-$version = 2019101800.01; // YYYYMMDD = weekly release date of this DEV branch.
+$version = 2019101800.02; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.