Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
   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   * This file contains the class to import a competency framework.
  19   *
  20   * @package   tool_lpimportcsv
  21   * @copyright 2015 Damyon Wiese
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace tool_lpimportcsv;
  26  
  27  defined('MOODLE_INTERNAL') || die('Direct access to this script is forbidden.');
  28  
  29  use core_competency\api;
  30  use grade_scale;
  31  use stdClass;
  32  use context_system;
  33  use csv_import_reader;
  34  
  35  /**
  36   * This file contains the class to import a competency framework.
  37   *
  38   * @package   tool_lpimportcsv
  39   * @copyright 2015 Damyon Wiese
  40   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  41   */
  42  class framework_importer {
  43  
  44      /** @var string $error The errors message from reading the xml */
  45      protected $error = '';
  46  
  47      /** @var array $flat The flat competencies tree */
  48      protected $flat = array();
  49      /** @var array $framework The framework info */
  50      protected $framework = array();
  51      protected $mappings = array();
  52      protected $importid = 0;
  53      protected $importer = null;
  54      protected $foundheaders = array();
  55      protected $scalecache = array();
  56      /** @var bool $useprogressbar Control whether importing should use progress bars or not. */
  57      protected $useprogressbar = false;
  58      /** @var \core\progress\display_if_slow|null $progress The progress bar instance. */
  59      protected $progress = null;
  60  
  61      /**
  62       * Store an error message for display later
  63       * @param string $msg
  64       */
  65      public function fail($msg) {
  66          $this->error = $msg;
  67          return false;
  68      }
  69  
  70      /**
  71       * Get the CSV import id
  72       * @return string The import id.
  73       */
  74      public function get_importid() {
  75          return $this->importid;
  76      }
  77  
  78      /**
  79       * Get the list of headers required for import.
  80       * @return array The headers (lang strings)
  81       */
  82      public static function list_required_headers() {
  83          return array(
  84              get_string('parentidnumber', 'tool_lpimportcsv'),
  85              get_string('idnumber', 'tool_lpimportcsv'),
  86              get_string('shortname', 'tool_lpimportcsv'),
  87              get_string('description', 'tool_lpimportcsv'),
  88              get_string('descriptionformat', 'tool_lpimportcsv'),
  89              get_string('scalevalues', 'tool_lpimportcsv'),
  90              get_string('scaleconfiguration', 'tool_lpimportcsv'),
  91              get_string('ruletype', 'tool_lpimportcsv'),
  92              get_string('ruleoutcome', 'tool_lpimportcsv'),
  93              get_string('ruleconfig', 'tool_lpimportcsv'),
  94              get_string('relatedidnumbers', 'tool_lpimportcsv'),
  95              get_string('exportid', 'tool_lpimportcsv'),
  96              get_string('isframework', 'tool_lpimportcsv'),
  97              get_string('taxonomy', 'tool_lpimportcsv'),
  98          );
  99      }
 100  
 101      /**
 102       * Get the list of headers found in the import.
 103       * @return array The found headers (names from import)
 104       */
 105      public function list_found_headers() {
 106          return $this->foundheaders;
 107      }
 108  
 109      /**
 110       * Read the data from the mapping form.
 111       * @param array The mapping data.
 112       */
 113      protected function read_mapping_data($data) {
 114          if ($data) {
 115              return array(
 116                  'parentidnumber' => $data->header0,
 117                  'idnumber' => $data->header1,
 118                  'shortname' => $data->header2,
 119                  'description' => $data->header3,
 120                  'descriptionformat' => $data->header4,
 121                  'scalevalues' => $data->header5,
 122                  'scaleconfiguration' => $data->header6,
 123                  'ruletype' => $data->header7,
 124                  'ruleoutcome' => $data->header8,
 125                  'ruleconfig' => $data->header9,
 126                  'relatedidnumbers' => $data->header10,
 127                  'exportid' => $data->header11,
 128                  'isframework' => $data->header12,
 129                  'taxonomies' => $data->header13
 130              );
 131          } else {
 132              return array(
 133                  'parentidnumber' => 0,
 134                  'idnumber' => 1,
 135                  'shortname' => 2,
 136                  'description' => 3,
 137                  'descriptionformat' => 4,
 138                  'scalevalues' => 5,
 139                  'scaleconfiguration' => 6,
 140                  'ruletype' => 7,
 141                  'ruleoutcome' => 8,
 142                  'ruleconfig' => 9,
 143                  'relatedidnumbers' => 10,
 144                  'exportid' => 11,
 145                  'isframework' => 12,
 146                  'taxonomies' => 13
 147              );
 148          }
 149      }
 150  
 151      /**
 152       * Get the a column from the imported data.
 153       * @param array The imported raw row
 154       * @param index The column index we want
 155       * @return string The column data.
 156       */
 157      protected function get_column_data($row, $index) {
 158          if ($index < 0) {
 159              return '';
 160          }
 161          return isset($row[$index]) ? $row[$index] : '';
 162      }
 163  
 164      /**
 165       * Constructor - parses the raw text for sanity.
 166       * @param string $text The raw csv text.
 167       * @param string $encoding The encoding of the csv file.
 168       * @param string delimiter The specified delimiter for the file.
 169       * @param string importid The id of the csv import.
 170       * @param array mappingdata The mapping data from the import form.
 171       * @param bool $useprogressbar Whether progress bar should be displayed, to avoid html output on CLI.
 172       */
 173      public function __construct($text = null, $encoding = null, $delimiter = null, $importid = 0, $mappingdata = null,
 174              $useprogressbar = false) {
 175  
 176          global $CFG;
 177  
 178          // The format of our records is:
 179          // Parent ID number, ID number, Shortname, Description, Description format, Scale values, Scale configuration,
 180          // Rule type, Rule outcome, Rule config, Is framework, Taxonomy.
 181  
 182          // The idnumber is concatenated with the category names.
 183          require_once($CFG->libdir . '/csvlib.class.php');
 184  
 185          $type = 'competency_framework';
 186  
 187          if (!$importid) {
 188              if ($text === null) {
 189                  return;
 190              }
 191              $this->importid = csv_import_reader::get_new_iid($type);
 192  
 193              $this->importer = new csv_import_reader($this->importid, $type);
 194  
 195              if (!$this->importer->load_csv_content($text, $encoding, $delimiter)) {
 196                  $this->fail(get_string('invalidimportfile', 'tool_lpimportcsv'));
 197                  $this->importer->cleanup();
 198                  return;
 199              }
 200  
 201          } else {
 202              $this->importid = $importid;
 203  
 204              $this->importer = new csv_import_reader($this->importid, $type);
 205          }
 206  
 207          if (!$this->importer->init()) {
 208              $this->fail(get_string('invalidimportfile', 'tool_lpimportcsv'));
 209              $this->importer->cleanup();
 210              return;
 211          }
 212  
 213          $this->foundheaders = $this->importer->get_columns();
 214          $this->useprogressbar = $useprogressbar;
 215          $domainid = 1;
 216  
 217          $flat = array();
 218          $framework = null;
 219  
 220          while ($row = $this->importer->next()) {
 221              $mapping = $this->read_mapping_data($mappingdata);
 222  
 223              $parentidnumber = $this->get_column_data($row, $mapping['parentidnumber']);
 224              $idnumber = $this->get_column_data($row, $mapping['idnumber']);
 225              $shortname = $this->get_column_data($row, $mapping['shortname']);
 226              $description = $this->get_column_data($row, $mapping['description']);
 227              $descriptionformat = $this->get_column_data($row, $mapping['descriptionformat']);
 228              $scalevalues = $this->get_column_data($row, $mapping['scalevalues']);
 229              $scaleconfiguration = $this->get_column_data($row, $mapping['scaleconfiguration']);
 230              $ruletype = $this->get_column_data($row, $mapping['ruletype']);
 231              $ruleoutcome = $this->get_column_data($row, $mapping['ruleoutcome']);
 232              $ruleconfig = $this->get_column_data($row, $mapping['ruleconfig']);
 233              $relatedidnumbers = $this->get_column_data($row, $mapping['relatedidnumbers']);
 234              $exportid = $this->get_column_data($row, $mapping['exportid']);
 235              $isframework = $this->get_column_data($row, $mapping['isframework']);
 236              $taxonomies = $this->get_column_data($row, $mapping['taxonomies']);
 237  
 238              if ($isframework) {
 239                  $framework = new stdClass();
 240                  $framework->idnumber = shorten_text(clean_param($idnumber, PARAM_TEXT), 100);
 241                  $framework->shortname = shorten_text(clean_param($shortname, PARAM_TEXT), 100);
 242                  $framework->description = clean_param($description, PARAM_RAW);
 243                  $framework->descriptionformat = clean_param($descriptionformat, PARAM_INT);
 244                  $framework->scalevalues = $scalevalues;
 245                  $framework->scaleconfiguration = $scaleconfiguration;
 246                  $framework->taxonomies = $taxonomies;
 247                  $framework->children = array();
 248              } else {
 249                  $competency = new stdClass();
 250                  $competency->parentidnumber = clean_param($parentidnumber, PARAM_TEXT);
 251                  $competency->idnumber = shorten_text(clean_param($idnumber, PARAM_TEXT), 100);
 252                  $competency->shortname = shorten_text(clean_param($shortname, PARAM_TEXT), 100);
 253                  $competency->description = clean_param($description, PARAM_RAW);
 254                  $competency->descriptionformat = clean_param($descriptionformat, PARAM_INT);
 255                  $competency->ruletype = $ruletype;
 256                  $competency->ruleoutcome = clean_param($ruleoutcome, PARAM_INT);
 257                  $competency->ruleconfig = $ruleconfig;
 258                  $competency->relatedidnumbers = $relatedidnumbers;
 259                  $competency->exportid = $exportid;
 260                  $competency->scalevalues = $scalevalues;
 261                  $competency->scaleconfiguration = $scaleconfiguration;
 262                  $competency->children = array();
 263                  $flat[$idnumber] = $competency;
 264              }
 265          }
 266          $this->flat = $flat;
 267          $this->framework = $framework;
 268  
 269          $this->importer->close();
 270          if ($this->framework == null) {
 271              $this->fail(get_string('invalidimportfile', 'tool_lpimportcsv'));
 272              return;
 273          } else {
 274              // We are calling from browser, display progress bar.
 275              if ($this->useprogressbar === true) {
 276                  $this->progress = new \core\progress\display_if_slow(get_string('processingfile', 'tool_lpimportcsv'));
 277              } else {
 278                  // Avoid html output on CLI scripts.
 279                  $this->progress = new \core\progress\none();
 280              }
 281              $this->progress->start_progress('', count($this->flat));
 282              // Build a tree from this flat list.
 283              raise_memory_limit(MEMORY_EXTRA);
 284              $this->add_children($this->framework, '');
 285              $this->progress->end_progress();
 286          }
 287      }
 288  
 289      /**
 290       * Add a competency to the parent with the specified idnumber.
 291       *
 292       * @param competency $node (pass by reference)
 293       * @param string $parentidnumber Add this competency to the parent with this idnumber.
 294       */
 295      public function add_children(& $node, $parentidnumber) {
 296          foreach ($this->flat as $competency) {
 297              if ($competency->parentidnumber == $parentidnumber) {
 298                  $this->progress->increment_progress();
 299                  $node->children[] = $competency;
 300                  $this->add_children($competency, $competency->idnumber);
 301              }
 302          }
 303      }
 304  
 305      /**
 306       * Get parse errors.
 307       * @return array of errors from parsing the xml.
 308       */
 309      public function get_error() {
 310          return $this->error;
 311      }
 312  
 313      /**
 314       * Recursive function to add a competency with all it's children.
 315       *
 316       * @param stdClass $record Raw data for the new competency
 317       * @param competency $parent
 318       * @param competency_framework $framework
 319       */
 320      public function create_competency($record, $parent, $framework) {
 321          $competency = new stdClass();
 322          $competency->competencyframeworkid = $framework->get('id');
 323          $competency->shortname = $record->shortname;
 324          if (!empty($record->description)) {
 325              $competency->description = $record->description;
 326              $competency->descriptionformat = $record->descriptionformat;
 327          }
 328          if ($record->scalevalues) {
 329              $competency->scaleid = $this->get_scale_id($record->scalevalues, $competency->shortname);
 330              $competency->scaleconfiguration = $this->get_scale_configuration($competency->scaleid, $record->scaleconfiguration);
 331          }
 332          if ($parent) {
 333              $competency->parentid = $parent->get('id');
 334          } else {
 335              $competency->parentid = 0;
 336          }
 337          $competency->idnumber = $record->idnumber;
 338  
 339          if (!empty($competency->idnumber) && !empty($competency->shortname)) {
 340              $comp = api::create_competency($competency);
 341              if ($record->exportid) {
 342                  $this->mappings[$record->exportid] = $comp;
 343              }
 344              $record->createdcomp = $comp;
 345              foreach ($record->children as $child) {
 346                  $this->create_competency($child, $comp, $framework);
 347              }
 348  
 349              return $comp;
 350          }
 351          return false;
 352      }
 353  
 354      /**
 355       * Recreate the scale config to point to a new scaleid.
 356       * @param int $scaleid
 357       * @param string $config json encoded scale data.
 358       */
 359      public function get_scale_configuration($scaleid, $config) {
 360          $asarray = json_decode($config);
 361          $asarray[0]->scaleid = $scaleid;
 362          return json_encode($asarray);
 363      }
 364  
 365      /**
 366       * Search for a global scale that matches this set of scalevalues.
 367       * If one is not found it will be created.
 368       * @param array $scalevalues
 369       * @param string $competencyname (Used to create a new scale if required)
 370       * @return int The id of the scale
 371       */
 372      public function get_scale_id($scalevalues, $competencyname) {
 373          global $CFG, $USER;
 374  
 375          require_once($CFG->libdir . '/gradelib.php');
 376  
 377          if (empty($this->scalecache)) {
 378              $allscales = grade_scale::fetch_all_global();
 379              foreach ($allscales as $scale) {
 380                  $scale->load_items();
 381                  $this->scalecache[$scale->compact_items()] = $scale;
 382              }
 383          }
 384          $matchingscale = false;
 385          if (isset($this->scalecache[$scalevalues])) {
 386              $matchingscale = $this->scalecache[$scalevalues];
 387          }
 388          if (!$matchingscale) {
 389              // Create it.
 390              $newscale = new grade_scale();
 391              $newscale->name = get_string('competencyscale', 'tool_lpimportcsv', $competencyname);
 392              $newscale->courseid = 0;
 393              $newscale->userid = $USER->id;
 394              $newscale->scale = $scalevalues;
 395              $newscale->description = get_string('competencyscaledescription', 'tool_lpimportcsv');
 396              $newscale->insert();
 397              $this->scalecache[$scalevalues] = $newscale;
 398              return $newscale->id;
 399          }
 400          return $matchingscale->id;
 401      }
 402  
 403      /**
 404       * Walk through the idnumbers in the relatedidnumbers col and set the relations.
 405       * @param stdClass $record
 406       */
 407      protected function set_related($record) {
 408          $comp = $record->createdcomp;
 409          if ($record->relatedidnumbers) {
 410              $allidnumbers = explode(',', $record->relatedidnumbers);
 411              foreach ($allidnumbers as $rawidnumber) {
 412                  $idnumber = str_replace('%2C', ',', $rawidnumber);
 413  
 414                  if (isset($this->flat[$idnumber])) {
 415                      $relatedcomp = $this->flat[$idnumber]->createdcomp;
 416                      api::add_related_competency($comp->get('id'), $relatedcomp->get('id'));
 417                  }
 418              }
 419          }
 420          foreach ($record->children as $child) {
 421              $this->set_related($child);
 422          }
 423      }
 424  
 425      /**
 426       * Create any completion rule attached to this competency.
 427       * @param stdClass $record
 428       */
 429      protected function set_rules($record) {
 430          $comp = $record->createdcomp;
 431          if ($record->ruletype) {
 432              $class = $record->ruletype;
 433              if (class_exists($class)) {
 434                  $oldruleconfig = $record->ruleconfig;
 435                  if ($oldruleconfig == "null") {
 436                      $oldruleconfig = null;
 437                  }
 438                  $newruleconfig = $class::migrate_config($oldruleconfig, $this->mappings);
 439                  $comp->set('ruleconfig', $newruleconfig);
 440                  $comp->set('ruletype', $class);
 441                  $comp->set('ruleoutcome', $record->ruleoutcome);
 442                  $comp->update();
 443              }
 444          }
 445          foreach ($record->children as $child) {
 446              $this->set_rules($child);
 447          }
 448      }
 449  
 450      /**
 451       * Do the job.
 452       * @return competency_framework
 453       */
 454      public function import() {
 455          $record = clone $this->framework;
 456          unset($record->children);
 457  
 458          $record->scaleid = $this->get_scale_id($record->scalevalues, $record->shortname);
 459          $record->scaleconfiguration = $this->get_scale_configuration($record->scaleid, $record->scaleconfiguration);
 460          unset($record->scalevalues);
 461          $record->contextid = context_system::instance()->id;
 462  
 463          $framework = api::create_framework($record);
 464          if ($this->useprogressbar === true) {
 465              $this->progress = new \core\progress\display_if_slow(get_string('importingfile', 'tool_lpimportcsv'));
 466          } else {
 467              $this->progress = new \core\progress\none();
 468          }
 469  
 470          $this->progress->start_progress('', (count($this->framework->children) * 2));
 471          raise_memory_limit(MEMORY_EXTRA);
 472          // Now all the children.
 473          foreach ($this->framework->children as $comp) {
 474              $this->progress->increment_progress();
 475              $this->create_competency($comp, null, $framework);
 476          }
 477  
 478          // Now create the rules.
 479          foreach ($this->framework->children as $record) {
 480              $this->progress->increment_progress();
 481              $this->set_rules($record);
 482              $this->set_related($record);
 483          }
 484          $this->progress->end_progress();
 485  
 486          $this->importer->cleanup();
 487          return $framework;
 488      }
 489  }