Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are 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   * Processor.
  19   *
  20   * @package    tool_lpmigrate
  21   * @copyright  2016 Frédéric Massart - FMCorz.net
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace tool_lpmigrate;
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  use coding_exception;
  29  use moodle_exception;
  30  use core_competency\api;
  31  use core_competency\competency;
  32  use core_competency\course_competency;
  33  use core_competency\course_module_competency;
  34  
  35  /**
  36   * Processor class.
  37   *
  38   * @package    tool_lpmigrate
  39   * @copyright  2016 Frédéric Massart - FMCorz.net
  40   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  41   */
  42  class framework_processor {
  43  
  44      /** @var array Indexed as courseid => competencyids */
  45      protected $coursescompetencies = array();
  46      /** @var array Indexed as courseid => competencyid => ruleoutcome*/
  47      protected $coursescompetenciesoutcomes = array();
  48      /** @var array Indexed as courseid => cmid => competencyids */
  49      protected $modulecompetencies = array();
  50      /** @var array Indexed as courseid => cmid => competencyid => ruleoutcome*/
  51      protected $modulecompetenciesoutcomes = array();
  52      /** @var array The IDs of the objects of origin. */
  53      protected $fromids = array();
  54      /** @var array The mapping originId => destinationId. */
  55      protected $mappings = array();
  56  
  57      /** @var array Courses found. */
  58      protected $coursesfound = array();
  59      /** @var array Course modules found. */
  60      protected $cmsfound = array();
  61      /** @var integer Number of migrations expected in courses. */
  62      protected $coursecompetencyexpectedmigrations = 0;
  63      /** @var integer Count of migrations in the course level. */
  64      protected $coursecompetencymigrations = 0;
  65      /** @var integer Count of removals in the course level. */
  66      protected $coursecompetencyremovals = 0;
  67      /** @var integer Number of migrations expected in CMs. */
  68      protected $modulecompetencyexpectedmigrations = 0;
  69      /** @var integer Count of migrations in CMs. */
  70      protected $modulecompetencymigrations = 0;
  71      /** @var integer Count of removals in CMs. */
  72      protected $modulecompetencyremovals = 0;
  73      /** @var array IDs of objects missing a mapping in origin, originId => true. */
  74      protected $missingmappings = array();
  75      /** @var array List of errors. */
  76      protected $errors = array();
  77      /** @var array List of warnings. */
  78      protected $warnings = array();
  79  
  80      /** @var array List of course IDs that can be migrated. */
  81      protected $allowedcourses = array();
  82      /** @var int Minimum start date of courses that can be migrated. */
  83      protected $coursestartdatefrom = 0;
  84      /** @var array List of course IDs that cannot be migrated. */
  85      protected $disallowedcourses = array();
  86      /** @var bool Whether to remove the original competency when its destination was already there. */
  87      protected $removeoriginalwhenalreadypresent = false;
  88      /** @var bool Whether to remove the competency from course, or cm, when a mapping is not found. */
  89      protected $removewhenmappingismissing = false;
  90  
  91      /** @var boolean Has this processor run? */
  92      protected $proceeded = false;
  93      /** @var framework_mapper The mapper. */
  94      protected $mapper;
  95      /** @var \core\progress\base The progress. */
  96      protected $progress;
  97  
  98      /**
  99       * Constructor.
 100       *
 101       * @param framework_mapper $mapper The mapper.
 102       * @param \core\progress\base $progress The progress object.
 103       */
 104      public function __construct(framework_mapper $mapper, \core\progress\base $progress = null) {
 105          $this->mapper = $mapper;
 106  
 107          if ($progress == null) {
 108              $progress = new \core\progress\none();
 109          }
 110          $this->progress = $progress;
 111      }
 112  
 113      /**
 114       * Process the mapping.
 115       * @return void
 116       */
 117      protected function process_mapping() {
 118          $this->mappings = $this->mapper->get_mappings();
 119          $this->fromids = $this->mapper->get_all_from();
 120      }
 121  
 122      /**
 123       * Identifies what courses and their competencies to work with.
 124       * @return void
 125       */
 126      protected function find_coursescompetencies() {
 127          global $DB;
 128          $this->progress->start_progress(get_string('findingcoursecompetencies', 'tool_lpmigrate'), 3);
 129          $this->progress->increment_progress();
 130  
 131          $joins = array();
 132          $conditions = array();
 133          $params = array();
 134  
 135          // Limit to mapped objects.
 136          list($insql, $inparams) = $DB->get_in_or_equal($this->fromids, SQL_PARAMS_NAMED);
 137          $conditions[] = "c.id $insql";
 138          $params += $inparams;
 139  
 140          // Restriction on course IDs.
 141          if (!empty($this->allowedcourses)) {
 142              list($insql, $inparams) = $DB->get_in_or_equal($this->allowedcourses, SQL_PARAMS_NAMED);
 143              $conditions[] = "cc.courseid $insql";
 144              $params += $inparams;
 145          }
 146          if (!empty($this->disallowedcourses)) {
 147              list($insql, $inparams) = $DB->get_in_or_equal($this->disallowedcourses, SQL_PARAMS_NAMED, 'param', false);
 148              $conditions[] = "cc.courseid $insql";
 149              $params += $inparams;
 150          }
 151  
 152          // Restriction on start date.
 153          if (!empty($this->coursestartdatefrom)) {
 154              $joins[] = "JOIN {course} co
 155                            ON co.id = cc.courseid";
 156              $conditions[] = "co.startdate >= :startdate";
 157              $params += array('startdate' => $this->coursestartdatefrom);
 158          }
 159  
 160          // Find the courses.
 161          $ccs = array();
 162          $ccsoutcomes = array();
 163          $joins = implode(' ', $joins);
 164          $conditions = implode(' AND ', $conditions);
 165          $sql = "SELECT cc.id, cc.courseid, cc.competencyid, cc.ruleoutcome
 166                    FROM {" . course_competency::TABLE . "} cc
 167                    JOIN {" . competency::TABLE . "} c
 168                      ON c.id = cc.competencyid
 169                         $joins
 170                   WHERE $conditions
 171                ORDER BY cc.sortorder, cc.id";
 172  
 173          $records = $DB->get_recordset_sql($sql, $params);
 174          $this->progress->increment_progress();
 175  
 176          foreach ($records as $record) {
 177              if (!isset($ccs[$record->courseid])) {
 178                  $ccs[$record->courseid] = array();
 179                  $ccsoutcomes[$record->courseid] = array();
 180              }
 181              $ccs[$record->courseid][] = $record->competencyid;
 182              $ccsoutcomes[$record->courseid][$record->competencyid] = $record->ruleoutcome;
 183          }
 184          $records->close();
 185  
 186          $this->coursescompetencies = $ccs;
 187          $this->coursescompetenciesoutcomes = $ccsoutcomes;
 188          $this->coursesfound = $ccs;
 189  
 190          $this->progress->increment_progress();
 191          $this->progress->end_progress();
 192      }
 193  
 194      /**
 195       * Identifies what course modules and their competencies to work with.
 196       * @return void
 197       */
 198      protected function find_modulecompetencies() {
 199          global $DB;
 200          if (empty($this->coursescompetencies)) {
 201              return;
 202          }
 203  
 204          $this->progress->start_progress(get_string('findingmodulecompetencies', 'tool_lpmigrate'), 3);
 205          $this->progress->increment_progress();
 206  
 207          // Limit to mapped objects.
 208          list($inidsql, $inidparams) = $DB->get_in_or_equal($this->fromids, SQL_PARAMS_NAMED);
 209  
 210          // Limit to known courses.
 211          list($incoursesql, $incourseparams) = $DB->get_in_or_equal(array_keys($this->coursescompetencies), SQL_PARAMS_NAMED);
 212          $sql = "SELECT mc.id, cm.course AS courseid, mc.cmid, mc.competencyid, mc.ruleoutcome
 213                    FROM {" . course_module_competency::TABLE . "} mc
 214                    JOIN {course_modules} cm
 215                      ON cm.id = mc.cmid
 216                     AND cm.course $incoursesql
 217                    JOIN {" . competency::TABLE . "} c
 218                      ON c.id = mc.competencyid
 219                   WHERE c.id $inidsql
 220                ORDER BY mc.sortorder, mc.id";
 221          $params = $inidparams + $incourseparams;
 222  
 223          $records = $DB->get_recordset_sql($sql, $params);
 224          $this->progress->increment_progress();
 225          $cmsfound = array();
 226  
 227          $cmcs = array();
 228          $cmcsoutcomes = array();
 229          foreach ($records as $record) {
 230              if (!isset($cmcs[$record->courseid])) {
 231                  $cmcs[$record->courseid] = array();
 232                  $cmcsoutcomes[$record->courseid] = array();
 233              }
 234              if (!isset($cmcs[$record->courseid][$record->cmid])) {
 235                  $cmcs[$record->courseid][$record->cmid] = array();
 236                  $cmcsoutcomes[$record->courseid][$record->cmid] = array();
 237              }
 238              $cmcs[$record->courseid][$record->cmid][] = $record->competencyid;
 239              $cmcsoutcomes[$record->courseid][$record->cmid][$record->competencyid] = $record->ruleoutcome;
 240              $cmsfound[$record->cmid] = true;
 241          }
 242          $records->close();
 243  
 244          $this->modulecompetencies = $cmcs;
 245          $this->modulecompetenciesoutcomes = $cmcsoutcomes;
 246          $this->cmsfound = $cmsfound;
 247  
 248          $this->progress->increment_progress();
 249          $this->progress->end_progress();
 250      }
 251  
 252      /**
 253       * Return a list of CMs found.
 254       * @return int
 255       */
 256      public function get_cms_found() {
 257          return $this->cmsfound;
 258      }
 259  
 260      /**
 261       * Return the number of CMs found.
 262       * @return int
 263       */
 264      public function get_cms_found_count() {
 265          return count($this->cmsfound);
 266      }
 267  
 268      /**
 269       * Return a list of courses found.
 270       * @return int
 271       */
 272      public function get_courses_found() {
 273          return $this->coursesfound;
 274      }
 275  
 276      /**
 277       * Return the number of courses found.
 278       * @return int
 279       */
 280      public function get_courses_found_count() {
 281          return count($this->coursesfound);
 282      }
 283  
 284      /**
 285       * Get the number of course migrations.
 286       * @return int
 287       */
 288      public function get_course_competency_migrations() {
 289          return $this->coursecompetencymigrations;
 290      }
 291  
 292      /**
 293       * Get the number of removals.
 294       * @return int
 295       */
 296      public function get_course_competency_removals() {
 297          return $this->coursecompetencyremovals;
 298      }
 299  
 300      /**
 301       * Get the number of expected course migrations.
 302       * @return int
 303       */
 304      public function get_expected_course_competency_migrations() {
 305          return $this->coursecompetencyexpectedmigrations;
 306      }
 307  
 308      /**
 309       * Get the number of expected course module migrations.
 310       * @return int
 311       */
 312      public function get_expected_module_competency_migrations() {
 313          return $this->modulecompetencyexpectedmigrations;
 314      }
 315  
 316      /**
 317       * Get the number of course module migrations.
 318       * @return int
 319       */
 320      public function get_module_competency_migrations() {
 321          return $this->modulecompetencymigrations;
 322      }
 323  
 324      /**
 325       * Get the number of removals.
 326       * @return int
 327       */
 328      public function get_module_competency_removals() {
 329          return $this->modulecompetencyremovals;
 330      }
 331  
 332      /**
 333       * Return a list of errors.
 334       * @return array
 335       */
 336      public function get_errors() {
 337          return $this->errors;
 338      }
 339  
 340      /**
 341       * Get the missing mappings.
 342       * @return array Where keys are origin IDs.
 343       */
 344      public function get_missing_mappings() {
 345          if (!$this->has_run()) {
 346              throw new coding_exception('The processor has not run yet.');
 347          }
 348          return $this->missingmappings;
 349      }
 350  
 351      /**
 352       * Return a list of warnings.
 353       * @return array
 354       */
 355      public function get_warnings() {
 356          return $this->warnings;
 357      }
 358  
 359      /**
 360       * Whether the processor has run.
 361       * @return boolean
 362       */
 363      public function has_run() {
 364          return $this->proceeded;
 365      }
 366  
 367      /**
 368       * Log an error.
 369       * @param int $courseid The course ID.
 370       * @param int $competencyid The competency ID.
 371       * @param int $cmid The CM ID.
 372       * @param string $message The error message.
 373       * @return void
 374       */
 375      protected function log_error($courseid, $competencyid, $cmid, $message) {
 376          $this->errors[] = array(
 377              'courseid' => $courseid,
 378              'competencyid' => $competencyid,
 379              'cmid' => $cmid,
 380              'message' => $message
 381          );
 382      }
 383  
 384      /**
 385       * Log a warning.
 386       * @param int $courseid The course ID.
 387       * @param int $competencyid The competency ID.
 388       * @param int $cmid The CM ID.
 389       * @param string $message The warning message.
 390       * @return void
 391       */
 392      protected function log_warning($courseid, $competencyid, $cmid, $message) {
 393          $this->warnings[] = array(
 394              'courseid' => $courseid,
 395              'competencyid' => $competencyid,
 396              'cmid' => $cmid,
 397              'message' => $message
 398          );
 399      }
 400  
 401      /**
 402       * Execute the whole task.
 403       * @return void
 404       */
 405      public function proceed() {
 406          if ($this->has_run()) {
 407              throw new coding_exception('The processor has already run.');
 408          } else if (!$this->mapper->has_mappings()) {
 409              throw new coding_exception('Mapping was not set.');
 410          }
 411  
 412          $this->proceeded = true;
 413          $this->process_mapping();
 414          $this->find_coursescompetencies();
 415          $this->find_modulecompetencies();
 416          $this->process_courses();
 417      }
 418  
 419      /**
 420       * Process each course individually.
 421       * @return void
 422       */
 423      protected function process_courses() {
 424          global $DB;
 425          $this->progress->start_progress(get_string('migratingcourses', 'tool_lpmigrate'), count($this->coursescompetencies));
 426  
 427          // Process each course.
 428          foreach ($this->coursescompetencies as $courseid => $competencyids) {
 429              $this->progress->increment_progress();
 430  
 431              $competenciestoremovefromcourse = array();
 432              $skipcompetencies = array();
 433  
 434              // First, add all the new competencies to the course.
 435              foreach ($competencyids as $key => $competencyid) {
 436                  $this->coursecompetencyexpectedmigrations++;
 437                  $mapto = isset($this->mappings[$competencyid]) ? $this->mappings[$competencyid] : false;
 438  
 439                  // Skip the competencies that are not mapped.
 440                  if ($mapto === false) {
 441                      $this->missingmappings[$competencyid] = true;
 442  
 443                      if ($this->removewhenmappingismissing) {
 444                          $competenciestoremovefromcourse[$competencyid] = true;
 445                      }
 446  
 447                      continue;
 448                  }
 449  
 450                  $transaction = $DB->start_delegated_transaction();
 451                  try {
 452                      // Add the new competency to the course.
 453                      if (api::add_competency_to_course($courseid, $mapto)) {
 454  
 455                          // Find the added course competency.
 456                          $cc = course_competency::get_record(array('courseid' => $courseid, 'competencyid' => $mapto));
 457  
 458                          // Set the rule.
 459                          api::set_course_competency_ruleoutcome($cc, $this->coursescompetenciesoutcomes[$courseid][$competencyid]);
 460  
 461                          // Adapt the sortorder.
 462                          api::reorder_course_competency($courseid, $mapto, $competencyid);
 463  
 464                          $competenciestoremovefromcourse[$competencyid] = true;
 465                          $this->coursecompetencymigrations++;
 466  
 467                      } else {
 468                          // The competency was already in the course...
 469                          if ($this->removeoriginalwhenalreadypresent) {
 470                              $competenciestoremovefromcourse[$competencyid] = true;
 471                          } else {
 472                              $this->log_warning($courseid, $competencyid, null,
 473                                  get_string('warningdestinationcoursecompetencyalreadyexists', 'tool_lpmigrate'));
 474                          }
 475                      }
 476  
 477                  } catch (moodle_exception $e) {
 478                      // There was a major problem with this competency, we will ignore it entirely for the course.
 479                      $skipcompetencies[$competencyid] = true;
 480  
 481                      $this->log_error($courseid, $competencyid, null,
 482                          get_string('errorwhilemigratingcoursecompetencywithexception', 'tool_lpmigrate', $e->getMessage()));
 483  
 484                      try {
 485                          $transaction->rollback($e);
 486                      } catch (moodle_exception $e) {
 487                          // Catch the re-thrown exception.
 488                      }
 489  
 490                      continue;
 491                  }
 492                  $transaction->allow_commit();
 493              }
 494  
 495              // Then, convert the module competencies.
 496              if (!empty($this->modulecompetencies[$courseid])) {
 497                  foreach ($this->modulecompetencies[$courseid] as $cmid => $competencyids) {
 498                      foreach ($competencyids as $competencyid) {
 499                          $this->modulecompetencyexpectedmigrations++;
 500  
 501                          // This mapped competency was not added to the course.
 502                          if (!empty($skipcompetencies[$competencyid])) {
 503                              continue;
 504                          }
 505  
 506                          $remove = true;
 507                          $mapto = isset($this->mappings[$competencyid]) ? $this->mappings[$competencyid] : false;
 508  
 509                          // We don't have mapping.
 510                          if ($mapto === false) {
 511                              if (!$this->removewhenmappingismissing) {
 512                                  $remove = false;
 513                              }
 514  
 515                          } else {
 516                              // We have a mapping.
 517                              $transaction = $DB->start_delegated_transaction();
 518                              try {
 519                                  // The competency was added successfully.
 520                                  if (api::add_competency_to_course_module($cmid, $mapto)) {
 521  
 522                                      // Find the added module competency.
 523                                      $mc = course_module_competency::get_record(array('cmid' => $cmid, 'competencyid' => $mapto));
 524  
 525                                      // Set the competency rule.
 526                                      api::set_course_module_competency_ruleoutcome($mc,
 527                                          $this->modulecompetenciesoutcomes[$courseid][$cmid][$competencyid]);
 528  
 529                                      // Adapt the sortorder.
 530                                      api::reorder_course_module_competency($cmid, $mapto, $competencyid);
 531  
 532                                      $this->modulecompetencymigrations++;
 533  
 534                                  } else {
 535                                      // The competency was already in the module.
 536                                      if (!$this->removeoriginalwhenalreadypresent) {
 537                                          $remove = false;
 538                                          $competencieswithissues[$competencyid] = true;
 539                                          $this->log_warning($courseid, $competencyid, $cmid,
 540                                              get_string('warningdestinationmodulecompetencyalreadyexists', 'tool_lpmigrate'));
 541                                      }
 542                                  }
 543  
 544                              } catch (moodle_exception $e) {
 545                                  // There was a major problem with this competency in this module.
 546                                  $competencieswithissues[$competencyid] = true;
 547                                  $message = get_string('errorwhilemigratingmodulecompetencywithexception', 'tool_lpmigrate',
 548                                      $e->getMessage());
 549                                  $this->log_error($courseid, $competencyid, $cmid, $message);
 550  
 551                                  try {
 552                                      $transaction->rollback($e);
 553                                  } catch (moodle_exception $e) {
 554                                      // Catch the re-thrown exception.
 555                                  }
 556  
 557                                  continue;
 558                              }
 559                              $transaction->allow_commit();
 560                          }
 561  
 562                          try {
 563                              // Go away competency!
 564                              if ($remove && api::remove_competency_from_course_module($cmid, $competencyid)) {
 565                                  $this->modulecompetencyremovals++;
 566                              }
 567                          } catch (moodle_exception $e) {
 568                              $competencieswithissues[$competencyid] = true;
 569                              $this->log_warning($courseid, $competencyid, $cmid,
 570                                  get_string('warningcouldnotremovemodulecompetency', 'tool_lpmigrate'));
 571                          }
 572                      }
 573                  }
 574              }
 575  
 576              // Finally, we remove the course competencies, but only for the 100% successful ones.
 577              foreach ($competenciestoremovefromcourse as $competencyid => $unused) {
 578  
 579                  // Skip competencies with issues.
 580                  if (isset($competencieswithissues[$competencyid])) {
 581                      continue;
 582                  }
 583  
 584                  try {
 585                      // Process the course competency.
 586                      api::remove_competency_from_course($courseid, $competencyid);
 587                      $this->coursecompetencyremovals++;
 588                  } catch (moodle_exception $e) {
 589                      $this->log_warning($courseid, $competencyid, null,
 590                          get_string('warningcouldnotremovecoursecompetency', 'tool_lpmigrate'));
 591                  }
 592              }
 593          }
 594  
 595          $this->progress->end_progress();
 596      }
 597  
 598      /**
 599       * Set the IDs of the courses that are allowed.
 600       * @param array $courseids
 601       */
 602      public function set_allowedcourses(array $courseids) {
 603          $this->allowedcourses = $courseids;
 604      }
 605  
 606      /**
 607       * Set the minimum start date for courses to be migrated.
 608       * @param int $value Timestamp, or 0.
 609       */
 610      public function set_course_start_date_from($value) {
 611          $this->coursestartdatefrom = intval($value);
 612      }
 613  
 614      /**
 615       * Set the IDs of the courses that are not allowed.
 616       * @param array $courseids
 617       */
 618      public function set_disallowedcourses(array $courseids) {
 619          $this->disallowedcourses = $courseids;
 620      }
 621  
 622      /**
 623       * Set whether we should remove original competencies when the destination competency was already there.
 624       * @param bool $value
 625       */
 626      public function set_remove_original_when_destination_already_present($value) {
 627          $this->removeoriginalwhenalreadypresent = $value;
 628      }
 629  
 630      /**
 631       * Set whether we should remove unmapped competencies.
 632       * @param bool $value
 633       */
 634      public function set_remove_when_mapping_is_missing($value) {
 635          $this->removewhenmappingismissing = $value;
 636      }
 637  
 638  }