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.

Differences Between: [Versions 401 and 402] [Versions 401 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  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          $expected = [$this->course->id, 'quiz', 'addcategory', 'view.php?id=' . $this->quiz->cmid , $categoryid, $this->quiz->cmid];
 265          $this->assertEventLegacyLogData($expected, $event);
 266          $this->assertEventContextNotUsed($event);
 267      }
 268  
 269      /**
 270       * Test the question category deleted event.
 271       *
 272       * @covers ::delete_category
 273       */
 274      public function test_question_category_deleted() {
 275          // Create the category.
 276          $categoryid = $this->qcobjectquiz->add_category($this->defaultcategory, 'newcategory', '', true);
 277  
 278          // Trigger and capture the event.
 279          $sink = $this->redirectEvents();
 280          $this->qcobjectquiz->delete_category($categoryid);
 281          $events = $sink->get_events();
 282          $event = reset($events);
 283  
 284          // Check that the event data is valid.
 285          $this->assertInstanceOf('\core\event\question_category_deleted', $event);
 286          $this->assertEquals(context_module::instance($this->quiz->cmid), $event->get_context());
 287          $this->assertEquals($categoryid, $event->objectid);
 288          $this->assertDebuggingNotCalled();
 289      }
 290  
 291      /**
 292       * Test the question category updated event.
 293       *
 294       * @covers ::update_category
 295       */
 296      public function test_question_category_updated() {
 297          // Create the category.
 298          $categoryid = $this->qcobjectquiz->add_category($this->defaultcategory, 'newcategory', '', true);
 299  
 300          // Trigger and capture the event.
 301          $sink = $this->redirectEvents();
 302          $this->qcobjectquiz->update_category($categoryid, $this->defaultcategory, 'updatedcategory', '', FORMAT_HTML, '', false);
 303          $events = $sink->get_events();
 304          $event = reset($events);
 305  
 306          // Check that the event data is valid.
 307          $this->assertInstanceOf('\core\event\question_category_updated', $event);
 308          $this->assertEquals(context_module::instance($this->quiz->cmid), $event->get_context());
 309          $this->assertEquals($categoryid, $event->objectid);
 310          $this->assertDebuggingNotCalled();
 311      }
 312  
 313      /**
 314       * Test the question category viewed event.
 315       * There is no external API for viewing the category, so the unit test will simply
 316       * create and trigger the event and ensure data is returned as expected.
 317       *
 318       * @covers ::add_category
 319       */
 320      public function test_question_category_viewed() {
 321          // Create the category.
 322          $categoryid = $this->qcobjectquiz->add_category($this->defaultcategory, 'newcategory', '', true);
 323  
 324          // Log the view of this category.
 325          $category = new stdClass();
 326          $category->id = $categoryid;
 327          $context = context_module::instance($this->quiz->cmid);
 328          $event = \core\event\question_category_viewed::create_from_question_category_instance($category, $context);
 329  
 330          // Trigger and capture the event.
 331          $sink = $this->redirectEvents();
 332          $event->trigger();
 333          $events = $sink->get_events();
 334          $event = reset($events);
 335  
 336          // Check that the event data is valid.
 337          $this->assertInstanceOf('\core\event\question_category_viewed', $event);
 338          $this->assertEquals(context_module::instance($this->quiz->cmid), $event->get_context());
 339          $this->assertEquals($categoryid, $event->objectid);
 340          $this->assertDebuggingNotCalled();
 341  
 342      }
 343  
 344      /**
 345       * Test that get_real_question_ids_in_category() returns question id
 346       * of a shortanswer question in a category.
 347       *
 348       * @covers ::get_real_question_ids_in_category
 349       */
 350      public function test_get_real_question_ids_in_category_shortanswer() {
 351          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 352          $categoryid = $this->defaultcategoryobj->id;
 353  
 354          // Short answer question is made of one question.
 355          $shortanswer = $generator->create_question('shortanswer', null, ['category' => $categoryid]);
 356          $questionids = $this->qcobject->get_real_question_ids_in_category($categoryid);
 357          $this->assertCount(1, $questionids);
 358          $this->assertContains($shortanswer->id, $questionids);
 359      }
 360  
 361      /**
 362       * Test that get_real_question_ids_in_category() returns question id
 363       * of a multianswer question in a category.
 364       *
 365       * @covers ::get_real_question_ids_in_category
 366       */
 367      public function test_get_real_question_ids_in_category_multianswer() {
 368          global $DB;
 369          $countq = $DB->count_records('question');
 370          $countqbe = $DB->count_records('question_bank_entries');
 371  
 372          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 373          $categoryid = $this->defaultcategoryobj->id;
 374  
 375          // Multi answer question is made of one parent and two child questions.
 376          $multianswer = $generator->create_question('multianswer', null, ['category' => $categoryid]);
 377          $questionids = $this->qcobject->get_real_question_ids_in_category($categoryid);
 378          $this->assertCount(1, $questionids);
 379          $this->assertContains($multianswer->id, $questionids);
 380          $this->assertEquals(3, $DB->count_records('question') - $countq);
 381          $this->assertEquals(3, $DB->count_records('question_bank_entries') - $countqbe);
 382      }
 383  
 384      /**
 385       * Test that get_real_question_ids_in_category() returns question ids
 386       * of two versions of a multianswer question in a category.
 387       *
 388       * @covers ::get_real_question_ids_in_category
 389       */
 390      public function test_get_real_question_ids_in_category_multianswer_two_versions() {
 391          global $DB;
 392          $countq = $DB->count_records('question');
 393          $countqv = $DB->count_records('question_versions');
 394          $countqbe = $DB->count_records('question_bank_entries');
 395  
 396          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 397          $categoryid = $this->defaultcategoryobj->id;
 398  
 399          // Create two versions of a multianswer question which will lead to
 400          // 2 parents and 4 child questions in the question bank.
 401          $multianswer = $generator->create_question('multianswer', null, ['category' => $categoryid]);
 402          $multianswernew = $generator->update_question($multianswer, null, ['name' => 'This is a new version']);
 403          $questionids = $this->qcobject->get_real_question_ids_in_category($categoryid);
 404          $this->assertCount(2, $questionids);
 405          $this->assertContains($multianswer->id, $questionids);
 406          $this->assertContains($multianswernew->id, $questionids);
 407          $this->assertEquals(6, $DB->count_records('question') - $countq);
 408          $this->assertEquals(6, $DB->count_records('question_versions') - $countqv);
 409          $this->assertEquals(3, $DB->count_records('question_bank_entries') - $countqbe);
 410      }
 411  
 412      /**
 413       * Test that get_real_question_ids_in_category() returns question id
 414       * of a multianswer question in a category even if their child questions are
 415       * linked to a category that doesn't exist.
 416       *
 417       * @covers ::get_real_question_ids_in_category
 418       */
 419      public function test_get_real_question_ids_in_category_multianswer_bad_data() {
 420          global $DB;
 421          $countqbe = $DB->count_records('question_bank_entries');
 422  
 423          $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
 424          $categoryid = $this->defaultcategoryobj->id;
 425  
 426          // Multi answer question is made of one parent and two child questions.
 427          $multianswer = $generator->create_question('multianswer', null, ['category' => $categoryid]);
 428          $qversion = $DB->get_record('question_versions', ['questionid' => $multianswer->id]);
 429  
 430          // Update category id for child questions to a category that doesn't exist.
 431          $DB->set_field_select('question_bank_entries', 'questioncategoryid',
 432              123456, 'id <> :id', ['id' => $qversion->questionbankentryid]);
 433  
 434          $questionids = $this->qcobject->get_real_question_ids_in_category($categoryid);
 435          $this->assertCount(1, $questionids);
 436          $this->assertContains($multianswer->id, $questionids);
 437          $this->assertEquals(3, $DB->count_records('question_bank_entries') - $countqbe);
 438      }
 439  }