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 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [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 core;
  18  
  19  defined('MOODLE_INTERNAL') || die();
  20  
  21  global $CFG;
  22  require_once($CFG->libdir . '/gradelib.php');
  23  
  24  /**
  25   * Unit tests for /lib/gradelib.php.
  26   *
  27   * @package   core
  28   * @category  test
  29   * @copyright 2012 Andrew Davis
  30   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  31   */
  32  class gradelib_test extends \advanced_testcase {
  33  
  34      public function test_grade_update_mod_grades() {
  35  
  36          $this->resetAfterTest(true);
  37  
  38          // Create a broken module instance.
  39          $modinstance = new \stdClass();
  40          $modinstance->modname = 'doesntexist';
  41  
  42          $this->assertFalse(grade_update_mod_grades($modinstance));
  43          // A debug message should have been generated.
  44          $this->assertDebuggingCalled();
  45  
  46          // Create a course and instance of mod_assign.
  47          $course = $this->getDataGenerator()->create_course();
  48  
  49          $assigndata['course'] = $course->id;
  50          $assigndata['name'] = 'lightwork assignment';
  51          $modinstance = self::getDataGenerator()->create_module('assign', $assigndata);
  52  
  53          // Function grade_update_mod_grades() requires 2 additional properties, cmidnumber and modname.
  54          $cm = get_coursemodule_from_instance('assign', $modinstance->id, 0, false, MUST_EXIST);
  55          $modinstance->cmidnumber = $cm->id;
  56          $modinstance->modname = 'assign';
  57  
  58          $this->assertTrue(grade_update_mod_grades($modinstance));
  59      }
  60  
  61      /**
  62       * Tests the function remove_grade_letters().
  63       */
  64      public function test_remove_grade_letters() {
  65          global $DB;
  66  
  67          $this->resetAfterTest();
  68  
  69          $course = $this->getDataGenerator()->create_course();
  70  
  71          $context = \context_course::instance($course->id);
  72  
  73          // Add a grade letter to the course.
  74          $letter = new \stdClass();
  75          $letter->letter = 'M';
  76          $letter->lowerboundary = '100';
  77          $letter->contextid = $context->id;
  78          $DB->insert_record('grade_letters', $letter);
  79  
  80          // Pre-warm the cache, ensure that that the letter is cached.
  81          $cache = \cache::make('core', 'grade_letters');
  82  
  83          // Check that the cache is empty beforehand.
  84          $letters = $cache->get($context->id);
  85          $this->assertFalse($letters);
  86  
  87          // Call the function.
  88          grade_get_letters($context);
  89  
  90          $letters = $cache->get($context->id);
  91          $this->assertEquals(1, count($letters));
  92          $this->assertTrue(in_array($letter->letter, $letters));
  93  
  94          remove_grade_letters($context, false);
  95  
  96          // Confirm grade letter was deleted.
  97          $this->assertEquals(0, $DB->count_records('grade_letters'));
  98  
  99          // Confirm grade letter is also deleted from cache.
 100          $letters = $cache->get($context->id);
 101          $this->assertFalse($letters);
 102      }
 103  
 104      /**
 105       * Tests the function grade_course_category_delete().
 106       */
 107      public function test_grade_course_category_delete() {
 108          global $DB;
 109  
 110          $this->resetAfterTest();
 111  
 112          $category = \core_course_category::create(array('name' => 'Cat1'));
 113  
 114          // Add a grade letter to the category.
 115          $letter = new \stdClass();
 116          $letter->letter = 'M';
 117          $letter->lowerboundary = '100';
 118          $letter->contextid = \context_coursecat::instance($category->id)->id;
 119          $DB->insert_record('grade_letters', $letter);
 120  
 121          grade_course_category_delete($category->id, '', false);
 122  
 123          // Confirm grade letter was deleted.
 124          $this->assertEquals(0, $DB->count_records('grade_letters'));
 125      }
 126  
 127      /**
 128       * Tests the function grade_regrade_final_grades().
 129       */
 130      public function test_grade_regrade_final_grades() {
 131          global $DB;
 132  
 133          $this->resetAfterTest();
 134  
 135          // Setup some basics.
 136          $course = $this->getDataGenerator()->create_course();
 137          $user = $this->getDataGenerator()->create_user();
 138          $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
 139  
 140          // We need two grade items.
 141          $params = ['idnumber' => 'g1', 'courseid' => $course->id];
 142          $g1 = new \grade_item($this->getDataGenerator()->create_grade_item($params));
 143          unset($params['idnumber']);
 144          $g2 = new \grade_item($this->getDataGenerator()->create_grade_item($params));
 145  
 146          $category = new \grade_category($this->getDataGenerator()->create_grade_category($params));
 147          $catitem = $category->get_grade_item();
 148  
 149          // Now set a calculation.
 150          $catitem->set_calculation('=[[g1]]');
 151  
 152          $catitem->update();
 153  
 154          // Everything needs updating.
 155          $this->assertEquals(4, $DB->count_records('grade_items', ['courseid' => $course->id, 'needsupdate' => 1]));
 156  
 157          grade_regrade_final_grades($course->id);
 158  
 159          // See that everything has been updated.
 160          $this->assertEquals(0, $DB->count_records('grade_items', ['courseid' => $course->id, 'needsupdate' => 1]));
 161  
 162          $g1->delete();
 163  
 164          // Now there is one that needs updating.
 165          $this->assertEquals(1, $DB->count_records('grade_items', ['courseid' => $course->id, 'needsupdate' => 1]));
 166  
 167          // This can cause an infinite loop if things go... poorly.
 168          grade_regrade_final_grades($course->id);
 169  
 170          // Now because of the failure, two things need updating.
 171          $this->assertEquals(2, $DB->count_records('grade_items', ['courseid' => $course->id, 'needsupdate' => 1]));
 172      }
 173  
 174      /**
 175       * Tests for the grade_get_date_for_user_grade function.
 176       *
 177       * @dataProvider grade_get_date_for_user_grade_provider
 178       * @param \stdClass $grade
 179       * @param \stdClass $user
 180       * @param int $expected
 181       */
 182      public function test_grade_get_date_for_user_grade(\stdClass $grade, \stdClass $user, ?int $expected): void {
 183          $this->assertEquals($expected, grade_get_date_for_user_grade($grade, $user));
 184      }
 185  
 186      /**
 187       * Data provider for tests of the grade_get_date_for_user_grade function.
 188       *
 189       * @return array
 190       */
 191      public function grade_get_date_for_user_grade_provider(): array {
 192          $u1 = (object) [
 193              'id' => 42,
 194          ];
 195          $u2 = (object) [
 196              'id' => 930,
 197          ];
 198  
 199          $d1 = 1234567890;
 200          $d2 = 9876543210;
 201  
 202          $g1 = (object) [
 203              'usermodified' => $u1->id,
 204              'dategraded' => $d1,
 205              'datesubmitted' => $d2,
 206          ];
 207          $g2 = (object) [
 208              'usermodified' => $u1->id,
 209              'dategraded' => $d1,
 210              'datesubmitted' => 0,
 211          ];
 212  
 213          $g3 = (object) [
 214              'usermodified' => $u1->id,
 215              'dategraded' => null,
 216              'datesubmitted' => $d2,
 217          ];
 218  
 219          return [
 220              'If the user is the last person to have modified the grade_item then show the date that it was graded' => [
 221                  $g1,
 222                  $u1,
 223                  $d1,
 224              ],
 225              'If there is no grade and there is no feedback, then show graded date as null' => [
 226                  $g3,
 227                  $u1,
 228                  null,
 229              ],
 230              'If the user is not the last person to have modified the grade_item, ' .
 231              'and there is no submission date, then show the date that it was submitted' => [
 232                  $g1,
 233                  $u2,
 234                  $d2,
 235              ],
 236              'If the user is not the last person to have modified the grade_item, ' .
 237              'but there is no submission date, then show the date that it was graded' => [
 238                  $g2,
 239                  $u2,
 240                  $d1,
 241              ],
 242              'If the user is the last person to have modified the grade_item, ' .
 243              'and there is no submission date, then still show the date that it was graded' => [
 244                  $g2,
 245                  $u1,
 246                  $d1,
 247              ],
 248          ];
 249      }
 250  
 251      /**
 252       * Test the caching of grade letters.
 253       */
 254      public function test_get_grade_letters() {
 255  
 256          $this->resetAfterTest();
 257  
 258          // Setup some basics.
 259          $course = $this->getDataGenerator()->create_course();
 260          $context = \context_course::instance($course->id);
 261  
 262          $cache = \cache::make('core', 'grade_letters');
 263          $letters = $cache->get($context->id);
 264  
 265          // Make sure the cache is empty.
 266          $this->assertFalse($letters);
 267  
 268          // Now check to see if the letters get cached.
 269          $actual = grade_get_letters($context);
 270  
 271          $expected = $cache->get($context->id);
 272  
 273          $this->assertEquals($expected, $actual);
 274      }
 275  
 276      /**
 277       * Test custom letters.
 278       */
 279      public function test_get_grade_letters_custom() {
 280          global $DB;
 281  
 282          $this->resetAfterTest();
 283  
 284          $course = $this->getDataGenerator()->create_course();
 285          $context = \context_course::instance($course->id);
 286  
 287          $cache = \cache::make('core', 'grade_letters');
 288          $letters = $cache->get($context->id);
 289  
 290          // Make sure the cache is empty.
 291          $this->assertFalse($letters);
 292  
 293          // Add a grade letter to the course.
 294          $letter = new \stdClass();
 295          $letter->letter = 'M';
 296          $letter->lowerboundary = '100';
 297          $letter->contextid = $context->id;
 298          $DB->insert_record('grade_letters', $letter);
 299  
 300          $actual = grade_get_letters($context);
 301          $expected = $cache->get($context->id);
 302  
 303          $this->assertEquals($expected, $actual);
 304      }
 305  
 306      /**
 307       * When getting a calculated grade containing an error, we mark grading finished and don't keep trying to regrade.
 308       *
 309       * @covers \grade_get_grades()
 310       * @return void
 311       */
 312      public function test_grade_get_grades_errors() {
 313          $this->resetAfterTest();
 314  
 315          // Setup some basics.
 316          $course = $this->getDataGenerator()->create_course();
 317          $user1 = $this->getDataGenerator()->create_user();
 318          $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student');
 319          $user2 = $this->getDataGenerator()->create_user();
 320          $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'editingteacher');
 321          // Set up 2 gradeable activities.
 322          $assign = $this->getDataGenerator()->create_module('assign', ['idnumber' => 'a1', 'course' => $course->id]);
 323          $quiz = $this->getDataGenerator()->create_module('quiz', ['idnumber' => 'q1', 'course' => $course->id]);
 324  
 325          // Create a calculated grade item using the activities.
 326          $params = ['courseid' => $course->id];
 327          $g1 = new \grade_item($this->getDataGenerator()->create_grade_item($params));
 328          $g1->set_calculation('=[[a1]] + [[q1]]');
 329  
 330          // Now delete one of the activities to break the calculation.
 331          course_delete_module($assign->cmid);
 332  
 333          // Course grade item has needsupdate.
 334          $this->assertEquals(1, \grade_item::fetch_course_item($course->id)->needsupdate);
 335  
 336          // Get grades for the quiz, to trigger a regrade.
 337          $this->setUser($user2);
 338          $grades1 = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id);
 339          // We should get an error for the broken calculation.
 340          $this->assertNotEmpty($grades1->errors);
 341          $this->assertEquals(get_string('errorcalculationbroken', 'grades', $g1->itemname), reset($grades1->errors));
 342          // Course grade item should not have needsupdate so that we don't try to regrade again.
 343          $this->assertEquals(0, \grade_item::fetch_course_item($course->id)->needsupdate);
 344  
 345          // Get grades for the quiz again. This should not trigger the regrade and resulting error this time.
 346          $grades2 = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id);
 347          $this->assertEmpty($grades2->errors);
 348      }
 349  }