Skip to content

Commit

Permalink
MDL-64739 tool_analytics: Restrict models to specific contexts
Browse files Browse the repository at this point in the history
  • Loading branch information
David Monllaó committed Oct 21, 2019
1 parent c345aa7 commit 76b5ee4
Show file tree
Hide file tree
Showing 15 changed files with 273 additions and 67 deletions.
44 changes: 30 additions & 14 deletions admin/tool/analytics/classes/output/form/edit_model.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,6 @@ public function definition() {
$mform->addHelpButton('indicators', 'indicators', 'tool_analytics');
}

// Contexts restriction.
$contexts = array();
if ($this->_customdata['contexts']) {
$contexts = \tool_analytics\output\helper::contexts_to_options($this->_customdata['contexts']);
}
$options = array(
'multiple' => true
);
$mform->addElement('autocomplete', 'contexts', get_string('contexts', 'tool_analytics'), $contexts, $options);
$mform->setType('contexts', PARAM_ALPHANUMEXT);
$mform->addHelpButton('contexts', 'indicators', 'tool_analytics');

// Time-splitting methods.
if (!empty($this->_customdata['invalidcurrenttimesplitting'])) {
$mform->addElement('html', $OUTPUT->notification(
Expand All @@ -117,6 +105,17 @@ public function definition() {
$mform->addElement('select', 'timesplitting', get_string('timesplittingmethod', 'analytics'), $timesplittings);
$mform->addHelpButton('timesplitting', 'timesplittingmethod', 'analytics');

// Contexts restriction.
if (!empty($this->_customdata['contexts'])) {

\core_collator::asort($this->_customdata['contexts']);
$options = ['multiple' => true, 'noselectionstring' => get_string('all')];
$mform->addElement('autocomplete', 'contexts', get_string('contexts', 'tool_analytics'), $this->_customdata['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(
Expand Down Expand Up @@ -158,20 +157,37 @@ public function definition() {
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');
Expand Down
35 changes: 0 additions & 35 deletions admin/tool/analytics/classes/output/helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,39 +151,4 @@ public static function prediction_context_selector(array $contexts, \moodle_url
$singleselect = new \single_select($url, 'contextid', $contexts, $selected, $nothing);
return $singleselect->export_for_template($output);
}

/**
* Converts a list of contexts to an array of options that can be used in a autocomplete moodleform field.
*
* @param array $contexts Array of context ids.
* @param bool $includeall Wether to include the "all" context option (the system context).
* @param bool $shortentext Wether to shorten the context names.
* @return array Associative array with context ids as keys and context names as values.
*/
public static function contexts_to_options(array $contexts, ?bool $includeall = false, ?bool $shortentext = true): array {

foreach ($contexts as $contextid) {
$context = \context::instance_by_id($contextid);

// Special name for system level predictions as showing "System is not visually nice".
if ($contextid == SYSCONTEXTID) {
$contextname = get_string('allpredictions', 'tool_analytics');
} else {
if ($shortentext) {
$contextname = shorten_text($context->get_context_name(false, true), 40);
} else {
$contextname = $context->get_context_name(false, true);
}
}
$contexts[$contextid] = $contextname;
}

if ($includeall) {
$contexts[0] = get_string('all');
}

\core_collator::asort($contexts);

return $contexts;
}
}
3 changes: 2 additions & 1 deletion admin/tool/analytics/classes/output/invalid_analysables.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ public function export_for_template(\renderer_base $output) {

$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;
Expand Down
5 changes: 3 additions & 2 deletions admin/tool/analytics/createmodel.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
'indicators' => \core_analytics\manager::get_all_indicators(),
'timesplittings' => \core_analytics\manager::get_all_time_splittings(),
'predictionprocessors' => \core_analytics\manager::get_all_prediction_processors(),
'contexts' => \core_analytics\manager::get_potential_context_restrictions(),
);
$mform = new \tool_analytics\output\form\edit_model(null, $customdata);

Expand Down Expand Up @@ -86,8 +87,8 @@
$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;
Expand Down
2 changes: 2 additions & 0 deletions admin/tool/analytics/lang/en/tool_analytics.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
$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';
Expand Down
8 changes: 6 additions & 2 deletions admin/tool/analytics/model.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@
'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(),
'contexts' => ($model->get_analyser())::potential_context_restrictions()
);
$mform = new \tool_analytics\output\form\edit_model(null, $customdata);

Expand All @@ -157,7 +158,7 @@
$predictionsprocessor = false;
}

$model->update($data->enabled, $indicators, $timesplitting, $predictionsprocessor);
$model->update($data->enabled, $indicators, $timesplitting, $predictionsprocessor, $data->contexts);
redirect($returnurl);
}

Expand All @@ -168,6 +169,9 @@
$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();
Expand Down
5 changes: 3 additions & 2 deletions analytics/classes/analysis.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,10 @@ public function __construct(\core_analytics\local\analyser\base $analyser, bool
/**
* 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();

Expand All @@ -89,7 +90,7 @@ public function run() {
} else {
$action = 'prediction';
}
$analysables = $this->analyser->get_analysables_iterator($action);
$analysables = $this->analyser->get_analysables_iterator($action, $contexts);

$processedanalysables = $this->get_processed_analysables();

Expand Down
43 changes: 37 additions & 6 deletions analytics/classes/local/analyser/base.php
Original file line number Diff line number Diff line change
Expand Up @@ -267,38 +267,42 @@ protected function provided_sample_data() {
/**
* 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();
}

Expand Down Expand Up @@ -423,6 +427,33 @@ public static function one_sample_per_analysable() {
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.
*
* @return int[]
*/
public static function potential_context_restrictions() {
return \core_analytics\manager::get_potential_context_restrictions(static::context_restriction_support());
}

/**
* Get the sql of a default implementation of the iterator.
*
Expand Down
9 changes: 9 additions & 0 deletions analytics/classes/local/analyser/by_course.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,13 @@ public function get_analysables_iterator(?string $action = null, array $contexts
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];
}
}
59 changes: 59 additions & 0 deletions analytics/classes/manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,8 @@ public static function cleanup() {
$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 = [];
Expand Down Expand Up @@ -913,4 +915,61 @@ public static function get_declared_target_and_indicators_instances(array $defin

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.
* @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) {
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";
$coursecats = $DB->get_recordset_sql($sql, ['ctxlevel' => CONTEXT_COURSECAT]);
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";
$courses = $DB->get_recordset_sql($sql, ['ctxlevel' => CONTEXT_COURSE]);
foreach ($courses as $record) {
$contexts[$record->contextid] = get_string('course') . ': ' .
format_string($record->shortname, true, array('context' => $contextsystem));
}
$courses->close();
}

return $contexts;
}

}
Loading

0 comments on commit 76b5ee4

Please sign in to comment.