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