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 400 and 402] [Versions 401 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  namespace qbank_managecategories;
  18  
  19  defined('MOODLE_INTERNAL') || die();
  20  
  21  global $CFG;
  22  require_once($CFG->dirroot . '/question/editlib.php');
  23  
  24  use context;
  25  use context_course;
  26  use context_module;
  27  use moodle_url;
  28  use core_question\local\bank\question_edit_contexts;
  29  use stdClass;
  30  
  31  /**
  32   * Unit tests for qbank_managecategories\question_category_object.
  33   *
  34   * @package     qbank_managecategories
  35   * @copyright   2019 the Open University
  36   * @author      2021, Guillermo Gomez Arias <guillermogomez@catalyst-au.net>
  37   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   * @coversDefaultClass \qbank_managecategories\question_category_object
  39   */
  40  class question_category_object_test extends \advanced_testcase {
  41  
  42      /**
  43       * @var question_category_object used in the tests.
  44       */
  45      protected $qcobject;
  46  
  47      /**
  48       * @var context a context to use.
  49       */
  50      protected $context;
  51  
  52      /**
  53       * @var stdClass top category in context.
  54       */
  55      protected $topcat;
  56  
  57      /**
  58       * @var stdClass course object.
  59       */
  60      protected $course;
  61  
  62      /**
  63       * @var stdClass quiz object.
  64       */
  65      protected $quiz;
  66  
  67      /**
  68       * @var question_edit_contexts
  69       */
  70      private $qcontexts;
  71  
  72      /**
  73       * @var false|object|stdClass|null
  74       */
  75      private $defaultcategoryobj;
  76  
  77      /**
  78       * @var string
  79       */
  80      private $defaultcategory;
  81  
  82      /**
  83       * @var question_category_object
  84       */
  85      private $qcobjectquiz;
  86  
  87      protected function setUp(): void {
  88          parent::setUp();
  89          self::setAdminUser();
  90          $this->resetAfterTest();
  91          $this->context = context_course::instance(SITEID);
  92          $contexts = new question_edit_contexts($this->context);
  93          $this->topcat = question_get_top_category($this->context->id, true);
  94          $this->qcobject = new question_category_object(null,
  95              new moodle_url('/question/bank/managecategories/category.php', ['courseid' => SITEID]),
  96              $contexts->having_one_edit_tab_cap('categories'), 0, null, 0,
  97              $contexts->having_cap('moodle/question:add'));
  98  
  99          // Set up tests in a quiz context.
 100          $this->course = $this->getDataGenerator()->create_course();
 101          $this->quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $this->course->id]);
 102          $this->qcontexts = new question_edit_contexts(context_module::instance($this->quiz->cmid));
 103  
 104          $this->defaultcategoryobj = question_make_default_categories([$this->qcontexts->lowest()]);
 105          $this->defaultcategory = $this->defaultcategoryobj->id . ',' . $this->defaultcategoryobj->contextid;
 106  
 107          $this->qcobjectquiz = new question_category_object(
 108              1,
 109              new moodle_url('/mod/quiz/edit.php', ['cmid' => $this->quiz->cmid]),
 110              $this->qcontexts->having_one_edit_tab_cap('categories'),
 111              $this->defaultcategoryobj->id,
 112              $this->defaultcategory,
 113              null,
 114              $this->qcontexts->having_cap('moodle/question:add'));
 115  
 116      }
 117  
 118      /**
 119       * Test creating a category.
 120       *
 121       * @covers ::add_category
 122       */
 123      public function test_add_category_no_idnumber() {
 124          global $DB;
 125  
 126          $id = $this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
 127              'New category', '', true, FORMAT_HTML, ''); // No idnumber passed as '' to match form data.
 128  
 129          $newcat = $DB->get_record('question_categories', ['id' => $id], '*', MUST_EXIST);
 130          $this->assertSame('New category', $newcat->name);
 131          $this->assertNull($newcat->idnumber);
 132      }
 133  
 134      /**
 135       * Test creating a category with a tricky idnumber.
 136       *
 137       * @covers ::add_category
 138       */
 139      public function test_add_category_set_idnumber_0() {
 140          global $DB;
 141  
 142          $id = $this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
 143              'New category', '', true, FORMAT_HTML, '0');
 144  
 145          $newcat = $DB->get_record('question_categories', ['id' => $id], '*', MUST_EXIST);
 146          $this->assertSame('New category', $newcat->name);
 147          $this->assertSame('0', $newcat->idnumber);
 148      }
 149  
 150      /**
 151       * Trying to add a category with duplicate idnumber blanks it.
 152       * (In reality, this would probably get caught by form validation.)
 153       *
 154       * @covers ::add_category
 155       */
 156      public function test_add_category_try_to_set_duplicate_idnumber() {
 157          global $DB;
 158  
 159          $this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
 160              'Existing category', '', true, FORMAT_HTML, 'frog');
 161  
 162          $id = $this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
 163              'New category', '', true, FORMAT_HTML, 'frog');
 164  
 165          $newcat = $DB->get_record('question_categories', ['id' => $id], '*', MUST_EXIST);
 166          $this->assertSame('New category', $newcat->name);
 167          $this->assertNull($newcat->idnumber);
 168      }
 169  
 170      /**
 171       * Test updating a category.
 172       *
 173       * @covers ::update_category
 174       */
 175      public function test_update_category() {
 176          global $DB;
 177  
 178          $id = $this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
 179              'Old name', 'Description', true, FORMAT_HTML, 'frog');
 180  
 181          $this->qcobject->update_category($id, $this->topcat->id . ',' . $this->topcat->contextid,
 182              'New name', 'New description', FORMAT_HTML, '0', false);
 183  
 184          $newcat = $DB->get_record('question_categories', ['id' => $id], '*', MUST_EXIST);
 185          $this->assertSame('New name', $newcat->name);
 186          $this->assertSame('0', $newcat->idnumber);
 187      }
 188  
 189      /**
 190       * Test updating a category to remove the idnumber.
 191       *
 192       * @covers ::update_category
 193       */
 194      public function test_update_category_removing_idnumber() {
 195          global $DB;
 196  
 197          $id = $this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
 198              'Old name', 'Description', true, FORMAT_HTML, 'frog');
 199  
 200          $this->qcobject->update_category($id, $this->topcat->id . ',' . $this->topcat->contextid,
 201              'New name', 'New description', FORMAT_HTML, '', false);
 202  
 203          $newcat = $DB->get_record('question_categories', ['id' => $id], '*', MUST_EXIST);
 204          $this->assertSame('New name', $newcat->name);
 205          $this->assertNull($newcat->idnumber);
 206      }
 207  
 208      /**
 209       * Test updating a category without changing the idnumber.
 210       *
 211       * @covers ::update_category
 212       */
 213      public function test_update_category_dont_change_idnumber() {
 214          global $DB;
 215  
 216          $id = $this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
 217              'Old name', 'Description', true, FORMAT_HTML, 'frog');
 218  
 219          $this->qcobject->update_category($id, $this->topcat->id . ',' . $this->topcat->contextid,
 220              'New name', 'New description', FORMAT_HTML, 'frog', false);
 221  
 222          $newcat = $DB->get_record('question_categories', ['id' => $id], '*', MUST_EXIST);
 223          $this->assertSame('New name', $newcat->name);
 224          $this->assertSame('frog', $newcat->idnumber);
 225      }
 226  
 227      /**
 228       * Trying to update a category so its idnumber duplicates idnumber blanks it.
 229       * (In reality, this would probably get caught by form validation.)
 230       *
 231       * @covers ::update_category
 232       */
 233      public function test_update_category_try_to_set_duplicate_idnumber() {
 234          global $DB;
 235  
 236          $this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
 237              'Existing category', '', true, FORMAT_HTML, 'toad');
 238          $id = $this->qcobject->add_category($this->topcat->id . ',' . $this->topcat->contextid,
 239              'old name', '', true, FORMAT_HTML, 'frog');
 240  
 241          $this->qcobject->update_category($id, $this->topcat->id . ',' . $this->topcat->contextid,
 242              'New name', '', FORMAT_HTML, 'toad', false);
 243  
 244          $newcat = $DB->get_record('question_categories', ['id' => $id], '*', MUST_EXIST);
 245          $this->assertSame('New name', $newcat->name);
 246          $this->assertNull($newcat->idnumber);
 247      }
 248  
 249      /**
 250       * Test the question category created event.
 251       *
 252       * @covers ::add_category
 253       */
 254      public function test_question_category_created() {
 255          // Trigger and capture the event.
 256          $sink = $this->redirectEvents();
 257          $categoryid = $this->qcobjectquiz->add_category($this->defaultcategory, 'newcategory', '', true);
 258          $events = $sink->get_events();
 259          $event = reset($events);
 260  
 261          // Check that the event data is valid.
 262          $this->assertInstanceOf('\core\event\question_category_created', $event);
 263          $this->assertEquals(context_module::instance($this->quiz->cmid), $event->get_context());
 264          $this->assertEventContextNotUsed($event);
 265      }
 266  
 267      /**
 268       * Test the question category deleted event.
 269       *
 270       * @covers ::delete_category
 271       */
 272      public function test_question_category_deleted() {
 273          // Create the category.
 274          $categoryid = $this->qcobjectquiz->add_category($this->defaultcategory, 'newcategory', '', true);
 275  
 276          // Trigger and capture the event.
 277          $sink = $this->redirectEvents();
 278          $this->qcobjectquiz->delete_category($categoryid);
 279          $events = $sink->get_events();
 280          $event = reset($events);
 281  
 282          // Check that the event data is valid.
 283          $this->assertInstanceOf('\core\event\question_category_deleted', $event);
 284          $this->assertEquals(context_module::instance($this->quiz->cmid), $event->get_context());
 285          $this->assertEquals($categoryid, $event->objectid);
 286          $this->assertDebuggingNotCalled();
 287      }
 288  
 289      /**
 290       * Test the question category updated event.
 291       *
 292       * @covers ::update_category
 293       */
 294      public function test_question_category_updated() {
 295          // Create the category.
 296          $categoryid = $this->qcobjectquiz->add_category($this->defaultcategory, 'newcategory', '', true);
 297  
 298          // Trigger and capture the event.
 299          $sink = $this->redirectEvents();
 300          $this->qcobjectquiz->update_category($categoryid, $this->defaultcategory, 'updatedcategory', '', FORMAT_HTML, '', false);
 301          $events = $sink->get_events();
 302          $event = reset($events);
 303  
 304          // Check that the event data is valid.
 305          $this->assertInstanceOf('\core\event\question_category_updated', $event);
 306          $this->assertEquals(context_module::instance($this->quiz->cmid), $event->get_context());
 307          $this->assertEquals($categoryid, $event->objectid);
 308          $this->assertDebuggingNotCalled();
 309      }
 310  
 311      /**
 312       * Test the question category viewed event.
 313       * There is no external API for viewing the category, so the unit test will simply
 314       * create and trigger the event and ensure data is returned as expected.
 315       *
 316       * @covers ::add_category
 317       */
 318      public function test_question_category_viewed() {
 319          // Create the category.
 320          $categoryid = $this->qcobjectquiz->add_category($this->defaultcategory, 'newcategory', '', true);
 321  
 322          // Log the view of this category.
 323          $category = new stdClass();
 324          $category->id = $categoryid;
 325          $context = context_module::instance($this->quiz->cmid);
 326          $event = \core\event\question_category_viewed::create_from_question_category_instance($category, $context);
 327  
 328          // Trigger and capture the event.
 329          $sink = $this->redirectEvents();
 330          $event->trigger();
 331          $events = $sink->get_events();
 332          $event = reset($events);
 333  
 334          // Check that the event data is valid.
 335          $this->assertInstanceOf('\core\event\question_category_viewed', $event);
 336          $this->assertEquals(context_module::instance($this->quiz->cmid), $event->get_context());
 337          $this->assertEquals($categoryid, $event->objectid);
 338          $this->assertDebuggingNotCalled();
 339  
 340      }
 341  
 342      /**
 343       * Test that get_real_question_ids_in_category() returns question id
 344       * of a shortanswer question in a category.
 345       *
 346       * @covers ::get_real_question_ids_in_category
 347       */
 348      public function test_get_real_question_ids_in_category_shortanswer() {
 349          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 350          $categoryid = $this->defaultcategoryobj->id;
 351  
 352          // Short answer question is made of one question.
 353          $shortanswer = $generator->create_question('shortanswer', null, ['category' => $categoryid]);
 354          $questionids = $this->qcobject->get_real_question_ids_in_category($categoryid);
 355          $this->assertCount(1, $questionids);
 356          $this->assertContains($shortanswer->id, $questionids);
 357      }
 358  
 359      /**
 360       * Test that get_real_question_ids_in_category() returns question id
 361       * of a multianswer question in a category.
 362       *
 363       * @covers ::get_real_question_ids_in_category
 364       */
 365      public function test_get_real_question_ids_in_category_multianswer() {
 366          global $DB;
 367          $countq = $DB->count_records('question');
 368          $countqbe = $DB->count_records('question_bank_entries');
 369  
 370          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 371          $categoryid = $this->defaultcategoryobj->id;
 372  
 373          // Multi answer question is made of one parent and two child questions.
 374          $multianswer = $generator->create_question('multianswer', null, ['category' => $categoryid]);
 375          $questionids = $this->qcobject->get_real_question_ids_in_category($categoryid);
 376          $this->assertCount(1, $questionids);
 377          $this->assertContains($multianswer->id, $questionids);
 378          $this->assertEquals(3, $DB->count_records('question') - $countq);
 379          $this->assertEquals(3, $DB->count_records('question_bank_entries') - $countqbe);
 380      }
 381  
 382      /**
 383       * Test that get_real_question_ids_in_category() returns question ids
 384       * of two versions of a multianswer question in a category.
 385       *
 386       * @covers ::get_real_question_ids_in_category
 387       */
 388      public function test_get_real_question_ids_in_category_multianswer_two_versions() {
 389          global $DB;
 390          $countq = $DB->count_records('question');
 391          $countqv = $DB->count_records('question_versions');
 392          $countqbe = $DB->count_records('question_bank_entries');
 393  
 394          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 395          $categoryid = $this->defaultcategoryobj->id;
 396  
 397          // Create two versions of a multianswer question which will lead to
 398          // 2 parents and 4 child questions in the question bank.
 399          $multianswer = $generator->create_question('multianswer', null, ['category' => $categoryid]);
 400          $multianswernew = $generator->update_question($multianswer, null, ['name' => 'This is a new version']);
 401          $questionids = $this->qcobject->get_real_question_ids_in_category($categoryid);
 402          $this->assertCount(2, $questionids);
 403          $this->assertContains($multianswer->id, $questionids);
 404          $this->assertContains($multianswernew->id, $questionids);
 405          $this->assertEquals(6, $DB->count_records('question') - $countq);
 406          $this->assertEquals(6, $DB->count_records('question_versions') - $countqv);
 407          $this->assertEquals(3, $DB->count_records('question_bank_entries') - $countqbe);
 408      }
 409  
 410      /**
 411       * Test that get_real_question_ids_in_category() returns question id
 412       * of a multianswer question in a category even if their child questions are
 413       * linked to a category that doesn't exist.
 414       *
 415       * @covers ::get_real_question_ids_in_category
 416       */
 417      public function test_get_real_question_ids_in_category_multianswer_bad_data() {
 418          global $DB;
 419          $countqbe = $DB->count_records('question_bank_entries');
 420  
 421          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 422          $categoryid = $this->defaultcategoryobj->id;
 423  
 424          // Multi answer question is made of one parent and two child questions.
 425          $multianswer = $generator->create_question('multianswer', null, ['category' => $categoryid]);
 426          $qversion = $DB->get_record('question_versions', ['questionid' => $multianswer->id]);
 427  
 428          // Update category id for child questions to a category that doesn't exist.
 429          $DB->set_field_select('question_bank_entries', 'questioncategoryid',
 430              123456, 'id <> :id', ['id' => $qversion->questionbankentryid]);
 431  
 432          $questionids = $this->qcobject->get_real_question_ids_in_category($categoryid);
 433          $this->assertCount(1, $questionids);
 434          $this->assertContains($multianswer->id, $questionids);
 435          $this->assertEquals(3, $DB->count_records('question_bank_entries') - $countqbe);
 436      }
 437  }