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