diff --git a/admin/tool/analytics/classes/clihelper.php b/admin/tool/analytics/classes/clihelper.php index c46e6fc8239..87365807f35 100644 --- a/admin/tool/analytics/classes/clihelper.php +++ b/admin/tool/analytics/classes/clihelper.php @@ -48,7 +48,7 @@ class clihelper { foreach ($models as $model) { $modelid = $model->get_id(); $isenabled = $model->is_enabled() ? get_string('enabled', 'tool_analytics') : get_string('disabled', 'tool_analytics'); - $name = $model->get_target()->get_name(); + $name = $model->get_name(); echo str_pad($modelid, 15, ' ') . ' ' . str_pad($name, 50, ' ') . ' ' . str_pad($isenabled, 15, ' ') . "\n"; } } diff --git a/admin/tool/analytics/classes/output/invalid_analysables.php b/admin/tool/analytics/classes/output/invalid_analysables.php index dbdaa00ef41..fd205f1dd8e 100644 --- a/admin/tool/analytics/classes/output/invalid_analysables.php +++ b/admin/tool/analytics/classes/output/invalid_analysables.php @@ -125,7 +125,7 @@ class invalid_analysables implements \renderable, \templatable { // Prepare the context object. $data = new \stdClass(); - $data->modelname = $this->model->get_target()->get_name(); + $data->modelname = $this->model->get_name(); if ($this->page > 0) { $prev = clone $PAGE->url; diff --git a/admin/tool/analytics/classes/output/models_list.php b/admin/tool/analytics/classes/output/models_list.php index b0431a35da5..97b6c060f7a 100644 --- a/admin/tool/analytics/classes/output/models_list.php +++ b/admin/tool/analytics/classes/output/models_list.php @@ -84,7 +84,7 @@ class models_list implements \renderable, \templatable { $data->models = array(); foreach ($this->models as $model) { - $modeldata = $model->export(); + $modeldata = $model->export($output); // Check if there is a help icon for the target to show. $identifier = $modeldata->target->get_identifier(); @@ -120,6 +120,8 @@ class models_list implements \renderable, \templatable { $modeldata->indicators = $indicators; } + $modeldata->indicatorsnum = count($modeldata->indicators); + // Check if there is a help icon for the time splitting method. if (!empty($modeldata->timesplitting)) { $identifier = $modeldata->timesplitting->get_identifier(); diff --git a/admin/tool/analytics/classes/task/predict_models.php b/admin/tool/analytics/classes/task/predict_models.php index 91f3c850688..6ea0d0d6356 100644 --- a/admin/tool/analytics/classes/task/predict_models.php +++ b/admin/tool/analytics/classes/task/predict_models.php @@ -65,7 +65,7 @@ class predict_models extends \core\task\scheduled_task { \tool_analytics\output\helper::reset_page(); if ($result) { - echo $OUTPUT->heading(get_string('modelresults', 'tool_analytics', $model->get_target()->get_name())); + echo $OUTPUT->heading(get_string('modelresults', 'tool_analytics', $model->get_name())); $renderer = $PAGE->get_renderer('tool_analytics'); echo $renderer->render_get_predictions_results(false, array(), $result, $model->get_analyser()->get_logs()); } diff --git a/admin/tool/analytics/classes/task/train_models.php b/admin/tool/analytics/classes/task/train_models.php index 3c0c3c96019..67c0a3a5f81 100644 --- a/admin/tool/analytics/classes/task/train_models.php +++ b/admin/tool/analytics/classes/task/train_models.php @@ -76,7 +76,7 @@ class train_models extends \core\task\scheduled_task { \tool_analytics\output\helper::reset_page(); if ($result) { - echo $OUTPUT->heading(get_string('modelresults', 'tool_analytics', $model->get_target()->get_name())); + echo $OUTPUT->heading(get_string('modelresults', 'tool_analytics', $model->get_name())); $renderer = $PAGE->get_renderer('tool_analytics'); echo $renderer->render_get_predictions_results($result, $model->get_analyser()->get_logs()); diff --git a/admin/tool/analytics/lang/en/tool_analytics.php b/admin/tool/analytics/lang/en/tool_analytics.php index 34c1585ff71..4c8fe86a313 100644 --- a/admin/tool/analytics/lang/en/tool_analytics.php +++ b/admin/tool/analytics/lang/en/tool_analytics.php @@ -81,6 +81,7 @@ $string['importmodel'] = 'Import model'; $string['indicators'] = 'Indicators'; $string['indicators_help'] = 'The indicators are what you think will lead to an accurate prediction of the target.'; $string['indicators_link'] = 'Indicators'; +$string['indicatorsnum'] = 'Number of indicators: {$a}'; $string['info'] = 'Info'; $string['ignoreversionmismatches'] = 'Ignore version mismatches'; $string['ignoreversionmismatchescheckbox'] = 'Ignore the differences between this site version and the original site version.'; @@ -96,6 +97,7 @@ $string['loginfo'] = 'Log extra info'; $string['missingmoodleversion'] = 'Imported file does not define a moodle version number'; $string['modelid'] = 'Model ID'; $string['modelinvalidanalysables'] = 'Invalid analysable elements for "{$a}" model'; +$string['modelname'] = 'Model name'; $string['modelresults'] = '{$a} results'; $string['modeltimesplitting'] = 'Time splitting'; $string['nextpage'] = 'Next page'; diff --git a/admin/tool/analytics/model.php b/admin/tool/analytics/model.php index d70fad3660a..1a65f88a363 100644 --- a/admin/tool/analytics/model.php +++ b/admin/tool/analytics/model.php @@ -40,7 +40,7 @@ $url = new \moodle_url('/admin/tool/analytics/model.php', $params); switch ($action) { case 'edit': - $title = get_string('editmodel', 'tool_analytics', $model->get_target()->get_name()); + $title = get_string('editmodel', 'tool_analytics', $model->get_name()); break; case 'evaluate': $title = get_string('evaluatemodel', 'tool_analytics'); diff --git a/admin/tool/analytics/templates/models_list.mustache b/admin/tool/analytics/templates/models_list.mustache index b49c9e212cd..601dd5296d7 100644 --- a/admin/tool/analytics/templates/models_list.mustache +++ b/admin/tool/analytics/templates/models_list.mustache @@ -20,85 +20,118 @@ Template for models list. Classes required for JS: - * none + * The list od models wrapped within a id="predictionmodelslist" element. Data attributes required for JS: - * none + * [data-widget="toggle"] indicates the clickable element for expanding/collapsing + the list of indicators used by the given model. Context variables required for this template: - * none + * models: array - list of models to display + - id: int - model unique identifier + - name: object - data for the inplace editable element template + - target: string - name of the target associated with the model + - targetclass: string - fully qualified name of the target class + - targethelp: object - data for the help tooltip template + - enabled: bool - is the model enabled + - indicatorsnum: int - number of indicators + - indicators: array - list of indicators used by the model + + name: string - name of the indicator + + help: object - data for the help tooltip template + - insights: object - data for the single select template + - noinsights: string - text to display instead of insights + * warnings: array - list of data for notification warning template + * infos: array - list of data for notification info template + * createmodelurl: string - URL to create a new model + * importmodelurl: string - URL to import a model Example context (json): { "models": [ { + "id": 11, + "name": { + "component": "local_analyticsdemo", + "itemtype": "modelname", + "itemid": 42, + "displayvalue": "Prevent devs at risk", + "value": "" + }, "target": "Prevent devs at risk", - "targethelp": [ - { - "title": "Help with something", - "url": "http://example.org/help", - "linktext": "", - "icon":{ - "extraclasses": "iconhelp", - "attributes": [ - {"name": "src", "value": "../../../pix/help.svg"}, - {"name": "alt", "value": "Help icon"} - ] - } + "targetclass": "\\local_analyticsdemo\\analytics\\target\\dev_risk", + "targethelp": { + "title": "Help with Prevent devs at risk", + "text": "This target blah blah ...", + "url": "http://example.org/help", + "linktext": "", + "icon": { + "extraclasses": "iconhelp", + "attributes": [ + {"name": "src", "value": "../../../pix/help.svg"}, + {"name": "alt", "value": "Help icon"} + ] } - ], + }, "enabled": 1, - "indicators": [{ - "name": "Indicator 1", - "help": [{ - "title": "Help with something", + "indicatorsnum": 2, + "indicators": [ + { + "name": "Indicator 1", + "help": { + "text": "This indicator blah blah ...", + "title": "Help with Indicator 1", "url": "http://example.org/help", "linktext": "", - "icon":{ + "icon": { "extraclasses": "iconhelp", "attributes": [ {"name": "src", "value": "../../../pix/help.svg"}, {"name": "alt", "value": "Help icon"} ] } - }] + } }, { - "name": "Indicator 2", - "help": [{ - "title": "Help with something", + "name": "Indicator 2", + "help": { + "text": "This indicator blah blah ...", + "title": "Help with Indicator 2", "url": "http://example.org/help", "linktext": "", - "icon":{ + "icon": { "extraclasses": "iconhelp", "attributes": [ {"name": "src", "value": "../../../pix/help.svg"}, {"name": "alt", "value": "Help icon"} ] } - }] - }], - "timesplitting": "Quarters", - "timesplittinghelp": [ - { - "title": "Help with something", - "url": "http://example.org/help", - "linktext": "", - "icon":{ - "extraclasses": "iconhelp", - "attributes": [ - {"name": "src", "value": "../../../pix/help.svg"}, - {"name": "alt", "value": "Help icon"} - ] } } ], + "timesplitting": "Quarters", + "timesplittinghelp": { + "text": "This time splitting methof blah blah ...", + "title": "Help with Quarters", + "url": "http://example.org/help", + "linktext": "", + "icon": { + "extraclasses": "iconhelp", + "attributes": [ + {"name": "src", "value": "../../../pix/help.svg"}, + {"name": "alt", "value": "Help icon"} + ] + } + }, "noinsights": "No insights available yet" } ], - "warnings": { - "message": "Hey, this is a warning" - } + "warnings": [ + { + "message": "Be ware, this is just an example!" + } + ], + "createmodelurl": "#", + "importmodelurl": "#" } }} @@ -114,11 +147,11 @@ {{#str}}createmodel, tool_analytics{{/str}} {{#str}}importmodel, tool_analytics{{/str}} - +
- + @@ -130,10 +163,15 @@ {{#models}}
{{#str}}analyticmodels, tool_analytics{{/str}}
{{#str}}target, tool_analytics{{/str}}{{#str}}modelname, tool_analytics{{/str}} {{#str}}enabled, tool_analytics{{/str}} {{#str}}indicators, tool_analytics{{/str}} {{#str}}modeltimesplitting, tool_analytics{{/str}}
- {{target}} - {{#targethelp}} - {{>core/help_icon}} - {{/targethelp}} + {{#name}} + {{>core/inplace_editable}} + {{/name}} +
+ {{targetclass}} + {{#targethelp}} + {{>core/help_icon}} + {{/targethelp}} +
{{#enabled}} @@ -144,7 +182,15 @@ {{/enabled}} -
+{{#js}} +require(['jquery'], function($) { + + // Toggle the visibility of the indicators list. + $('#predictionmodelslist').on('click', '[data-widget="toggle"]', function(e) { + e.preventDefault(); + var toggle = $(e.currentTarget); + var listid = toggle.attr('aria-controls'); + + $(document.getElementById(listid)).toggle(); + + if (toggle.attr('aria-expanded') == 'false') { + toggle.attr('aria-expanded', 'true'); + } else { + toggle.attr('aria-expanded', 'false'); + } + }); +}); +{{/js}} diff --git a/analytics/classes/model.php b/analytics/classes/model.php index 5113a854c1a..ced29b272a0 100644 --- a/analytics/classes/model.php +++ b/analytics/classes/model.php @@ -1438,14 +1438,18 @@ class model { /** * Exports the model data for displaying it in a template. * + * @param \renderer_base $output The renderer to use for exporting * @return \stdClass */ - public function export() { + public function export(\renderer_base $output) { \core_analytics\manager::check_can_manage_models(); $data = clone $this->model; + + $data->name = $this->inplace_editable_name()->export_for_template($output); $data->target = $this->get_target()->get_name(); + $data->targetclass = $this->get_target()->get_id(); if ($timesplitting = $this->get_time_splitting()) { $data->timesplitting = $timesplitting->get_name(); @@ -1690,6 +1694,54 @@ class model { $DB->update_record('analytics_models', $this->model); } + /** + * Returns the name of the model. + * + * By default, models use their target's name as their own name. They can have their explicit name, too. In which + * case, the explicit name is used instead of the default one. + * + * @return string|lang_string + */ + public function get_name() { + + if (trim($this->model->name) === '') { + return $this->get_target()->get_name(); + + } else { + return $this->model->name; + } + } + + /** + * Renames the model to the given name. + * + * When given an empty string, the model falls back to using the associated target's name as its name. + * + * @param string $name The new name for the model, empty string for using the default name. + */ + public function rename(string $name) { + global $DB, $USER; + + $this->model->name = $name; + $this->model->timemodified = time(); + $this->model->usermodified = $USER->id; + + $DB->update_record('analytics_models', $this->model); + } + + /** + * Returns an inplace editable element with the model's name. + * + * @return \core\output\inplace_editable + */ + public function inplace_editable_name() { + + $displayname = format_string($this->get_name()); + + return new \core\output\inplace_editable('core_analytics', 'modelname', $this->model->id, + has_capability('moodle/analytics:managemodels', \context_system::instance()), $displayname, $this->model->name); + } + /** * Adds the id from {analytics_predictions} db table to the prediction \stdClass objects. * diff --git a/analytics/lib.php b/analytics/lib.php new file mode 100644 index 00000000000..9b6b41d4bb6 --- /dev/null +++ b/analytics/lib.php @@ -0,0 +1,46 @@ +. + +/** + * The interface library between the core and the subsystem. + * + * @package core_analytics + * @copyright 2019 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Implements the inplace editable feature. + * + * @param string $itemtype Type if the inplace editable element + * @param int $itemid Identifier of the element + * @param string $newvalue New value for the element + * @return \core\output\inplace_editable + */ +function core_analytics_inplace_editable($itemtype, $itemid, $newvalue) { + + if ($itemtype === 'modelname') { + \external_api::validate_context(context_system::instance()); + require_capability('moodle/analytics:managemodels', \context_system::instance()); + + $model = new \core_analytics\model($itemid); + $model->rename(clean_param($newvalue, PARAM_NOTAGS)); + + return $model->inplace_editable_name(); + } +} diff --git a/analytics/tests/model_test.php b/analytics/tests/model_test.php index 97a7e3b2ed3..e2e0468ad08 100644 --- a/analytics/tests/model_test.php +++ b/analytics/tests/model_test.php @@ -437,6 +437,63 @@ class analytics_model_testcase extends advanced_testcase { $this->assertCount(1, $modeldata->indicators); } + /** + * Test the implementation of {@link \core_analytics\model::inplace_editable_name()}. + */ + public function test_inplace_editable_name() { + global $PAGE; + + $this->resetAfterTest(); + + $output = new \core_renderer($PAGE, RENDERER_TARGET_GENERAL); + + // Check as a user with permission to edit the name. + $this->setAdminUser(); + $ie = $this->model->inplace_editable_name(); + $this->assertInstanceOf(\core\output\inplace_editable::class, $ie); + $data = $ie->export_for_template($output); + $this->assertEquals('core_analytics', $data['component']); + $this->assertEquals('modelname', $data['itemtype']); + + // Check as a user without permission to edit the name. + $this->setGuestUser(); + $ie = $this->model->inplace_editable_name(); + $this->assertInstanceOf(\core\output\inplace_editable::class, $ie); + $data = $ie->export_for_template($output); + $this->assertArrayHasKey('displayvalue', $data); + } + + /** + * Test how the models present themselves in the UI and that they can be renamed. + */ + public function test_get_name_and_rename() { + global $PAGE; + + $this->resetAfterTest(); + + $output = new \core_renderer($PAGE, RENDERER_TARGET_GENERAL); + + // By default, the model exported for template uses its target's name in the name inplace editable element. + $this->assertEquals($this->model->get_name(), $this->model->get_target()->get_name()); + $data = $this->model->export($output); + $this->assertEquals($data->name['displayvalue'], $this->model->get_target()->get_name()); + $this->assertEquals($data->name['value'], ''); + + // Rename the model. + $this->model->rename('Nějaký pokusný model'); + $this->assertEquals($this->model->get_name(), 'Nějaký pokusný model'); + $data = $this->model->export($output); + $this->assertEquals($data->name['displayvalue'], 'Nějaký pokusný model'); + $this->assertEquals($data->name['value'], 'Nějaký pokusný model'); + + // Undo the renaming. + $this->model->rename(''); + $this->assertEquals($this->model->get_name(), $this->model->get_target()->get_name()); + $data = $this->model->export($output); + $this->assertEquals($data->name['displayvalue'], $this->model->get_target()->get_name()); + $this->assertEquals($data->name['value'], ''); + } + /** * Generates a model log record. */ diff --git a/analytics/upgrade.txt b/analytics/upgrade.txt index 31316d4973d..2ae086965cc 100644 --- a/analytics/upgrade.txt +++ b/analytics/upgrade.txt @@ -15,6 +15,7 @@ information provided here is intended especially for developers. by updating the lib/db/analytics.php file and bumping the core version. * \core_analytics\model::execute_prediction_callbacks now returns an array with both sample's contexts and the prediction records. +* \core_analytics\model::export() now expects the renderer instance as an argument. * Time splitting methods: * \core_analytics\local\time_splitting\base::append_rangeindex and \core_analytics\local\time_splitting\base::infer_sample_info are now marked as final and can not diff --git a/lib/db/install.xml b/lib/db/install.xml index 519bb11f9c2..96312907b57 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ - @@ -3821,6 +3821,7 @@ + diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 8bf80630525..76dc1431776 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2988,5 +2988,17 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2019041000.02); } + if ($oldversion < 2019041300.01) { + // Add the field 'name' to the 'analytics_models' table. + $table = new xmldb_table('analytics_models'); + $field = new xmldb_field('name', XMLDB_TYPE_CHAR, '1333', null, null, null, null, 'trained'); + + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + upgrade_main_savepoint(true, 2019041300.01); + } + return true; }