Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 39 and 311]

   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   * mod_h5pactivity grader tests
  19   *
  20   * @package    mod_h5pactivity
  21   * @category   test
  22   * @copyright  2020 Ferran Recio <ferran@moodle.com>
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  namespace mod_h5pactivity\local;
  27  
  28  use grade_item;
  29  use stdClass;
  30  
  31  /**
  32   * Grader tests class for mod_h5pactivity.
  33   *
  34   * @package    mod_h5pactivity
  35   * @category   test
  36   * @copyright  2020 Ferran Recio <ferran@moodle.com>
  37   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   */
  39  class grader_test extends \advanced_testcase {
  40  
  41      /**
  42       * Setup to ensure that fixtures are loaded.
  43       */
  44      public static function setupBeforeClass(): void {
  45          global $CFG;
  46          require_once($CFG->libdir.'/gradelib.php');
  47      }
  48  
  49      /**
  50       * Test for grade item delete.
  51       */
  52      public function test_grade_item_delete() {
  53  
  54          $this->resetAfterTest();
  55          $this->setAdminUser();
  56  
  57          $course = $this->getDataGenerator()->create_course();
  58          $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
  59          $user = $this->getDataGenerator()->create_and_enrol($course, 'student');
  60  
  61          $grader = new grader($activity);
  62  
  63          // Force a user grade.
  64          $this->generate_fake_attempt($activity, $user, 5, 10);
  65          $grader->update_grades($user->id);
  66  
  67          $gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, $user->id);
  68          $this->assertNotEquals(0, count($gradeinfo->items));
  69          $this->assertArrayHasKey($user->id, $gradeinfo->items[0]->grades);
  70  
  71          $grader->grade_item_delete();
  72  
  73          $gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, $user->id);
  74          $this->assertEquals(0, count($gradeinfo->items));
  75      }
  76  
  77      /**
  78       * Test for grade item update.
  79       *
  80       * @dataProvider grade_item_update_data
  81       * @param int $newgrade new activity grade
  82       * @param bool $reset if has to reset grades
  83       * @param string $idnumber the new idnumber
  84       */
  85      public function test_grade_item_update(int $newgrade, bool $reset, string $idnumber) {
  86  
  87          $this->resetAfterTest();
  88          $this->setAdminUser();
  89  
  90          $course = $this->getDataGenerator()->create_course();
  91          $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
  92          $user = $this->getDataGenerator()->create_and_enrol($course, 'student');
  93  
  94          // Force a user initial grade.
  95          $grader = new grader($activity);
  96          $this->generate_fake_attempt($activity, $user, 5, 10);
  97          $grader->update_grades($user->id);
  98  
  99          $gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, $user->id);
 100          $this->assertNotEquals(0, count($gradeinfo->items));
 101          $item = array_shift($gradeinfo->items);
 102          $this->assertArrayHasKey($user->id, $item->grades);
 103          $this->assertEquals(50, round($item->grades[$user->id]->grade));
 104  
 105          // Module grade value determine the way gradebook acts. That means that the expected
 106          // result depends on this value.
 107          // - Grade > 0: regular max grade value.
 108          // - Grade = 0: no grading is used (but grademax remains the same).
 109          // - Grade < 0: a scaleid is used (value = -scaleid).
 110          if ($newgrade > 0) {
 111              $grademax = $newgrade;
 112              $scaleid = null;
 113              $usergrade = ($newgrade > 50) ? 50 : $newgrade;
 114          } else if ($newgrade == 0) {
 115              $grademax = 100;
 116              $scaleid = null;
 117              $usergrade = null; // No user grades expected.
 118          } else if ($newgrade < 0) {
 119              $scale = $this->getDataGenerator()->create_scale(array("scale" => "value1, value2, value3"));
 120              $newgrade = -1 * $scale->id;
 121              $grademax = 3;
 122              $scaleid = $scale->id;
 123              $usergrade = 3; // 50 value will ve converted to "value 3" on scale.
 124          }
 125  
 126          // Update grade item.
 127          $activity->grade = $newgrade;
 128  
 129          // In case a reset is need, usergrade will be empty.
 130          if ($reset) {
 131              $param = 'reset';
 132              $usergrade = null;
 133          } else {
 134              // Individual user gradings will be tested as a subcall of update_grades.
 135              $param = null;
 136          }
 137  
 138          $grader = new grader($activity, $idnumber);
 139          $grader->grade_item_update($param);
 140  
 141          // Check new grade item and grades.
 142          $gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, $user->id);
 143          $item = array_shift($gradeinfo->items);
 144          $this->assertEquals($scaleid, $item->scaleid);
 145          $this->assertEquals($grademax, $item->grademax);
 146          $this->assertArrayHasKey($user->id, $item->grades);
 147          if ($usergrade) {
 148              $this->assertEquals($usergrade, round($item->grades[$user->id]->grade));
 149          } else {
 150              $this->assertEmpty($item->grades[$user->id]->grade);
 151          }
 152          if (!empty($idnumber)) {
 153              $gradeitem = grade_item::fetch(['idnumber' => $idnumber, 'courseid' => $course->id]);
 154              $this->assertInstanceOf('grade_item', $gradeitem);
 155          }
 156      }
 157  
 158      /**
 159       * Data provider for test_grade_item_update.
 160       *
 161       * @return array
 162       */
 163      public function grade_item_update_data(): array {
 164          return [
 165              'Change idnumber' => [
 166                  100, false, 'newidnumber'
 167              ],
 168              'Increase max grade to 110' => [
 169                  110, false, ''
 170              ],
 171              'Decrease max grade to 80' => [
 172                  40, false, ''
 173              ],
 174              'Decrease max grade to 40 (less than actual grades)' => [
 175                  40, false, ''
 176              ],
 177              'Reset grades' => [
 178                  100, true, ''
 179              ],
 180              'Disable grades' => [
 181                  0, false, ''
 182              ],
 183              'Use scales' => [
 184                  -1, false, ''
 185              ],
 186              'Use scales with reset' => [
 187                  -1, true, ''
 188              ],
 189          ];
 190      }
 191  
 192      /**
 193       * Test for grade update.
 194       *
 195       * @dataProvider update_grades_data
 196       * @param int $newgrade the new activity grade
 197       * @param bool $all if has to be applied to all students or just to one
 198       * @param int $completion 1 all student have the activity completed, 0 one have incompleted
 199       * @param array $results expected results (user1 grade, user2 grade)
 200       */
 201      public function test_update_grades(int $newgrade, bool $all, int $completion, array $results) {
 202  
 203          $this->resetAfterTest();
 204          $this->setAdminUser();
 205  
 206          $course = $this->getDataGenerator()->create_course();
 207          $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
 208          $user1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
 209          $user2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
 210  
 211          // Force a user initial grade.
 212          $grader = new grader($activity);
 213          $this->generate_fake_attempt($activity, $user1, 5, 10);
 214          $this->generate_fake_attempt($activity, $user2, 3, 12, $completion);
 215          $grader->update_grades();
 216  
 217          $gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, [$user1->id, $user2->id]);
 218          $this->assertNotEquals(0, count($gradeinfo->items));
 219          $item = array_shift($gradeinfo->items);
 220          $this->assertArrayHasKey($user1->id, $item->grades);
 221          $this->assertArrayHasKey($user2->id, $item->grades);
 222          $this->assertEquals(50, $item->grades[$user1->id]->grade);
 223          // Uncompleted attempts does not generate grades.
 224          if ($completion) {
 225              $this->assertEquals(25, $item->grades[$user2->id]->grade);
 226          } else {
 227              $this->assertNull($item->grades[$user2->id]->grade);
 228  
 229          }
 230  
 231          // Module grade value determine the way gradebook acts. That means that the expected
 232          // result depends on this value.
 233          // - Grade > 0: regular max grade value.
 234          // - Grade <= 0: no grade calculation is used (scale and no grading).
 235          if ($newgrade < 0) {
 236              $scale = $this->getDataGenerator()->create_scale(array("scale" => "value1, value2, value3"));
 237              $activity->grade = -1 * $scale->id;
 238          } else {
 239              $activity->grade = $newgrade;
 240          }
 241  
 242          $userid = ($all) ? 0 : $user1->id;
 243  
 244          $grader = new grader($activity);
 245          $grader->update_grades($userid);
 246  
 247          // Check new grade item and grades.
 248          $gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, [$user1->id, $user2->id]);
 249          $item = array_shift($gradeinfo->items);
 250          $this->assertArrayHasKey($user1->id, $item->grades);
 251          $this->assertArrayHasKey($user2->id, $item->grades);
 252          $this->assertEquals($results[0], $item->grades[$user1->id]->grade);
 253          $this->assertEquals($results[1], $item->grades[$user2->id]->grade);
 254      }
 255  
 256      /**
 257       * Data provider for test_grade_item_update.
 258       *
 259       * @return array
 260       */
 261      public function update_grades_data(): array {
 262          return [
 263              // Quantitative grade, all attempts completed.
 264              'Same grademax, all users, all completed' => [
 265                  100, true, 1, [50, 25]
 266              ],
 267              'Same grademax, one user, all completed' => [
 268                  100, false, 1, [50, 25]
 269              ],
 270              'Increade max, all users, all completed' => [
 271                  200, true, 1, [100, 50]
 272              ],
 273              'Increade max, one user, all completed' => [
 274                  200, false, 1, [100, 25]
 275              ],
 276              'Decrease max, all users, all completed' => [
 277                  50, true, 1, [25, 12.5]
 278              ],
 279              'Decrease max, one user, all completed' => [
 280                  50, false, 1, [25, 25]
 281              ],
 282              // Quantitative grade, some attempts not completed.
 283              'Same grademax, all users, not completed' => [
 284                  100, true, 0, [50, null]
 285              ],
 286              'Same grademax, one user, not completed' => [
 287                  100, false, 0, [50, null]
 288              ],
 289              'Increade max, all users, not completed' => [
 290                  200, true, 0, [100, null]
 291              ],
 292              'Increade max, one user, not completed' => [
 293                  200, false, 0, [100, null]
 294              ],
 295              'Decrease max, all users, not completed' => [
 296                  50, true, 0, [25, null]
 297              ],
 298              'Decrease max, one user, not completed' => [
 299                  50, false, 0, [25, null]
 300              ],
 301              // No grade (no grade will be used).
 302              'No grade, all users, all completed' => [
 303                  0, true, 1, [null, null]
 304              ],
 305              'No grade, one user, all completed' => [
 306                  0, false, 1, [null, null]
 307              ],
 308              'No grade, all users, not completed' => [
 309                  0, true, 0, [null, null]
 310              ],
 311              'No grade, one user, not completed' => [
 312                  0, false, 0, [null, null]
 313              ],
 314              // Scale (grate item will updated but without regrading).
 315              'Scale, all users, all completed' => [
 316                  -1, true, 1, [3, 3]
 317              ],
 318              'Scale, one user, all completed' => [
 319                  -1, false, 1, [3, 3]
 320              ],
 321              'Scale, all users, not completed' => [
 322                  -1, true, 0, [3, null]
 323              ],
 324              'Scale, one user, not completed' => [
 325                  -1, false, 0, [3, null]
 326              ],
 327          ];
 328      }
 329  
 330      /**
 331       * Create a fake attempt for a specific user.
 332       *
 333       * @param stdClass $activity activity instance record.
 334       * @param stdClass $user user record
 335       * @param int $rawscore score obtained
 336       * @param int $maxscore attempt max score
 337       * @param int $completion 1 for activity completed, 0 for not completed yet
 338       * @return stdClass the attempt record
 339       */
 340      private function generate_fake_attempt(stdClass $activity, stdClass $user,
 341              int $rawscore, int $maxscore, int $completion = 1): stdClass {
 342          global $DB;
 343  
 344          $attempt = (object)[
 345              'h5pactivityid' => $activity->id,
 346              'userid' => $user->id,
 347              'timecreated' => 10,
 348              'timemodified' => 20,
 349              'attempt' => 1,
 350              'rawscore' => $rawscore,
 351              'maxscore' => $maxscore,
 352              'duration' => 2,
 353              'completion' => $completion,
 354              'success' => 0,
 355          ];
 356          $attempt->scaled = $attempt->rawscore / $attempt->maxscore;
 357          $attempt->id = $DB->insert_record('h5pactivity_attempts', $attempt);
 358          return $attempt;
 359      }
 360  }