Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 400 and 403] [Versions 401 and 403] [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  namespace qbank_managecategories;
  18  
  19  defined('MOODLE_INTERNAL') || die();
  20  
  21  use moodle_url;
  22  use core_question\local\bank\question_edit_contexts;
  23  
  24  global $CFG;
  25  require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
  26  
  27  /**
  28   * Unit tests for helper class.
  29   *
  30   * @package    qbank_managecategories
  31   * @copyright  2006 The Open University
  32   * @author     2021, Guillermo Gomez Arias <guillermogomez@catalyst-au.net>
  33   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  34   * @coversDefaultClass \qbank_managecategories\helper
  35   */
  36  class helper_test extends \advanced_testcase {
  37  
  38      use \quiz_question_helper_test_trait;
  39  
  40      /**
  41       * @var \context_module module context.
  42       */
  43      protected $context;
  44  
  45      /**
  46       * @var \stdClass course object.
  47       */
  48      protected $course;
  49  
  50      /**
  51       * @var \component_generator_base question generator.
  52       */
  53      protected $qgenerator;
  54  
  55      /**
  56       * @var \stdClass quiz object.
  57       */
  58      protected $quiz;
  59  
  60      /**
  61       * @var question_category_object used in the tests.
  62       */
  63      protected $qcobject;
  64  
  65      /**
  66       * Tests initial setup.
  67       */
  68      protected function setUp(): void {
  69          parent::setUp();
  70          self::setAdminUser();
  71          $this->resetAfterTest();
  72  
  73          $datagenerator = $this->getDataGenerator();
  74          $this->course = $datagenerator->create_course();
  75          $this->quiz = $datagenerator->create_module('quiz',
  76                  ['course' => $this->course->id, 'name' => 'Quiz 1']);
  77          $this->qgenerator = $datagenerator->get_plugin_generator('core_question');
  78          $this->context = \context_module::instance($this->quiz->cmid);
  79  
  80          $contexts = new question_edit_contexts($this->context);
  81          $this->qcobject = new question_category_object(null,
  82              new moodle_url('/question/bank/managecategories/category.php', ['courseid' => SITEID]),
  83              $contexts->having_one_edit_tab_cap('categories'), 0, null, 0,
  84              $contexts->having_cap('moodle/question:add'));
  85      }
  86  
  87      /**
  88       * Test question_remove_stale_questions_from_category function.
  89       *
  90       * @covers ::question_remove_stale_questions_from_category
  91       */
  92      public function test_question_remove_stale_questions_from_category() {
  93          global $DB;
  94  
  95          $qcat1 = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
  96          $q1a = $this->qgenerator->create_question('shortanswer', null, ['category' => $qcat1->id]);     // Will be hidden.
  97          $DB->set_field('question_versions', 'status', 'hidden', ['questionid' => $q1a->id]);
  98  
  99          $qcat2 = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
 100          $q2a = $this->qgenerator->create_question('shortanswer', null, ['category' => $qcat2->id]);     // Will be hidden.
 101          $q2b = $this->qgenerator->create_question('shortanswer', null, ['category' => $qcat2->id]);     // Will be hidden but used.
 102          $DB->set_field('question_versions', 'status', 'hidden', ['questionid' => $q2a->id]);
 103          $DB->set_field('question_versions', 'status', 'hidden', ['questionid' => $q2b->id]);
 104          quiz_add_quiz_question($q2b->id, $this->quiz);
 105  
 106          // Adding a new random question does not add a new question, adds a question_set_references record.
 107          $this->add_random_questions($this->quiz->id, 0, $qcat2->id, 1);
 108  
 109          // We added one random question to the quiz and we expect the quiz to have only one random question.
 110          $q2d = $DB->get_record_sql("SELECT qsr.*
 111                                        FROM {quiz_slots} qs
 112                                        JOIN {question_set_references} qsr ON qsr.itemid = qs.id
 113                                       WHERE qs.quizid = ?
 114                                         AND qsr.component = ?
 115                                         AND qsr.questionarea = ?",
 116              [$this->quiz->id, 'mod_quiz', 'slot'], MUST_EXIST);
 117  
 118          // The following 2 lines have to be after the quiz_add_random_questions() call above.
 119          // Otherwise, quiz_add_random_questions() will to be "smart" and use them instead of creating a new "random" question.
 120          $q1b = $this->qgenerator->create_question('random', null, ['category' => $qcat1->id]);          // Will not be used.
 121          $q2c = $this->qgenerator->create_question('random', null, ['category' => $qcat2->id]);          // Will not be used.
 122  
 123          $this->assertEquals(2, count($this->qcobject->get_real_question_ids_in_category($qcat1->id)));
 124          $this->assertEquals(3, count($this->qcobject->get_real_question_ids_in_category($qcat2->id)));
 125  
 126          // Non-existing category, nothing will happen.
 127          helper::question_remove_stale_questions_from_category(0);
 128          $this->assertEquals(2, count($this->qcobject->get_real_question_ids_in_category($qcat1->id)));
 129          $this->assertEquals(3, count($this->qcobject->get_real_question_ids_in_category($qcat2->id)));
 130  
 131          // First category, should be empty afterwards.
 132          helper::question_remove_stale_questions_from_category($qcat1->id);
 133          $this->assertEquals(0, count($this->qcobject->get_real_question_ids_in_category($qcat1->id)));
 134          $this->assertEquals(3, count($this->qcobject->get_real_question_ids_in_category($qcat2->id)));
 135          $this->assertFalse($DB->record_exists('question', ['id' => $q1a->id]));
 136          $this->assertFalse($DB->record_exists('question', ['id' => $q1b->id]));
 137  
 138          // Second category, used questions should be left untouched.
 139          helper::question_remove_stale_questions_from_category($qcat2->id);
 140          $this->assertEquals(0, count($this->qcobject->get_real_question_ids_in_category($qcat1->id)));
 141          $this->assertEquals(1, count($this->qcobject->get_real_question_ids_in_category($qcat2->id)));
 142          $this->assertFalse($DB->record_exists('question', ['id' => $q2a->id]));
 143          $this->assertTrue($DB->record_exists('question', ['id' => $q2b->id]));
 144          $this->assertFalse($DB->record_exists('question', ['id' => $q2c->id]));
 145          $this->assertTrue($DB->record_exists('question_set_references',
 146              ['id' => $q2d->id, 'component' => 'mod_quiz', 'questionarea' => 'slot']));
 147      }
 148  
 149      /**
 150       * Test delete top category in function question_can_delete_cat.
 151       *
 152       * @covers ::question_can_delete_cat
 153       * @covers ::question_is_top_category
 154       */
 155      public function test_question_can_delete_cat_top_category() {
 156  
 157          $qcategory1 = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
 158  
 159          // Try to delete a top category.
 160          $categorytop = question_get_top_category($qcategory1->id, true)->id;
 161          $this->expectException('moodle_exception');
 162          $this->expectExceptionMessage(get_string('cannotdeletetopcat', 'question'));
 163          helper::question_can_delete_cat($categorytop);
 164      }
 165  
 166      /**
 167       * Test delete only child category in function question_can_delete_cat.
 168       *
 169       * @covers ::question_can_delete_cat
 170       * @covers ::question_is_only_child_of_top_category_in_context
 171       */
 172      public function test_question_can_delete_cat_child_category() {
 173  
 174          $qcategory1 = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
 175  
 176          // Try to delete an only child of top category having also at least one child.
 177          $this->expectException('moodle_exception');
 178          $this->expectExceptionMessage(get_string('cannotdeletecate', 'question'));
 179          helper::question_can_delete_cat($qcategory1->id);
 180      }
 181  
 182      /**
 183       * Test delete category in function question_can_delete_cat without capabilities.
 184       *
 185       * @covers ::question_can_delete_cat
 186       */
 187      public function test_question_can_delete_cat_capability() {
 188  
 189          $qcategory1 = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
 190          $qcategory2 = $this->qgenerator->create_question_category(['contextid' => $this->context->id, 'parent' => $qcategory1->id]);
 191  
 192          // This call should not throw an exception as admin user has the capabilities moodle/question:managecategory.
 193          helper::question_can_delete_cat($qcategory2->id);
 194  
 195          // Try to delete a category with and user without the capability.
 196          $user = $this->getDataGenerator()->create_user();
 197          $this->setUser($user);
 198  
 199          $this->expectException(\required_capability_exception::class);
 200          $this->expectExceptionMessage(get_string('nopermissions', 'error', get_string('question:managecategory', 'role')));
 201          helper::question_can_delete_cat($qcategory2->id);
 202      }
 203  
 204      /**
 205       * Test question_category_select_menu function.
 206       *
 207       * @covers ::question_category_select_menu
 208       * @covers ::question_category_options
 209       */
 210      public function test_question_category_select_menu() {
 211  
 212          $this->qgenerator->create_question_category(['contextid' => $this->context->id, 'name' => 'Test this question category']);
 213          $contexts = new \core_question\local\bank\question_edit_contexts($this->context);
 214  
 215          ob_start();
 216          helper::question_category_select_menu($contexts->having_cap('moodle/question:add'));
 217          $output = ob_get_clean();
 218  
 219          // Test the select menu of question categories output.
 220          $this->assertStringContainsString('Question category', $output);
 221          $this->assertStringContainsString('Test this question category', $output);
 222      }
 223  
 224      /**
 225       * Test that question_category_options function returns the correct category tree.
 226       *
 227       * @covers ::question_category_options
 228       * @covers ::get_categories_for_contexts
 229       * @covers ::question_fix_top_names
 230       * @covers ::question_add_context_in_key
 231       * @covers ::add_indented_names
 232       */
 233      public function test_question_category_options() {
 234  
 235          $qcategory1 = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
 236          $qcategory2 = $this->qgenerator->create_question_category(['contextid' => $this->context->id, 'parent' => $qcategory1->id]);
 237          $qcategory3 = $this->qgenerator->create_question_category(['contextid' => $this->context->id]);
 238  
 239          $contexts = new \core_question\local\bank\question_edit_contexts($this->context);
 240  
 241          // Validate that we have the array with the categories tree.
 242          $categorycontexts = helper::question_category_options($contexts->having_cap('moodle/question:add'));
 243          // The quiz name 'Quiz 1' is set in setUp function.
 244          $categorycontext = $categorycontexts['Quiz: Quiz 1'];
 245          $this->assertCount(3, $categorycontext);
 246  
 247          // Validate that we have the array with the categories tree and that top category is there.
 248          $newcategorycontexts = helper::question_category_options($contexts->having_cap('moodle/question:add'), true);
 249          foreach ($newcategorycontexts as $key => $categorycontext) {
 250              $oldcategorycontext = $categorycontexts[$key];
 251              $count = count($oldcategorycontext);
 252              $this->assertCount($count + 1, $categorycontext);
 253          }
 254      }
 255  }