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