diff --git a/admin/tool/analytics/classes/output/effectiveness_report.php b/admin/tool/analytics/classes/output/effectiveness_report.php new file mode 100644 index 00000000000..906ecb7d27a --- /dev/null +++ b/admin/tool/analytics/classes/output/effectiveness_report.php @@ -0,0 +1,179 @@ +. + +/** + * Effectiveness report renderable. + * + * @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\output; + +defined('MOODLE_INTERNAL') || die; + +/** + * Effectiveness report renderable. + * + * @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 + */ +class effectiveness_report implements \renderable, \templatable { + + /** + * @var \core_analytics\model + */ + private $model = null; + + /** + * @var \context + */ + private $context = null; + + /** + * Inits the effectiveness report renderable. + * + * @param \core_analytics\model $model + * @param int|null $contextid + * @return null + */ + public function __construct(\core_analytics\model $model, ?int $contextid = null) { + $this->model = $model; + if ($contextid) { + $this->context = \context::instance_by_id($contextid); + } + } + + /** + * Export the data. + * + * @param \renderer_base $output + * @return \stdClass + */ + public function export_for_template(\renderer_base $output): \stdClass { + + // Prepare the context object. + $data = new \stdClass(); + $data->modelname = $this->model->get_name(); + + $data->charts = []; + + $predictionactionrecords = $this->model->get_prediction_actions($this->context); + + // Context selector. + $predictioncontexts = $this->model->get_predictions_contexts(false); + if ($predictioncontexts && count($predictioncontexts) > 1) { + $url = new \moodle_url('/admin/tool/analytics/model.php', ['id' => $this->model->get_id(), + 'action' => 'effectivenessreport']); + + if ($this->context) { + $selected = $this->context->id; + } else { + // This is the 'all' option. + $selected = 0; + } + $data->contextselect = \tool_analytics\output\helper::prediction_context_selector($predictioncontexts, + $url, $output, $selected, true, false); + } + + if ($predictionactionrecords->valid()) { + + foreach ($predictionactionrecords as $record) { + + // Using this unusual execution flow to init the chart data because $predictionactionrecords + // is a \moodle_recordset. + if (empty($actionlabels)) { + list($actionlabels, $actionvalues) = $this->init_action_labels($record); + } + + // One value for each action. + $actionvalues['separated'][$record->actionname]++; + + // Data grouped in three boxes. + if ($record->actionname == 'notuseful') { + $actionvalues['grouped']['negative']++; + } else if ($record->actionname == 'predictiondetails') { + $actionvalues['grouped']['neutral']++; + } else { + $actionvalues['grouped']['positive']++; + } + } + $predictionactionrecords->close(); + + // Actions doughtnut. + $chart = new \core\chart_pie(); + $chart->set_doughnut(true); + $chart->set_title(get_string('actionsexecutedbyusers', 'tool_analytics')); + $series = new \core\chart_series(get_string('actions', 'tool_analytics'), + array_values($actionvalues['separated'])); + $chart->add_series($series); + $chart->set_labels(array_values($actionlabels['separated'])); + $data->separatedchart = $output->render($chart); + + // Positive/negative/neutral bar chart. + $chart = new \core\chart_bar(); + $chart->set_title(get_string('actionexecutedgroupedusefulness', 'tool_analytics')); + $series = new \core\chart_series(get_string('actions', 'tool_analytics'), + array_values($actionvalues['grouped'])); + $chart->add_series($series); + $chart->set_labels(array_values($actionlabels['grouped'])); + $data->groupedchart = $output->render($chart); + + } else { + $predictionactionrecords->close(); + $data->noactions = [ + 'message' => get_string('noactionsfound', 'tool_analytics'), + 'announce' => true, + ]; + } + return $data; + } + + /** + * Initialises the action labels and values in this model. + * + * @param \stdClass $predictionactionrecord + * @return array Two-dimensional array with the labels and values initialised to zero. + */ + private function init_action_labels(\stdClass $predictionactionrecord): array { + + $predictioncontext = \context::instance_by_id($predictionactionrecord->contextid); + + // Just 1 result, we just want to retrieve the prediction action names. + list ($unused, $predictions) = $this->model->get_predictions($predictioncontext, false, 0, 1); + + // We pass 'true' for $isinsightuser so all the prediction actions available for this target are returning. + $predictionactions = $this->model->get_target()->prediction_actions(reset($predictions), true, true); + + $actionlabels = []; + $actionvalues = ['separated' => [], 'grouped' => []]; + foreach ($predictionactions as $action) { + $actionlabels['separated'][$action->get_action_name()] = $action->get_text(); + $actionvalues['separated'][$action->get_action_name()] = 0; + } + + $actionlabels['grouped']['positive'] = get_string('useful', 'analytics'); + $actionlabels['grouped']['neutral'] = get_string('neutral', 'analytics'); + $actionlabels['grouped']['negative'] = get_string('notuseful', 'analytics'); + $actionvalues['grouped']['positive'] = 0; + $actionvalues['grouped']['neutral'] = 0; + $actionvalues['grouped']['negative'] = 0; + + return [$actionlabels, $actionvalues]; + } +} diff --git a/admin/tool/analytics/classes/output/helper.php b/admin/tool/analytics/classes/output/helper.php index 1db998157f6..7d286253f41 100644 --- a/admin/tool/analytics/classes/output/helper.php +++ b/admin/tool/analytics/classes/output/helper.php @@ -105,4 +105,50 @@ class helper { $PAGE->reset_theme_and_output(); $PAGE->set_context(\context_system::instance()); } + /** + * Convert a list of contexts to an associative array where the value is the context name. + * + * @param array $contexts + * @param \moodle_url $url + * @param \renderer_base $output + * @param int|null $selected + * @param bool $includeall + * @param bool $shortentext + * @return \stdClass + */ + public static function prediction_context_selector(array $contexts, \moodle_url $url, \renderer_base $output, + ?int $selected = null, ?bool $includeall = false, ?bool $shortentext = true): \stdClass { + + foreach ($contexts as $contextid => $unused) { + // We prepare this to be used as single_select template options. + $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'); + $nothing = ''; + } else { + $nothing = array('' => 'choosedots'); + } + + \core_collator::asort($contexts); + + if (!$selected) { + $selected = ''; + } + $singleselect = new \single_select($url, 'contextid', $contexts, $selected, $nothing); + return $singleselect->export_for_template($output); + } } diff --git a/admin/tool/analytics/classes/output/models_list.php b/admin/tool/analytics/classes/output/models_list.php index fbbc2275da2..5002af020e8 100644 --- a/admin/tool/analytics/classes/output/models_list.php +++ b/admin/tool/analytics/classes/output/models_list.php @@ -166,36 +166,20 @@ class models_list implements \renderable, \templatable { // Has this model generated predictions?. $predictioncontexts = $model->get_predictions_contexts(); + $anypredictionobtained = $model->any_prediction_obtained(); // Model predictions list. if (!$model->is_enabled()) { $modeldata->noinsights = get_string('disabledmodel', 'analytics'); } else if ($model->uses_insights()) { if ($predictioncontexts) { - - foreach ($predictioncontexts as $contextid => $unused) { - // We prepare this to be used as single_select template options. - $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 { - $contextname = shorten_text($context->get_context_name(false, true), 40); - } - $predictioncontexts[$contextid] = $contextname; - } - \core_collator::asort($predictioncontexts); - - if (!empty($predictioncontexts)) { - $url = new \moodle_url('/report/insights/insights.php', array('modelid' => $model->get_id())); - $singleselect = new \single_select($url, 'contextid', $predictioncontexts); - $modeldata->insights = $singleselect->export_for_template($output); - } + $url = new \moodle_url('/report/insights/insights.php', array('modelid' => $model->get_id())); + $modeldata->insights = \tool_analytics\output\helper::prediction_context_selector($predictioncontexts, + $url, $output); } if (empty($modeldata->insights)) { - if ($model->any_prediction_obtained()) { + if ($anypredictionobtained) { $modeldata->noinsights = get_string('noinsights', 'analytics'); } else { $modeldata->noinsights = get_string('nopredictionsyet', 'analytics'); @@ -304,6 +288,15 @@ class models_list implements \renderable, \templatable { } } + // Effectivity report. + if (!empty($anypredictionobtained) && $model->uses_insights()) { + $urlparams['action'] = 'effectivenessreport'; + $url = new \moodle_url('/admin/tool/analytics/model.php', $urlparams); + $pix = new \pix_icon('i/report', get_string('effectivenessreport', 'tool_analytics')); + $icon = new \action_menu_link_secondary($url, $pix, get_string('effectivenessreport', 'tool_analytics')); + $actionsmenu->add($icon); + } + // Invalid analysables. $analyser = $model->get_analyser(['notimesplitting' => true]); if (!$analyser instanceof \core_analytics\local\analyser\sitewide) { @@ -315,7 +308,7 @@ class models_list implements \renderable, \templatable { } // Clear model. - if (!empty($predictioncontexts) || $model->is_trained()) { + if (!empty($anypredictionobtained) || $model->is_trained()) { $actionid = 'clear-' . $model->get_id(); $PAGE->requires->js_call_amd('tool_analytics/model', 'confirmAction', [$actionid, 'clear']); $urlparams['action'] = 'clear'; diff --git a/admin/tool/analytics/classes/output/renderer.php b/admin/tool/analytics/classes/output/renderer.php index 743844ea0b6..60bc2a22503 100644 --- a/admin/tool/analytics/classes/output/renderer.php +++ b/admin/tool/analytics/classes/output/renderer.php @@ -208,6 +208,17 @@ class renderer extends plugin_renderer_base { return $output; } + /** + * Defer to template. + * + * @param \tool_analytics\output\effectiveness_report $effectivenessreport + * @return string HTML + */ + protected function render_effectiveness_report(\tool_analytics\output\effectiveness_report $effectivenessreport): string { + $data = $effectivenessreport->export_for_template($this); + return parent::render_from_template('tool_analytics/effectiveness_report', $data); + } + /** * Defer to template. * diff --git a/admin/tool/analytics/lang/en/tool_analytics.php b/admin/tool/analytics/lang/en/tool_analytics.php index 0979652d5a8..b1fa75d924f 100644 --- a/admin/tool/analytics/lang/en/tool_analytics.php +++ b/admin/tool/analytics/lang/en/tool_analytics.php @@ -23,6 +23,9 @@ */ $string['accuracy'] = 'Accuracy'; +$string['actions'] = 'Actions'; +$string['actionsexecutedbyusers'] = 'Actions executed by users'; +$string['actionexecutedgroupedusefulness'] = 'Grouped actions'; $string['allpredictions'] = 'All predictions'; $string['alltimesplittingmethods'] = 'All analysis intervals'; $string['analysingsitedata'] = 'Analysing the site'; @@ -48,6 +51,8 @@ $string['deletemodelconfirmation'] = 'Are you sure you want to delete "{$a}"? Th $string['disabled'] = 'Disabled'; $string['editmodel'] = 'Edit "{$a}" model'; $string['edittrainedwarning'] = 'This model has already been trained. Note that changing its indicators or its analysis interval will delete its previous predictions and start generating new predictions.'; +$string['effectivenessreport'] = 'Effectiveness report'; +$string['effectivenessreportfor'] = 'Model "{$a}" effectiveness'; $string['enabled'] = 'Enabled'; $string['errorcantenablenotimesplitting'] = 'You need to select an analysis interval before enabling the model'; $string['errornoenabledandtrainedmodels'] = 'There are no enabled and trained models to predict.'; @@ -105,6 +110,7 @@ $string['modelresults'] = '{$a} results'; $string['modeltimesplitting'] = 'Analysis interval'; $string['newmodel'] = 'New model'; $string['nextpage'] = 'Next page'; +$string['noactionsfound'] = 'Users have not executed any actions on the generated insights.'; $string['nodatatoevaluate'] = 'There is no data to evaluate the model'; $string['nodatatopredict'] = 'No new elements to get predictions for.'; $string['nodatatotrain'] = 'There is no new data that can be used for training.'; @@ -143,4 +149,4 @@ $string['weeksenddatedefault'] = 'End date automatically calculated from the cou $string['privacy:metadata'] = 'The Analytic models plugin does not store any personal data.'; // Deprecated since Moodle 3.8. -$string['getpredictions'] = 'Get predictions'; \ No newline at end of file +$string['getpredictions'] = 'Get predictions'; diff --git a/admin/tool/analytics/model.php b/admin/tool/analytics/model.php index ae2c8dc84a6..4174ce11d7d 100644 --- a/admin/tool/analytics/model.php +++ b/admin/tool/analytics/model.php @@ -69,6 +69,9 @@ switch ($action) { case 'clear': $title = get_string('clearpredictions', 'tool_analytics'); break; + case 'effectivenessreport': + $title = get_string('effectivenessreport', 'tool_analytics'); + break; case 'invalidanalysables': $title = get_string('invalidanalysables', 'tool_analytics'); break; @@ -273,6 +276,18 @@ switch ($action) { redirect($returnurl); break; + case 'effectivenessreport': + + $contextid = optional_param('contextid', null, PARAM_INT); + + echo $OUTPUT->header(); + + $renderable = new \tool_analytics\output\effectiveness_report($model, $contextid); + $renderer = $PAGE->get_renderer('tool_analytics'); + echo $renderer->render($renderable); + + break; + case 'invalidanalysables': echo $OUTPUT->header(); diff --git a/admin/tool/analytics/templates/effectiveness_report.mustache b/admin/tool/analytics/templates/effectiveness_report.mustache new file mode 100644 index 00000000000..c603d609de4 --- /dev/null +++ b/admin/tool/analytics/templates/effectiveness_report.mustache @@ -0,0 +1,65 @@ +{{! + 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 . +}} +{{! + @template tool_analytics/effectiveness_report + + Template for the effectiveness report. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * none + + Example context (json): + { + "modelname": "Not engaging courses", + "noactions": { + "message": "Users have not executed any actions on the generated insights.", + "announce": "true" + } + } +}} + +
+

{{#str}}effectivenessreportfor, tool_analytics, {{modelname}}{{/str}}

+ + {{#contextselect}} +
+ {{> core/single_select }} +
+ {{/contextselect}} + + {{#noactions}} +
+ {{> core/notification_info}} +
+ {{/noactions}} + {{^noanalysables}} +
+
+ {{{separatedchart}}} +
+
+ {{{groupedchart}}} +
+
+ {{/noanalysables}} +
diff --git a/analytics/classes/model.php b/analytics/classes/model.php index cfe614b74aa..41345ae4dba 100644 --- a/analytics/classes/model.php +++ b/analytics/classes/model.php @@ -1348,6 +1348,30 @@ class model { return [$current, $predictions]; } + /** + * Returns the actions executed by users on the predictions. + * + * @param \context|null $context + * @return \moodle_recordset + */ + public function get_prediction_actions(?\context $context): \moodle_recordset { + global $DB; + + $sql = "SELECT apa.id, apa.predictionid, apa.userid, apa.actionname, apa.timecreated, + ap.contextid, ap.sampleid, ap.rangeindex, ap.prediction, ap.predictionscore + FROM {analytics_prediction_actions} apa + JOIN {analytics_predictions} ap ON ap.id = apa.predictionid + WHERE ap.modelid = :modelid"; + $params = ['modelid' => $this->model->id]; + + if ($context) { + $sql .= " AND ap.contextid = :contextid"; + $params['contextid'] = $context->id; + } + + return $DB->get_recordset_sql($sql, $params); + } + /** * Returns the sample data of a prediction. * diff --git a/analytics/tests/prediction_actions_test.php b/analytics/tests/prediction_actions_test.php index 3acd0a16bdf..508d1f9c0cd 100644 --- a/analytics/tests/prediction_actions_test.php +++ b/analytics/tests/prediction_actions_test.php @@ -96,11 +96,17 @@ class analytics_prediction_actions_testcase extends advanced_testcase { $prediction = reset($predictions); $prediction->action_executed(\core_analytics\prediction::ACTION_FIXED, $this->model->get_target()); + $recordset = $this->model->get_prediction_actions($this->context); + $this->assertCount(1, $recordset); + $recordset->close(); $this->assertEquals(1, $DB->count_records('analytics_prediction_actions')); $action = $DB->get_record('analytics_prediction_actions', array('userid' => $this->teacher2->id)); $this->assertEquals(\core_analytics\prediction::ACTION_FIXED, $action->actionname); $prediction->action_executed(\core_analytics\prediction::ACTION_NOT_USEFUL, $this->model->get_target()); + $recordset = $this->model->get_prediction_actions($this->context); + $this->assertCount(2, $recordset); + $recordset->close(); $this->assertEquals(2, $DB->count_records('analytics_prediction_actions')); } @@ -125,6 +131,10 @@ class analytics_prediction_actions_testcase extends advanced_testcase { $prediction = reset($predictions); $prediction->action_executed(\core_analytics\prediction::ACTION_FIXED, $this->model->get_target()); + $recordset = $this->model->get_prediction_actions($this->context); + $this->assertCount(1, $recordset); + $recordset->close(); + list($ignored, $predictions) = $this->model->get_predictions($this->context, true); $this->assertCount(1, $predictions); list($ignored, $predictions) = $this->model->get_predictions($this->context, false); @@ -136,5 +146,9 @@ class analytics_prediction_actions_testcase extends advanced_testcase { $this->assertCount(2, $predictions); list($ignored, $predictions) = $this->model->get_predictions($this->context, false); $this->assertCount(2, $predictions); + + $recordset = $this->model->get_prediction_actions($this->context); + $this->assertCount(1, $recordset); + $recordset->close(); } } diff --git a/lang/en/analytics.php b/lang/en/analytics.php index 99627374bd8..f768e8e820d 100644 --- a/lang/en/analytics.php +++ b/lang/en/analytics.php @@ -91,6 +91,7 @@ $string['modeloutputdir'] = 'Models output directory'; $string['modeloutputdirinfo'] = 'Directory where prediction processors store all evaluation info. Useful for debugging and research.'; $string['modeltimelimit'] = 'Analysis time limit per model'; $string['modeltimelimitinfo'] = 'This setting limits the time each model spends analysing the site contents.'; +$string['neutral'] = 'Neutral'; $string['noevaluationbasedassumptions'] = 'Models based on assumptions cannot be evaluated.'; $string['nodata'] = 'No data to analyse'; $string['noinsightsmodel'] = 'This model does not generate insights'; @@ -143,6 +144,7 @@ $string['typeinstitution'] = 'Type of institution'; $string['typeinstitutionacademic'] = 'Academic'; $string['typeinstitutiontraining'] = 'Corporate training'; $string['typeinstitutionngo'] = 'Non-governmental organization (NGO)'; +$string['useful'] = 'Useful'; $string['viewdetails'] = 'View details'; $string['viewinsight'] = 'View insight'; $string['viewinsightdetails'] = 'View insight details';