mirror of
https://github.com/moodle/moodle.git
synced 2025-04-13 04:22:07 +02:00
MDL-60944 tool_analytics: Adding create and delete features
Extra modifications to refine the preexisting patch.
This commit is contained in:
parent
349c44128d
commit
e4453adc55
2
admin/tool/analytics/amd/build/model.min.js
vendored
2
admin/tool/analytics/amd/build/model.min.js
vendored
@ -1 +1 @@
|
||||
define(["jquery","core/str","core/log","core/notification","core/modal_factory","core/modal_events"],function(a,b,c,d,e,f){var g={clear:{title:{key:"clearpredictions",component:"tool_analytics"},body:{key:"clearmodelpredictions",component:"tool_analytics"}}},h=function(b){return a(b.closest("tr")[0]).find("span.target-name").text()};return{confirmAction:function(i,j){a('[data-action-id="'+i+'"]').on("click",function(i){i.preventDefault();var k=a(i.currentTarget);if("undefined"==typeof g[j])return void c.error('Action "'+j+'" is not allowed.');var l=[g[j].title,g[j].body];l[1].param=h(k);var m=b.get_strings(l),n=e.create({type:e.types.SAVE_CANCEL});a.when(m,n).then(function(a,b){return b.setTitle(a[0]),b.setBody(a[1]),b.setSaveButtonText(a[0]),b.getRoot().on(f.save,function(){window.location.href=k.attr("href")}),b.show(),b}).fail(d.exception)})}}});
|
||||
define(["jquery","core/str","core/log","core/notification","core/modal_factory","core/modal_events"],function(a,b,c,d,e,f){var g={clear:{title:{key:"clearpredictions",component:"tool_analytics"},body:{key:"clearmodelpredictions",component:"tool_analytics"}},"delete":{title:{key:"delete",component:"tool_analytics"},body:{key:"deletemodelconfirmation",component:"tool_analytics"}}},h=function(b){return a(b.closest("tr")[0]).find("span.target-name").text()};return{confirmAction:function(i,j){a('[data-action-id="'+i+'"]').on("click",function(i){i.preventDefault();var k=a(i.currentTarget);if("undefined"==typeof g[j])return void c.error('Action "'+j+'" is not allowed.');var l=[g[j].title,g[j].body];l[1].param=h(k);var m=b.get_strings(l),n=e.create({type:e.types.SAVE_CANCEL});a.when(m,n).then(function(a,b){return b.setTitle(a[0]),b.setBody(a[1]),b.setSaveButtonText(a[0]),b.getRoot().on(f.save,function(){window.location.href=k.attr("href")}),b.show(),b}).fail(d.exception)})}}});
|
@ -36,6 +36,15 @@ define(['jquery', 'core/str', 'core/log', 'core/notification', 'core/modal_facto
|
||||
component: 'tool_analytics'
|
||||
}
|
||||
|
||||
},
|
||||
'delete': {
|
||||
title: {
|
||||
key: 'delete',
|
||||
component: 'tool_analytics'
|
||||
}, body: {
|
||||
key: 'deletemodelconfirmation',
|
||||
component: 'tool_analytics'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -45,13 +45,25 @@ class edit_model extends \moodleform {
|
||||
|
||||
$mform = $this->_form;
|
||||
|
||||
if ($this->_customdata['model']->is_trained()) {
|
||||
if ($this->_customdata['trainedmodel']) {
|
||||
$message = get_string('edittrainedwarning', 'tool_analytics');
|
||||
$mform->addElement('html', $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING));
|
||||
}
|
||||
|
||||
$mform->addElement('advcheckbox', 'enabled', get_string('enabled', 'tool_analytics'));
|
||||
|
||||
if (!empty($this->_customdata['targets'])) {
|
||||
$targets = array('' => '');
|
||||
foreach ($this->_customdata['targets'] as $classname => $target) {
|
||||
$optionname = \tool_analytics\output\helper::class_to_option($classname);
|
||||
$targets[$optionname] = $target->get_name();
|
||||
}
|
||||
|
||||
$mform->addElement('select', 'target', get_string('target', 'tool_analytics'), $targets);
|
||||
$mform->addHelpButton('target', 'target', 'tool_analytics');
|
||||
$mform->addRule('target', get_string('required'), 'required', null, 'client');
|
||||
}
|
||||
|
||||
$indicators = array();
|
||||
foreach ($this->_customdata['indicators'] as $classname => $indicator) {
|
||||
$optionname = \tool_analytics\output\helper::class_to_option($classname);
|
||||
@ -88,11 +100,13 @@ class edit_model extends \moodleform {
|
||||
$predictionprocessors);
|
||||
$mform->addHelpButton('predictionsprocessor', 'predictionsprocessor', 'analytics');
|
||||
|
||||
$mform->addElement('hidden', 'id', $this->_customdata['id']);
|
||||
$mform->setType('id', PARAM_INT);
|
||||
if (!empty($this->_customdata['id'])) {
|
||||
$mform->addElement('hidden', 'id', $this->_customdata['id']);
|
||||
$mform->setType('id', PARAM_INT);
|
||||
|
||||
$mform->addElement('hidden', 'action', 'edit');
|
||||
$mform->setType('action', PARAM_ALPHANUMEXT);
|
||||
$mform->addElement('hidden', 'action', 'edit');
|
||||
$mform->setType('action', PARAM_ALPHANUMEXT);
|
||||
}
|
||||
|
||||
$this->add_action_buttons();
|
||||
}
|
||||
|
@ -22,9 +22,9 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace tool_analytics;
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
namespace tool_analytics\output\form;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Model upload form.
|
||||
@ -33,15 +33,24 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* @copyright 2017 onwards Ankit Agarwal
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class import_model_form extends \moodleform {
|
||||
function definition () {
|
||||
class import_model extends \moodleform {
|
||||
|
||||
/**
|
||||
* Form definition.
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
public function definition () {
|
||||
$mform = $this->_form;
|
||||
|
||||
$mform->addElement('header', 'settingsheader', get_string('analyticsimportmodel', 'tool_analytics'));
|
||||
$mform->addElement('header', 'settingsheader', get_string('importmodel', 'tool_analytics'));
|
||||
|
||||
$mform->addElement('filepicker', 'modelfile', get_string('file'), null, ['accepted_types' => '.json']);
|
||||
$mform->addRule('modelfile', null, 'required');
|
||||
|
||||
$this->add_action_buttons(false, get_string('submit'));
|
||||
$mform->addElement('advcheckbox', 'ignoreversionmismatches', get_string('ignoreversionmismatches', 'tool_analytics'),
|
||||
get_string('ignoreversionmismatchescheckbox', 'tool_analytics'));
|
||||
|
||||
$this->add_action_buttons(true, get_string('import'));
|
||||
}
|
||||
}
|
||||
}
|
@ -58,4 +58,38 @@ class helper {
|
||||
// Really unlikely but yeah, I'm a bad booyyy.
|
||||
return str_replace('2015102400ouuu', '\\', $option);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an analytics > analytics models > $title breadcrumb.
|
||||
*
|
||||
* @param string $title
|
||||
* @param \moodle_url $url
|
||||
* @param \context|false $context Defaults to context_system
|
||||
* @return null
|
||||
*/
|
||||
public static function set_navbar(string $title, \moodle_url $url, ?\context $context = null) {
|
||||
global $PAGE;
|
||||
|
||||
if (!$context) {
|
||||
$context = \context_system::instance();
|
||||
}
|
||||
|
||||
$PAGE->set_context($context);
|
||||
$PAGE->set_url($url);
|
||||
|
||||
if ($siteadmin = $PAGE->settingsnav->find('root', \navigation_node::TYPE_SITE_ADMIN)) {
|
||||
$PAGE->navbar->add($siteadmin->get_content(), $siteadmin->action());
|
||||
}
|
||||
if ($analytics = $PAGE->settingsnav->find('analytics', \navigation_node::TYPE_SETTING)) {
|
||||
$PAGE->navbar->add($analytics->get_content(), $analytics->action());
|
||||
}
|
||||
if ($analyticmodels = $PAGE->settingsnav->find('analyticmodels', \navigation_node::TYPE_SETTING)) {
|
||||
$PAGE->navbar->add($analyticmodels->get_content(), $analyticmodels->action());
|
||||
}
|
||||
$PAGE->navbar->add($title, $url);
|
||||
|
||||
$PAGE->set_pagelayout('report');
|
||||
$PAGE->set_title($title);
|
||||
$PAGE->set_heading($title);
|
||||
}
|
||||
}
|
||||
|
@ -62,6 +62,8 @@ class models_list implements \renderable, \templatable {
|
||||
global $PAGE;
|
||||
|
||||
$data = new \stdClass();
|
||||
$data->importmodelurl = new \moodle_url('/admin/tool/analytics/importmodel.php');
|
||||
$data->createmodelurl = new \moodle_url('/admin/tool/analytics/createmodel.php');
|
||||
|
||||
$onlycli = get_config('analytics', 'onlycli');
|
||||
if ($onlycli === false) {
|
||||
@ -232,7 +234,7 @@ class models_list implements \renderable, \templatable {
|
||||
$urlparams['action'] = 'exportdata';
|
||||
$url = new \moodle_url('model.php', $urlparams);
|
||||
$icon = new \action_menu_link_secondary($url, new \pix_icon('i/export',
|
||||
get_string('exporttrainingdata', 'tool_analytics')), get_string('export', 'tool_analytics'));
|
||||
get_string('exporttrainingdata', 'tool_analytics')), get_string('exporttrainingdata', 'tool_analytics'));
|
||||
$actionsmenu->add($icon);
|
||||
}
|
||||
|
||||
@ -240,7 +242,7 @@ class models_list implements \renderable, \templatable {
|
||||
if (!$model->is_static() && $model->get_indicators() && !empty($modeldata->timesplitting)) {
|
||||
$urlparams['action'] = 'exportmodel';
|
||||
$url = new \moodle_url('model.php', $urlparams);
|
||||
$icon = new \action_menu_link_secondary($url, new \pix_icon('i/export',
|
||||
$icon = new \action_menu_link_secondary($url, new \pix_icon('i/backup',
|
||||
get_string('exportmodel', 'tool_analytics')), get_string('exportmodel', 'tool_analytics'));
|
||||
$actionsmenu->add($icon);
|
||||
}
|
||||
@ -267,6 +269,15 @@ class models_list implements \renderable, \templatable {
|
||||
$actionsmenu->add($icon);
|
||||
}
|
||||
|
||||
$actionid = 'delete-' . $model->get_id();
|
||||
$PAGE->requires->js_call_amd('tool_analytics/model', 'confirmAction', [$actionid, 'delete']);
|
||||
$urlparams['action'] = 'delete';
|
||||
$url = new \moodle_url('model.php', $urlparams);
|
||||
$icon = new \action_menu_link_secondary($url, new \pix_icon('t/delete',
|
||||
get_string('delete', 'tool_analytics')), get_string('delete', 'tool_analytics'),
|
||||
['data-action-id' => $actionid]);
|
||||
$actionsmenu->add($icon);
|
||||
|
||||
$modeldata->actions = $actionsmenu->export_for_template($output);
|
||||
|
||||
$data->models[] = $modeldata;
|
||||
|
91
admin/tool/analytics/createmodel.php
Normal file
91
admin/tool/analytics/createmodel.php
Normal file
@ -0,0 +1,91 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Create model form.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
|
||||
require_login();
|
||||
\core_analytics\manager::check_can_manage_models();
|
||||
|
||||
$returnurl = new \moodle_url('/admin/tool/analytics/index.php');
|
||||
$url = new \moodle_url('/admin/tool/analytics/createmodel.php');
|
||||
$title = get_string('createmodel', 'tool_analytics');
|
||||
|
||||
\tool_analytics\output\helper::set_navbar($title, $url);
|
||||
|
||||
// Static targets are not editable, we discard them.
|
||||
$targets = array_filter(\core_analytics\manager::get_all_targets(), function($target) {
|
||||
return (!$target->based_on_assumptions());
|
||||
});
|
||||
|
||||
$customdata = array(
|
||||
'trainedmodel' => false,
|
||||
'targets' => $targets,
|
||||
'indicators' => \core_analytics\manager::get_all_indicators(),
|
||||
'timesplittings' => \core_analytics\manager::get_enabled_time_splitting_methods(),
|
||||
'predictionprocessors' => \core_analytics\manager::get_all_prediction_processors(),
|
||||
);
|
||||
$mform = new \tool_analytics\output\form\edit_model(null, $customdata);
|
||||
|
||||
if ($mform->is_cancelled()) {
|
||||
redirect($returnurl);
|
||||
|
||||
} else if ($data = $mform->get_data()) {
|
||||
|
||||
// Converting option names to class names.
|
||||
$targetclass = \tool_analytics\output\helper::option_to_class($data->target);
|
||||
$target = \core_analytics\manager::get_target($targetclass);
|
||||
|
||||
$indicators = array();
|
||||
foreach ($data->indicators as $indicator) {
|
||||
$indicatorinstance = \core_analytics\manager::get_indicator(
|
||||
\tool_analytics\output\helper::option_to_class($indicator)
|
||||
);
|
||||
$indicators[$indicatorinstance->get_id()] = $indicatorinstance;
|
||||
}
|
||||
$timesplitting = \tool_analytics\output\helper::option_to_class($data->timesplitting);
|
||||
$predictionsprocessor = \tool_analytics\output\helper::option_to_class($data->predictionsprocessor);
|
||||
|
||||
// Insert the model into db.
|
||||
$model = \core_analytics\model::create($target, []);
|
||||
|
||||
// Filter out indicators that can not be used by this target.
|
||||
$invalidindicators = array_diff_key($indicators, $model->get_potential_indicators());
|
||||
if ($invalidindicators) {
|
||||
$indicators = array_diff_key($indicators, $invalidindicators);
|
||||
}
|
||||
|
||||
// Update the model with the valid list of indicators.
|
||||
$model->update($data->enabled, $indicators, $timesplitting, $predictionsprocessor);
|
||||
|
||||
$message = '';
|
||||
$messagetype = \core\output\notification::NOTIFY_SUCCESS;
|
||||
if (!empty($invalidindicators)) {
|
||||
$message = get_string('invalidindicatorsremoved', 'tool_analytics');
|
||||
}
|
||||
redirect($returnurl, $message, 0, $messagetype);
|
||||
}
|
||||
|
||||
echo $OUTPUT->header();
|
||||
$mform->display();
|
||||
echo $OUTPUT->footer();
|
@ -23,35 +23,41 @@
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
require_once($CFG->libdir . '/adminlib.php');
|
||||
|
||||
admin_externalpage_setup('analyticsmodelimport', '', null, '', array('pagelayout' => 'report'));
|
||||
echo $OUTPUT->header();
|
||||
require_login();
|
||||
\core_analytics\manager::check_can_manage_models();
|
||||
|
||||
$form = new tool_analytics\import_model_form();
|
||||
if ($data = $form->get_data()) {
|
||||
$content = json_decode($form->get_file_content('modelfile'));
|
||||
if (empty($content->moodleversion)) {
|
||||
// Should never happen.
|
||||
echo $OUTPUT->notification(get_string('missingmoodleversion', 'tool_analytics'), 'error');
|
||||
} else {
|
||||
if ($content->moodleversion != $CFG->version) {
|
||||
$a = new stdClass();
|
||||
$a->importedversion = $content->moodleversion;
|
||||
$a->version = $CFG->version;
|
||||
echo $OUTPUT->notification(get_string('versionnotsame', 'tool_analytics', $a), 'warning');
|
||||
}
|
||||
$model = \core_analytics\model::create_from_json($content);
|
||||
if ($model) {
|
||||
echo $OUTPUT->notification(get_string('success'), 'notifysuccess');
|
||||
} else {
|
||||
echo $OUTPUT->notification(get_string('error'), 'error');
|
||||
}
|
||||
$returnurl = new \moodle_url('/admin/tool/analytics/index.php');
|
||||
$url = new \moodle_url('/admin/tool/analytics/importmodel.php');
|
||||
$title = get_string('importmodel', 'tool_analytics');
|
||||
|
||||
\tool_analytics\output\helper::set_navbar($title, $url);
|
||||
|
||||
$form = new \tool_analytics\output\form\import_model();
|
||||
if ($form->is_cancelled()) {
|
||||
redirect($returnurl);
|
||||
} else if ($data = $form->get_data()) {
|
||||
|
||||
$modelconfig = new \core_analytics\model_config();
|
||||
|
||||
$json = $form->get_file_content('modelfile');
|
||||
|
||||
if ($error = $modelconfig->check_json_data($json)) {
|
||||
// The provided file is not ok.
|
||||
redirect($url, $error, 0, \core\output\notification::NOTIFY_ERROR);
|
||||
}
|
||||
echo $OUTPUT->single_button(new moodle_url("$CFG->wwwroot/$CFG->admin/tool/analytics/index.php"),
|
||||
get_string('continue'), 'get');
|
||||
} else {
|
||||
$form->display();
|
||||
|
||||
$modeldata = json_decode($json);
|
||||
if ($error = $modelconfig->check_dependencies($modeldata, $data->ignoreversionmismatches)) {
|
||||
// The file is not available until the form is validated so we need an alternative method to show errors.
|
||||
redirect($url, $error, 0, \core\output\notification::NOTIFY_ERROR);
|
||||
}
|
||||
$model = \core_analytics\model::create_from_import($modeldata, true);
|
||||
|
||||
redirect($returnurl, get_string('importedsuccessfully', 'tool_analytics'), 0,
|
||||
\core\output\notification::NOTIFY_SUCCESS);
|
||||
}
|
||||
|
||||
echo $OUTPUT->header();
|
||||
$form->display();
|
||||
echo $OUTPUT->footer();
|
@ -26,7 +26,6 @@ $string['accuracy'] = 'Accuracy';
|
||||
$string['allpredictions'] = 'All predictions';
|
||||
$string['analysingsitedata'] = 'Analysing the site';
|
||||
$string['analyticmodels'] = 'Analytics models';
|
||||
$string['analyticsimportmodel'] = 'Import analytics model';
|
||||
$string['bettercli'] = 'Evaluating models and generating predictions may involve heavy processing. It is recommended to run these actions from the command line.';
|
||||
$string['cantguessstartdate'] = 'Can\'t guess the start date';
|
||||
$string['cantguessenddate'] = 'Can\'t guess the end date';
|
||||
@ -36,15 +35,23 @@ $string['clearmodelpredictions'] = 'Are you sure you want to clear all "{$a}" pr
|
||||
$string['clienablemodel'] = 'You can enable the model by selecting a time-splitting method by its ID. Note that you can also enable it later using the web interface (\'none\' to exit).';
|
||||
$string['clievaluationandpredictions'] = 'A scheduled task iterates through enabled models and gets predictions. Models evaluation via the web interface is disabled. You can allow these processes to be executed manually via the web interface by disabling the <a href="{$a}">\'onlycli\'</a> analytics setting.';
|
||||
$string['clievaluationandpredictionsnoadmin'] = 'A scheduled task iterates through enabled models and gets predictions. Models evaluation via the web interface is disabled. It may be enabled by a site administrator.';
|
||||
$string['createmodel'] = 'Create model';
|
||||
$string['delete'] = 'Delete';
|
||||
$string['deletemodelconfirmation'] = 'Are you sure you want to delete "{$a}"?';
|
||||
$string['disabled'] = 'Disabled';
|
||||
$string['editmodel'] = 'Edit "{$a}" model';
|
||||
$string['edittrainedwarning'] = 'This model has already been trained. Note that changing its indicators or its time-splitting method will delete its previous predictions and start generating new predictions.';
|
||||
$string['enabled'] = 'Enabled';
|
||||
$string['errorcantenablenotimesplitting'] = 'You need to select a time-splitting method before enabling the model';
|
||||
$string['errorimport'] = 'Error importing the provided json file.';
|
||||
$string['errorimportmissingcomponents'] = 'The provided model requires the following plugins to be installed: {$a}. Note that the versions do not necessarily need to match with the versions installed in your system. To install the same or a newer version of the plugin should be enough in most cases.';
|
||||
$string['errorimportversionmismatches'] = 'The version of the following components differ from the version installed in this site: {$a}. You can use "Ignore version mismatches" option to ignore these differences.';
|
||||
$string['errorimportmissingclasses'] = 'The following analytics components are not available in this site: {$a->missingclasses}. ';
|
||||
$string['errornoenabledandtrainedmodels'] = 'There are no enabled and trained models to predict.';
|
||||
$string['errornoenabledmodels'] = 'There are no enabled models to train.';
|
||||
$string['errornoexport'] = 'Only trained models can be exported';
|
||||
$string['errornoexportconfg'] = 'Only non static models with timeplitting methods can be exported.';
|
||||
$string['errornoexportconfig'] = 'There was a problem exporting the model configuration.';
|
||||
$string['errornoexportconfigrequirements'] = 'Only non static models with timeplitting methods can be exported.';
|
||||
$string['errornostaticedit'] = 'Models based on assumptions cannot be edited.';
|
||||
$string['errornostaticevaluated'] = 'Models based on assumptions cannot be evaluated. They are always 100% correct according to how they were defined.';
|
||||
$string['errornostaticlog'] = 'Models based on assumptions cannot be evaluated because there is no performance log.';
|
||||
@ -53,8 +60,7 @@ $string['errortrainingdataexport'] = 'The model training data could not be expor
|
||||
$string['evaluate'] = 'Evaluate';
|
||||
$string['evaluatemodel'] = 'Evaluate model';
|
||||
$string['evaluationinbatches'] = 'The site contents are calculated and stored in batches. The evaluation process may be stopped at any time. The next time it is run, it will continue from the point when it was stopped.';
|
||||
$string['export'] = 'Export';
|
||||
$string['exportmodel'] = 'Export model configuration';
|
||||
$string['exportmodel'] = 'Export configuration';
|
||||
$string['exporttrainingdata'] = 'Export training data';
|
||||
$string['getpredictionsresultscli'] = 'Results using {$a->name} (id: {$a->id}) course duration splitting';
|
||||
$string['getpredictionsresults'] = 'Results using {$a->name} course duration splitting';
|
||||
@ -62,12 +68,17 @@ $string['extrainfo'] = 'Info';
|
||||
$string['generalerror'] = 'Evaluation error. Status code {$a}';
|
||||
$string['getpredictions'] = 'Get predictions';
|
||||
$string['goodmodel'] = 'This is a good model for using to obtain predictions. Enable it to start obtaining predictions.';
|
||||
$string['importmodel'] = 'Import model';
|
||||
$string['indicators'] = 'Indicators';
|
||||
$string['info'] = 'Info';
|
||||
$string['ignoreversionmismatches'] = 'Ignore version mismatches';
|
||||
$string['ignoreversionmismatchescheckbox'] = 'Ignore the differences between this site version and the original site version.';
|
||||
$string['importedsuccessfully'] = 'The model has been successfully imported.';
|
||||
$string['insights'] = 'Insights';
|
||||
$string['invalidanalysables'] = 'Invalid site elements';
|
||||
$string['invalidanalysablesinfo'] = 'This pages lists this site analysable elements that can not be used by this prediction model. The listed elements can not be used neither to train the prediction model nor the prediction model can get predictions for them.';
|
||||
$string['invalidanalysablestable'] = 'Invalid site analysable elements table';
|
||||
$string['invalidindicatorsremoved'] = 'A new model has been added. Indicators that do not work with the selected target have been automatically removed.';
|
||||
$string['invalidprediction'] = 'Invalid to get predictions';
|
||||
$string['invalidtraining'] = 'Invalid to train the model';
|
||||
$string['loginfo'] = 'Log extra info';
|
||||
@ -91,6 +102,7 @@ $string['previouspage'] = 'Previous page';
|
||||
$string['samestartdate'] = 'Current start date is good';
|
||||
$string['sameenddate'] = 'Current end date is good';
|
||||
$string['target'] = 'Target';
|
||||
$string['target_help'] = 'The target is what the model will predict.';
|
||||
$string['timesplittingnotdefined'] = 'Time splitting is not defined.';
|
||||
$string['timesplittingnotdefined_help'] = 'You need to select a time-splitting method before enabling the model.';
|
||||
$string['trainandpredictmodel'] = 'Training model and calculating predictions';
|
||||
|
@ -23,18 +23,17 @@
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
require_once($CFG->libdir . '/dataformatlib.php');
|
||||
require_once($CFG->libdir . '/filelib.php');
|
||||
|
||||
$id = required_param('id', PARAM_INT);
|
||||
$action = required_param('action', PARAM_ALPHANUMEXT);
|
||||
|
||||
$context = context_system::instance();
|
||||
|
||||
require_login();
|
||||
|
||||
$model = new \core_analytics\model($id);
|
||||
\core_analytics\manager::check_can_manage_models();
|
||||
|
||||
$returnurl = new \moodle_url('/admin/tool/analytics/index.php');
|
||||
$params = array('id' => $id, 'action' => $action);
|
||||
$url = new \moodle_url('/admin/tool/analytics/model.php', $params);
|
||||
|
||||
@ -58,6 +57,9 @@ switch ($action) {
|
||||
case 'disable':
|
||||
$title = get_string('disable');
|
||||
break;
|
||||
case 'delete':
|
||||
$title = get_string('delete');
|
||||
break;
|
||||
case 'exportdata':
|
||||
$title = get_string('exporttrainingdata', 'tool_analytics');
|
||||
break;
|
||||
@ -74,11 +76,7 @@ switch ($action) {
|
||||
throw new moodle_exception('errorunknownaction', 'analytics');
|
||||
}
|
||||
|
||||
$PAGE->set_context($context);
|
||||
$PAGE->set_url($url);
|
||||
$PAGE->set_pagelayout('report');
|
||||
$PAGE->set_title($title);
|
||||
$PAGE->set_heading($title);
|
||||
\tool_analytics\output\helper::set_navbar($title, $url);
|
||||
|
||||
$onlycli = get_config('analytics', 'onlycli');
|
||||
if ($onlycli === false) {
|
||||
@ -92,14 +90,21 @@ switch ($action) {
|
||||
confirm_sesskey();
|
||||
|
||||
$model->enable();
|
||||
redirect(new \moodle_url('/admin/tool/analytics/index.php'));
|
||||
redirect($returnurl);
|
||||
break;
|
||||
|
||||
case 'disable':
|
||||
confirm_sesskey();
|
||||
|
||||
$model->update(0, false, false);
|
||||
redirect(new \moodle_url('/admin/tool/analytics/index.php'));
|
||||
redirect($returnurl);
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
confirm_sesskey();
|
||||
|
||||
$model->delete();
|
||||
redirect($returnurl);
|
||||
break;
|
||||
|
||||
case 'edit':
|
||||
@ -112,7 +117,7 @@ switch ($action) {
|
||||
|
||||
$customdata = array(
|
||||
'id' => $model->get_id(),
|
||||
'model' => $model,
|
||||
'trainedmodel' => $model->is_trained(),
|
||||
'indicators' => $model->get_potential_indicators(),
|
||||
'timesplittings' => \core_analytics\manager::get_enabled_time_splitting_methods(),
|
||||
'predictionprocessors' => \core_analytics\manager::get_all_prediction_processors()
|
||||
@ -120,7 +125,7 @@ switch ($action) {
|
||||
$mform = new \tool_analytics\output\form\edit_model(null, $customdata);
|
||||
|
||||
if ($mform->is_cancelled()) {
|
||||
redirect(new \moodle_url('/admin/tool/analytics/index.php'));
|
||||
redirect($returnurl);
|
||||
|
||||
} else if ($data = $mform->get_data()) {
|
||||
|
||||
@ -133,7 +138,7 @@ switch ($action) {
|
||||
$timesplitting = \tool_analytics\output\helper::option_to_class($data->timesplitting);
|
||||
$predictionsprocessor = \tool_analytics\output\helper::option_to_class($data->predictionsprocessor);
|
||||
$model->update($data->enabled, $indicators, $timesplitting, $predictionsprocessor);
|
||||
redirect(new \moodle_url('/admin/tool/analytics/index.php'));
|
||||
redirect($returnurl);
|
||||
}
|
||||
|
||||
echo $OUTPUT->header();
|
||||
@ -215,7 +220,7 @@ switch ($action) {
|
||||
|
||||
$file = $model->get_training_data();
|
||||
if (!$file) {
|
||||
redirect(new \moodle_url('/admin/tool/analytics/index.php'), get_string('errortrainingdataexport', 'tool_analytics'),
|
||||
redirect($returnurl, get_string('errortrainingdataexport', 'tool_analytics'),
|
||||
null, \core\output\notification::NOTIFY_ERROR);
|
||||
}
|
||||
|
||||
@ -224,26 +229,17 @@ switch ($action) {
|
||||
break;
|
||||
|
||||
case 'exportmodel':
|
||||
|
||||
if (!$model->is_static() && $model->get_indicators() && !empty($model->timesplitting)) {
|
||||
throw new moodle_exception('errornoexportconfg', 'tool_analytics');
|
||||
}
|
||||
$downloadfilename = 'model-config.' . $model->get_id() . '.' . time() . '.json';
|
||||
$modelconfig = $model->export_as_json();
|
||||
make_temp_directory('analyticsexport');
|
||||
$tempfilename = $CFG->tempdir .'/analyticsexport/'. md5(sesskey() . microtime() . $downloadfilename);
|
||||
if (!file_put_contents($tempfilename, $modelconfig)) {
|
||||
print_error('cannotcreatetempdir');
|
||||
}
|
||||
$downloadfilename = 'model-config.' . $model->get_id() . '.' . microtime() . '.json';
|
||||
$filepath = $model->export_config($downloadfilename);
|
||||
@header("Content-type: text/json; charset=UTF-8");
|
||||
send_temp_file($tempfilename, $downloadfilename);
|
||||
send_temp_file($filepath, $downloadfilename);
|
||||
break;
|
||||
|
||||
case 'clear':
|
||||
confirm_sesskey();
|
||||
|
||||
$model->clear();
|
||||
redirect(new \moodle_url('/admin/tool/analytics/index.php'));
|
||||
redirect($returnurl);
|
||||
break;
|
||||
|
||||
case 'invalidanalysables':
|
||||
|
@ -26,5 +26,3 @@ defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$ADMIN->add('analytics', new admin_externalpage('analyticmodels', get_string('analyticmodels', 'tool_analytics'),
|
||||
"$CFG->wwwroot/$CFG->admin/tool/analytics/index.php", 'moodle/analytics:managemodels'));
|
||||
$ADMIN->add('analytics', new admin_externalpage('analyticsmodelimport', get_string('analyticsimportmodel', 'tool_analytics'),
|
||||
"$CFG->wwwroot/$CFG->admin/tool/analytics/importmodel.php", 'moodle/analytics:managemodels'));
|
||||
|
@ -110,6 +110,10 @@
|
||||
{{/infos}}
|
||||
|
||||
<div class="box">
|
||||
<div class="top-nav d-flex">
|
||||
<a href="{{createmodelurl}}" class="btn btn-default mr-2">{{#str}}createmodel, tool_analytics{{/str}}</a>
|
||||
<a href="{{importmodelurl}}" class="btn btn-default">{{#str}}importmodel, tool_analytics{{/str}}</a>
|
||||
</div>
|
||||
<table class="generaltable fullwidth">
|
||||
<caption>{{#str}}analyticmodels, tool_analytics{{/str}}</caption>
|
||||
<thead>
|
||||
|
@ -82,6 +82,7 @@ abstract class calculable {
|
||||
* @return string
|
||||
*/
|
||||
public function get_id() {
|
||||
// Using get_class as get_component_classes_in_namespace returns double escaped fully qualified class names.
|
||||
return '\\' . get_class($this);
|
||||
}
|
||||
|
||||
|
@ -45,6 +45,11 @@ class manager {
|
||||
*/
|
||||
protected static $predictionprocessors = null;
|
||||
|
||||
/**
|
||||
* @var \core_analytics\local\target\base[]
|
||||
*/
|
||||
protected static $alltargets = null;
|
||||
|
||||
/**
|
||||
* @var \core_analytics\local\indicator\base[]
|
||||
*/
|
||||
@ -281,6 +286,28 @@ class manager {
|
||||
return new $fullclassname();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all targets in the system.
|
||||
*
|
||||
* @return \core_analytics\local\target\base[]
|
||||
*/
|
||||
public static function get_all_targets() : array {
|
||||
if (self::$alltargets !== null) {
|
||||
return self::$alltargets;
|
||||
}
|
||||
|
||||
$classes = self::get_analytics_classes('target');
|
||||
|
||||
self::$alltargets = [];
|
||||
foreach ($classes as $fullclassname => $classpath) {
|
||||
$instance = self::get_target($fullclassname);
|
||||
if ($instance) {
|
||||
self::$alltargets[$instance->get_id()] = $instance;
|
||||
}
|
||||
}
|
||||
|
||||
return self::$alltargets;
|
||||
}
|
||||
/**
|
||||
* Return all system indicators.
|
||||
*
|
||||
@ -297,7 +324,6 @@ class manager {
|
||||
foreach ($classes as $fullclassname => $classpath) {
|
||||
$instance = self::get_indicator($fullclassname);
|
||||
if ($instance) {
|
||||
// Using get_class as get_component_classes_in_namespace returns double escaped fully qualified class names.
|
||||
self::$allindicators[$instance->get_id()] = $instance;
|
||||
}
|
||||
}
|
||||
|
@ -339,6 +339,7 @@ class model {
|
||||
* @param \core_analytics\local\target\base $target
|
||||
* @param \core_analytics\local\indicator\base[] $indicators
|
||||
* @param string $timesplittingid The time splitting method id (its fully qualified class name)
|
||||
* @param string $processor The machine learning backend this model will use.
|
||||
* @return \core_analytics\model
|
||||
*/
|
||||
public static function create(\core_analytics\local\target\base $target, array $indicators,
|
||||
@ -360,8 +361,8 @@ class model {
|
||||
$modelobj->usermodified = $USER->id;
|
||||
|
||||
if ($processor &&
|
||||
!self::is_valid($processor, '\core_analytics\classifier') &&
|
||||
!self::is_valid($processor, '\core_analytics\regressor')) {
|
||||
!manager::is_valid($processor, '\core_analytics\classifier') &&
|
||||
!manager::is_valid($processor, '\core_analytics\regressor')) {
|
||||
throw new \coding_exception('The provided predictions processor \\' . $processor . '\processor is not valid');
|
||||
} else {
|
||||
$modelobj->predictionsprocessor = $processor;
|
||||
@ -386,41 +387,56 @@ class model {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new model from json configuration.
|
||||
* Creates a new model from import configuration.
|
||||
*
|
||||
* @param string $json json data.
|
||||
* @return \core_analytics\model
|
||||
* It is recommended to call \core_analytics\model_config::check_dependencies first so the error message can be retrieved.
|
||||
*
|
||||
* @param \stdClass $modeldata Model data.
|
||||
* @param bool $skipcheckdependencies Useful if you already checked the dependencies.
|
||||
* @return \core_analytics\model|false False if the provided model data contain errors.
|
||||
*/
|
||||
public static function create_from_json($jsondata) {
|
||||
public static function create_from_import(\stdClass $modeldata, ?bool $skipcheckdependencies = false) : ?\core_analytics\model {
|
||||
|
||||
\core_analytics\manager::check_can_manage_models();
|
||||
if (empty($jsondata) || !isset($jsondata->target) || !isset($jsondata->indicators) || !isset($jsondata->timesplitting)) {
|
||||
throw new \coding_exception("invalid json data");
|
||||
|
||||
if (!$skipcheckdependencies) {
|
||||
$modelconfig = new model_config();
|
||||
if ($error = $modelconfig->check_dependencies($modeldata, false)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Target.
|
||||
$target = $jsondata->target;
|
||||
if (!class_exists($target)) {
|
||||
throw new \moodle_exception('classdoesnotexist', 'tool_analytics', $target);
|
||||
// At this stage we should be 100% sure that the model data is safe and can be imported.
|
||||
// If the caller explicitly set $skipcheckdependencies to false and there is a problem
|
||||
// in this process we trigger a coding exception.
|
||||
if (!$target = \core_analytics\manager::get_target($modeldata->target)) {
|
||||
throw new \coding_exception('The provided target is not available. Ensure that model_config::check_dependencies
|
||||
is called before importing the model.');
|
||||
}
|
||||
if (!$timesplitting = \core_analytics\manager::get_time_splitting($modeldata->timesplitting)) {
|
||||
throw new \coding_exception('The provided time splitting method is not available. Ensure that
|
||||
model_config::check_dependencies is called before importing the model.');
|
||||
}
|
||||
$target = \core_analytics\manager::get_target($target);
|
||||
|
||||
// Indicators.
|
||||
$indicators = [];
|
||||
foreach($jsondata->indicators as $indicator) {
|
||||
if (!class_exists($indicator)) {
|
||||
throw new \moodle_exception('classdoesnotexist', 'tool_analytics', $indicator);
|
||||
foreach ($modeldata->indicators as $indicator) {
|
||||
if (!$indicator = \core_analytics\manager::get_indicator($indicator)) {
|
||||
throw new \coding_exception('The provided indicator is not available. Ensure that
|
||||
model_config::check_dependencies is called before importing the model.');
|
||||
}
|
||||
$indicators[] = \core_analytics\manager::get_indicator($indicator);
|
||||
$indicators[] = $indicator;
|
||||
}
|
||||
|
||||
// Timesplitting.
|
||||
$timesplitting = $jsondata->timesplitting;
|
||||
if (!class_exists($timesplitting)) {
|
||||
throw new \moodle_exception('classdoesnotexist', 'tool_analytics', $timesplitting);
|
||||
if (!empty($modeldata->processor)) {
|
||||
if (!$processor = \core_analytics\manager::get_predictions_processor($modeldata->processor, false)) {
|
||||
throw new \coding_exception('The provided machine learning backend is not available. Ensure that
|
||||
model_config::check_dependencies is called before importing the model.');
|
||||
}
|
||||
} else {
|
||||
$modeldata->processor = false;
|
||||
}
|
||||
|
||||
return self::create($target, $indicators, $timesplitting);
|
||||
return self::create($target, $indicators, $modeldata->timesplitting, $modeldata->processor);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -500,6 +516,7 @@ class model {
|
||||
|
||||
// It needs to be reset as the version changes.
|
||||
$this->uniqueid = null;
|
||||
$this->indicators = null;
|
||||
|
||||
// We update the version of the model so different time splittings are not mixed up.
|
||||
$this->model->version = $now;
|
||||
@ -1418,30 +1435,40 @@ class model {
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the model data as JSON.
|
||||
* Exports the model data as a JSON file.
|
||||
*
|
||||
* @return string JSON encoded data.
|
||||
* @param string $downloadfilename Download file name.
|
||||
* @return string The filepath
|
||||
*/
|
||||
public function export_as_json() {
|
||||
public function export_config(string $downloadfilename) : string {
|
||||
global $CFG;
|
||||
|
||||
$data = new \stdClass();
|
||||
$data->target = $this->get_target()->get_id();
|
||||
\core_analytics\manager::check_can_manage_models();
|
||||
|
||||
$modelconfig = new model_config($this);
|
||||
$modeldata = $modelconfig->export();
|
||||
return $modelconfig->export_to_file($modeldata, $downloadfilename);
|
||||
}
|
||||
|
||||
if ($timesplitting = $this->get_time_splitting()) {
|
||||
$data->timesplitting = $timesplitting->get_id();
|
||||
} else {
|
||||
// We don't want to allow models without timesplitting to be exported.
|
||||
throw new \moodle_exception('errornotimesplittings', 'analytics');
|
||||
/**
|
||||
* Can this model be exported?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function can_export_configuration() : bool {
|
||||
|
||||
if (empty($this->model->timesplitting)) {
|
||||
return false;
|
||||
}
|
||||
if (!$this->get_indicators()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data->indicators = [];
|
||||
foreach ($this->get_indicators() as $indicator) {
|
||||
$data->indicators[] = $indicator->get_id();
|
||||
if ($this->is_static()) {
|
||||
return false;
|
||||
}
|
||||
$data->moodleversion = $CFG->version;
|
||||
return json_encode($data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
251
analytics/classes/model_config.php
Normal file
251
analytics/classes/model_config.php
Normal file
@ -0,0 +1,251 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Model configuration manager.
|
||||
*
|
||||
* @package core_analytics
|
||||
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core_analytics;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Model configuration manager.
|
||||
*
|
||||
* @package core_analytics
|
||||
* @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class model_config {
|
||||
|
||||
/**
|
||||
* @var \core_analytics\model
|
||||
*/
|
||||
private $model = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \core_analytics\model|null $model
|
||||
*/
|
||||
public function __construct(?model $model = null) {
|
||||
$this->model = $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a model to a temp file using the provided file name.
|
||||
*
|
||||
* @return \stdClass
|
||||
*/
|
||||
public function export() : \stdClass {
|
||||
|
||||
if (!$this->model) {
|
||||
throw new \coding_exception('No model object provided.');
|
||||
}
|
||||
|
||||
if (!$this->model->can_export_configuration()) {
|
||||
throw new \moodle_exception('errornoexportconfigrequirements', 'tool_analytics');
|
||||
}
|
||||
|
||||
$versions = \core_component::get_all_versions();
|
||||
|
||||
$data = new \stdClass();
|
||||
|
||||
// Target.
|
||||
$data->target = $this->model->get_target()->get_id();
|
||||
$requiredclasses[] = $data->target;
|
||||
|
||||
// Time splitting method.
|
||||
$data->timesplitting = $this->model->get_time_splitting()->get_id();
|
||||
$requiredclasses[] = $data->timesplitting;
|
||||
|
||||
// Model indicators.
|
||||
$data->indicators = [];
|
||||
foreach ($this->model->get_indicators() as $indicator) {
|
||||
$indicatorid = $indicator->get_id();
|
||||
$data->indicators[] = $indicatorid;
|
||||
$requiredclasses[] = $indicatorid;
|
||||
}
|
||||
|
||||
if ($processor = $this->model->get_model_obj()->predictionsprocessor) {
|
||||
$data->processor = $processor;
|
||||
}
|
||||
// Add information for versioning.
|
||||
$data->dependencies = [];
|
||||
foreach ($requiredclasses as $fullclassname) {
|
||||
$component = $this->get_class_component($fullclassname);
|
||||
$data->dependencies[$component] = $versions[$component];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Packages the configuration of a model into a .json file.
|
||||
*
|
||||
* @param \stdClass $data Model config data
|
||||
* @param string $downloadfilename The file name.
|
||||
* @return string Path to the file with the model configuration.
|
||||
*/
|
||||
public function export_to_file(\stdClass $data, string $downloadfilename) : string {
|
||||
|
||||
$modelconfig = json_encode($data);
|
||||
|
||||
$dir = make_temp_directory('analyticsexport');
|
||||
$filepath = $dir . DIRECTORY_SEPARATOR . $downloadfilename;
|
||||
if (!file_put_contents($filepath, $modelconfig)) {
|
||||
print_error('errornoexportconfig', 'tool_analytics');
|
||||
}
|
||||
|
||||
return $filepath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the provided json string.
|
||||
*
|
||||
* @param string $json A json string.
|
||||
* @return string|null Error string or null if all good.
|
||||
*/
|
||||
public function check_json_data(string $json) : ?string {
|
||||
|
||||
if (!$modeldata = json_decode($json)) {
|
||||
return get_string('errorimport', 'tool_analytics');
|
||||
}
|
||||
|
||||
if (empty($modeldata->target) || empty($modeldata->timesplitting) || empty($modeldata->indicators)) {
|
||||
return get_string('errorimport', 'tool_analytics');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the provided model configuration can be deployed in this site.
|
||||
*
|
||||
* @param \stdClass $importmodel
|
||||
* @param bool $ignoreversionmismatches
|
||||
* @return string|null Error string or null if all good.
|
||||
*/
|
||||
public function check_dependencies(\stdClass $importmodel, bool $ignoreversionmismatches) : ?string {
|
||||
|
||||
$siteversions = \core_component::get_all_versions();
|
||||
|
||||
// Possible issues.
|
||||
$missingcomponents = [];
|
||||
$versionmismatches = [];
|
||||
$missingclasses = [];
|
||||
|
||||
// We first check that this site has the required dependencies and the required versions.
|
||||
foreach ($importmodel->dependencies as $component => $importversion) {
|
||||
|
||||
if (empty($siteversions[$component])) {
|
||||
|
||||
if ($component === 'core') {
|
||||
$component = 'Moodle';
|
||||
}
|
||||
$missingcomponents[$component] = $component . ' (' . $importversion . ')';
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($siteversions[$component] == $importversion) {
|
||||
// All good here.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$ignoreversionmismatches) {
|
||||
if ($component === 'core') {
|
||||
$component = 'Moodle';
|
||||
}
|
||||
$versionmismatches[$component] = $component . ' (' . $importversion . ')';
|
||||
}
|
||||
}
|
||||
|
||||
// Checking that the each of the components is available.
|
||||
if (!$target = manager::get_target($importmodel->target)) {
|
||||
$missingclasses[] = $importmodel->target;
|
||||
}
|
||||
|
||||
if (!$timesplitting = manager::get_time_splitting($importmodel->timesplitting)) {
|
||||
$missingclasses[] = $importmodel->timesplitting;
|
||||
}
|
||||
|
||||
// Indicators.
|
||||
$indicators = [];
|
||||
foreach ($importmodel->indicators as $indicatorclass) {
|
||||
if (!$indicator = manager::get_indicator($indicatorclass)) {
|
||||
$missingclasses[] = $indicatorclass;
|
||||
}
|
||||
}
|
||||
|
||||
// ML backend.
|
||||
if (!empty($importmodel->processor)) {
|
||||
if (!$processor = \core_analytics\manager::get_predictions_processor($importmodel->processor, false)) {
|
||||
$missingclasses[] = $indicatorclass;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($missingcomponents)) {
|
||||
return get_string('errorimportmissingcomponents', 'tool_analytics', join(', ', $missingcomponents));
|
||||
}
|
||||
|
||||
if (!empty($versionmismatches)) {
|
||||
return get_string('errorimportversionmismatches', 'tool_analytics', implode(', ', $versionmismatches));
|
||||
}
|
||||
|
||||
if (!empty($missingclasses)) {
|
||||
$a = (object)[
|
||||
'missingclasses' => implode(', ', $missingclasses),
|
||||
'dependencyversions' => implode(', ', $dependencyversions)
|
||||
];
|
||||
return get_string('errorimportmissingclasses', 'tool_analytics', $a);
|
||||
}
|
||||
|
||||
// No issues found.
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the component the class belongs to.
|
||||
*
|
||||
* Note that this method does not work for global space classes.
|
||||
*
|
||||
* @param string $fullclassname Qualified name including the namespace.
|
||||
* @return string|null Frankenstyle component
|
||||
*/
|
||||
public static function get_class_component(string $fullclassname) : ?string {
|
||||
|
||||
// Strip out leading backslash.
|
||||
$fullclassname = ltrim($fullclassname, '\\');
|
||||
|
||||
$nextbackslash = strpos($fullclassname, '\\');
|
||||
if ($nextbackslash === false) {
|
||||
// Global space.
|
||||
return 'core';
|
||||
}
|
||||
$component = substr($fullclassname, 0, $nextbackslash);
|
||||
|
||||
// All core subsystems use core's version.php.
|
||||
if (strpos($component, 'core_') === 0) {
|
||||
$component = 'core';
|
||||
}
|
||||
|
||||
return $component;
|
||||
}
|
||||
}
|
@ -28,6 +28,7 @@ require_once(__DIR__ . '/fixtures/test_indicator_max.php');
|
||||
require_once(__DIR__ . '/fixtures/test_indicator_min.php');
|
||||
require_once(__DIR__ . '/fixtures/test_indicator_fullname.php');
|
||||
require_once(__DIR__ . '/fixtures/test_target_shortname.php');
|
||||
require_once(__DIR__ . '/fixtures/test_static_target_shortname.php');
|
||||
require_once(__DIR__ . '/fixtures/test_target_course_level_shortname.php');
|
||||
require_once(__DIR__ . '/fixtures/test_analyser.php');
|
||||
|
||||
@ -318,33 +319,99 @@ class analytics_model_testcase extends advanced_testcase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Test export_as_json() API.
|
||||
* Test model_config::get_class_component.
|
||||
*/
|
||||
public function test_export_as_json() {
|
||||
global $CFG;
|
||||
public function test_model_config_get_class_component() {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
$this->model->enable('\core\analytics\time_splitting\quarters');
|
||||
$obj = json_decode($this->model->export_as_json());
|
||||
$this->assertSame($CFG->version, $obj->moodleversion);
|
||||
$this->assertSame($this->modelobj->target, $obj->target);
|
||||
$this->assertSame(json_decode($this->modelobj->indicators), $obj->indicators);
|
||||
$this->assertSame($this->modelobj->timesplitting, $obj->timesplitting);
|
||||
$this->assertEquals('core',
|
||||
\core_analytics\model_config::get_class_component('\\core\\analytics\\indicator\\read_actions'));
|
||||
$this->assertEquals('core',
|
||||
\core_analytics\model_config::get_class_component('core\\analytics\\indicator\\read_actions'));
|
||||
$this->assertEquals('core',
|
||||
\core_analytics\model_config::get_class_component('\\core_course\\analytics\\indicator\\completion_enabled'));
|
||||
$this->assertEquals('mod_forum',
|
||||
\core_analytics\model_config::get_class_component('\\mod_forum\\analytics\\indicator\\cognitive_depth'));
|
||||
|
||||
$this->assertEquals('core', \core_analytics\model_config::get_class_component('\\core_class'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test export_from_json() API.
|
||||
*/
|
||||
public function test_create_from_json() {
|
||||
global $CFG;
|
||||
public function test_create_from_import() {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
$this->model->enable('\core\analytics\time_splitting\quarters');
|
||||
$json = $this->model->export_as_json();
|
||||
$obj = \core_analytics\model::create_from_json(json_decode($json))->get_model_obj();
|
||||
$this->assertSame($this->modelobj->target, $obj->target);
|
||||
$this->assertSame($this->modelobj->indicators, $obj->indicators);
|
||||
$this->assertSame($this->modelobj->timesplitting, $obj->timesplitting);
|
||||
$this->model->enable('\\core\\analytics\\time_splitting\\quarters');
|
||||
|
||||
$this->modelobj = $this->model->get_model_obj();
|
||||
|
||||
$modelconfig = new \core_analytics\model_config($this->model);
|
||||
$modeldata = $modelconfig->export();
|
||||
$importedmodel = \core_analytics\model::create_from_import($modeldata)->get_model_obj();
|
||||
|
||||
$this->assertSame($this->modelobj->target, $importedmodel->target);
|
||||
$this->assertSame($this->modelobj->indicators, $importedmodel->indicators);
|
||||
$this->assertSame($this->modelobj->timesplitting, $importedmodel->timesplitting);
|
||||
$this->assertEmpty($importedmodel->predictionsprocessor);
|
||||
|
||||
$this->model->update(true, false, false, '\\mlbackend_php\\processor');
|
||||
|
||||
$this->modelobj = $this->model->get_model_obj();
|
||||
|
||||
$modelconfig = new \core_analytics\model_config($this->model);
|
||||
$modeldata = $modelconfig->export();
|
||||
$importedmodel = \core_analytics\model::create_from_import($modeldata)->get_model_obj();
|
||||
|
||||
$this->assertSame($this->modelobj->target, $importedmodel->target);
|
||||
$this->assertSame($this->modelobj->indicators, $importedmodel->indicators);
|
||||
$this->assertSame($this->modelobj->timesplitting, $importedmodel->timesplitting);
|
||||
$this->assertSame($this->modelobj->predictionsprocessor, $importedmodel->predictionsprocessor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test can export configuration
|
||||
*/
|
||||
public function test_can_export_configuration() {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
// No time splitting method.
|
||||
$this->assertFalse($this->model->can_export_configuration());
|
||||
|
||||
$this->model->enable('\\core\\analytics\\time_splitting\\quarters');
|
||||
$this->assertTrue($this->model->can_export_configuration());
|
||||
|
||||
$this->model->update(true, [], false);
|
||||
$this->assertFalse($this->model->can_export_configuration());
|
||||
|
||||
$statictarget = new test_static_target_shortname();
|
||||
$indicators['test_indicator_max'] = \core_analytics\manager::get_indicator('test_indicator_max');
|
||||
$model = \core_analytics\model::create($statictarget, $indicators, '\\core\\analytics\\time_splitting\\quarters');
|
||||
$this->assertFalse($model->can_export_configuration());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test export_config
|
||||
*/
|
||||
public function test_export_config() {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
$this->model->enable('\\core\\analytics\\time_splitting\\quarters');
|
||||
|
||||
$modelconfig = new \core_analytics\model_config($this->model);
|
||||
$modeldata = $modelconfig->export();
|
||||
|
||||
$this->assertArrayHasKey('core', $modeldata->dependencies);
|
||||
$this->assertInternalType('float', $modeldata->dependencies['core']);
|
||||
$this->assertNotEmpty($modeldata->target);
|
||||
$this->assertNotEmpty($modeldata->timesplitting);
|
||||
$this->assertCount(3, $modeldata->indicators);
|
||||
|
||||
$indicators['test_indicator_max'] = \core_analytics\manager::get_indicator('test_indicator_max');
|
||||
$this->model->update(true, $indicators, false);
|
||||
$modeldata = $modelconfig->export();
|
||||
|
||||
$this->assertCount(1, $modeldata->indicators);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1141,6 +1141,17 @@ $cache = '.var_export($cache, true).';
|
||||
* @return string sha1 hash
|
||||
*/
|
||||
public static function get_all_versions_hash() {
|
||||
return sha1(serialize(self::get_all_versions()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns hash of all versions including core and all plugins.
|
||||
*
|
||||
* This is relatively slow and not fully cached, use with care!
|
||||
*
|
||||
* @return array as (string)plugintype_pluginname => (int)version
|
||||
*/
|
||||
public static function get_all_versions() : array {
|
||||
global $CFG;
|
||||
|
||||
self::init();
|
||||
@ -1174,7 +1185,7 @@ $cache = '.var_export($cache, true).';
|
||||
}
|
||||
}
|
||||
|
||||
return sha1(serialize($versions));
|
||||
return $versions;
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user