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_tagquestion\external;
  18  
  19  defined('MOODLE_INTERNAL') || die();
  20  
  21  global $CFG;
  22  require_once($CFG->dirroot . '/webservice/tests/helpers.php');
  23  
  24  /**
  25   * Question external functions tests.
  26   *
  27   * @package    qbank_tagquestion
  28   * @copyright  2016 Pau Ferrer <pau@moodle.com>
  29   * @author     2021 Safat Shahin <safatshahin@catalyst-au.net>
  30   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  31   */
  32  class submit_tags_test extends \externallib_advanced_testcase {
  33  
  34      /**
  35       * Set up for every test
  36       */
  37      public function setUp(): void {
  38          global $DB;
  39          $this->resetAfterTest();
  40          $this->setAdminUser();
  41  
  42          // Setup test data.
  43          $this->course = $this->getDataGenerator()->create_course();
  44  
  45          // Create users.
  46          $this->student = self::getDataGenerator()->create_user();
  47  
  48          // Users enrolments.
  49          $this->studentrole = $DB->get_record('role', ['shortname' => 'student']);
  50          $this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->studentrole->id, 'manual');
  51      }
  52  
  53      /**
  54       * submit_tags_form should throw an exception when the question id doesn't match
  55       * a question.
  56       */
  57      public function test_submit_tags_form_incorrect_question_id() {
  58          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
  59          list ($category, $course, $qcat, $questions) = $questiongenerator->setup_course_and_questions();
  60          $questioncontext = \context::instance_by_id($qcat->contextid);
  61          $editingcontext = $questioncontext;
  62          $question = $questions[0];
  63          // Generate an id for a question that doesn't exist.
  64          $missingquestionid = $questions[1]->id * 2;
  65          $question->id = $missingquestionid;
  66          $formdata = $this->generate_encoded_submit_tags_form_string($question, $qcat, $questioncontext, [], []);
  67  
  68          // We should receive an exception if the question doesn't exist.
  69          $this->expectException('moodle_exception');
  70          submit_tags::execute($missingquestionid, $editingcontext->id, $formdata);
  71      }
  72  
  73      /**
  74       * submit_tags_form should throw an exception when the context id doesn't match
  75       * a context.
  76       */
  77      public function test_submit_tags_form_incorrect_context_id() {
  78          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
  79          list ($category, $course, $qcat, $questions) = $questiongenerator->setup_course_and_questions();
  80          $questioncontext = \context::instance_by_id($qcat->contextid);
  81          $editingcontext = $questioncontext;
  82          $question = $questions[0];
  83          // Generate an id for a context that doesn't exist.
  84          $missingcontextid = $editingcontext->id * 200;
  85          $formdata = $this->generate_encoded_submit_tags_form_string($question, $qcat, $questioncontext, [], []);
  86  
  87          // We should receive an exception if the question doesn't exist.
  88          $this->expectException('moodle_exception');
  89          submit_tags::execute($question->id, $missingcontextid, $formdata);
  90      }
  91  
  92      /**
  93       * submit_tags_form should return false when tags are disabled.
  94       */
  95      public function test_submit_tags_form_tags_disabled() {
  96          global $CFG;
  97  
  98          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
  99          list ($category, $course, $qcat, $questions) = $questiongenerator->setup_course_and_questions();
 100          $questioncontext = \context::instance_by_id($qcat->contextid);
 101          $editingcontext = $questioncontext;
 102          $question = $questions[0];
 103          $user = $this->create_user_can_tag($course);
 104          $formdata = $this->generate_encoded_submit_tags_form_string($question, $qcat, $questioncontext, [], []);
 105  
 106          $this->setUser($user);
 107          $CFG->usetags = false;
 108          $result = submit_tags::execute($question->id, $editingcontext->id, $formdata);
 109          $CFG->usetags = true;
 110  
 111          $this->assertFalse($result['status']);
 112      }
 113  
 114      /**
 115       * submit_tags_form should return false if the user does not have any capability
 116       * to tag the question.
 117       */
 118      public function test_submit_tags_form_no_tag_permissions() {
 119          global $DB;
 120  
 121          $generator = $this->getDataGenerator();
 122          $user = $generator->create_user();
 123          $teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']);
 124          $questiongenerator = $generator->get_plugin_generator('core_question');
 125          list ($category, $course, $qcat, $questions) = $questiongenerator->setup_course_and_questions();
 126          $questioncontext = \context::instance_by_id($qcat->contextid);
 127          $editingcontext = $questioncontext;
 128          $question = $questions[0];
 129          $formdata = $this->generate_encoded_submit_tags_form_string(
 130                  $question,
 131                  $qcat,
 132                  $questioncontext,
 133                  ['foo'],
 134                  ['bar']
 135          );
 136  
 137          // Prohibit all of the tag capabilities.
 138          assign_capability('moodle/question:tagmine', CAP_PROHIBIT, $teacherrole->id, $questioncontext->id);
 139          assign_capability('moodle/question:tagall', CAP_PROHIBIT, $teacherrole->id, $questioncontext->id);
 140  
 141          $generator->enrol_user($user->id, $course->id, $teacherrole->id, 'manual');
 142          $user->ignoresesskey = true;
 143          $this->setUser($user);
 144  
 145          $result = submit_tags::execute($question->id, $editingcontext->id, $formdata);
 146  
 147          $this->assertFalse($result['status']);
 148      }
 149  
 150      /**
 151       * submit_tags_form should return false if the user only has the capability to
 152       * tag their own questions and the question is not theirs.
 153       */
 154      public function test_submit_tags_form_tagmine_permission_non_owner_question() {
 155          global $DB;
 156  
 157          $generator = $this->getDataGenerator();
 158          $user = $generator->create_user();
 159          $teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']);
 160          $questiongenerator = $generator->get_plugin_generator('core_question');
 161          list ($category, $course, $qcat, $questions) = $questiongenerator->setup_course_and_questions();
 162          $questioncontext = \context::instance_by_id($qcat->contextid);
 163          $editingcontext = $questioncontext;
 164          $question = $questions[0];
 165          $formdata = $this->generate_encoded_submit_tags_form_string(
 166                  $question,
 167                  $qcat,
 168                  $questioncontext,
 169                  ['foo'],
 170                  ['bar']
 171          );
 172  
 173          // Make sure the question isn't created by the user.
 174          $question->createdby = $user->id + 1;
 175  
 176          // Prohibit all of the tag capabilities.
 177          assign_capability('moodle/question:tagmine', CAP_ALLOW, $teacherrole->id, $questioncontext->id);
 178          assign_capability('moodle/question:tagall', CAP_PROHIBIT, $teacherrole->id, $questioncontext->id);
 179  
 180          $generator->enrol_user($user->id, $course->id, $teacherrole->id, 'manual');
 181          $user->ignoresesskey = true;
 182          $this->setUser($user);
 183  
 184          $result = submit_tags::execute($question->id, $editingcontext->id, $formdata);
 185  
 186          $this->assertFalse($result['status']);
 187      }
 188  
 189      /**
 190       * Data provided for the submit_tags_form test to check that course tags are
 191       * only created in the correct editing and question context combinations.
 192       *
 193       * @return array Test cases
 194       */
 195      public function get_submit_tags_form_testcases() {
 196          return [
 197                  'course - course' => [
 198                          'editingcontext' => 'course',
 199                          'questioncontext' => 'course',
 200                          'questiontags' => ['foo'],
 201                          'coursetags' => ['bar'],
 202                          'expectcoursetags' => false
 203                  ],
 204                  'course - course - empty tags' => [
 205                          'editingcontext' => 'course',
 206                          'questioncontext' => 'course',
 207                          'questiontags' => [],
 208                          'coursetags' => ['bar'],
 209                          'expectcoursetags' => false
 210                  ],
 211                  'course - course category' => [
 212                          'editingcontext' => 'course',
 213                          'questioncontext' => 'category',
 214                          'questiontags' => ['foo'],
 215                          'coursetags' => ['bar'],
 216                          'expectcoursetags' => true
 217                  ],
 218                  'course - system' => [
 219                          'editingcontext' => 'course',
 220                          'questioncontext' => 'system',
 221                          'questiontags' => ['foo'],
 222                          'coursetags' => ['bar'],
 223                          'expectcoursetags' => true
 224                  ],
 225                  'course category - course' => [
 226                          'editingcontext' => 'category',
 227                          'questioncontext' => 'course',
 228                          'questiontags' => ['foo'],
 229                          'coursetags' => ['bar'],
 230                          'expectcoursetags' => false
 231                  ],
 232                  'course category - course category' => [
 233                          'editingcontext' => 'category',
 234                          'questioncontext' => 'category',
 235                          'questiontags' => ['foo'],
 236                          'coursetags' => ['bar'],
 237                          'expectcoursetags' => false
 238                  ],
 239                  'course category - system' => [
 240                          'editingcontext' => 'category',
 241                          'questioncontext' => 'system',
 242                          'questiontags' => ['foo'],
 243                          'coursetags' => ['bar'],
 244                          'expectcoursetags' => false
 245                  ],
 246                  'system - course' => [
 247                          'editingcontext' => 'system',
 248                          'questioncontext' => 'course',
 249                          'questiontags' => ['foo'],
 250                          'coursetags' => ['bar'],
 251                          'expectcoursetags' => false
 252                  ],
 253                  'system - course category' => [
 254                          'editingcontext' => 'system',
 255                          'questioncontext' => 'category',
 256                          'questiontags' => ['foo'],
 257                          'coursetags' => ['bar'],
 258                          'expectcoursetags' => false
 259                  ],
 260                  'system - system' => [
 261                          'editingcontext' => 'system',
 262                          'questioncontext' => 'system',
 263                          'questiontags' => ['foo'],
 264                          'coursetags' => ['bar'],
 265                          'expectcoursetags' => false
 266                  ],
 267          ];
 268      }
 269  
 270      /**
 271       * Tests that submit_tags_form only creates course tags when the correct combination
 272       * of editing context and question context is provided.
 273       *
 274       * Course tags can only be set on a course category or system context question that
 275       * is being editing in a course context.
 276       *
 277       * @dataProvider get_submit_tags_form_testcases()
 278       * @param string $editingcontext The type of the context the question is being edited in
 279       * @param string $questioncontext The type of the context the question belongs to
 280       * @param string[] $questiontags The tag names to set as question tags
 281       * @param string[] $coursetags The tag names to set as course tags
 282       * @param bool $expectcoursetags If the given course tags should have been set or not
 283       */
 284      public function test_submit_tags_form_context_combinations(
 285              $editingcontext,
 286              $questioncontext,
 287              $questiontags,
 288              $coursetags,
 289              $expectcoursetags
 290      ) {
 291          $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
 292          list ($category, $course, $qcat, $questions) = $questiongenerator->setup_course_and_questions($questioncontext);
 293          $coursecontext = \context_course::instance($course->id);
 294          $questioncontext = \context::instance_by_id($qcat->contextid);
 295  
 296          switch($editingcontext) {
 297              case 'system':
 298                  $editingcontext = \context_system::instance();
 299                  break;
 300  
 301              case 'category':
 302                  $editingcontext = \context_coursecat::instance($category->id);
 303                  break;
 304  
 305              default:
 306                  $editingcontext = \context_course::instance($course->id);
 307          }
 308  
 309          $user = $this->create_user_can_tag($course);
 310          $question = $questions[0];
 311          $formdata = $this->generate_encoded_submit_tags_form_string(
 312                  $question,
 313                  $qcat,
 314                  $questioncontext,
 315                  $questiontags, // Question tags.
 316                  $coursetags // Course tags.
 317          );
 318  
 319          $this->setUser($user);
 320  
 321          $result = submit_tags::execute($question->id, $editingcontext->id, $formdata);
 322  
 323          $this->assertTrue($result['status']);
 324  
 325          $tagobjects = \core_tag_tag::get_item_tags('core_question', 'question', $question->id);
 326          $coursetagobjects = [];
 327          $questiontagobjects = [];
 328  
 329          if ($expectcoursetags) {
 330              // If the use case is expecting course tags to be created then split
 331              // the tags into course tags and question tags and ensure we have
 332              // the correct number of course tags.
 333  
 334              while ($tagobject = array_shift($tagobjects)) {
 335                  if ($tagobject->taginstancecontextid == $questioncontext->id) {
 336                      $questiontagobjects[] = $tagobject;
 337                  } else if ($tagobject->taginstancecontextid == $coursecontext->id) {
 338                      $coursetagobjects[] = $tagobject;
 339                  }
 340              }
 341  
 342              $this->assertCount(count($coursetags), $coursetagobjects);
 343          } else {
 344              $questiontagobjects = $tagobjects;
 345          }
 346  
 347          // Ensure the expected number of question tags was created.
 348          $this->assertCount(count($questiontags), $questiontagobjects);
 349  
 350          foreach ($questiontagobjects as $tagobject) {
 351              // If we have any question tags then make sure they are in the list
 352              // of expected tags and have the correct context.
 353              $this->assertContains($tagobject->name, $questiontags);
 354              $this->assertEquals($questioncontext->id, $tagobject->taginstancecontextid);
 355          }
 356  
 357          foreach ($coursetagobjects as $tagobject) {
 358              // If we have any course tags then make sure they are in the list
 359              // of expected course tags and have the correct context.
 360              $this->assertContains($tagobject->name, $coursetags);
 361              $this->assertEquals($coursecontext->id, $tagobject->taginstancecontextid);
 362          }
 363      }
 364  
 365      /**
 366       * Build the encoded form data expected by the submit_tags_form external function.
 367       *
 368       * @param  \stdClass $question         The question record
 369       * @param  \stdClass $questioncategory The question category record
 370       * @param  \context  $questioncontext  Context for the question category
 371       * @param  array  $tags               A list of tag names for the question
 372       * @param  array  $coursetags         A list of course tag names for the question
 373       * @return string                    HTML encoded string of the data
 374       */
 375      protected function generate_encoded_submit_tags_form_string($question, $questioncategory,
 376              $questioncontext, $tags = [], $coursetags = []) {
 377  
 378          $data = [
 379                  'id' => $question->id,
 380                  'categoryid' => $questioncategory->id,
 381                  'contextid' => $questioncontext->id,
 382                  'questionname' => $question->name,
 383                  'questioncategory' => $questioncategory->name,
 384                  'context' => $questioncontext->get_context_name(false),
 385                  'tags' => $tags,
 386                  'coursetags' => $coursetags
 387          ];
 388          $data = \qbank_tagquestion\form\tags_form::mock_generate_submit_keys($data);
 389  
 390          return http_build_query($data, '', '&');
 391      }
 392  
 393      /**
 394       * Create a user, enrol them in the course, and give them the capability to
 395       * tag all questions in the system context.
 396       *
 397       * @param  \stdClass $course The course record to enrol in
 398       * @return \stdClass         The user record
 399       */
 400      protected function create_user_can_tag($course) {
 401          global $DB;
 402  
 403          $generator = $this->getDataGenerator();
 404          $user = $generator->create_user();
 405          $roleid = $generator->create_role();
 406          $teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']);
 407          $systemcontext = \context_system::instance();
 408  
 409          $generator->role_assign($roleid, $user->id, $systemcontext->id);
 410          $generator->enrol_user($user->id, $course->id, $teacherrole->id, 'manual');
 411  
 412          // Give the user global ability to tag questions.
 413          assign_capability('moodle/question:tagall', CAP_ALLOW, $roleid, $systemcontext, true);
 414          // Allow the user to submit form data.
 415          $user->ignoresesskey = true;
 416  
 417          return $user;
 418      }
 419  
 420  }