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] [Versions 401 and 402] [Versions 402 and 403]

   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   * Module generator base class.
  19   *
  20   * @package    core
  21   * @category   test
  22   * @copyright  2012 Petr Skoda {@link http://skodak.org}
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  /**
  29   * Module generator base class.
  30   *
  31   * Extend in mod/xxxx/tests/generator/lib.php as class mod_xxxx_generator.
  32   *
  33   * @package    core
  34   * @category   test
  35   * @copyright  2012 Petr Skoda {@link http://skodak.org}
  36   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37   */
  38  abstract class testing_module_generator extends component_generator_base {
  39  
  40      /**
  41       * @var int number of created instances
  42       */
  43      protected $instancecount = 0;
  44  
  45      /**
  46       * To be called from data reset code only,
  47       * do not use in tests.
  48       * @return void
  49       */
  50      public function reset() {
  51          $this->instancecount = 0;
  52      }
  53  
  54      /**
  55       * Returns module name
  56       * @return string name of module that this class describes
  57       * @throws coding_exception if class invalid
  58       */
  59      public function get_modulename() {
  60          $matches = null;
  61          if (!preg_match('/^mod_([a-z0-9]+)_generator$/', get_class($this), $matches)) {
  62              throw new coding_exception('Invalid module generator class name: '.get_class($this));
  63          }
  64  
  65          if (empty($matches[1])) {
  66              throw new coding_exception('Invalid module generator class name: '.get_class($this));
  67          }
  68          return $matches[1];
  69      }
  70  
  71      /**
  72       * Create course module and link it to course
  73       *
  74       * Since 2.6 it is recommended to use function add_moduleinfo() to create a module.
  75       *
  76       * @deprecated since 2.6
  77       * @see testing_module_generator::create_instance()
  78       *
  79       * @param integer $courseid
  80       * @param array $options section, visible
  81       * @return integer $cm instance id
  82       */
  83      protected function precreate_course_module($courseid, array $options) {
  84          global $DB, $CFG;
  85          require_once("$CFG->dirroot/course/lib.php");
  86  
  87          $modulename = $this->get_modulename();
  88          $sectionnum = isset($options['section']) ? $options['section'] : 0;
  89          unset($options['section']); // Prevent confusion, it would be overridden later in course_add_cm_to_section() anyway.
  90  
  91          $cm = new stdClass();
  92          $cm->course             = $courseid;
  93          $cm->module             = $DB->get_field('modules', 'id', array('name'=>$modulename));
  94          $cm->instance           = 0;
  95          $cm->section            = 0;
  96          $cm->idnumber           = isset($options['idnumber']) ? $options['idnumber'] : 0;
  97          $cm->added              = time();
  98  
  99          $columns = $DB->get_columns('course_modules');
 100          foreach ($options as $key => $value) {
 101              if ($key === 'id' or !isset($columns[$key])) {
 102                  continue;
 103              }
 104              if (property_exists($cm, $key)) {
 105                  continue;
 106              }
 107              $cm->$key = $value;
 108          }
 109  
 110          $cm->id = $DB->insert_record('course_modules', $cm);
 111  
 112          course_add_cm_to_section($courseid, $cm->id, $sectionnum);
 113  
 114          return $cm->id;
 115      }
 116  
 117      /**
 118       * Called after *_add_instance()
 119       *
 120       * Since 2.6 it is recommended to use function add_moduleinfo() to create a module.
 121       *
 122       * @deprecated since 2.6
 123       * @see testing_module_generator::create_instance()
 124       *
 125       * @param int $id
 126       * @param int $cmid
 127       * @return stdClass module instance
 128       */
 129      protected function post_add_instance($id, $cmid) {
 130          global $DB;
 131  
 132          $DB->set_field('course_modules', 'instance', $id, array('id'=>$cmid));
 133  
 134          $instance = $DB->get_record($this->get_modulename(), array('id'=>$id), '*', MUST_EXIST);
 135  
 136          $cm = get_coursemodule_from_id($this->get_modulename(), $cmid, $instance->course, true, MUST_EXIST);
 137          context_module::instance($cm->id);
 138  
 139          $instance->cmid = $cm->id;
 140  
 141          return $instance;
 142      }
 143  
 144      /**
 145       * Merges together arguments $record and $options and fills default module
 146       * fields that are shared by all module types
 147       *
 148       * @param object|array $record fields (different from defaults) for this module
 149       * @param null|array $options for backward-compatibility this may include fields from course_modules
 150       *     table. They are merged into $record
 151       * @throws coding_exception if $record->course is not specified
 152       */
 153      protected function prepare_moduleinfo_record($record, $options) {
 154          global $DB;
 155          // Make sure we don't modify the original object.
 156          $moduleinfo = (object)(array)$record;
 157  
 158          if (empty($moduleinfo->course)) {
 159              throw new coding_exception('module generator requires $record->course');
 160          }
 161  
 162          $moduleinfo->modulename = $this->get_modulename();
 163          $moduleinfo->module = $DB->get_field('modules', 'id', array('name' => $moduleinfo->modulename));
 164  
 165          // Allow idnumber to be set as either $options['idnumber'] or $moduleinfo->cmidnumber or $moduleinfo->idnumber.
 166          // The actual field name is 'idnumber' but add_moduleinfo() expects 'cmidnumber'.
 167          if (isset($options['idnumber'])) {
 168              $moduleinfo->cmidnumber = $options['idnumber'];
 169          } else if (!isset($moduleinfo->cmidnumber) && isset($moduleinfo->idnumber)) {
 170              $moduleinfo->cmidnumber = $moduleinfo->idnumber;
 171          }
 172  
 173          // These are the fields from table 'course_modules' in 2.6 when the second
 174          // argument $options is being deprecated.
 175          // List excludes fields: instance (does not exist yet), course, module and idnumber (set above)
 176          $easymergefields = array('section', 'added', 'score', 'indent',
 177              'visible', 'visibleold', 'groupmode', 'groupingid',
 178              'completion', 'completiongradeitemnumber', 'completionview', 'completionexpected',
 179              'completionpassgrade', 'availability', 'showdescription');
 180          foreach ($easymergefields as $key) {
 181              if (isset($options[$key])) {
 182                  $moduleinfo->$key = $options[$key];
 183              }
 184          }
 185  
 186          // Set default values. Note that visibleold and completiongradeitemnumber are not used when creating a module.
 187          $defaults = array(
 188              'section' => 0,
 189              'visible' => 1,
 190              'visibleoncoursepage' => 1,
 191              'cmidnumber' => '',
 192              'groupmode' => 0,
 193              'groupingid' => 0,
 194              'availability' => null,
 195              'completion' => 0,
 196              'completionview' => 0,
 197              'completionexpected' => 0,
 198              'completionpassgrade' => 0,
 199              'conditiongradegroup' => array(),
 200              'conditionfieldgroup' => array(),
 201              'conditioncompletiongroup' => array()
 202          );
 203  
 204          foreach ($defaults as $key => $value) {
 205              if (!isset($moduleinfo->$key)) {
 206                  $moduleinfo->$key = $value;
 207              }
 208          }
 209  
 210          return $moduleinfo;
 211      }
 212  
 213      /**
 214       * Creates an instance of the module for testing purposes.
 215       *
 216       * Module type will be taken from the class name. Each module type may overwrite
 217       * this function to add other default values used by it.
 218       *
 219       * @param array|stdClass $record data for module being generated. Requires 'course' key
 220       *     (an id or the full object). Also can have any fields from add module form.
 221       * @param null|array $options general options for course module. Since 2.6 it is
 222       *     possible to omit this argument by merging options into $record
 223       * @return stdClass record from module-defined table with additional field
 224       *     cmid (corresponding id in course_modules table)
 225       */
 226      public function create_instance($record = null, array $options = null) {
 227          global $CFG, $DB, $PAGE;
 228          require_once($CFG->dirroot.'/course/modlib.php');
 229  
 230          $this->instancecount++;
 231  
 232          // Creating an activity is a back end operation, which should not cause any output to happen.
 233          // This will allow us to check that the theme was not initialised while creating the module instance.
 234          $outputstartedbefore = $PAGE->get_where_theme_was_initialised();
 235  
 236          // Merge options into record and add default values.
 237          $record = $this->prepare_moduleinfo_record($record, $options);
 238  
 239          // Retrieve the course record.
 240          if (!empty($record->course->id)) {
 241              $course = $record->course;
 242              $record->course = $record->course->id;
 243          } else {
 244              $course = get_course($record->course);
 245          }
 246  
 247          // Fill the name and intro with default values (if missing).
 248          if (empty($record->name)) {
 249              $record->name = get_string('pluginname', $this->get_modulename()).' '.$this->instancecount;
 250          }
 251          if (empty($record->introeditor) && empty($record->intro)) {
 252              $record->intro = 'Test '.$this->get_modulename().' ' . $this->instancecount;
 253          }
 254          if (empty($record->introeditor) && empty($record->introformat)) {
 255              $record->introformat = FORMAT_MOODLE;
 256          }
 257  
 258          if (isset($record->tags) && !is_array($record->tags)) {
 259              $record->tags = preg_split('/\s*,\s*/', trim($record->tags), -1, PREG_SPLIT_NO_EMPTY);
 260          }
 261  
 262          // Before Moodle 2.6 it was possible to create a module with completion tracking when
 263          // it is not setup for course and/or site-wide. Display debugging message so it is
 264          // easier to trace an error in unittests.
 265          if ($record->completion && empty($CFG->enablecompletion)) {
 266              debugging('Did you forget to set $CFG->enablecompletion before generating module with completion tracking?', DEBUG_DEVELOPER);
 267          }
 268          if ($record->completion && empty($course->enablecompletion)) {
 269              debugging('Did you forget to enable completion tracking for the course before generating module with completion tracking?', DEBUG_DEVELOPER);
 270          }
 271  
 272          if (!empty($record->lang) && !has_capability('moodle/course:setforcedlanguage', context_course::instance($course->id))) {
 273              throw new coding_exception('Attempt to generate an activity when the current user does not have ' .
 274                      'permission moodle/course:setforcedlanguage. This does not work.');
 275          }
 276  
 277          // Add the module to the course.
 278          $moduleinfo = add_moduleinfo($record, $course);
 279  
 280          // Prepare object to return with additional field cmid.
 281          $modulename = $this->get_modulename();
 282          $instance = $DB->get_record($modulename, ['id' => $moduleinfo->instance], '*', MUST_EXIST);
 283          $instance->cmid = $moduleinfo->coursemodule;
 284  
 285          // Insert files for the 'intro' file area.
 286          $instance = $this->insert_files(
 287              $instance,
 288              $record,
 289              $modulename,
 290              \context_module::instance($instance->cmid),
 291              "mod_{$modulename}",
 292              'intro',
 293              0
 294          );
 295  
 296          // If the theme was initialised while creating the module instance, something somewhere called an output
 297          // function. Rather than leaving this as a hard-to-debug situation, let's make it fail with a clear error.
 298          $outputstartedafter = $PAGE->get_where_theme_was_initialised();
 299  
 300          if ($outputstartedbefore === null && $outputstartedafter !== null) {
 301              throw new coding_exception('Creating a mod_' . $this->get_modulename() . ' activity initialised the theme and output!',
 302                  'This should not happen. Creating an activity should be a pure back-end operation. Unnecessarily initialising ' .
 303                  'the output mechanism at the wrong time can cause subtle bugs and is a significant performance hit. There is ' .
 304                  'likely a call to an output function that caused it:' . PHP_EOL . PHP_EOL .
 305                  format_backtrace($outputstartedafter, true));
 306          }
 307  
 308          return $instance;
 309      }
 310  
 311      /**
 312       * Generates a piece of content for the module.
 313       * User is usually taken from global $USER variable.
 314       * @param stdClass $instance object returned from create_instance() call
 315       * @param stdClass|array $record
 316       * @return stdClass generated object
 317       * @throws coding_exception if function is not implemented by module
 318       */
 319      public function create_content($instance, $record = array()) {
 320          throw new coding_exception('Module generator for '.$this->get_modulename().' does not implement method create_content()');
 321      }
 322  }