Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Model configuration manager.
  19   *
  20   * @package   core_analytics
  21   * @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace core_analytics;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  /**
  30   * Model configuration manager.
  31   *
  32   * @package   core_analytics
  33   * @copyright 2019 David Monllao {@link http://www.davidmonllao.com}
  34   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   */
  36  class model_config {
  37  
  38      /**
  39       * @var \core_analytics\model
  40       */
  41      private $model = null;
  42  
  43      /**
  44       * The name of the file where config is held.
  45       */
  46      const CONFIG_FILE_NAME = 'model-config.json';
  47  
  48      /**
  49       * Constructor.
  50       *
  51       * @param \core_analytics\model|null $model
  52       */
  53      public function __construct(?model $model = null) {
  54          $this->model = $model;
  55      }
  56  
  57      /**
  58       * Exports a model to a zip using the provided file name.
  59       *
  60       * @param string $zipfilename
  61       * @param bool $includeweights Include the model weights if available
  62       * @return string
  63       */
  64      public function export(string $zipfilename, bool $includeweights = true) : string {
  65  
  66          if (!$this->model) {
  67              throw new \coding_exception('No model object provided.');
  68          }
  69  
  70          if (!$this->model->can_export_configuration()) {
  71              throw new \moodle_exception('errornoexportconfigrequirements', 'analytics');
  72          }
  73  
  74          $zip = new \zip_packer();
  75          $zipfiles = [];
  76  
  77          // Model config in JSON.
  78          $modeldata = $this->export_model_data();
  79  
  80          $exporttmpdir = make_request_directory();
  81          $jsonfilepath = $exporttmpdir . DIRECTORY_SEPARATOR . 'model-config.json';
  82          if (!file_put_contents($jsonfilepath, json_encode($modeldata))) {
  83              throw new \moodle_exception('errornoexportconfig', 'analytics');
  84          }
  85          $zipfiles[self::CONFIG_FILE_NAME] = $jsonfilepath;
  86  
  87          // ML backend.
  88          if ($includeweights && $this->model->is_trained()) {
  89              $processor = $this->model->get_predictions_processor(true);
  90              $outputdir = $this->model->get_output_dir(array('execution'));
  91              $mlbackenddir = $processor->export($this->model->get_unique_id(), $outputdir);
  92              $mlbackendfiles = get_directory_list($mlbackenddir);
  93              foreach ($mlbackendfiles as $mlbackendfile) {
  94                  $fullpath = $mlbackenddir . DIRECTORY_SEPARATOR . $mlbackendfile;
  95                  // Place the ML backend files inside a mlbackend/ dir.
  96                  $zipfiles['mlbackend/' . $mlbackendfile] = $fullpath;
  97              }
  98          }
  99  
 100          $zipfilepath = $exporttmpdir . DIRECTORY_SEPARATOR . $zipfilename;
 101          $zip->archive_to_pathname($zipfiles, $zipfilepath);
 102  
 103          return $zipfilepath;
 104      }
 105  
 106      /**
 107       * Imports the provided model configuration into a new model.
 108       *
 109       * Note that this method assumes that self::check_dependencies has already been called.
 110       *
 111       * @param  string $zipfilepath Path to the zip file to import
 112       * @return \core_analytics\model
 113       */
 114      public function import(string $zipfilepath) : \core_analytics\model {
 115  
 116          list($modeldata, $mlbackenddir) = $this->extract_import_contents($zipfilepath);
 117  
 118          $target = \core_analytics\manager::get_target($modeldata->target);
 119          $indicators = [];
 120          foreach ($modeldata->indicators as $indicatorclass) {
 121              $indicator = \core_analytics\manager::get_indicator($indicatorclass);
 122              $indicators[$indicator->get_id()] = $indicator;
 123          }
 124          $model = \core_analytics\model::create($target, $indicators, $modeldata->timesplitting, $modeldata->processor);
 125  
 126          // Import them disabled.
 127          $model->update(false, false, false, false);
 128  
 129          if ($mlbackenddir) {
 130              $modeldir = $model->get_output_dir(['execution']);
 131              if (!$model->get_predictions_processor(true)->import($model->get_unique_id(), $modeldir, $mlbackenddir)) {
 132                  throw new \moodle_exception('errorimport', 'analytics');
 133              }
 134              $model->mark_as_trained();
 135          }
 136  
 137          return $model;
 138      }
 139  
 140      /**
 141       * Check that the provided model configuration can be deployed in this site.
 142       *
 143       * @param  \stdClass $modeldata
 144       * @param  bool $ignoreversionmismatches
 145       * @return string|null Error string or null if all good.
 146       */
 147      public function check_dependencies(\stdClass $modeldata, bool $ignoreversionmismatches) : ?string {
 148  
 149          $siteversions = \core_component::get_all_versions();
 150  
 151          // Possible issues.
 152          $missingcomponents = [];
 153          $versionmismatches = [];
 154          $missingclasses = [];
 155  
 156          // We first check that this site has the required dependencies and the required versions.
 157          foreach ($modeldata->dependencies as $component => $importversion) {
 158  
 159              if (empty($siteversions[$component])) {
 160  
 161                  if ($component === 'core') {
 162                      $component = 'Moodle';
 163                  }
 164                  $missingcomponents[$component] = $component . ' (' . $importversion . ')';
 165                  continue;
 166              }
 167  
 168              if ($siteversions[$component] == $importversion) {
 169                  // All good here.
 170                  continue;
 171              }
 172  
 173              if (!$ignoreversionmismatches) {
 174                  if ($component === 'core') {
 175                      $component = 'Moodle';
 176                  }
 177                  $versionmismatches[$component] = $component . ' (' . $importversion . ')';
 178              }
 179          }
 180  
 181          // Checking that each of the components is available.
 182          if (!$target = manager::get_target($modeldata->target)) {
 183              $missingclasses[] = $modeldata->target;
 184          }
 185  
 186          if (!$timesplitting = manager::get_time_splitting($modeldata->timesplitting)) {
 187              $missingclasses[] = $modeldata->timesplitting;
 188          }
 189  
 190          // Indicators.
 191          foreach ($modeldata->indicators as $indicatorclass) {
 192              if (!$indicator = manager::get_indicator($indicatorclass)) {
 193                  $missingclasses[] = $indicatorclass;
 194              }
 195          }
 196  
 197          // ML backend.
 198          if (!empty($modeldata->processor)) {
 199              if (!$processor = \core_analytics\manager::get_predictions_processor($modeldata->processor, false)) {
 200                  $missingclasses[] = $indicatorclass;
 201              }
 202          }
 203  
 204          if (!empty($missingcomponents)) {
 205              return get_string('errorimportmissingcomponents', 'analytics', join(', ', $missingcomponents));
 206          }
 207  
 208          if (!empty($versionmismatches)) {
 209              return get_string('errorimportversionmismatches', 'analytics', implode(', ', $versionmismatches));
 210          }
 211  
 212          if (!empty($missingclasses)) {
 213              $a = (object)[
 214                  'missingclasses' => implode(', ', $missingclasses),
 215              ];
 216              return get_string('errorimportmissingclasses', 'analytics', $a);
 217          }
 218  
 219          // No issues found.
 220          return null;
 221      }
 222  
 223      /**
 224       * Returns the component the class belongs to.
 225       *
 226       * Note that this method does not work for global space classes.
 227       *
 228       * @param  string $fullclassname Qualified name including the namespace.
 229       * @return string|null Frankenstyle component
 230       */
 231      public static function get_class_component(string $fullclassname) : ?string {
 232  
 233          // Strip out leading backslash.
 234          $fullclassname = ltrim($fullclassname, '\\');
 235  
 236          $nextbackslash = strpos($fullclassname, '\\');
 237          if ($nextbackslash === false) {
 238              // Global space.
 239              return 'core';
 240          }
 241          $component = substr($fullclassname, 0, $nextbackslash);
 242  
 243          // All core subsystems use core's version.php.
 244          if (strpos($component, 'core_') === 0) {
 245              $component = 'core';
 246          }
 247  
 248          return $component;
 249      }
 250  
 251      /**
 252       * Extracts the import zip contents.
 253       *
 254       * @param  string $zipfilepath Zip file path
 255       * @return array [0] => \stdClass, [1] => string
 256       */
 257      public function extract_import_contents(string $zipfilepath) : array {
 258  
 259          $importtempdir = make_request_directory();
 260  
 261          $zip = new \zip_packer();
 262          $filelist = $zip->extract_to_pathname($zipfilepath, $importtempdir);
 263  
 264          if (empty($filelist[self::CONFIG_FILE_NAME])) {
 265              // Missing required file.
 266              throw new \moodle_exception('errorimport', 'analytics');
 267          }
 268  
 269          $jsonmodeldata = file_get_contents($importtempdir . DIRECTORY_SEPARATOR . self::CONFIG_FILE_NAME);
 270  
 271          if (!$modeldata = json_decode($jsonmodeldata)) {
 272              throw new \moodle_exception('errorimport', 'analytics');
 273          }
 274  
 275          if (empty($modeldata->target) || empty($modeldata->timesplitting) || empty($modeldata->indicators)) {
 276              throw new \moodle_exception('errorimport', 'analytics');
 277          }
 278  
 279          $mlbackenddir = $importtempdir . DIRECTORY_SEPARATOR . 'mlbackend';
 280          if (!is_dir($mlbackenddir)) {
 281              $mlbackenddir = false;
 282          }
 283  
 284          return [$modeldata, $mlbackenddir];
 285      }
 286      /**
 287       * Exports the configuration of the model.
 288       * @return \stdClass
 289       */
 290      protected function export_model_data() : \stdClass {
 291  
 292          $versions = \core_component::get_all_versions();
 293  
 294          $data = new \stdClass();
 295  
 296          // Target.
 297          $data->target = $this->model->get_target()->get_id();
 298          $requiredclasses[] = $data->target;
 299  
 300          // Time splitting method.
 301          $data->timesplitting = $this->model->get_time_splitting()->get_id();
 302          $requiredclasses[] = $data->timesplitting;
 303  
 304          // Model indicators.
 305          $data->indicators = [];
 306          foreach ($this->model->get_indicators() as $indicator) {
 307              $indicatorid = $indicator->get_id();
 308              $data->indicators[] = $indicatorid;
 309              $requiredclasses[] = $indicatorid;
 310          }
 311  
 312          // Return the predictions processor this model is using, even if no predictions processor
 313          // was explicitly selected.
 314          $predictionsprocessor = $this->model->get_predictions_processor();
 315          $data->processor = '\\' . get_class($predictionsprocessor);
 316          $requiredclasses[] = $data->processor;
 317  
 318          // Add information for versioning.
 319          $data->dependencies = [];
 320          foreach ($requiredclasses as $fullclassname) {
 321              $component = $this->get_class_component($fullclassname);
 322              $data->dependencies[$component] = $versions[$component];
 323          }
 324  
 325          return $data;
 326      }
 327  }