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  namespace qbank_customfields;
  18  
  19  defined('MOODLE_INTERNAL') || die();
  20  
  21  global $CFG;
  22  require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
  23  require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
  24  
  25  /**
  26   * Class qbank_customfields_customfield_testcase
  27   *
  28   * @package     qbank_customfields
  29   * @copyright   2021 Catalyst IT Australia Pty Ltd
  30   * @author      Matt Porritt <mattp@catalyst-au.net>
  31   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  32   */
  33  class customfield_test extends \advanced_testcase {
  34  
  35      /**
  36       * @var array Data object for generating a question.
  37       */
  38      protected $question1data;
  39  
  40      /**
  41       * @var array Data object for generating a question.
  42       */
  43      protected $question2data;
  44  
  45      /**
  46       * @var component_generator_base Question Generator.
  47       */
  48      protected $qgen;
  49  
  50      /**
  51       * @var core_course_category Course category.
  52       */
  53      protected $category;
  54  
  55      /**
  56       * @var stdClass Course object.
  57       */
  58      protected $course;
  59  
  60      /**
  61       * @var int Timestamp to use in tests.
  62       */
  63      protected $testnow = 1632278491;
  64  
  65      /**
  66       * Helper to assist with setting up custom fields.
  67       * This is creating custom field category and the fields, not adding instance field data.
  68       */
  69      protected function setup_custom_fields(): void {
  70  
  71          $dg = self::getDataGenerator();
  72          $data = new \stdClass();
  73          $data->component = 'qbank_customfields';
  74          $data->area = 'question';
  75  
  76          $catid = $dg->create_custom_field_category($data)->get('id');
  77          $dg->create_custom_field(['categoryid' => $catid, 'type' => 'text', 'shortname' => 'f1']);
  78          $dg->create_custom_field(['categoryid' => $catid, 'type' => 'checkbox', 'shortname' => 'f2']);
  79          $dg->create_custom_field(['categoryid' => $catid, 'type' => 'date', 'shortname' => 'f3',
  80                  'configdata' => ['startyear' => 2000, 'endyear' => 3000, 'includetime' => 1]]);
  81          $dg->create_custom_field(['categoryid' => $catid, 'type' => 'select', 'shortname' => 'f4',
  82                  'configdata' => ['options' => "a\nb\nc"]]);
  83          $dg->create_custom_field(['categoryid' => $catid, 'type' => 'textarea', 'shortname' => 'f5']);
  84  
  85      }
  86  
  87      /**
  88       * Helper to assist with setting up questions used in tests.
  89       */
  90      protected function setup_questions(): void {
  91          // Question initial set up.
  92          $this->category = $this->getDataGenerator()->create_category();
  93          $this->course = $this->getDataGenerator()->create_course(['category' => $this->category->id]);
  94          $context = \context_coursecat::instance($this->category->id);
  95          $this->qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
  96          $qcat = $this->qgen->create_question_category(['contextid' => $context->id]);
  97  
  98          $this->question1data = [
  99                  'category' => $qcat->id, 'idnumber' => 'q1',
 100                  'customfield_f1' => 'some text', 'customfield_f2' => 1,
 101                  'customfield_f3' => $this->testnow, 'customfield_f4' => 2,
 102                  'customfield_f5_editor' => ['text' => 'test', 'format' => FORMAT_HTML]];
 103  
 104          $this->question2data = [
 105                  'category' => $qcat->id, 'idnumber' => 'q2',
 106                  'customfield_f1' => 'some more text', 'customfield_f2' => 0,
 107                  'customfield_f3' => $this->testnow, 'customfield_f4' => 1,
 108                  'customfield_f5_editor' => ['text' => 'test text', 'format' => FORMAT_HTML]];
 109      }
 110  
 111      /**
 112       * Makes a backup of the course.
 113       *
 114       * @param \stdClass $course The course object.
 115       * @return string Unique identifier for this backup.
 116       */
 117      protected function backup_course(\stdClass $course): string {
 118          global $CFG, $USER;
 119  
 120          // Turn off file logging, otherwise it can't delete the file (Windows).
 121          $CFG->backup_file_logger_level = \backup::LOG_NONE;
 122  
 123          // Do backup with default settings. MODE_IMPORT means it will just
 124          // create the directory and not zip it.
 125          $bc = new \backup_controller(\backup::TYPE_1COURSE, $course->id,
 126                  \backup::FORMAT_MOODLE, \backup::INTERACTIVE_NO, \backup::MODE_IMPORT,
 127                  $USER->id);
 128          $backupid = $bc->get_backupid();
 129          $bc->execute_plan();
 130          $bc->destroy();
 131  
 132          return $backupid;
 133      }
 134  
 135      /**
 136       * Restores a backup that has been made earlier.
 137       *
 138       * @param string $backupid The unique identifier of the backup.
 139       * @param string $fullname Full name of the new course that is going to be created.
 140       * @param string $shortname Short name of the new course that is going to be created.
 141       * @param int $categoryid The course category the backup is going to be restored in.
 142       * @return int The new course id.
 143       */
 144      protected function restore_course(string $backupid, string $fullname, string $shortname, int $categoryid): int {
 145          global $CFG, $USER;
 146  
 147          // Turn off file logging, otherwise it can't delete the file (Windows).
 148          $CFG->backup_file_logger_level = \backup::LOG_NONE;
 149  
 150          // Do restore to new course with default settings.
 151          $newcourseid = \restore_dbops::create_new_course($fullname, $shortname, $categoryid);
 152          $rc = new \restore_controller($backupid, $newcourseid,
 153                  \backup::INTERACTIVE_NO, \backup::MODE_GENERAL, $USER->id,
 154                  \backup::TARGET_NEW_COURSE);
 155  
 156          $rc->execute_precheck();
 157          $rc->execute_plan();
 158          $rc->destroy();
 159  
 160          return $newcourseid;
 161      }
 162  
 163      /**
 164       * Test creating questions with custom fields.
 165       */
 166      public function test_create_question(): void {
 167          $this->resetAfterTest();
 168          $this->setAdminUser();
 169          $this->setup_custom_fields();
 170          $this->setup_questions();
 171  
 172          // Create 2 questions.
 173          $question1 = $this->qgen->create_question('shortanswer', null, $this->question1data);
 174          $question2 = $this->qgen->create_question('shortanswer', null, $this->question2data);
 175  
 176          // Explicitly save the custom field data for the questions, like a form would.
 177          $customfieldhandler = \qbank_customfields\customfield\question_handler::create();
 178          $this->question1data['id'] = $question1->id;
 179          $this->question2data['id'] = $question2->id;
 180          $customfieldhandler->instance_form_save((object)$this->question1data);
 181          $customfieldhandler->instance_form_save((object)$this->question2data);
 182  
 183          // Get the custom field data associated with these question ids.
 184          $q1cfdata = $customfieldhandler->export_instance_data_object($question1->id);
 185          $q2cfdata = $customfieldhandler->export_instance_data_object($question2->id);
 186  
 187          $this->assertEquals('some text', $q1cfdata->f1);
 188          $this->assertEquals('Yes', $q1cfdata->f2);
 189          $this->assertEquals(userdate($this->testnow, get_string('strftimedaydatetime')), $q1cfdata->f3);
 190          $this->assertEquals('b', $q1cfdata->f4);
 191          $this->assertEquals('test', $q1cfdata->f5);
 192  
 193          $this->assertEquals('some more text', $q2cfdata->f1);
 194          $this->assertEquals('No', $q2cfdata->f2);
 195          $this->assertEquals(userdate($this->testnow, get_string('strftimedaydatetime')), $q2cfdata->f3);
 196          $this->assertEquals('a', $q2cfdata->f4);
 197          $this->assertEquals('test text', $q2cfdata->f5);
 198      }
 199  
 200      /**
 201       * Test deleting questions with custom fields.
 202       */
 203      public function test_delete_question(): void {
 204          $this->resetAfterTest();
 205          $this->setAdminUser();
 206          $this->setup_custom_fields();
 207          $this->setup_questions();
 208  
 209          // Create 2 questions.
 210          $question1 = $this->qgen->create_question('shortanswer', null, $this->question1data);
 211          $question2 = $this->qgen->create_question('shortanswer', null, $this->question2data);
 212  
 213          // Explicitly save the custom field data for the questions, like a form would.
 214          $customfieldhandler = \qbank_customfields\customfield\question_handler::create();
 215          $this->question1data['id'] = $question1->id;
 216          $this->question2data['id'] = $question2->id;
 217          $customfieldhandler->instance_form_save((object)$this->question1data);
 218          $customfieldhandler->instance_form_save((object)$this->question2data);
 219  
 220          // Get the custom field data associated with these question ids.
 221          $q1cfdata = $customfieldhandler->export_instance_data_object($question1->id);
 222          $q2cfdata = $customfieldhandler->export_instance_data_object($question2->id);
 223  
 224          // Quick check that we have data for the custom fields.
 225          $this->assertEquals('some text', $q1cfdata->f1);
 226          $this->assertEquals('some more text', $q2cfdata->f1);
 227  
 228          // Delete the questions.
 229          question_delete_question($question1->id);
 230          question_delete_question($question2->id);
 231  
 232          // Check the custom field data for the questions has also gone.
 233          $q1cfdata = $customfieldhandler->export_instance_data_object($question1->id);
 234          $q2cfdata = $customfieldhandler->export_instance_data_object($question2->id);
 235  
 236          $this->assertEmpty($q1cfdata->f1);
 237          $this->assertEmpty($q2cfdata->f1);
 238      }
 239  
 240      /**
 241       * Test custom fields attached to questions persist
 242       * across the backup and restore process.
 243       */
 244      public function test_backup_restore(): void {
 245          global $DB;
 246  
 247          $this->resetAfterTest();
 248          $this->setAdminUser();
 249          $this->setup_custom_fields();
 250          $this->setup_questions();
 251  
 252          $courseshortname = $this->course->shortname;
 253          $coursefullname = $this->course->fullname;
 254  
 255          // Create 2 questions.
 256          $question1 = $this->qgen->create_question('shortanswer', null, $this->question1data);
 257          $question2 = $this->qgen->create_question('shortanswer', null, $this->question2data);
 258  
 259          // Explicitly save the custom field data for the questions, like a form would.
 260          $customfieldhandler = \qbank_customfields\customfield\question_handler::create();
 261          $this->question1data['id'] = $question1->id;
 262          $this->question2data['id'] = $question2->id;
 263          $customfieldhandler->instance_form_save((object)$this->question1data);
 264          $customfieldhandler->instance_form_save((object)$this->question2data);
 265  
 266          // Create a quiz and the questions to that.
 267          $quiz = $this->getDataGenerator()->create_module(
 268                  'quiz', ['course' => $this->course->id, 'name' => 'restored_quiz']);
 269          quiz_add_quiz_question($question1->id, $quiz);
 270          quiz_add_quiz_question($question2->id, $quiz);
 271  
 272          // Backup the course.
 273          $backupid = $this->backup_course($this->course);
 274  
 275          // Now delete everything.
 276          delete_course($this->course, false);
 277          question_delete_question($question1->id);
 278          question_delete_question($question2->id);
 279  
 280          // Check the custom field data for the questions has also gone.
 281          $q1cfdata = $customfieldhandler->export_instance_data_object($question1->id);
 282          $q2cfdata = $customfieldhandler->export_instance_data_object($question2->id);
 283  
 284          $this->assertEmpty($q1cfdata->f1);
 285          $this->assertEmpty($q2cfdata->f1);
 286  
 287          // Restore the backup we had made earlier into a new course.
 288          $newcategory = $this->getDataGenerator()->create_category();
 289          $this->restore_course($backupid, $coursefullname, $courseshortname . '_2', $newcategory->id);
 290  
 291          // The questions and their associated custom fields should have been restored.
 292          $sql = 'SELECT q.*
 293                   FROM {question} q
 294                   JOIN {question_versions} qv ON qv.questionid = q.id
 295                   JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
 296                  WHERE qbe.idnumber = ?';
 297          $newquestion1 = $DB->get_record_sql($sql, ['q1']);
 298          $newquestion1cfdata = $customfieldhandler->export_instance_data_object($newquestion1->id);
 299          $this->assertEquals('some text', $newquestion1cfdata->f1);
 300          $this->assertEquals('Yes', $newquestion1cfdata->f2);
 301          $this->assertEquals(userdate($this->testnow, get_string('strftimedaydatetime')), $newquestion1cfdata->f3);
 302          $this->assertEquals('b', $newquestion1cfdata->f4);
 303          $this->assertEquals('test', $newquestion1cfdata->f5);
 304  
 305          $newquestion2 = $DB->get_record_sql($sql, ['q2']);
 306          $newquestion2cfdata = $customfieldhandler->export_instance_data_object($newquestion2->id);
 307          $this->assertEquals('some more text', $newquestion2cfdata->f1);
 308          $this->assertEquals('No', $newquestion2cfdata->f2);
 309          $this->assertEquals(userdate($this->testnow, get_string('strftimedaydatetime')), $newquestion2cfdata->f3);
 310          $this->assertEquals('a', $newquestion2cfdata->f4);
 311          $this->assertEquals('test text', $newquestion2cfdata->f5);
 312      }
 313  }