mirror of
https://github.com/moodle/moodle.git
synced 2025-01-17 21:49:15 +01:00
Merge branch 'MDL-66004_master' of git://github.com/dmonllao/moodle
This commit is contained in:
commit
20502fa30c
@ -175,6 +175,13 @@ if ($hassiteconfig) {
|
||||
$plugin->load_settings($ADMIN, 'antivirussettings', $hassiteconfig);
|
||||
}
|
||||
|
||||
// Machine learning backend plugins.
|
||||
$ADMIN->add('modules', new admin_category('mlbackendsettings', new lang_string('mlbackendsettings', 'admin')));
|
||||
$plugins = core_plugin_manager::instance()->get_plugins_of_type('mlbackend');
|
||||
foreach ($plugins as $plugin) {
|
||||
$plugin->load_settings($ADMIN, 'mlbackendsettings', $hassiteconfig);
|
||||
}
|
||||
|
||||
/// License types
|
||||
$ADMIN->add('modules', new admin_category('licensesettings', new lang_string('licenses')));
|
||||
$temp = new admin_settingpage('managelicenses', new lang_string('managelicenses', 'admin'));
|
||||
|
@ -48,7 +48,7 @@ class manager {
|
||||
/**
|
||||
* @var \core_analytics\predictor[]
|
||||
*/
|
||||
protected static $predictionprocessors = null;
|
||||
protected static $predictionprocessors = [];
|
||||
|
||||
/**
|
||||
* @var \core_analytics\local\target\base[]
|
||||
@ -213,6 +213,14 @@ class manager {
|
||||
return $predictionprocessors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the cached prediction processors.
|
||||
* @return null
|
||||
*/
|
||||
public static function reset_prediction_processors() {
|
||||
self::$predictionprocessors = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the provided predictions processor.
|
||||
*
|
||||
|
@ -539,7 +539,7 @@ class model {
|
||||
debugging('Prediction processor ' . $predictorname . ' is not ready to be used. Model ' .
|
||||
$this->model->id . ' could not be deleted.');
|
||||
} else {
|
||||
$predictor->delete_output_dir($this->get_output_dir(array(), true));
|
||||
$predictor->delete_output_dir($this->get_output_dir(array(), true), $this->get_unique_id());
|
||||
}
|
||||
|
||||
$DB->delete_records('analytics_models', array('id' => $this->model->id));
|
||||
|
@ -70,8 +70,9 @@ interface predictor {
|
||||
* can only be named 'execution', 'evaluation' or 'testing'.
|
||||
*
|
||||
* @param string $modeloutputdir The model directory id (parent of all model versions subdirectories).
|
||||
* @param string $uniqueid
|
||||
* @return null
|
||||
*/
|
||||
public function delete_output_dir($modeloutputdir);
|
||||
public function delete_output_dir($modeloutputdir, $uniqueid);
|
||||
|
||||
}
|
||||
|
@ -17,6 +17,14 @@
|
||||
/**
|
||||
* Unit tests for evaluation, training and prediction.
|
||||
*
|
||||
* NOTE: in order to execute this test using a separate server for the
|
||||
* python ML backend you need to define these variables in your config.php file:
|
||||
*
|
||||
* define('TEST_MLBACKEND_PYTHON_HOST', '127.0.0.1');
|
||||
* define('TEST_MLBACKEND_PYTHON_PORT', 5000);
|
||||
* define('TEST_MLBACKEND_PYTHON_USERNAME', 'default');
|
||||
* define('TEST_MLBACKEND_PYTHON_PASSWORD', 'sshhhh');
|
||||
*
|
||||
* @package core_analytics
|
||||
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
@ -109,12 +117,18 @@ class core_analytics_prediction_testcase extends advanced_testcase {
|
||||
* @param int $predictedrangeindex
|
||||
* @param int $nranges
|
||||
* @param string $predictionsprocessorclass
|
||||
* @param array $forcedconfig
|
||||
* @return void
|
||||
*/
|
||||
public function test_ml_training_and_prediction($timesplittingid, $predictedrangeindex, $nranges, $predictionsprocessorclass) {
|
||||
public function test_ml_training_and_prediction($timesplittingid, $predictedrangeindex, $nranges, $predictionsprocessorclass,
|
||||
$forcedconfig) {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
$this->set_forced_config($forcedconfig);
|
||||
$predictionsprocessor = $this->is_predictions_processor_ready($predictionsprocessorclass);
|
||||
|
||||
$this->setAdminuser();
|
||||
set_config('enabled_stores', 'logstore_standard', 'tool_log');
|
||||
|
||||
@ -122,13 +136,8 @@ class core_analytics_prediction_testcase extends advanced_testcase {
|
||||
$ncourses = 10;
|
||||
$this->generate_courses($ncourses);
|
||||
|
||||
// We repeat the test for all prediction processors.
|
||||
$predictionsprocessor = \core_analytics\manager::get_predictions_processor($predictionsprocessorclass, false);
|
||||
if ($predictionsprocessor->is_ready() !== true) {
|
||||
$this->markTestSkipped('Skipping ' . $predictionsprocessorclass . ' as the predictor is not ready.');
|
||||
}
|
||||
|
||||
$model = $this->add_perfect_model();
|
||||
|
||||
$model->update(true, false, $timesplittingid, get_class($predictionsprocessor));
|
||||
|
||||
// No samples trained yet.
|
||||
@ -250,6 +259,17 @@ class core_analytics_prediction_testcase extends advanced_testcase {
|
||||
$this->assertCount(2, $fs->get_directory_files(\context_system::instance()->id, 'analytics',
|
||||
\core_analytics\dataset_manager::UNLABELLED_FILEAREA, $model->get_id(), '/analysable/', true, false));
|
||||
|
||||
// Confirm that the files associated to the model are deleted on clear and on delete. The ML backend deletion
|
||||
// processes will be triggered by these actions and any exception there would result in a failed test.
|
||||
$model->clear();
|
||||
$this->assertEquals(0, $DB->count_records('analytics_used_files',
|
||||
array('modelid' => $model->get_id(), 'action' => 'trained')));
|
||||
$this->assertCount(0, $fs->get_directory_files(\context_system::instance()->id, 'analytics',
|
||||
\core_analytics\dataset_manager::LABELLED_FILEAREA, $model->get_id(), '/analysable/', true, false));
|
||||
$this->assertCount(0, $fs->get_directory_files(\context_system::instance()->id, 'analytics',
|
||||
\core_analytics\dataset_manager::UNLABELLED_FILEAREA, $model->get_id(), '/analysable/', true, false));
|
||||
$model->delete();
|
||||
|
||||
set_config('enabled_stores', '', 'tool_log');
|
||||
get_log_manager(true);
|
||||
}
|
||||
@ -273,11 +293,15 @@ class core_analytics_prediction_testcase extends advanced_testcase {
|
||||
* test_ml_export_import
|
||||
*
|
||||
* @param string $predictionsprocessorclass The class name
|
||||
* @param array $forcedconfig
|
||||
* @dataProvider provider_ml_processors
|
||||
*/
|
||||
public function test_ml_export_import($predictionsprocessorclass) {
|
||||
|
||||
public function test_ml_export_import($predictionsprocessorclass, $forcedconfig) {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
$this->set_forced_config($forcedconfig);
|
||||
$predictionsprocessor = $this->is_predictions_processor_ready($predictionsprocessorclass);
|
||||
|
||||
$this->setAdminuser();
|
||||
set_config('enabled_stores', 'logstore_standard', 'tool_log');
|
||||
|
||||
@ -285,13 +309,8 @@ class core_analytics_prediction_testcase extends advanced_testcase {
|
||||
$ncourses = 10;
|
||||
$this->generate_courses($ncourses);
|
||||
|
||||
// We repeat the test for all prediction processors.
|
||||
$predictionsprocessor = \core_analytics\manager::get_predictions_processor($predictionsprocessorclass, false);
|
||||
if ($predictionsprocessor->is_ready() !== true) {
|
||||
$this->markTestSkipped('Skipping ' . $predictionsprocessorclass . ' as the predictor is not ready.');
|
||||
}
|
||||
|
||||
$model = $this->add_perfect_model();
|
||||
|
||||
$model->update(true, false, '\core\analytics\time_splitting\quarters', get_class($predictionsprocessor));
|
||||
|
||||
$model->train();
|
||||
@ -355,15 +374,14 @@ class core_analytics_prediction_testcase extends advanced_testcase {
|
||||
* @param int $nsamples
|
||||
* @param int $classes
|
||||
* @param string $predictionsprocessorclass
|
||||
* @param array $forcedconfig
|
||||
* @return void
|
||||
*/
|
||||
public function test_ml_classifiers_return($success, $nsamples, $classes, $predictionsprocessorclass) {
|
||||
public function test_ml_classifiers_return($success, $nsamples, $classes, $predictionsprocessorclass, $forcedconfig) {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$predictionsprocessor = \core_analytics\manager::get_predictions_processor($predictionsprocessorclass, false);
|
||||
if ($predictionsprocessor->is_ready() !== true) {
|
||||
$this->markTestSkipped('Skipping ' . $predictionsprocessorclass . ' as the predictor is not ready.');
|
||||
}
|
||||
$this->set_forced_config($forcedconfig);
|
||||
$predictionsprocessor = $this->is_predictions_processor_ready($predictionsprocessorclass);
|
||||
|
||||
if ($nsamples % count($classes) != 0) {
|
||||
throw new \coding_exception('The number of samples should be divisible by the number of classes');
|
||||
@ -396,7 +414,7 @@ class core_analytics_prediction_testcase extends advanced_testcase {
|
||||
|
||||
// Training should work correctly if at least 1 sample of each class is included.
|
||||
$dir = make_request_directory();
|
||||
$result = $predictionsprocessor->train_classification('whatever', $dataset, $dir);
|
||||
$result = $predictionsprocessor->train_classification('whatever' . microtime(), $dataset, $dir);
|
||||
|
||||
switch ($success) {
|
||||
case 'yes':
|
||||
@ -441,16 +459,19 @@ class core_analytics_prediction_testcase extends advanced_testcase {
|
||||
* @dataProvider provider_test_multi_classifier
|
||||
* @param string $timesplittingid
|
||||
* @param string $predictionsprocessorclass
|
||||
* @param array|null $forcedconfig
|
||||
* @throws coding_exception
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public function test_ml_multi_classifier($timesplittingid, $predictionsprocessorclass) {
|
||||
public function test_ml_multi_classifier($timesplittingid, $predictionsprocessorclass, $forcedconfig) {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest(true);
|
||||
$this->setAdminuser();
|
||||
set_config('enabled_stores', 'logstore_standard', 'tool_log');
|
||||
|
||||
$this->set_forced_config($forcedconfig);
|
||||
|
||||
$predictionsprocessor = \core_analytics\manager::get_predictions_processor($predictionsprocessorclass, false);
|
||||
if ($predictionsprocessor->is_ready() !== true) {
|
||||
$this->markTestSkipped('Skipping ' . $predictionsprocessorclass . ' as the predictor is not ready.');
|
||||
@ -483,6 +504,9 @@ class core_analytics_prediction_testcase extends advanced_testcase {
|
||||
// The range index is not important here, both ranges prediction will be the same.
|
||||
$this->assertEquals($correct[$sampleid], $predictiondata->prediction);
|
||||
}
|
||||
|
||||
set_config('enabled_stores', '', 'tool_log');
|
||||
get_log_manager(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -508,10 +532,16 @@ class core_analytics_prediction_testcase extends advanced_testcase {
|
||||
* @param int $ncourses
|
||||
* @param array $expected
|
||||
* @param string $predictionsprocessorclass
|
||||
* @param array $forcedconfig
|
||||
* @return void
|
||||
*/
|
||||
public function test_ml_evaluation_configuration($modelquality, $ncourses, $expected, $predictionsprocessorclass) {
|
||||
public function test_ml_evaluation_configuration($modelquality, $ncourses, $expected, $predictionsprocessorclass,
|
||||
$forcedconfig) {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
$this->set_forced_config($forcedconfig);
|
||||
$predictionsprocessor = $this->is_predictions_processor_ready($predictionsprocessorclass);
|
||||
|
||||
$this->setAdminuser();
|
||||
set_config('enabled_stores', 'logstore_standard', 'tool_log');
|
||||
|
||||
@ -530,12 +560,6 @@ class core_analytics_prediction_testcase extends advanced_testcase {
|
||||
// Generate training data.
|
||||
$this->generate_courses($ncourses);
|
||||
|
||||
// We repeat the test for all prediction processors.
|
||||
$predictionsprocessor = \core_analytics\manager::get_predictions_processor($predictionsprocessorclass, false);
|
||||
if ($predictionsprocessor->is_ready() !== true) {
|
||||
$this->markTestSkipped('Skipping ' . $predictionsprocessorclass . ' as the predictor is not ready.');
|
||||
}
|
||||
|
||||
$model->update(false, false, false, get_class($predictionsprocessor));
|
||||
$results = $model->evaluate();
|
||||
|
||||
@ -563,10 +587,15 @@ class core_analytics_prediction_testcase extends advanced_testcase {
|
||||
* @coversNothing
|
||||
* @dataProvider provider_ml_processors
|
||||
* @param string $predictionsprocessorclass
|
||||
* @param array $forcedconfig
|
||||
* @return null
|
||||
*/
|
||||
public function test_ml_evaluation_trained_model($predictionsprocessorclass) {
|
||||
public function test_ml_evaluation_trained_model($predictionsprocessorclass, $forcedconfig) {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
$this->set_forced_config($forcedconfig);
|
||||
$predictionsprocessor = $this->is_predictions_processor_ready($predictionsprocessorclass);
|
||||
|
||||
$this->setAdminuser();
|
||||
set_config('enabled_stores', 'logstore_standard', 'tool_log');
|
||||
|
||||
@ -575,12 +604,6 @@ class core_analytics_prediction_testcase extends advanced_testcase {
|
||||
// Generate training data.
|
||||
$this->generate_courses(50);
|
||||
|
||||
// We repeat the test for all prediction processors.
|
||||
$predictionsprocessor = \core_analytics\manager::get_predictions_processor($predictionsprocessorclass, false);
|
||||
if ($predictionsprocessor->is_ready() !== true) {
|
||||
$this->markTestSkipped('Skipping ' . $predictionsprocessorclass . ' as the predictor is not ready.');
|
||||
}
|
||||
|
||||
$model->update(true, false, '\\core\\analytics\\time_splitting\\quarters', get_class($predictionsprocessor));
|
||||
$model->train();
|
||||
|
||||
@ -824,6 +847,41 @@ class core_analytics_prediction_testcase extends advanced_testcase {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces some configuration values.
|
||||
*
|
||||
* @param array $forcedconfig
|
||||
*/
|
||||
protected function set_forced_config($forcedconfig) {
|
||||
\core_analytics\manager::reset_prediction_processors();
|
||||
|
||||
if (empty($forcedconfig)) {
|
||||
return;
|
||||
}
|
||||
foreach ($forcedconfig as $pluginname => $pluginconfig) {
|
||||
foreach ($pluginconfig as $name => $value) {
|
||||
set_config($name, $value, $pluginname);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the provided processor ready using the current configuration in the site?
|
||||
*
|
||||
* @param string $predictionsprocessorclass
|
||||
* @return \core_analytics\predictor
|
||||
*/
|
||||
protected function is_predictions_processor_ready(string $predictionsprocessorclass) {
|
||||
// We repeat the test for all prediction processors.
|
||||
$predictionsprocessor = \core_analytics\manager::get_predictions_processor($predictionsprocessorclass, false);
|
||||
$ready = $predictionsprocessor->is_ready();
|
||||
if ($ready !== true) {
|
||||
$this->markTestSkipped('Skipping ' . $predictionsprocessorclass . ' as the predictor is not ready: ' . $ready);
|
||||
}
|
||||
|
||||
return $predictionsprocessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* add_prediction_processors
|
||||
*
|
||||
@ -834,12 +892,29 @@ class core_analytics_prediction_testcase extends advanced_testcase {
|
||||
|
||||
$return = array();
|
||||
|
||||
// We need to test all system prediction processors.
|
||||
if (defined('TEST_MLBACKEND_PYTHON_HOST') && defined('TEST_MLBACKEND_PYTHON_PORT')
|
||||
&& defined('TEST_MLBACKEND_PYTHON_USERNAME') && defined('TEST_MLBACKEND_PYTHON_USERNAME')) {
|
||||
$testpythonserver = true;
|
||||
}
|
||||
|
||||
// We need to test all prediction processors in the system.
|
||||
$predictionprocessors = \core_analytics\manager::get_all_prediction_processors();
|
||||
foreach ($predictionprocessors as $classfullname => $unused) {
|
||||
foreach ($predictionprocessors as $classfullname => $predictionsprocessor) {
|
||||
foreach ($cases as $key => $case) {
|
||||
$newkey = $key . '-' . $classfullname;
|
||||
$return[$newkey] = $case + array('predictionsprocessorclass' => $classfullname);
|
||||
|
||||
if (!$predictionsprocessor instanceof \mlbackend_python\processor || empty($testpythonserver)) {
|
||||
$extraparams = ['predictionsprocessor' => $classfullname, 'forcedconfig' => null];
|
||||
$return[$key . '-' . $classfullname] = $case + $extraparams;
|
||||
} else {
|
||||
|
||||
// We want the configuration to be forced during the test as things like importing models create new
|
||||
// instances of ML backend processors during the process.
|
||||
$forcedconfig = ['mlbackend_python' => ['useserver' => true, 'host' => TEST_MLBACKEND_PYTHON_HOST,
|
||||
'port' => TEST_MLBACKEND_PYTHON_PORT, 'secure' => false, 'username' => TEST_MLBACKEND_PYTHON_USERNAME,
|
||||
'password' => TEST_MLBACKEND_PYTHON_PASSWORD]];
|
||||
$casekey = $key . '-' . $classfullname . '-server';
|
||||
$return[$casekey] = $case + ['predictionsprocessor' => $classfullname, 'forcedconfig' => $forcedconfig];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,7 @@ information provided here is intended especially for developers.
|
||||
analysable (e.g. upcoming activities due) have been updated to "Useful" flag.
|
||||
* Predictions flagged as "Not useful" in models whose targets use analysers that provide multiple samples
|
||||
per analysable (e.g. students at risk or no teaching) have been updated to "Incorrectly flagged".
|
||||
* \core_analytics\predictor::delete_output_dir has a new 2nd parameter, $uniquemodelid.
|
||||
|
||||
=== 3.7 ===
|
||||
|
||||
|
@ -790,6 +790,7 @@ $string['minpasswordlower'] = 'Lowercase letters';
|
||||
$string['minpasswordnonalphanum'] = 'Non-alphanumeric characters';
|
||||
$string['minpasswordupper'] = 'Uppercase letters';
|
||||
$string['misc'] = 'Miscellaneous';
|
||||
$string['mlbackendsettings'] = 'Machine learning backend settings';
|
||||
$string['mnetrestore_extusers'] = '<strong>Note:</strong> This backup file contains remote Moodle Network user accounts which will be restored as part of the process.';
|
||||
$string['mnetrestore_extusers_admin'] = '<strong>Note:</strong> This backup file seems to come from a different Moodle installation and contains remote Moodle Network user accounts. The restore process will try to match the Moodle Network hosts for all created users. Those not matching will be automatically switched to internal authentication (instead of mnet one). The restore log will inform you about that.';
|
||||
$string['mnetrestore_extusers_mismatch'] = '<strong>Note:</strong> This backup file apparently originates from a different Moodle installation and contains remote Moodle Network user accounts that may fail to restore. This operation is unsupported. If you are certain that it was created on this Moodle installation, or you can ensure that all the needed Moodle Network Hosts are configured, you may want to still try the restore.';
|
||||
|
@ -50,6 +50,39 @@ class mlbackend extends base {
|
||||
* @return null|string node name or null if plugin does not create settings node (default)
|
||||
*/
|
||||
public function get_settings_section_name() {
|
||||
return 'mlbackendsetting' . $this->name;
|
||||
return 'mlbackendsettings' . $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the global settings for a particular availability plugin (if there are any)
|
||||
*
|
||||
* @param \part_of_admin_tree $adminroot
|
||||
* @param string $parentnodename
|
||||
* @param bool $hassiteconfig
|
||||
* @return void
|
||||
*/
|
||||
public function load_settings(\part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
|
||||
global $CFG, $USER, $DB, $OUTPUT, $PAGE; // In case settings.php wants to refer to them.
|
||||
$ADMIN = $adminroot; // May be used in settings.php.
|
||||
$plugininfo = $this; // Also can be used inside settings.php.
|
||||
|
||||
if (!$this->is_installed_and_upgraded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$hassiteconfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
$section = $this->get_settings_section_name();
|
||||
|
||||
$settings = null;
|
||||
if (file_exists($this->full_path('settings.php'))) {
|
||||
$settings = new \admin_settingpage($section, $this->displayname, 'moodle/site:config', $this->is_enabled() === false);
|
||||
include($this->full_path('settings.php')); // This may also set $settings to null.
|
||||
}
|
||||
if ($settings) {
|
||||
$ADMIN->add($parentnodename, $settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -88,9 +88,10 @@ class processor implements \core_analytics\classifier, \core_analytics\regressor
|
||||
* Delete the output directory.
|
||||
*
|
||||
* @param string $modeloutputdir
|
||||
* @param string $uniqueid
|
||||
* @return null
|
||||
*/
|
||||
public function delete_output_dir($modeloutputdir) {
|
||||
public function delete_output_dir($modeloutputdir, $uniqueid) {
|
||||
remove_dir($modeloutputdir);
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,13 @@ class processor implements \core_analytics\classifier, \core_analytics\regresso
|
||||
/**
|
||||
* The required version of the python package that performs all calculations.
|
||||
*/
|
||||
const REQUIRED_PIP_PACKAGE_VERSION = '2.1.0';
|
||||
const REQUIRED_PIP_PACKAGE_VERSION = '2.2.0';
|
||||
|
||||
/**
|
||||
* The python package is installed in a server.
|
||||
* @var bool
|
||||
*/
|
||||
protected $useserver;
|
||||
|
||||
/**
|
||||
* The path to the Python bin.
|
||||
@ -47,15 +53,58 @@ class processor implements \core_analytics\classifier, \core_analytics\regresso
|
||||
*/
|
||||
protected $pathtopython;
|
||||
|
||||
/**
|
||||
* Remote server host
|
||||
* @var string
|
||||
*/
|
||||
protected $host;
|
||||
|
||||
/**
|
||||
* Remote server port
|
||||
* @var int
|
||||
*/
|
||||
protected $port;
|
||||
|
||||
/**
|
||||
* Whether to use http or https.
|
||||
* @var bool
|
||||
*/
|
||||
protected $secure;
|
||||
|
||||
/**
|
||||
* Server username.
|
||||
* @var string
|
||||
*/
|
||||
protected $username;
|
||||
|
||||
/**
|
||||
* Server password for $this->username.
|
||||
* @var string
|
||||
*/
|
||||
protected $password;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
*/
|
||||
public function __construct() {
|
||||
global $CFG;
|
||||
|
||||
// Set the python location if there is a value.
|
||||
if (!empty($CFG->pathtopython)) {
|
||||
$this->pathtopython = $CFG->pathtopython;
|
||||
$config = get_config('mlbackend_python');
|
||||
|
||||
$this->useserver = !empty($config->useserver);
|
||||
|
||||
if (!$this->useserver) {
|
||||
// Set the python location if there is a value.
|
||||
if (!empty($CFG->pathtopython)) {
|
||||
$this->pathtopython = $CFG->pathtopython;
|
||||
}
|
||||
} else {
|
||||
$this->host = $config->host ?? '';
|
||||
$this->port = $config->port ?? '';
|
||||
$this->secure = $config->secure ?? false;
|
||||
$this->username = $config->username ?? '';
|
||||
$this->password = $config->password ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,6 +114,20 @@ class processor implements \core_analytics\classifier, \core_analytics\regresso
|
||||
* @return bool|string Returns true on success, a string detailing the error otherwise
|
||||
*/
|
||||
public function is_ready() {
|
||||
|
||||
if (!$this->useserver) {
|
||||
return $this->is_webserver_ready();
|
||||
} else {
|
||||
return $this->is_python_server_ready();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the python package is available in the web server executing this script.
|
||||
*
|
||||
* @return bool|string Returns true on success, a string detailing the error otherwise
|
||||
*/
|
||||
protected function is_webserver_ready() {
|
||||
if (empty($this->pathtopython)) {
|
||||
$settingurl = new \moodle_url('/admin/settings.php', array('section' => 'systempaths'));
|
||||
return get_string('pythonpathnotdefined', 'mlbackend_python', $settingurl->out());
|
||||
@ -78,52 +141,72 @@ class processor implements \core_analytics\classifier, \core_analytics\regresso
|
||||
// Execute it sending the standard error to $output.
|
||||
$result = exec($cmd . ' 2>&1', $output, $exitcode);
|
||||
|
||||
$vercheck = self::check_pip_package_version($result);
|
||||
|
||||
if ($vercheck === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($exitcode != 0) {
|
||||
return get_string('pythonpackagenotinstalled', 'mlbackend_python', $cmd);
|
||||
}
|
||||
|
||||
if ($result) {
|
||||
$a = [
|
||||
'installed' => $result,
|
||||
'required' => self::REQUIRED_PIP_PACKAGE_VERSION,
|
||||
];
|
||||
$vercheck = self::check_pip_package_version($result);
|
||||
return $this->version_check_return($result, $vercheck);
|
||||
}
|
||||
|
||||
if ($vercheck < 0) {
|
||||
return get_string('packageinstalledshouldbe', 'mlbackend_python', $a);
|
||||
/**
|
||||
* Checks if the server can be accessed.
|
||||
*
|
||||
* @return bool|string True or an error string.
|
||||
*/
|
||||
protected function is_python_server_ready() {
|
||||
|
||||
} else if ($vercheck > 0) {
|
||||
return get_string('packageinstalledtoohigh', 'mlbackend_python', $a);
|
||||
}
|
||||
if (empty($this->host) || empty($this->port) || empty($this->username) || empty($this->password)) {
|
||||
return get_string('errornoconfigdata', 'mlbackend_python');
|
||||
}
|
||||
|
||||
return get_string('pythonpackagenotinstalled', 'mlbackend_python', $cmd);
|
||||
// Connection is allowed to use 'localhost' and other potentially blocked hosts/ports.
|
||||
$curl = new \curl(['ignoresecurity' => true]);
|
||||
$responsebody = $curl->get($this->get_server_url('version')->out(false));
|
||||
if ($curl->info['http_code'] !== 200) {
|
||||
return get_string('errorserver', 'mlbackend_python', $this->server_error_str($curl->info['http_code'], $responsebody));
|
||||
}
|
||||
|
||||
$vercheck = self::check_pip_package_version($responsebody);
|
||||
return $this->version_check_return($responsebody, $vercheck);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the model version output directory.
|
||||
*
|
||||
* @throws \moodle_exception
|
||||
* @param string $uniqueid
|
||||
* @param string $modelversionoutputdir
|
||||
* @return null
|
||||
*/
|
||||
public function clear_model($uniqueid, $modelversionoutputdir) {
|
||||
remove_dir($modelversionoutputdir);
|
||||
if (!$this->useserver) {
|
||||
remove_dir($modelversionoutputdir);
|
||||
} else {
|
||||
// Use the server.
|
||||
|
||||
$url = $this->get_server_url('deletemodel');
|
||||
list($responsebody, $httpcode) = $this->server_request($url, 'post', ['uniqueid' => $uniqueid]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the model output directory.
|
||||
*
|
||||
* @throws \moodle_exception
|
||||
* @param string $modeloutputdir
|
||||
* @param string $uniqueid
|
||||
* @return null
|
||||
*/
|
||||
public function delete_output_dir($modeloutputdir) {
|
||||
remove_dir($modeloutputdir);
|
||||
public function delete_output_dir($modeloutputdir, $uniqueid) {
|
||||
if (!$this->useserver) {
|
||||
remove_dir($modeloutputdir);
|
||||
} else {
|
||||
|
||||
$url = $this->get_server_url('deletemodel');
|
||||
list($responsebody, $httpcode) = $this->server_request($url, 'post', ['uniqueid' => $uniqueid]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -136,44 +219,28 @@ class processor implements \core_analytics\classifier, \core_analytics\regresso
|
||||
*/
|
||||
public function train_classification($uniqueid, \stored_file $dataset, $outputdir) {
|
||||
|
||||
// Obtain the physical route to the file.
|
||||
$datasetpath = $this->get_file_path($dataset);
|
||||
if (!$this->useserver) {
|
||||
// Use the local file system.
|
||||
|
||||
$cmd = "{$this->pathtopython} -m moodlemlbackend.training " .
|
||||
escapeshellarg($uniqueid) . ' ' .
|
||||
escapeshellarg($outputdir) . ' ' .
|
||||
escapeshellarg($datasetpath);
|
||||
list($result, $exitcode) = $this->exec_command('training', [$uniqueid, $outputdir,
|
||||
$this->get_file_path($dataset)], 'errornopredictresults');
|
||||
|
||||
if (!PHPUNIT_TEST && CLI_SCRIPT) {
|
||||
debugging($cmd, DEBUG_DEVELOPER);
|
||||
}
|
||||
} else {
|
||||
// Use the server.
|
||||
|
||||
$output = null;
|
||||
$exitcode = null;
|
||||
$result = exec($cmd, $output, $exitcode);
|
||||
$requestparams = ['uniqueid' => $uniqueid, 'dirhash' => $this->hash_dir($outputdir),
|
||||
'dataset' => $dataset];
|
||||
|
||||
if (!$result) {
|
||||
throw new \moodle_exception('errornopredictresults', 'analytics');
|
||||
$url = $this->get_server_url('training');
|
||||
list($result, $httpcode) = $this->server_request($url, 'post', $requestparams);
|
||||
}
|
||||
|
||||
if (!$resultobj = json_decode($result)) {
|
||||
throw new \moodle_exception('errorpredictwrongformat', 'analytics', '', json_last_error_msg());
|
||||
}
|
||||
|
||||
if ($exitcode != 0) {
|
||||
if (!empty($resultobj->errors)) {
|
||||
$errors = $resultobj->errors;
|
||||
if (is_array($errors)) {
|
||||
$errors = implode(', ', $errors);
|
||||
}
|
||||
} else if (!empty($resultobj->info)) {
|
||||
// Show info if no errors are returned.
|
||||
$errors = $resultobj->info;
|
||||
if (is_array($errors)) {
|
||||
$errors = implode(', ', $errors);
|
||||
}
|
||||
}
|
||||
$resultobj->info = array(get_string('errorpredictionsprocessor', 'analytics', $errors));
|
||||
if ($resultobj->status != 0) {
|
||||
$resultobj = $this->format_error_info($resultobj);
|
||||
}
|
||||
|
||||
return $resultobj;
|
||||
@ -189,44 +256,29 @@ class processor implements \core_analytics\classifier, \core_analytics\regresso
|
||||
*/
|
||||
public function classify($uniqueid, \stored_file $dataset, $outputdir) {
|
||||
|
||||
// Obtain the physical route to the file.
|
||||
$datasetpath = $this->get_file_path($dataset);
|
||||
if (!$this->useserver) {
|
||||
// Use the local file system.
|
||||
|
||||
$cmd = "{$this->pathtopython} -m moodlemlbackend.prediction " .
|
||||
escapeshellarg($uniqueid) . ' ' .
|
||||
escapeshellarg($outputdir) . ' ' .
|
||||
escapeshellarg($datasetpath);
|
||||
list($result, $exitcode) = $this->exec_command('prediction', [$uniqueid, $outputdir,
|
||||
$this->get_file_path($dataset)], 'errornopredictresults');
|
||||
|
||||
if (!PHPUNIT_TEST && CLI_SCRIPT) {
|
||||
debugging($cmd, DEBUG_DEVELOPER);
|
||||
}
|
||||
} else {
|
||||
// Use the server.
|
||||
|
||||
$output = null;
|
||||
$exitcode = null;
|
||||
$result = exec($cmd, $output, $exitcode);
|
||||
$requestparams = ['uniqueid' => $uniqueid, 'dirhash' => $this->hash_dir($outputdir),
|
||||
'dataset' => $dataset];
|
||||
|
||||
if (!$result) {
|
||||
throw new \moodle_exception('errornopredictresults', 'analytics');
|
||||
$url = $this->get_server_url('prediction');
|
||||
list($result, $httpcode) = $this->server_request($url, 'post', $requestparams);
|
||||
}
|
||||
|
||||
if (!$resultobj = json_decode($result)) {
|
||||
throw new \moodle_exception('errorpredictwrongformat', 'analytics', '', json_last_error_msg());
|
||||
}
|
||||
|
||||
if ($exitcode != 0) {
|
||||
if (!empty($resultobj->errors)) {
|
||||
$errors = $resultobj->errors;
|
||||
if (is_array($errors)) {
|
||||
$errors = implode(', ', $errors);
|
||||
}
|
||||
} else if (!empty($resultobj->info)) {
|
||||
// Show info if no errors are returned.
|
||||
$errors = $resultobj->info;
|
||||
if (is_array($errors)) {
|
||||
$errors = implode(', ', $errors);
|
||||
}
|
||||
}
|
||||
$resultobj->info = array(get_string('errorpredictionsprocessor', 'analytics', $errors));
|
||||
|
||||
if ($resultobj->status != 0) {
|
||||
$resultobj = $this->format_error_info($resultobj);
|
||||
}
|
||||
|
||||
return $resultobj;
|
||||
@ -245,37 +297,72 @@ class processor implements \core_analytics\classifier, \core_analytics\regresso
|
||||
*/
|
||||
public function evaluate_classification($uniqueid, $maxdeviation, $niterations, \stored_file $dataset,
|
||||
$outputdir, $trainedmodeldir) {
|
||||
global $CFG;
|
||||
|
||||
// Obtain the physical route to the file.
|
||||
$datasetpath = $this->get_file_path($dataset);
|
||||
if (!$this->useserver) {
|
||||
// Use the local file system.
|
||||
|
||||
$cmd = "{$this->pathtopython} -m moodlemlbackend.evaluation " .
|
||||
escapeshellarg($uniqueid) . ' ' .
|
||||
escapeshellarg($outputdir) . ' ' .
|
||||
escapeshellarg($datasetpath) . ' ' .
|
||||
escapeshellarg(\core_analytics\model::MIN_SCORE) . ' ' .
|
||||
escapeshellarg($maxdeviation) . ' ' .
|
||||
escapeshellarg($niterations);
|
||||
$datasetpath = $this->get_file_path($dataset);
|
||||
|
||||
if ($trainedmodeldir) {
|
||||
$cmd .= ' ' . escapeshellarg($trainedmodeldir);
|
||||
$params = [$uniqueid, $outputdir, $datasetpath, \core_analytics\model::MIN_SCORE,
|
||||
$maxdeviation, $niterations];
|
||||
|
||||
if ($trainedmodeldir) {
|
||||
$params[] = $trainedmodeldir;
|
||||
}
|
||||
|
||||
list($result, $exitcode) = $this->exec_command('evaluation', $params, 'errornopredictresults');
|
||||
if (!$resultobj = json_decode($result)) {
|
||||
throw new \moodle_exception('errorpredictwrongformat', 'analytics', '', json_last_error_msg());
|
||||
}
|
||||
|
||||
} else {
|
||||
// Use the server.
|
||||
|
||||
$requestparams = ['uniqueid' => $uniqueid, 'minscore' => \core_analytics\model::MIN_SCORE,
|
||||
'maxdeviation' => $maxdeviation, 'niterations' => $niterations,
|
||||
'dirhash' => $this->hash_dir($outputdir), 'dataset' => $dataset];
|
||||
|
||||
if ($trainedmodeldir) {
|
||||
$requestparams['trainedmodeldirhash'] = $this->hash_dir($trainedmodeldir);
|
||||
}
|
||||
|
||||
$url = $this->get_server_url('evaluation');
|
||||
list($result, $httpcode) = $this->server_request($url, 'post', $requestparams);
|
||||
|
||||
if (!$resultobj = json_decode($result)) {
|
||||
throw new \moodle_exception('errorpredictwrongformat', 'analytics', '', json_last_error_msg());
|
||||
}
|
||||
|
||||
// We need an extra request to get the resources generated during the evaluation process.
|
||||
|
||||
// Directory to temporarly store the evaluation log zip returned by the server.
|
||||
$evaluationtmpdir = make_request_directory('mlbackend_python_evaluationlog');
|
||||
$evaluationzippath = $evaluationtmpdir . DIRECTORY_SEPARATOR . 'evaluationlog.zip';
|
||||
|
||||
$requestparams = ['uniqueid' => $uniqueid, 'dirhash' => $this->hash_dir($outputdir),
|
||||
'runid' => $resultobj->runid];
|
||||
|
||||
$url = $this->get_server_url('evaluationlog');
|
||||
list($result, $httpcode) = $this->server_request($url, 'download_one', $requestparams,
|
||||
['filepath' => $evaluationzippath]);
|
||||
|
||||
$rundir = $outputdir . DIRECTORY_SEPARATOR . 'logs' . DIRECTORY_SEPARATOR . $resultobj->runid;
|
||||
if (!mkdir($rundir, $CFG->directorypermissions, true)) {
|
||||
throw new \moodle_exception('errorexportmodelresult', 'analytics');
|
||||
}
|
||||
|
||||
$zip = new \zip_packer();
|
||||
$success = $zip->extract_to_pathname($evaluationzippath, $rundir, null, null, true);
|
||||
if (!$success) {
|
||||
$a = 'The evaluation files can not be exported to ' . $rundir;
|
||||
throw new \moodle_exception('errorpredictionsprocessor', 'analytics', '', $a);
|
||||
}
|
||||
|
||||
$resultobj->dir = $rundir;
|
||||
}
|
||||
|
||||
if (!PHPUNIT_TEST && CLI_SCRIPT) {
|
||||
debugging($cmd, DEBUG_DEVELOPER);
|
||||
}
|
||||
|
||||
$output = null;
|
||||
$exitcode = null;
|
||||
$result = exec($cmd, $output, $exitcode);
|
||||
|
||||
if (!$result) {
|
||||
throw new \moodle_exception('errornopredictresults', 'analytics');
|
||||
}
|
||||
|
||||
if (!$resultobj = json_decode($result)) {
|
||||
throw new \moodle_exception('errorpredictwrongformat', 'analytics', '', json_last_error_msg());
|
||||
}
|
||||
$resultobj = $this->add_extra_result_info($resultobj);
|
||||
|
||||
return $resultobj;
|
||||
}
|
||||
@ -290,29 +377,36 @@ class processor implements \core_analytics\classifier, \core_analytics\regresso
|
||||
*/
|
||||
public function export(string $uniqueid, string $modeldir) : string {
|
||||
|
||||
// We include an exporttmpdir as we want to be sure that the file is not deleted after the
|
||||
// python process finishes.
|
||||
$exporttmpdir = make_request_directory('mlbackend_python_export');
|
||||
|
||||
$cmd = "{$this->pathtopython} -m moodlemlbackend.export " .
|
||||
escapeshellarg($uniqueid) . ' ' .
|
||||
escapeshellarg($modeldir) . ' ' .
|
||||
escapeshellarg($exporttmpdir);
|
||||
if (!$this->useserver) {
|
||||
// Use the local file system.
|
||||
|
||||
if (!PHPUNIT_TEST && CLI_SCRIPT) {
|
||||
debugging($cmd, DEBUG_DEVELOPER);
|
||||
}
|
||||
// We include an exporttmpdir as we want to be sure that the file is not deleted after the
|
||||
// python process finishes.
|
||||
list($exportdir, $exitcode) = $this->exec_command('export', [$uniqueid, $modeldir, $exporttmpdir],
|
||||
'errorexportmodelresult');
|
||||
|
||||
$output = null;
|
||||
$exitcode = null;
|
||||
$exportdir = exec($cmd, $output, $exitcode);
|
||||
if ($exitcode != 0) {
|
||||
throw new \moodle_exception('errorexportmodelresult', 'analytics');
|
||||
}
|
||||
|
||||
if ($exitcode != 0) {
|
||||
throw new \moodle_exception('errorexportmodelresult', 'analytics');
|
||||
}
|
||||
} else {
|
||||
// Use the server.
|
||||
|
||||
if (!$exportdir) {
|
||||
throw new \moodle_exception('errorexportmodelresult', 'analytics');
|
||||
$requestparams = ['uniqueid' => $uniqueid, 'dirhash' => $this->hash_dir($modeldir)];
|
||||
|
||||
$exportzippath = $exporttmpdir . DIRECTORY_SEPARATOR . 'export.zip';
|
||||
$url = $this->get_server_url('export');
|
||||
list($result, $httpcode) = $this->server_request($url, 'download_one', $requestparams,
|
||||
['filepath' => $exportzippath]);
|
||||
|
||||
$exportdir = make_request_directory();
|
||||
$zip = new \zip_packer();
|
||||
$success = $zip->extract_to_pathname($exportzippath, $exportdir, null, null, true);
|
||||
if (!$success) {
|
||||
throw new \moodle_exception('errorexportmodelresult', 'analytics');
|
||||
}
|
||||
}
|
||||
|
||||
return $exportdir;
|
||||
@ -328,28 +422,33 @@ class processor implements \core_analytics\classifier, \core_analytics\regresso
|
||||
*/
|
||||
public function import(string $uniqueid, string $modeldir, string $importdir) : bool {
|
||||
|
||||
$cmd = "{$this->pathtopython} -m moodlemlbackend.import " .
|
||||
escapeshellarg($uniqueid) . ' ' .
|
||||
escapeshellarg($modeldir) . ' ' .
|
||||
escapeshellarg($importdir);
|
||||
if (!$this->useserver) {
|
||||
// Use the local file system.
|
||||
|
||||
if (!PHPUNIT_TEST && CLI_SCRIPT) {
|
||||
debugging($cmd, DEBUG_DEVELOPER);
|
||||
list($result, $exitcode) = $this->exec_command('import', [$uniqueid, $modeldir, $importdir],
|
||||
'errorimportmodelresult');
|
||||
|
||||
if ($exitcode != 0) {
|
||||
throw new \moodle_exception('errorimportmodelresult', 'analytics');
|
||||
}
|
||||
|
||||
} else {
|
||||
// Use the server.
|
||||
|
||||
// Zip the $importdir to send a single file.
|
||||
$importzipfile = $this->zip_dir($importdir);
|
||||
if (!$importzipfile) {
|
||||
// There was an error zipping the directory.
|
||||
throw new \moodle_exception('errorimportmodelresult', 'analytics');
|
||||
}
|
||||
|
||||
$requestparams = ['uniqueid' => $uniqueid, 'dirhash' => $this->hash_dir($modeldir),
|
||||
'importzip' => curl_file_create($importzipfile, null, 'import.zip')];
|
||||
$url = $this->get_server_url('import');
|
||||
list($result, $httpcode) = $this->server_request($url, 'post', $requestparams);
|
||||
}
|
||||
|
||||
$output = null;
|
||||
$exitcode = null;
|
||||
$success = exec($cmd, $output, $exitcode);
|
||||
|
||||
if ($exitcode != 0) {
|
||||
throw new \moodle_exception('errorimportmodelresult', 'analytics');
|
||||
}
|
||||
|
||||
if (!$success) {
|
||||
throw new \moodle_exception('errorimportmodelresult', 'analytics');
|
||||
}
|
||||
|
||||
return $success;
|
||||
return (bool)$result;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -441,4 +540,209 @@ class processor implements \core_analytics\classifier, \core_analytics\regresso
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the specified module.
|
||||
*
|
||||
* @param string $modulename
|
||||
* @param array $params
|
||||
* @param string $errorlangstr
|
||||
* @return array [0] is the result body and [1] the exit code.
|
||||
*/
|
||||
protected function exec_command(string $modulename, array $params, string $errorlangstr) {
|
||||
|
||||
$cmd = $this->pathtopython . ' -m moodlemlbackend.' . $modulename . ' ';
|
||||
foreach ($params as $param) {
|
||||
$cmd .= escapeshellarg($param) . ' ';
|
||||
}
|
||||
|
||||
if (!PHPUNIT_TEST && CLI_SCRIPT) {
|
||||
debugging($cmd, DEBUG_DEVELOPER);
|
||||
}
|
||||
|
||||
$output = null;
|
||||
$exitcode = null;
|
||||
$result = exec($cmd, $output, $exitcode);
|
||||
|
||||
if (!$result) {
|
||||
throw new \moodle_exception($errorlangstr, 'analytics');
|
||||
}
|
||||
|
||||
return [$result, $exitcode];
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the errors and info in a single info string.
|
||||
*
|
||||
* @param \stdClass $resultobj
|
||||
* @return \stdClass
|
||||
*/
|
||||
private function format_error_info(\stdClass $resultobj) {
|
||||
if (!empty($resultobj->errors)) {
|
||||
$errors = $resultobj->errors;
|
||||
if (is_array($errors)) {
|
||||
$errors = implode(', ', $errors);
|
||||
}
|
||||
} else if (!empty($resultobj->info)) {
|
||||
// Show info if no errors are returned.
|
||||
$errors = $resultobj->info;
|
||||
if (is_array($errors)) {
|
||||
$errors = implode(', ', $errors);
|
||||
}
|
||||
}
|
||||
$resultobj->info = array(get_string('errorpredictionsprocessor', 'analytics', $errors));
|
||||
|
||||
return $resultobj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url to the python ML server.
|
||||
*
|
||||
* @param string|null $path
|
||||
* @return \moodle_url
|
||||
*/
|
||||
private function get_server_url(?string $path = null) {
|
||||
$protocol = !empty($this->secure) ? 'https' : 'http';
|
||||
$url = $protocol . '://' . rtrim($this->host, '/');
|
||||
if (!empty($this->port)) {
|
||||
$url .= ':' . $this->port;
|
||||
}
|
||||
|
||||
if ($path) {
|
||||
$url .= '/' . $path;
|
||||
}
|
||||
|
||||
return new \moodle_url($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the python ML server.
|
||||
*
|
||||
* @param \moodle_url $url The requested url in the python ML server
|
||||
* @param string $method The curl method to use
|
||||
* @param array $requestparams Curl request params
|
||||
* @param array|null $options Curl request options
|
||||
* @return array [0] for the response body and [1] for the http code
|
||||
*/
|
||||
protected function server_request($url, string $method, array $requestparams, ?array $options = null) {
|
||||
|
||||
if ($method !== 'post' && $method !== 'get' && $method !== 'download_one') {
|
||||
throw new \coding_exception('Incorrect request method provided. Only "get", "post" and "download_one"
|
||||
actions are available.');
|
||||
}
|
||||
|
||||
// Connection is allowed to use 'localhost' and other potentially blocked hosts/ports.
|
||||
$curl = new \curl(['ignoresecurity' => true]);
|
||||
|
||||
$authorization = $this->username . ':' . $this->password;
|
||||
$curl->setHeader('Authorization: Basic ' . base64_encode($authorization));
|
||||
|
||||
$responsebody = $curl->{$method}($url, $requestparams, $options);
|
||||
|
||||
if ($curl->info['http_code'] !== 200) {
|
||||
throw new \moodle_exception('errorserver', 'mlbackend_python', '',
|
||||
$this->server_error_str($curl->info['http_code'], $responsebody));
|
||||
}
|
||||
|
||||
return [$responsebody, $curl->info['http_code']];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds extra information to results info.
|
||||
*
|
||||
* @param \stdClass $resultobj
|
||||
* @return \stdClass
|
||||
*/
|
||||
protected function add_extra_result_info(\stdClass $resultobj): \stdClass {
|
||||
|
||||
if (!empty($resultobj->dir)) {
|
||||
$dir = $resultobj->dir . DIRECTORY_SEPARATOR . 'tensor';
|
||||
$resultobj->info[] = get_string('tensorboardinfo', 'mlbackend_python', $dir);
|
||||
}
|
||||
return $resultobj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the proper return value for the version checking.
|
||||
*
|
||||
* @param string $actual Actual moodlemlbackend version
|
||||
* @param int $vercheck Version checking result
|
||||
* @return true|string Returns true on success, a string detailing the error otherwise
|
||||
*/
|
||||
private function version_check_return($actual, $vercheck) {
|
||||
|
||||
if ($vercheck === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($actual) {
|
||||
$a = [
|
||||
'installed' => $actual,
|
||||
'required' => self::REQUIRED_PIP_PACKAGE_VERSION,
|
||||
];
|
||||
|
||||
if ($vercheck < 0) {
|
||||
return get_string('packageinstalledshouldbe', 'mlbackend_python', $a);
|
||||
|
||||
} else if ($vercheck > 0) {
|
||||
return get_string('packageinstalledtoohigh', 'mlbackend_python', $a);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->useserver) {
|
||||
$cmd = "{$this->pathtopython} -m moodlemlbackend.version";
|
||||
} else {
|
||||
// We can't not know which is the python bin in the python ML server, the most likely
|
||||
// value is 'python'.
|
||||
$cmd = "python -m moodlemlbackend.version";
|
||||
}
|
||||
return get_string('pythonpackagenotinstalled', 'mlbackend_python', $cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashes the provided dir as a string.
|
||||
*
|
||||
* @param string $dir Directory path
|
||||
* @return string Hash
|
||||
*/
|
||||
private function hash_dir(string $dir) {
|
||||
return md5($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zips the provided directory.
|
||||
*
|
||||
* @param string $dir Directory path
|
||||
* @return string The zip filename
|
||||
*/
|
||||
private function zip_dir(string $dir) {
|
||||
|
||||
$ziptmpdir = make_request_directory('mlbackend_python');
|
||||
$ziptmpfile = $ziptmpdir . DIRECTORY_SEPARATOR . 'mlbackend.zip';
|
||||
|
||||
$files = get_directory_list($dir);
|
||||
$zipfiles = [];
|
||||
foreach ($files as $file) {
|
||||
$fullpath = $dir . DIRECTORY_SEPARATOR . $file;
|
||||
// Use the relative path to the file as the path in the zip.
|
||||
$zipfiles[$file] = $fullpath;
|
||||
}
|
||||
|
||||
$zip = new \zip_packer();
|
||||
if (!$zip->archive_to_pathname($zipfiles, $ziptmpfile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $ziptmpfile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error string for httpcode !== 200
|
||||
*
|
||||
* @param int $httpstatuscode The HTTP status code
|
||||
* @param string $responsebody The body of the response
|
||||
*/
|
||||
private function server_error_str(int $httpstatuscode, string $responsebody): string {
|
||||
return 'HTTP status code ' . $httpstatuscode . ': ' . $responsebody;
|
||||
}
|
||||
}
|
||||
|
@ -22,9 +22,25 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
$string['errornoconfigdata'] = 'The server configuration is not complete.';
|
||||
$string['errorserver'] = 'Server error {$a}';
|
||||
$string['host'] = 'Host';
|
||||
$string['hostdesc'] = 'Host';
|
||||
$string['packageinstalledshouldbe'] = '"moodlemlbackend" python package should be updated. The required version is "{$a->required}" and the installed version is "{$a->installed}"';
|
||||
$string['packageinstalledtoohigh'] = '"moodlemlbackend" python package is not compatible with this Moodle version. The required version is "{$a->required}" or higher as long as it is API-compatible. The installed version "{$a->installed}" is too high.';
|
||||
$string['pluginname'] = 'Python machine learning backend';
|
||||
$string['port'] = 'Port';
|
||||
$string['portdesc'] = 'Port';
|
||||
$string['privacy:metadata'] = 'The Python machine learning backend plugin does not store any personal data.';
|
||||
$string['pythonpackagenotinstalled'] = '"moodlemlbackend" python package is not installed or there is a problem with it. Please execute "{$a}" from command line interface for more info';
|
||||
$string['pythonpathnotdefined'] = 'The path to your executable Python binary has not been defined. Please visit "{$a}" to set it.';
|
||||
$string['serversettingsinfo'] = 'Tick "Use a server" setting to show the server settings.';
|
||||
$string['username'] = 'Username';
|
||||
$string['usernamedesc'] = 'String of characters used as a username to communicate between your Moodle server and the python server';
|
||||
$string['password'] = 'Password';
|
||||
$string['passworddesc'] = 'String of characters used as a password to communicate between your Moodle server and the python server';
|
||||
$string['secure'] = 'Use HTTPS';
|
||||
$string['securedesc'] = 'Whether to use HTTP or HTTPS';
|
||||
$string['useserver'] = 'Use a server';
|
||||
$string['useserverdesc'] = 'The machine learning backend python package is not installed in the web server but in a different server.';
|
||||
$string['tensorboardinfo'] = 'Launch TensorBoard from command line by typing tensorboard --logdir=\'{$a}\' in your web server.';
|
54
lib/mlbackend/python/settings.php
Normal file
54
lib/mlbackend/python/settings.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Administration settings definitions for mlbackend_python.
|
||||
*
|
||||
* @package mlbackend_python
|
||||
* @copyright 2019 David Monllaó
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
if ($ADMIN->fulltree) {
|
||||
|
||||
$info = $OUTPUT->notification(get_string('serversettingsinfo', 'mlbackend_python'), 'info');
|
||||
$settings->add(new admin_setting_heading('mlbackend_python/serversettingsinfo', '', $info));
|
||||
|
||||
$settings->add(new admin_setting_configcheckbox('mlbackend_python/useserver', get_string('useserver', 'mlbackend_python'),
|
||||
get_string('useserverdesc', 'mlbackend_python'), 0));
|
||||
|
||||
$settings->add(new admin_setting_configtext('mlbackend_python/host', get_string('host', 'mlbackend_python'),
|
||||
get_string('host', 'mlbackend_python'), '', PARAM_HOST));
|
||||
$settings->hide_if('mlbackend_python/host', 'mlbackend_python/useserver', 'neq', '1');
|
||||
|
||||
$settings->add(new admin_setting_configtext('mlbackend_python/port', get_string('port', 'mlbackend_python'),
|
||||
get_string('port', 'mlbackend_python'), '', PARAM_INT));
|
||||
$settings->hide_if('mlbackend_python/port', 'mlbackend_python/useserver', 'neq', '1');
|
||||
|
||||
$settings->add(new admin_setting_configcheckbox('mlbackend_python/secure', get_string('secure', 'mlbackend_python'),
|
||||
get_string('securedesc', 'mlbackend_python'), 0));
|
||||
$settings->hide_if('mlbackend_python/secure', 'mlbackend_python/useserver', 'neq', '1');
|
||||
|
||||
$settings->add(new admin_setting_configtext('mlbackend_python/username', get_string('username', 'mlbackend_python'),
|
||||
get_string('usernamedesc', 'mlbackend_python'), 'default', PARAM_ALPHANUMEXT));
|
||||
$settings->hide_if('mlbackend_python/username', 'mlbackend_python/useserver', 'neq', '1');
|
||||
|
||||
$settings->add(new admin_setting_configtext('mlbackend_python/password', get_string('password', 'mlbackend_python'),
|
||||
get_string('passworddesc', 'mlbackend_python'), '', PARAM_ALPHANUMEXT));
|
||||
$settings->hide_if('mlbackend_python/password', 'mlbackend_python/useserver', 'neq', '1');
|
||||
}
|
@ -24,6 +24,6 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2019052000; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->version = 2019052001; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->requires = 2019051100; // Requires this Moodle version.
|
||||
$plugin->component = 'mlbackend_python'; // Full name of the plugin (used for diagnostics).
|
||||
|
Loading…
x
Reference in New Issue
Block a user