Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310]

   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  /**
  18   * Unit tests for core_grades\component_gradeitems;
  19   *
  20   * @package   core_grades
  21   * @category  test
  22   * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
  23   * @license   http://www.gnu.org/copyleft/gpl.html GNU Public License
  24   */
  25  
  26  declare(strict_types = 1);
  27  
  28  namespace core_grades\grades\grader\gradingpanel\scale\external;
  29  
  30  use advanced_testcase;
  31  use coding_exception;
  32  use core_grades\component_gradeitem;
  33  use external_api;
  34  use mod_forum\local\entities\forum as forum_entity;
  35  use moodle_exception;
  36  use grade_grade;
  37  use grade_item;
  38  
  39  /**
  40   * Unit tests for core_grades\component_gradeitems;
  41   *
  42   * @package   core_grades
  43   * @category  test
  44   * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
  45   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  46   */
  47  class store_test extends advanced_testcase {
  48  
  49      public static function setupBeforeClass(): void {
  50          global $CFG;
  51          require_once("{$CFG->libdir}/externallib.php");
  52      }
  53  
  54      /**
  55       * Ensure that an execute with an invalid component is rejected.
  56       */
  57      public function test_execute_invalid_component(): void {
  58          $this->resetAfterTest();
  59          $user = $this->getDataGenerator()->create_user();
  60          $this->setUser($user);
  61  
  62          $this->expectException(coding_exception::class);
  63          $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_invalid' component");
  64          store::execute('mod_invalid', 1, 'foo', 2, false, 'formdata');
  65      }
  66  
  67      /**
  68       * Ensure that an execute with an invalid itemname on a valid component is rejected.
  69       */
  70      public function test_execute_invalid_itemname(): void {
  71          $this->resetAfterTest();
  72          $user = $this->getDataGenerator()->create_user();
  73          $this->setUser($user);
  74  
  75          $this->expectException(coding_exception::class);
  76          $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_forum' component");
  77          store::execute('mod_forum', 1, 'foo', 2, false, 'formdata');
  78      }
  79  
  80      /**
  81       * Ensure that an execute against a different grading method is rejected.
  82       */
  83      public function test_execute_incorrect_type(): void {
  84          $this->resetAfterTest();
  85  
  86          $forum = $this->get_forum_instance([
  87              // Negative numbers mean a scale.
  88              'grade_forum' => 5,
  89          ]);
  90          $course = $forum->get_course_record();
  91          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
  92          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
  93          $this->setUser($teacher);
  94  
  95          $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
  96  
  97          $this->expectException(moodle_exception::class);
  98          $this->expectExceptionMessage("not configured for grading with scales");
  99          store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, false, 'formdata');
 100      }
 101  
 102      /**
 103       * Ensure that an execute against a different grading method is rejected.
 104       */
 105      public function test_execute_disabled(): void {
 106          $this->resetAfterTest();
 107  
 108          $forum = $this->get_forum_instance();
 109          $course = $forum->get_course_record();
 110          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
 111          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 112          $this->setUser($teacher);
 113  
 114          $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
 115  
 116          $this->expectException(moodle_exception::class);
 117          $this->expectExceptionMessage("Grading is not enabled");
 118          store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, false, 'formdata');
 119      }
 120  
 121      /**
 122       * Ensure that an execute against the correct grading method returns the current state of the user.
 123       */
 124      public function test_execute_store_empty(): void {
 125          [
 126              'forum' => $forum,
 127              'options' => $options,
 128              'student' => $student,
 129              'teacher' => $teacher,
 130          ] = $this->get_test_data();
 131  
 132          $this->setUser($teacher);
 133  
 134          $formdata = [
 135              'grade' => null,
 136          ];
 137  
 138          $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
 139  
 140          $result = store::execute('mod_forum', (int) $forum->get_context()->id, 'forum',
 141                  (int) $student->id, false, http_build_query($formdata));
 142          $result = external_api::clean_returnvalue(store::execute_returns(), $result);
 143  
 144          // The result should still be empty.
 145          $this->assertIsArray($result);
 146          $this->assertArrayHasKey('templatename', $result);
 147  
 148          $this->assertEquals('core_grades/grades/grader/gradingpanel/scale', $result['templatename']);
 149  
 150          $this->assertArrayHasKey('warnings', $result);
 151          $this->assertIsArray($result['warnings']);
 152          $this->assertEmpty($result['warnings']);
 153  
 154          // Test the grade array items.
 155          $this->assertArrayHasKey('grade', $result);
 156          $this->assertIsArray($result['grade']);
 157  
 158          $this->assertIsInt($result['grade']['timecreated']);
 159          $this->assertArrayHasKey('timemodified', $result['grade']);
 160          $this->assertIsInt($result['grade']['timemodified']);
 161  
 162          $this->assertArrayHasKey('usergrade', $result['grade']);
 163          $this->assertEquals('-', $result['grade']['usergrade']);
 164  
 165          $this->assertArrayHasKey('maxgrade', $result['grade']);
 166          $this->assertIsInt($result['grade']['maxgrade']);
 167          $this->assertEquals(3, $result['grade']['maxgrade']);
 168  
 169          $this->assertArrayHasKey('gradedby', $result['grade']);
 170          $this->assertEquals(fullname($teacher), $result['grade']['gradedby']);
 171  
 172          $this->assertArrayHasKey('options', $result['grade']);
 173          $this->assertCount(count($options), $result['grade']['options']);
 174          rsort($options);
 175          foreach ($options as $index => $option) {
 176              $this->assertArrayHasKey($index, $result['grade']['options']);
 177  
 178              $returnedoption = $result['grade']['options'][$index];
 179              $this->assertArrayHasKey('value', $returnedoption);
 180              $this->assertEquals(3 - $index, $returnedoption['value']);
 181  
 182              $this->assertArrayHasKey('title', $returnedoption);
 183              $this->assertEquals($option, $returnedoption['title']);
 184  
 185              $this->assertArrayHasKey('selected', $returnedoption);
 186              $this->assertFalse($returnedoption['selected']);
 187          }
 188  
 189          // Compare against the grade stored in the database.
 190          $storedgradeitem = grade_item::fetch([
 191              'courseid' => $forum->get_course_id(),
 192              'itemtype' => 'mod',
 193              'itemmodule' => 'forum',
 194              'iteminstance' => $forum->get_id(),
 195              'itemnumber' => $gradeitem->get_grade_itemid(),
 196          ]);
 197          $storedgrade = grade_grade::fetch([
 198              'userid' => $student->id,
 199              'itemid' => $storedgradeitem->id,
 200          ]);
 201  
 202          $this->assertEmpty($storedgrade->rawgrade);
 203      }
 204  
 205      /**
 206       * Ensure that an execute against the correct grading method returns the current state of the user.
 207       */
 208      public function test_execute_store_not_selected(): void {
 209          [
 210              'forum' => $forum,
 211              'options' => $options,
 212              'student' => $student,
 213              'teacher' => $teacher,
 214          ] = $this->get_test_data();
 215  
 216          $this->setUser($teacher);
 217  
 218          $formdata = [
 219              'grade' => -1,
 220          ];
 221  
 222          $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
 223  
 224          $result = store::execute('mod_forum', (int) $forum->get_context()->id, 'forum',
 225                  (int) $student->id, false, http_build_query($formdata));
 226          $result = external_api::clean_returnvalue(store::execute_returns(), $result);
 227  
 228          // The result should still be empty.
 229          $this->assertIsArray($result);
 230          $this->assertArrayHasKey('templatename', $result);
 231  
 232          $this->assertEquals('core_grades/grades/grader/gradingpanel/scale', $result['templatename']);
 233  
 234          $this->assertArrayHasKey('warnings', $result);
 235          $this->assertIsArray($result['warnings']);
 236          $this->assertEmpty($result['warnings']);
 237  
 238          // Test the grade array items.
 239          $this->assertArrayHasKey('grade', $result);
 240          $this->assertIsArray($result['grade']);
 241  
 242          $this->assertIsInt($result['grade']['timecreated']);
 243          $this->assertArrayHasKey('timemodified', $result['grade']);
 244          $this->assertIsInt($result['grade']['timemodified']);
 245  
 246          $this->assertArrayHasKey('usergrade', $result['grade']);
 247          $this->assertEquals('-', $result['grade']['usergrade']);
 248  
 249          $this->assertArrayHasKey('maxgrade', $result['grade']);
 250          $this->assertIsInt($result['grade']['maxgrade']);
 251          $this->assertEquals(3, $result['grade']['maxgrade']);
 252  
 253          $this->assertArrayHasKey('gradedby', $result['grade']);
 254          $this->assertEquals(null, $result['grade']['gradedby']);
 255  
 256          $this->assertArrayHasKey('options', $result['grade']);
 257          $this->assertCount(count($options), $result['grade']['options']);
 258          rsort($options);
 259          foreach ($options as $index => $option) {
 260              $this->assertArrayHasKey($index, $result['grade']['options']);
 261  
 262              $returnedoption = $result['grade']['options'][$index];
 263              $this->assertArrayHasKey('value', $returnedoption);
 264              $this->assertEquals(3 - $index, $returnedoption['value']);
 265  
 266              $this->assertArrayHasKey('title', $returnedoption);
 267              $this->assertEquals($option, $returnedoption['title']);
 268  
 269              $this->assertArrayHasKey('selected', $returnedoption);
 270              $this->assertFalse($returnedoption['selected']);
 271          }
 272  
 273          // Compare against the grade stored in the database.
 274          $storedgradeitem = grade_item::fetch([
 275              'courseid' => $forum->get_course_id(),
 276              'itemtype' => 'mod',
 277              'itemmodule' => 'forum',
 278              'iteminstance' => $forum->get_id(),
 279              'itemnumber' => $gradeitem->get_grade_itemid(),
 280          ]);
 281          $storedgrade = grade_grade::fetch([
 282              'userid' => $student->id,
 283              'itemid' => $storedgradeitem->id,
 284          ]);
 285  
 286          // No grade will have been saved.
 287          $this->assertFalse($storedgrade);
 288      }
 289  
 290      /**
 291       * Ensure that an execute against the correct grading method returns the current state of the user.
 292       */
 293      public function test_execute_store_graded(): void {
 294          [
 295              'scale' => $scale,
 296              'forum' => $forum,
 297              'options' => $options,
 298              'student' => $student,
 299              'teacher' => $teacher,
 300          ] = $this->get_test_data();
 301  
 302          $this->setUser($teacher);
 303  
 304          $formdata = [
 305              'grade' => 2,
 306          ];
 307          $formattedvalue = grade_floatval(unformat_float($formdata['grade']));
 308  
 309          $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
 310  
 311          $result = store::execute('mod_forum', (int) $forum->get_context()->id, 'forum',
 312                  (int) $student->id, false, http_build_query($formdata));
 313          $result = external_api::clean_returnvalue(store::execute_returns(), $result);
 314  
 315          // The result should still be empty.
 316          $this->assertIsArray($result);
 317          $this->assertArrayHasKey('templatename', $result);
 318  
 319          $this->assertEquals('core_grades/grades/grader/gradingpanel/scale', $result['templatename']);
 320  
 321          $this->assertArrayHasKey('warnings', $result);
 322          $this->assertIsArray($result['warnings']);
 323          $this->assertEmpty($result['warnings']);
 324  
 325          // Test the grade array items.
 326          $this->assertArrayHasKey('grade', $result);
 327          $this->assertIsArray($result['grade']);
 328  
 329          $this->assertIsInt($result['grade']['timecreated']);
 330          $this->assertArrayHasKey('timemodified', $result['grade']);
 331          $this->assertIsInt($result['grade']['timemodified']);
 332  
 333          $this->assertArrayHasKey('usergrade', $result['grade']);
 334          $this->assertEquals('B', $result['grade']['usergrade']);
 335  
 336          $this->assertArrayHasKey('maxgrade', $result['grade']);
 337          $this->assertIsInt($result['grade']['maxgrade']);
 338          $this->assertEquals(3, $result['grade']['maxgrade']);
 339  
 340          $this->assertArrayHasKey('gradedby', $result['grade']);
 341          $this->assertEquals(fullname($teacher), $result['grade']['gradedby']);
 342  
 343          $this->assertArrayHasKey('options', $result['grade']);
 344          $this->assertCount(count($options), $result['grade']['options']);
 345          rsort($options);
 346          foreach ($options as $index => $option) {
 347              $this->assertArrayHasKey($index, $result['grade']['options']);
 348  
 349              $returnedoption = $result['grade']['options'][$index];
 350              $this->assertArrayHasKey('value', $returnedoption);
 351              $this->assertEquals(3 - $index, $returnedoption['value']);
 352  
 353              $this->assertArrayHasKey('title', $returnedoption);
 354              $this->assertEquals($option, $returnedoption['title']);
 355  
 356              $this->assertArrayHasKey('selected', $returnedoption);
 357          }
 358  
 359          // Compare against the grade stored in the database.
 360          $storedgradeitem = grade_item::fetch([
 361              'courseid' => $forum->get_course_id(),
 362              'itemtype' => 'mod',
 363              'itemmodule' => 'forum',
 364              'iteminstance' => $forum->get_id(),
 365              'itemnumber' => $gradeitem->get_grade_itemid(),
 366          ]);
 367          $storedgrade = grade_grade::fetch([
 368              'userid' => $student->id,
 369              'itemid' => $storedgradeitem->id,
 370          ]);
 371  
 372          $this->assertEquals($formattedvalue, $storedgrade->rawgrade);
 373          $this->assertEquals($scale->id, $storedgrade->rawscaleid);
 374  
 375          // The grade was 2, which relates to the middle option.
 376          $this->assertFalse($result['grade']['options'][0]['selected']);
 377          $this->assertTrue($result['grade']['options'][1]['selected']);
 378          $this->assertFalse($result['grade']['options'][2]['selected']);
 379      }
 380  
 381      /**
 382       * Ensure that an out-of-range value is rejected.
 383       *
 384       * @dataProvider execute_out_of_range_provider
 385       * @param int $suppliedvalue The value that was submitted
 386       */
 387      public function test_execute_store_out_of_range(int $suppliedvalue): void {
 388          [
 389              'scale' => $scale,
 390              'forum' => $forum,
 391              'options' => $options,
 392              'student' => $student,
 393              'teacher' => $teacher,
 394          ] = $this->get_test_data();
 395  
 396          $this->setUser($teacher);
 397  
 398          $formdata = [
 399              'grade' => $suppliedvalue,
 400          ];
 401  
 402          $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
 403  
 404          $this->expectException(moodle_exception::class);
 405          $this->expectExceptionMessage("Invalid grade '{$suppliedvalue}' provided. Grades must be between 0 and 3.");
 406          store::execute('mod_forum', (int) $forum->get_context()->id, 'forum',
 407                  (int) $student->id, false, http_build_query($formdata));
 408      }
 409  
 410      /**
 411       * Data provider for out of range tests.
 412       *
 413       * @return array
 414       */
 415      public function execute_out_of_range_provider(): array {
 416          return [
 417              'above' => [
 418                  'supplied' => 500,
 419              ],
 420              'above just' => [
 421                  'supplied' => 4,
 422              ],
 423              'below' => [
 424                  'supplied' => -100,
 425              ],
 426              '-10' => [
 427                  'supplied' => -10,
 428              ],
 429          ];
 430      }
 431  
 432  
 433      /**
 434       * Get a forum instance.
 435       *
 436       * @param array $config
 437       * @return forum_entity
 438       */
 439      protected function get_forum_instance(array $config = []): forum_entity {
 440          $this->resetAfterTest();
 441  
 442          $datagenerator = $this->getDataGenerator();
 443          $course = $datagenerator->create_course();
 444          $forum = $datagenerator->create_module('forum', array_merge($config, ['course' => $course->id]));
 445  
 446          $vaultfactory = \mod_forum\local\container::get_vault_factory();
 447          $vault = $vaultfactory->get_forum_vault();
 448  
 449          return $vault->get_from_id((int) $forum->id);
 450      }
 451  
 452      /**
 453       * Get test data for scaled forums.
 454       *
 455       * @return array
 456       */
 457      protected function get_test_data(): array {
 458          $this->resetAfterTest();
 459  
 460          $options = [
 461              'A',
 462              'B',
 463              'C'
 464          ];
 465          $scale = $this->getDataGenerator()->create_scale(['scale' => implode(',', $options)]);
 466  
 467          $forum = $this->get_forum_instance([
 468              // Negative numbers mean a scale.
 469              'grade_forum' => -1 * $scale->id
 470          ]);
 471          $course = $forum->get_course_record();
 472          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
 473          $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
 474  
 475          return [
 476              'forum' => $forum,
 477              'scale' => $scale,
 478              'options' => $options,
 479              'student' => $student,
 480              'teacher' => $teacher,
 481          ];
 482      }
 483  }