Merge branch 'MDL-64739_master' of git://github.com/dmonllao/moodle

This commit is contained in:
Eloy Lafuente (stronk7) 2019-10-21 13:29:29 +02:00
commit 7ba9c635c5
28 changed files with 719 additions and 71 deletions

View File

@ -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

View File

@ -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 <http://www.gnu.org/licenses/>.\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"}

View File

@ -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 <http://www.gnu.org/licenses/>.
/**
* 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);
}
};
});

View File

@ -0,0 +1,113 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* 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')
])
);
}
}

View File

@ -105,6 +105,28 @@ class edit_model extends \moodleform {
$mform->addElement('select', 'timesplitting', get_string('timesplittingmethod', 'analytics'), $timesplittings); $mform->addElement('select', 'timesplitting', get_string('timesplittingmethod', 'analytics'), $timesplittings);
$mform->addHelpButton('timesplitting', 'timesplittingmethod', 'analytics'); $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. // Predictions processor.
if (!$this->_customdata['staticmodel']) { if (!$this->_customdata['staticmodel']) {
$defaultprocessor = \core_analytics\manager::get_predictions_processor_name( $defaultprocessor = \core_analytics\manager::get_predictions_processor_name(
@ -146,20 +168,37 @@ class edit_model extends \moodleform {
public function validation($data, $files) { public function validation($data, $files) {
$errors = parent::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'])) { if (!empty($data['timesplitting'])) {
$timesplittingclass = \tool_analytics\output\helper::option_to_class($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) { if (\core_analytics\manager::is_valid($timesplittingclass, '\core_analytics\local\time_splitting\base') === false) {
$errors['timesplitting'] = get_string('errorinvalidtimesplitting', 'analytics'); $errors['timesplitting'] = get_string('errorinvalidtimesplitting', 'analytics');
} }
$targetclass = \tool_analytics\output\helper::option_to_class($data['target']);
$timesplitting = \core_analytics\manager::get_time_splitting($timesplittingclass); $timesplitting = \core_analytics\manager::get_time_splitting($timesplittingclass);
$target = \core_analytics\manager::get_target($targetclass);
if (!$target->can_use_timesplitting($timesplitting)) { if (!$target->can_use_timesplitting($timesplitting)) {
$errors['timesplitting'] = get_string('invalidtimesplitting', 'tool_analytics'); $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 (!$this->_customdata['staticmodel']) {
if (empty($data['indicators'])) { if (empty($data['indicators'])) {
$errors['indicators'] = get_string('errornoindicators', 'analytics'); $errors['indicators'] = get_string('errornoindicators', 'analytics');
@ -179,4 +218,18 @@ class edit_model extends \moodleform {
return $errors; 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;
}
} }

View File

@ -76,7 +76,8 @@ class invalid_analysables implements \renderable, \templatable {
$offset = $this->page * $this->perpage; $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; $skipped = 0;
$enoughresults = false; $enoughresults = false;

View File

@ -34,7 +34,6 @@ Options:
--list List models --list List models
--non-interactive Not interactive questions --non-interactive Not interactive questions
--analysisinterval Restrict the evaluation to 1 single analysis interval (Optional) --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" . --mode 'configuration' or 'trainedmodel'. You can only use mode=trainedmodel when the trained" .
" model was imported" . " " model was imported" . "
--reuse-prev-analysed Reuse recently analysed courses instead of analysing the whole site. Set it to false while" . --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 -h, --help Print out this help
Example: 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. // Now get cli options.
@ -55,7 +54,6 @@ list($options, $unrecognized) = cli_get_params(
'mode' => 'configuration', 'mode' => 'configuration',
'reuse-prev-analysed' => true, 'reuse-prev-analysed' => true,
'non-interactive' => false, 'non-interactive' => false,
'filter' => false
), ),
array( array(
'h' => 'help', 'h' => 'help',
@ -83,11 +81,6 @@ if ($options['modelid'] === false) {
exit(0); exit(0);
} }
// Reformat them as an array.
if ($options['filter'] !== false) {
$options['filter'] = explode(',', $options['filter']);
}
if ($options['mode'] !== 'configuration' && $options['mode'] !== 'trainedmodel') { if ($options['mode'] !== 'configuration' && $options['mode'] !== 'trainedmodel') {
cli_error('Error: The provided mode is not supported'); cli_error('Error: The provided mode is not supported');
} }
@ -110,7 +103,6 @@ if ($options['reuse-prev-analysed']) {
$renderer = $PAGE->get_renderer('tool_analytics'); $renderer = $PAGE->get_renderer('tool_analytics');
$analyseroptions = array( $analyseroptions = array(
'filter' => $options['filter'],
'timesplitting' => $options['analysisinterval'], 'timesplitting' => $options['analysisinterval'],
'reuseprevanalysed' => $options['reuse-prev-analysed'], 'reuseprevanalysed' => $options['reuse-prev-analysed'],
'mode' => $options['mode'], 'mode' => $options['mode'],

View File

@ -45,6 +45,8 @@ $targets = array_filter(\core_analytics\manager::get_all_targets(), function($ta
return (!$target->based_on_assumptions()); 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( $customdata = array(
'trainedmodel' => false, 'trainedmodel' => false,
'staticmodel' => false, 'staticmodel' => false,
@ -52,6 +54,7 @@ $customdata = array(
'indicators' => \core_analytics\manager::get_all_indicators(), 'indicators' => \core_analytics\manager::get_all_indicators(),
'timesplittings' => \core_analytics\manager::get_all_time_splittings(), 'timesplittings' => \core_analytics\manager::get_all_time_splittings(),
'predictionprocessors' => \core_analytics\manager::get_all_prediction_processors(), 'predictionprocessors' => \core_analytics\manager::get_all_prediction_processors(),
'supportscontexts' => true,
); );
$mform = new \tool_analytics\output\form\edit_model(null, $customdata); $mform = new \tool_analytics\output\form\edit_model(null, $customdata);
@ -86,8 +89,8 @@ if ($mform->is_cancelled()) {
$indicators = array_diff_key($indicators, $invalidindicators); $indicators = array_diff_key($indicators, $invalidindicators);
} }
// Update the model with the valid list of indicators. // Update the model with the rest of the data provided in the form.
$model->update($data->enabled, $indicators, $timesplitting, $predictionsprocessor); $model->update($data->enabled, $indicators, $timesplitting, $predictionsprocessor, $data->contexts);
$message = ''; $message = '';
$messagetype = \core\output\notification::NOTIFY_SUCCESS; $messagetype = \core\output\notification::NOTIFY_SUCCESS;

View File

@ -0,0 +1,37 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* 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)
),
);

View File

@ -45,6 +45,8 @@ $string['component'] = 'Component';
$string['componentcore'] = 'Core'; $string['componentcore'] = 'Core';
$string['componentselect'] = 'Select all models provided by the component \'{$a}\''; $string['componentselect'] = 'Select all models provided by the component \'{$a}\'';
$string['componentselectnone'] = 'Unselect all'; $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['createmodel'] = 'Create model';
$string['currenttimesplitting'] = 'Current analysis interval'; $string['currenttimesplitting'] = 'Current analysis interval';
$string['delete'] = 'Delete'; $string['delete'] = 'Delete';

View File

@ -122,6 +122,7 @@ switch ($action) {
$invalidcurrenttimesplitting = $model->invalid_timesplitting_selected(); $invalidcurrenttimesplitting = $model->invalid_timesplitting_selected();
$potentialtimesplittings = $model->get_potential_timesplittings(); $potentialtimesplittings = $model->get_potential_timesplittings();
$analyser = $model->get_analyser();
$customdata = array( $customdata = array(
'id' => $model->get_id(), 'id' => $model->get_id(),
@ -132,7 +133,9 @@ switch ($action) {
'targetname' => $model->get_target()->get_name(), 'targetname' => $model->get_target()->get_name(),
'indicators' => $model->get_potential_indicators(), 'indicators' => $model->get_potential_indicators(),
'timesplittings' => $potentialtimesplittings, '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); $mform = new \tool_analytics\output\form\edit_model(null, $customdata);
@ -157,7 +160,7 @@ switch ($action) {
$predictionsprocessor = false; $predictionsprocessor = false;
} }
$model->update($data->enabled, $indicators, $timesplitting, $predictionsprocessor); $model->update($data->enabled, $indicators, $timesplitting, $predictionsprocessor, $data->contexts);
redirect($returnurl); redirect($returnurl);
} }
@ -168,6 +171,9 @@ switch ($action) {
$callable = array('\tool_analytics\output\helper', 'class_to_option'); $callable = array('\tool_analytics\output\helper', 'class_to_option');
$modelobj->indicators = array_map($callable, json_decode($modelobj->indicators)); $modelobj->indicators = array_map($callable, json_decode($modelobj->indicators));
$modelobj->timesplitting = \tool_analytics\output\helper::class_to_option($modelobj->timesplitting); $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); $modelobj->predictionsprocessor = \tool_analytics\output\helper::class_to_option($modelobj->predictionsprocessor);
$mform->set_data($modelobj); $mform->set_data($modelobj);
$mform->display(); $mform->display();

View File

@ -0,0 +1,80 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* 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());
}
}

View File

@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die(); 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->requires = 2019051100; // Requires this Moodle version.
$plugin->component = 'tool_analytics'; // Full name of the plugin (used for diagnostics). $plugin->component = 'tool_analytics'; // Full name of the plugin (used for diagnostics).

View File

@ -75,9 +75,10 @@ class analysis {
/** /**
* Runs the analysis. * Runs the analysis.
* *
* @param \context[] $contexts Restrict the analysis to these contexts. No context restrictions if null.
* @return null * @return null
*/ */
public function run() { public function run(array $contexts = []) {
$options = $this->analyser->get_options(); $options = $this->analyser->get_options();
@ -89,7 +90,7 @@ class analysis {
} else { } else {
$action = 'prediction'; $action = 'prediction';
} }
$analysables = $this->analyser->get_analysables_iterator($action); $analysables = $this->analyser->get_analysables_iterator($action, $contexts);
$processedanalysables = $this->get_processed_analysables(); $processedanalysables = $this->get_processed_analysables();

View File

@ -131,9 +131,10 @@ abstract class base {
* to ease to implementation of get_analysables_iterator: get_iterator_sql and order_sql. * 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 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 * @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 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 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). * Returns labelled data (training and evaluation).
* *
* @param \context[] $contexts Restrict the analysis to these contexts. No context restrictions if null.
* @return \stored_file[] * @return \stored_file[]
*/ */
public function get_labelled_data() { public function get_labelled_data(array $contexts = []) {
// Delegates all processing to the analysis. // Delegates all processing to the analysis.
$result = new \core_analytics\local\analysis\result_file($this->get_modelid(), true, $this->get_options()); $result = new \core_analytics\local\analysis\result_file($this->get_modelid(), true, $this->get_options());
$analysis = new \core_analytics\analysis($this, true, $result); $analysis = new \core_analytics\analysis($this, true, $result);
$analysis->run(); $analysis->run($contexts);
return $result->get(); return $result->get();
} }
/** /**
* Returns unlabelled data (prediction). * Returns unlabelled data (prediction).
* *
* @param \context[] $contexts Restrict the analysis to these contexts. No context restrictions if null.
* @return \stored_file[] * @return \stored_file[]
*/ */
public function get_unlabelled_data() { public function get_unlabelled_data(array $contexts = []) {
// Delegates all processing to the analysis. // Delegates all processing to the analysis.
$result = new \core_analytics\local\analysis\result_file($this->get_modelid(), false, $this->get_options()); $result = new \core_analytics\local\analysis\result_file($this->get_modelid(), false, $this->get_options());
$analysis = new \core_analytics\analysis($this, false, $result); $analysis = new \core_analytics\analysis($this, false, $result);
$analysis->run(); $analysis->run($contexts);
return $result->get(); return $result->get();
} }
/** /**
* Returns indicator calculations as an array. * Returns indicator calculations as an array.
*
* @param \context[] $contexts Restrict the analysis to these contexts. No context restrictions if null.
* @return array * @return array
*/ */
public function get_static_data() { public function get_static_data(array $contexts = []) {
// Delegates all processing to the analysis. // Delegates all processing to the analysis.
$result = new \core_analytics\local\analysis\result_array($this->get_modelid(), false, $this->get_options()); $result = new \core_analytics\local\analysis\result_array($this->get_modelid(), false, $this->get_options());
$analysis = new \core_analytics\analysis($this, false, $result); $analysis = new \core_analytics\analysis($this, false, $result);
$analysis->run(); $analysis->run($contexts);
return $result->get(); return $result->get();
} }
@ -422,6 +427,34 @@ abstract class base {
return false; 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. * 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 int $contextlevel The context level of the analysable
* @param string|null $action * @param string|null $action
* @param string|null $tablealias The table alias * @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 * @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) { if (!$tablealias) {
$tablealias = 'analysable'; $tablealias = 'analysable';
@ -452,13 +488,30 @@ abstract class base {
$params = $params + ['action' => $action]; $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 . ' $sql = 'SELECT ' . $select . '
FROM {' . $tablename . '} ' . $tablealias . ' FROM {' . $tablename . '} ' . $tablealias . '
' . $usedanalysablesjoin . ' ' . $usedanalysablesjoin . '
JOIN {context} ctx ON (ctx.contextlevel = :contextlevel AND ctx.instanceid = ' . $tablealias . '.id) JOIN {context} ctx ON (ctx.contextlevel = :contextlevel AND ctx.instanceid = ' . $tablealias . '.id) ';
WHERE 1 = 1';
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]; return [$sql, $params];
} }

View File

@ -39,24 +39,13 @@ abstract class by_course extends base {
* Return the list of courses to analyse. * Return the list of courses to analyse.
* *
* @param string|null $action 'prediction', 'training' or null if no specific action needed. * @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 * @return \Iterator
*/ */
public function get_analysables_iterator(?string $action = null) { public function get_analysables_iterator(?string $action = null, array $contexts = []) {
global $DB; global $DB;
list($sql, $params) = $this->get_iterator_sql('course', CONTEXT_COURSE, $action, 'c'); list($sql, $params) = $this->get_iterator_sql('course', CONTEXT_COURSE, $action, 'c', $contexts);
// 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;
}
$ordersql = $this->order_sql('sortorder', 'ASC', 'c'); $ordersql = $this->order_sql('sortorder', 'ASC', 'c');
@ -76,4 +65,13 @@ abstract class by_course extends base {
return \core_analytics\course::instance($record, $context); return \core_analytics\course::instance($record, $context);
}); });
} }
}
/**
* Can be limited to course categories or specific courses.
*
* @return array
*/
public static function context_restriction_support(): array {
return [CONTEXT_COURSE, CONTEXT_COURSECAT];
}
}

View File

@ -39,9 +39,10 @@ abstract class sitewide extends base {
* Return the list of analysables to analyse. * Return the list of analysables to analyse.
* *
* @param string|null $action 'prediction', 'training' or null if no specific action needed. * @param string|null $action 'prediction', 'training' or null if no specific action needed.
* @param \context[] $contexts Ignored here.
* @return \Iterator * @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. // We can safely ignore $action as we have 1 single analysable element in this analyser.
return new \ArrayIterator([new \core_analytics\site()]); return new \ArrayIterator([new \core_analytics\site()]);
} }

View File

@ -646,6 +646,8 @@ class manager {
$usedanalysablesanalysableids = array_flip($usedanalysablesanalysableids); $usedanalysablesanalysableids = array_flip($usedanalysablesanalysableids);
$analyser = $model->get_analyser(array('notimesplitting' => true)); $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(); $analysables = $analyser->get_analysables_iterator();
$analysableids = []; $analysableids = [];
@ -913,4 +915,79 @@ class manager {
return [$target, $indicators]; 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;
}
} }

View File

@ -120,6 +120,11 @@ class model {
*/ */
protected $indicators = null; protected $indicators = null;
/**
* @var \context[]
*/
protected $contexts = null;
/** /**
* Unique Model id created from site info and last model modification. * 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 \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 $timesplittingid False to respect current time splitting method
* @param string|false $predictionsprocessor False to respect current predictors processor value * @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 * @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; global $USER, $DB;
\core_analytics\manager::check_can_manage_models(); \core_analytics\manager::check_can_manage_models();
@ -486,6 +493,15 @@ class model {
$predictionsprocessor = $this->model->predictionsprocessor; $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 || if ($this->model->timesplitting !== $timesplittingid ||
$this->model->indicators !== $indicatorsstr || $this->model->indicators !== $indicatorsstr ||
$this->model->predictionsprocessor !== $predictionsprocessor) { $this->model->predictionsprocessor !== $predictionsprocessor) {
@ -514,6 +530,7 @@ class model {
$this->model->indicators = $indicatorsstr; $this->model->indicators = $indicatorsstr;
$this->model->timesplitting = $timesplittingid; $this->model->timesplitting = $timesplittingid;
$this->model->predictionsprocessor = $predictionsprocessor; $this->model->predictionsprocessor = $predictionsprocessor;
$this->model->contextids = $contextsstr;
$this->model->timemodified = $now; $this->model->timemodified = $now;
$this->model->usermodified = $USER->id; $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. // Before get_labelled_data call so we get an early exception if it is not ready.
$predictor = $this->get_predictions_processor(); $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. // No datasets generated.
if (empty($datasets)) { 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. // Before get_labelled_data call so we get an early exception if it is not ready.
$predictor = $this->get_predictions_processor(); $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. // No training if no files have been provided.
if (empty($datasets) || empty($datasets[$this->model->timesplitting])) { 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. // Before get_unlabelled_data call so we get an early exception if it is not ready.
$predictor = $this->get_predictions_processor(); $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. // Get the prediction samples file.
if (empty($samplesdata) || empty($samplesdata[$this->model->timesplitting])) { if (empty($samplesdata) || empty($samplesdata[$this->model->timesplitting])) {
@ -802,7 +819,7 @@ class model {
} else { } else {
// Predictions based on assumptions. // 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. // Get the prediction samples file.
if (empty($indicatorcalculations) || empty($indicatorcalculations[$this->model->timesplitting])) { if (empty($indicatorcalculations) || empty($indicatorcalculations[$this->model->timesplitting])) {
@ -1956,6 +1973,30 @@ class model {
return [$allsampleids, $allsamplesdata]; 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. * Purges the insights cache.
*/ */

View File

@ -487,4 +487,31 @@ class analytics_manager_testcase extends advanced_testcase {
$this->assertNotEmpty($indicators); $this->assertNotEmpty($indicators);
$this->assertContainsOnlyInstancesOf(\core_analytics\local\indicator\base::class, $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'));
}
} }

View File

@ -126,6 +126,57 @@ class core_analytics_prediction_testcase extends advanced_testcase {
array('modelid' => $model->get_id()))); 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 * test_ml_training_and_prediction
* *

View File

@ -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 * 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". 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. * \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 === === 3.7 ===

View File

@ -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['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['errorimportmissingclasses'] = 'The following analytics components are not available on this site: {$a->missingclasses}.';
$string['errorinvalidindicator'] = 'Invalid {$a} indicator'; $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['errorinvalidtarget'] = 'Invalid {$a} target';
$string['errorinvalidtimesplitting'] = 'Invalid analysis interval; please ensure you add the fully qualified class name.'; $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['errornoexportconfig'] = 'There was a problem exporting the model configuration.';
$string['errornoexportconfigrequirements'] = 'Only non-static models with an analysis interval can be exported.'; $string['errornoexportconfigrequirements'] = 'Only non-static models with an analysis interval can be exported.';
$string['errornoindicators'] = 'This model does not have any indicators.'; $string['errornoindicators'] = 'This model does not have any indicators.';

View File

@ -39,14 +39,15 @@ class users extends \core_analytics\local\analyser\base {
* The site users are the analysable elements returned by this analyser. * 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 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 * @return \Iterator
*/ */
public function get_analysables_iterator(?string $action = null) { public function get_analysables_iterator(?string $action = null, array $contexts = []) {
global $DB, $CFG; global $DB, $CFG;
$siteadmins = explode(',', $CFG->siteadmins); $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"; $sql .= " AND u.deleted = :deleted AND u.confirmed = :confirmed AND u.suspended = :suspended";
$params = $params + ['deleted' => 0, 'confirmed' => 1, 'suspended' => 0]; $params = $params + ['deleted' => 0, 'confirmed' => 1, 'suspended' => 0];

View File

@ -3844,6 +3844,7 @@
<FIELD NAME="timesplitting" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/> <FIELD NAME="timesplitting" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="predictionsprocessor" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/> <FIELD NAME="predictionsprocessor" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="version" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="version" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="contextids" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="The model will be restricted to this contexts"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/> <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/> <FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>

View File

@ -3619,5 +3619,24 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2019101600.01); 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; return true;
} }

View File

@ -48,16 +48,16 @@ class core_analytics_analysers_testcase extends advanced_testcase {
public function test_courses_analyser() { public function test_courses_analyser() {
$this->resetAfterTest(true); $this->resetAfterTest(true);
$course = $this->getDataGenerator()->create_course(); $course1 = $this->getDataGenerator()->create_course();
$coursecontext = \context_course::instance($course->id); $coursecontext = \context_course::instance($course1->id);
$target = new test_target_shortname(); $target = new test_target_shortname();
$analyser = new \core\analytics\analyser\courses(1, $target, [], [], []); $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. // Just 1 sample per course.
$class = new ReflectionClass('\core\analytics\analyser\courses'); $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); list($sampleids, $samplesdata) = $method->invoke($analyser, $analysable);
$this->assertCount(1, $sampleids); $this->assertCount(1, $sampleids);
$sampleid = reset($sampleids); $sampleid = reset($sampleids);
$this->assertEquals($course->id, $sampleid); $this->assertEquals($course1->id, $sampleid);
$this->assertEquals($course->fullname, $samplesdata[$sampleid]['course']->fullname); $this->assertEquals($course1->fullname, $samplesdata[$sampleid]['course']->fullname);
$this->assertEquals($coursecontext, $samplesdata[$sampleid]['context']); $this->assertEquals($coursecontext, $samplesdata[$sampleid]['context']);
// To compare it later. // To compare it later.
@ -75,6 +75,16 @@ class core_analytics_analysers_testcase extends advanced_testcase {
list($sampleids, $samplesdata) = $analyser->get_samples(array($sampleid)); list($sampleids, $samplesdata) = $analyser->get_samples(array($sampleid));
$this->assertEquals($prevsampledata['context'], $samplesdata[$sampleid]['context']); $this->assertEquals($prevsampledata['context'], $samplesdata[$sampleid]['context']);
$this->assertEquals($prevsampledata['course']->shortname, $samplesdata[$sampleid]['course']->shortname); $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); $this->resetAfterTest(true);
$course = $this->getDataGenerator()->create_course(); $course1 = $this->getDataGenerator()->create_course();
$coursecontext = \context_course::instance($course->id); $course1context = \context_course::instance($course1->id);
$user1 = $this->getDataGenerator()->create_user(); $user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user();
$user3 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user();
// Checking that suspended users are also included. // Checking that suspended users are also included.
$this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student'); $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'student');
$this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student', 'manual', 0, 0, ENROL_USER_SUSPENDED); $this->getDataGenerator()->enrol_user($user2->id, $course1->id, 'student', 'manual', 0, 0, ENROL_USER_SUSPENDED);
$this->getDataGenerator()->enrol_user($user3->id, $course->id, 'editingteacher'); $this->getDataGenerator()->enrol_user($user3->id, $course1->id, 'editingteacher');
$enrol = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'manual')); $enrol = $DB->get_record('enrol', array('courseid' => $course1->id, 'enrol' => 'manual'));
$ue1 = $DB->get_record('user_enrolments', array('userid' => $user1->id, 'enrolid' => $enrol->id)); $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)); $ue2 = $DB->get_record('user_enrolments', array('userid' => $user2->id, 'enrolid' => $enrol->id));
$target = new test_target_shortname(); $target = new test_target_shortname();
$analyser = new \core\analytics\analyser\student_enrolments(1, $target, [], [], []); $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('\core_analytics\course', $analyser->get_sample_analysable($ue1->id));
$this->assertInstanceOf('\context_course', $analyser->sample_access_context($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. // Shouldn't matter which one we select.
$sampleid = $ue1->id; $sampleid = $ue1->id;
$this->assertEquals($ue1, $samplesdata[$sampleid]['user_enrolments']); $this->assertEquals($ue1, $samplesdata[$sampleid]['user_enrolments']);
$this->assertEquals($course->fullname, $samplesdata[$sampleid]['course']->fullname); $this->assertEquals($course1->fullname, $samplesdata[$sampleid]['course']->fullname);
$this->assertEquals($coursecontext, $samplesdata[$sampleid]['context']); $this->assertEquals($course1context, $samplesdata[$sampleid]['context']);
$this->assertEquals($user1->firstname, $samplesdata[$sampleid]['user']->firstname); $this->assertEquals($user1->firstname, $samplesdata[$sampleid]['user']->firstname);
// To compare it later. // 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['context'], $samplesdata[$sampleid]['context']);
$this->assertEquals($prevsampledata['course']->shortname, $samplesdata[$sampleid]['course']->shortname); $this->assertEquals($prevsampledata['course']->shortname, $samplesdata[$sampleid]['course']->shortname);
$this->assertEquals($prevsampledata['user']->firstname, $samplesdata[$sampleid]['user']->firstname); $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]));
} }
/** /**

View File

@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die(); 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. // RR = release increments - 00 in DEV branches.
// .XX = incremental changes. // .XX = incremental changes.