MDL-60944 analytics: Add base support for import / export
authorAnkit Agarwal <ankit@moodle.com>
Tue, 5 Dec 2017 05:04:43 +0000 (10:34 +0530)
committerDavid Monllaó <davidm@moodle.com>
Mon, 25 Feb 2019 08:54:07 +0000 (09:54 +0100)
admin/tool/analytics/classes/import_model_form.php [new file with mode: 0644]
admin/tool/analytics/classes/output/models_list.php
admin/tool/analytics/importmodel.php [new file with mode: 0644]
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/analytics/model.php
admin/tool/analytics/settings.php
analytics/classes/model.php
analytics/tests/model_test.php

diff --git a/admin/tool/analytics/classes/import_model_form.php b/admin/tool/analytics/classes/import_model_form.php
new file mode 100644 (file)
index 0000000..ae0709e
--- /dev/null
@@ -0,0 +1,47 @@
+<?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 upload form.
+ *
+ * @package   tool_analytics
+ * @copyright 2017 onwards Ankit Agarwal
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_analytics;
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Model upload form.
+ *
+ * @package   tool_analytics
+ * @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 () {
+        $mform = $this->_form;
+
+        $mform->addElement('header', 'settingsheader', get_string('analyticsimportmodel', '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'));
+    }
+}
\ No newline at end of file
index 83e2755..a0785df 100644 (file)
@@ -229,13 +229,22 @@ class models_list implements \renderable, \templatable {
 
             // Export training data.
             if (!$model->is_static() && $model->is_trained()) {
-                $urlparams['action'] = 'export';
+                $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'));
                 $actionsmenu->add($icon);
             }
 
+            // Export model.
+            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',
+                    get_string('exportmodel', 'tool_analytics')), get_string('exportmodel', 'tool_analytics'));
+                $actionsmenu->add($icon);
+            }
+
             // Invalid analysables.
             $analyser = $model->get_analyser(['notimesplitting' => true]);
             if (!$analyser instanceof \core_analytics\local\analyser\sitewide) {
diff --git a/admin/tool/analytics/importmodel.php b/admin/tool/analytics/importmodel.php
new file mode 100644 (file)
index 0000000..18d2e31
--- /dev/null
@@ -0,0 +1,57 @@
+<?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/>.
+
+/**
+ * Import models tool frontend.
+ *
+ * @package tool_analytics
+ * @copyright 2017 onwards Ankit Agarwal
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../config.php');
+require_once($CFG->libdir . '/adminlib.php');
+
+admin_externalpage_setup('analyticsmodelimport', '', null, '', array('pagelayout' => 'report'));
+echo $OUTPUT->header();
+
+$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');
+        }
+    }
+    echo $OUTPUT->single_button(new moodle_url("$CFG->wwwroot/$CFG->admin/tool/analytics/index.php"),
+        get_string('continue'), 'get');
+} else {
+    $form->display();
+}
+
+echo $OUTPUT->footer();
\ No newline at end of file
index 1c3bba2..9844c58 100644 (file)
@@ -26,9 +26,11 @@ $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';
+$string['classdoesnotexist'] = 'Class {$a} does not exist';
 $string['clearpredictions'] = 'Clear predictions';
 $string['clearmodelpredictions'] = 'Are you sure you want to clear all "{$a}" predictions?';
 $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).';
@@ -42,6 +44,7 @@ $string['errorcantenablenotimesplitting'] = 'You need to select a time-splitting
 $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['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.';
@@ -51,6 +54,7 @@ $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['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';
@@ -67,6 +71,7 @@ $string['invalidanalysablestable'] = 'Invalid site analysable elements table';
 $string['invalidprediction'] = 'Invalid to get predictions';
 $string['invalidtraining'] = 'Invalid to train the model';
 $string['loginfo'] = 'Log extra info';
+$string['missingmoodleversion'] = 'Imported file does not define a moodle version number';
 $string['modelid'] = 'Model ID';
 $string['modelinvalidanalysables'] = 'Invalid analysable elements for "{$a}" model';
 $string['modelresults'] = '{$a} results';
@@ -92,6 +97,7 @@ $string['trainandpredictmodel'] = 'Training model and calculating predictions';
 $string['trainingprocessfinished'] = 'Training process finished';
 $string['trainingresults'] = 'Training results';
 $string['trainmodels'] = 'Train models';
+$string['versionnotsame'] = 'Imported file was from a different moodle version ({$a->importedversion}) than the current one ({$a->version})';
 $string['viewlog'] = 'Log';
 $string['weeksenddateautomaticallyset'] = 'End date automatically set based on start date and the number of sections';
 $string['weeksenddatedefault'] = 'End date automatically calculated from the course start date.';
index b70fc24..5e5f72a 100644 (file)
@@ -23,6 +23,7 @@
  */
 
 require_once(__DIR__ . '/../../../config.php');
+require_once($CFG->libdir . '/dataformatlib.php');
 
 $id = required_param('id', PARAM_INT);
 $action = required_param('action', PARAM_ALPHANUMEXT);
@@ -57,8 +58,11 @@ switch ($action) {
     case 'disable':
         $title = get_string('disable');
         break;
-    case 'export':
-        $title = get_string('export', 'tool_analytics');
+    case 'exportdata':
+        $title = get_string('exporttrainingdata', 'tool_analytics');
+        break;
+    case 'exportmodel':
+        $title = get_string('exportmodel', 'tool_analytics');
         break;
     case 'clear':
         $title = get_string('clearpredictions', 'tool_analytics');
@@ -203,7 +207,7 @@ switch ($action) {
         echo $renderer->render_table($modellogstable);
         break;
 
-    case 'export':
+    case 'exportdata':
 
         if ($model->is_static() || !$model->is_trained()) {
             throw new moodle_exception('errornoexport', 'tool_analytics');
@@ -219,6 +223,22 @@ switch ($action) {
         send_file($file, $filename, null, 0, false, true);
         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');
+        }
+        @header("Content-type: text/json; charset=UTF-8");
+        send_temp_file($tempfilename, $downloadfilename);
+        break;
+
     case 'clear':
         confirm_sesskey();
 
index aad459a..c70405e 100644 (file)
@@ -26,3 +26,5 @@ 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'));
index fa6e91e..97c6460 100644 (file)
@@ -385,6 +385,44 @@ class model {
         return $model;
     }
 
+    /**
+     * Creates a new model from json configuration.
+     *
+     * @param string $json json data.
+     * @return \core_analytics\model
+     */
+    public static function create_from_json($jsondata) {
+
+        \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");
+        }
+
+        // Target.
+        $target = $jsondata->target;
+        if (!class_exists($target)) {
+            throw new \moodle_exception('classdoesnotexist', 'tool_analytics', $target);
+        }
+        $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);
+            }
+            $indicators[] = \core_analytics\manager::get_indicator($indicator);
+        }
+
+        // Timesplitting.
+        $timesplitting = $jsondata->timesplitting;
+        if (!class_exists($timesplitting)) {
+            throw new \moodle_exception('classdoesnotexist', 'tool_analytics', $timesplitting);
+        }
+
+       return self::create($target, $indicators, $timesplitting);
+    }
+
     /**
      * Does this model exist?
      *
@@ -1379,6 +1417,33 @@ class model {
         return $data;
     }
 
+    /**
+     * Exports the model data as JSON.
+     *
+     * @return string JSON encoded data.
+     */
+    public function export_as_json() {
+        global $CFG;
+
+        $data = new \stdClass();
+        $data->target = $this->get_target()->get_id();
+
+
+        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');
+        }
+
+        $data->indicators = [];
+        foreach ($this->get_indicators() as $indicator) {
+            $data->indicators[] = $indicator->get_id();
+        }
+        $data->moodleversion = $CFG->version;
+        return json_encode($data);
+    }
+
     /**
      * Returns the model logs data.
      *
index 21009ba..a4e18d4 100644 (file)
@@ -317,6 +317,36 @@ class analytics_model_testcase extends advanced_testcase {
         $this->assertLessThanOrEqual(2, $DB->count_records('analytics_used_analysables', $params));
     }
 
+    /**
+     * Test export_as_json() API.
+     */
+    public function test_export_as_json() {
+        global $CFG;
+        $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);
+    }
+
+    /**
+     * Test export_from_json() API.
+     */
+    public function test_create_from_json() {
+        global $CFG;
+        $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);
+    }
+
     /**
      * Generates a model log record.
      */