Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 311 and 401] [Versions 401 and 402] [Versions 401 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  namespace core;
  18  
  19  use core_grades_external;
  20  
  21  defined('MOODLE_INTERNAL') || die();
  22  
  23  global $CFG;
  24  
  25  require_once($CFG->dirroot . '/webservice/tests/helpers.php');
  26  
  27  /**
  28   * Grades functions unit tests
  29   *
  30   * Unit tests for the grade API at /lib/classes/grades_external.php
  31   *
  32   * @package core
  33   * @category test
  34   * @copyright 2012 Andrew Davis
  35   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   */
  37  class grades_external_test extends \externallib_advanced_testcase {
  38  
  39      /**
  40       * Load initial test information
  41       *
  42       * @param  string $assignmentname   Assignment name
  43       * @param  int $student1rawgrade    Student 1 grade
  44       * @param  int $student2rawgrade    Student 2 grade
  45       * @return array                    Array of vars with test information
  46       */
  47      protected function load_test_data($assignmentname, $student1rawgrade, $student2rawgrade) {
  48          global $DB;
  49  
  50          // Adds a course, a teacher, 2 students, an assignment and grades for the students.
  51          $course = $this->getDataGenerator()->create_course();
  52          $coursecontext = \context_course::instance($course->id);
  53  
  54          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
  55  
  56          $student1 = $this->getDataGenerator()->create_user();
  57          $this->getDataGenerator()->enrol_user($student1->id, $course->id, $studentrole->id);
  58  
  59          $student2 = $this->getDataGenerator()->create_user();
  60          $this->getDataGenerator()->enrol_user($student2->id, $course->id, $studentrole->id);
  61  
  62          $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
  63          $teacher = $this->getDataGenerator()->create_user();
  64          $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
  65  
  66          $parent = $this->getDataGenerator()->create_user();
  67          $this->setUser($parent);
  68          $student1context = \context_user::instance($student1->id);
  69          // Creates a new role, gives it the capability and gives $USER that role.
  70          $parentroleid = $this->assignUserCapability('moodle/grade:viewall', $student1context->id);
  71          // Enrol the user in the course using the new role.
  72          $this->getDataGenerator()->enrol_user($parent->id, $course->id, $parentroleid);
  73  
  74          $assignment = $this->getDataGenerator()->create_module('assign', array('name' => $assignmentname, 'course' => $course->id));
  75          $modcontext = get_coursemodule_from_instance('assign', $assignment->id, $course->id);
  76          $assignment->cmidnumber = $modcontext->id;
  77  
  78          $student1grade = array('userid' => $student1->id, 'rawgrade' => $student1rawgrade);
  79          $student2grade = array('userid' => $student2->id, 'rawgrade' => $student2rawgrade);
  80          $studentgrades = array($student1->id => $student1grade, $student2->id => $student2grade);
  81          assign_grade_item_update($assignment, $studentgrades);
  82  
  83          // Insert a custom grade scale to be used by an outcome.
  84          $gradescale = new \grade_scale();
  85          $gradescale->name        = 'unittestscale3';
  86          $gradescale->courseid    = $course->id;
  87          $gradescale->userid      = 0;
  88          $gradescale->scale       = 'Distinction, Very Good, Good, Pass, Fail';
  89          $gradescale->description = 'This scale is used to mark standard assignments.';
  90          $gradescale->insert();
  91  
  92          // Insert an outcome.
  93          $data = new \stdClass();
  94          $data->courseid = $course->id;
  95          $data->fullname = 'Team work';
  96          $data->shortname = 'Team work';
  97          $data->scaleid = $gradescale->id;
  98          $outcome = new \grade_outcome($data, false);
  99          $outcome->insert();
 100  
 101          $outcomegradeitem = new \grade_item();
 102          $outcomegradeitem->itemname = $outcome->shortname;
 103          $outcomegradeitem->itemtype = 'mod';
 104          $outcomegradeitem->itemmodule = 'assign';
 105          $outcomegradeitem->iteminstance = $assignment->id;
 106          $outcomegradeitem->outcomeid = $outcome->id;
 107          $outcomegradeitem->cmid = 0;
 108          $outcomegradeitem->courseid = $course->id;
 109          $outcomegradeitem->aggregationcoef = 0;
 110          $outcomegradeitem->itemnumber = 1; // The activity's original grade item will be 0.
 111          $outcomegradeitem->gradetype = GRADE_TYPE_SCALE;
 112          $outcomegradeitem->scaleid = $outcome->scaleid;
 113          // This next two values for testing that returns parameters are correcly formatted.
 114          $outcomegradeitem->set_locked(true);
 115          $outcomegradeitem->hidden = '';
 116          $outcomegradeitem->insert();
 117  
 118          $assignmentgradeitem = \grade_item::fetch(
 119              array(
 120                  'itemtype' => 'mod',
 121                  'itemmodule' => 'assign',
 122                  'iteminstance' => $assignment->id,
 123                  'itemnumber' => 0,
 124                  'courseid' => $course->id
 125              )
 126          );
 127          $outcomegradeitem->set_parent($assignmentgradeitem->categoryid);
 128          $outcomegradeitem->move_after_sortorder($assignmentgradeitem->sortorder);
 129  
 130          return array($course, $assignment, $student1, $student2, $teacher, $parent);
 131      }
 132  
 133      /**
 134       * Test update_grades()
 135       */
 136      public function test_update_grades() {
 137          global $DB;
 138  
 139          $this->resetAfterTest(true);
 140  
 141          $assignmentname = 'The assignment';
 142          $student1rawgrade = 10;
 143          $student2rawgrade = 20;
 144          list($course, $assignment, $student1, $student2, $teacher, $parent) =
 145              $this->load_test_data($assignmentname, $student1rawgrade, $student2rawgrade);
 146          $assigmentcm = get_coursemodule_from_id('assign', $assignment->cmid, 0, false, MUST_EXIST);
 147  
 148          $this->setUser($teacher);
 149  
 150          // Teacher updating grade item information.
 151          $changedmax = 93;
 152          $result = core_grades_external::update_grades(
 153              'test',
 154              $course->id,
 155              'mod_assign',
 156              $assigmentcm->id,
 157              0,
 158              array(),
 159              array('grademax' => $changedmax)
 160          );
 161          $result = \external_api::clean_returnvalue(core_grades_external::update_grades_returns(), $result);
 162          $this->assertTrue($result == GRADE_UPDATE_OK);
 163          $grades = grade_get_grades($course->id, 'mod', 'assign', $assignment->id);
 164          $this->assertTrue($grades->items[0]->grademax == $changedmax);
 165  
 166          // Teacher updating 1 student grade.
 167          $student1grade = 23;
 168          $result = core_grades_external::update_grades(
 169              'test',
 170              $course->id,
 171              'mod_assign',
 172              $assigmentcm->id,
 173              0,
 174              array(array('studentid' => $student1->id, 'grade' => $student1grade))
 175          );
 176          $result = \external_api::clean_returnvalue(core_grades_external::update_grades_returns(), $result);
 177          $this->assertTrue($result == GRADE_UPDATE_OK);
 178          $grades = grade_get_grades($course->id, 'mod', 'assign', $assignment->id, array($student1->id));
 179          $this->assertTrue($grades->items[0]->grades[$student1->id]->grade == $student1grade);
 180  
 181          // Teacher updating multiple student grades.
 182          $student1grade = 11;
 183          $student2grade = 13;
 184          $result = core_grades_external::update_grades(
 185              'test',
 186              $course->id,
 187              'mod_assign',
 188              $assigmentcm->id,
 189              0,
 190              array(
 191                  array('studentid' => $student1->id, 'grade' => $student1grade),
 192                  array('studentid' => $student2->id, 'grade' => $student2grade)
 193              )
 194          );
 195          $result = \external_api::clean_returnvalue(core_grades_external::update_grades_returns(), $result);
 196          $this->assertTrue($result == GRADE_UPDATE_OK);
 197          $grades = grade_get_grades($course->id, 'mod', 'assign', $assignment->id, array($student1->id, $student2->id));
 198          $this->assertTrue($grades->items[0]->grades[$student1->id]->grade == $student1grade);
 199          $this->assertTrue($grades->items[0]->grades[$student2->id]->grade == $student2grade);
 200  
 201          // Student attempting to update their own grade (should fail).
 202          $this->setUser($student1);
 203          try {
 204              $student1grade = 17;
 205              $result = core_grades_external::update_grades(
 206                  'test',
 207                  $course->id,
 208                  'mod_assign',
 209                  $assigmentcm->id,
 210                  0,
 211                  array( array('studentid' => $student1->id, 'grade' => $student1grade))
 212              );
 213              $this->fail('moodle_exception expected');
 214          } catch (\moodle_exception $ex) {
 215              $this->assertTrue(true);
 216          }
 217  
 218          // Parent attempting to update their child's grade (should fail).
 219          $this->setUser($parent);
 220          try {
 221              $student1grade = 13;
 222              $result = core_grades_external::update_grades(
 223                  'test',
 224                  $course->id,
 225                  'mod_assign',
 226                  $assigmentcm->id,
 227                  0,
 228                  array( array('studentid' => $student1->id, 'grade' => $student1grade))
 229              );
 230              $this->fail('moodle_exception expected');
 231          } catch (\moodle_exception $ex) {
 232              $this->assertTrue(true);
 233          }
 234  
 235          // Student trying to hide a grade item (should fail).
 236          $this->setUser($student1);
 237          try {
 238              $result = core_grades_external::update_grades(
 239                  'test',
 240                  $course->id,
 241                  'mod_assign',
 242                  $assigmentcm->id,
 243                  0,
 244                  array(),
 245                  array('hidden' => 1)
 246              );
 247              $this->fail('moodle_exception expected');
 248          } catch (\moodle_exception $ex) {
 249              $this->assertTrue(true);
 250          }
 251  
 252          // Give the student role 'moodle/grade:hide' and they should now be able to hide the grade item.
 253          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
 254          $coursecontext = \context_course::instance($course->id);
 255          assign_capability('moodle/grade:hide', CAP_ALLOW, $studentrole->id, $coursecontext->id);
 256          accesslib_clear_all_caches_for_unit_testing();
 257  
 258          // Check the activity isn't already hidden.
 259          $grades = grade_get_grades($course->id, 'mod', 'assign', $assignment->id);
 260          $this->assertTrue($grades->items[0]->hidden == 0);
 261  
 262          $result = core_grades_external::update_grades(
 263              'test',
 264              $course->id,
 265              'mod_assign',
 266              $assigmentcm->id,
 267              0,
 268              array(),
 269              array('hidden' => 1)
 270          );
 271          $result = \external_api::clean_returnvalue(core_grades_external::update_grades_returns(), $result);
 272          $this->assertTrue($result == GRADE_UPDATE_OK);
 273          $grades = grade_get_grades($course->id, 'mod', 'assign', $assignment->id);
 274          $this->assertTrue($grades->items[0]->hidden == 1);
 275      }
 276  
 277      /**
 278       * Test create_gradecategory.
 279       *
 280       * @return void
 281       */
 282      public function test_create_gradecategory() {
 283          global $DB;
 284          $this->resetAfterTest(true);
 285          $course = $this->getDataGenerator()->create_course();
 286          $this->setAdminUser();
 287  
 288          // Test the most basic gradecategory creation.
 289          $status1 = core_grades_external::create_gradecategory($course->id, 'Test Category 1', []);
 290  
 291          $courseparentcat = new \grade_category(['courseid' => $course->id, 'depth' => 1], true);
 292          $record1 = $DB->get_record('grade_categories', ['id' => $status1['categoryid']]);
 293          $this->assertEquals('Test Category 1', $record1->fullname);
 294          // Confirm that the parent category for this category is the top level category for the course.
 295          $this->assertEquals($courseparentcat->id, $record1->parent);
 296          $this->assertEquals(2, $record1->depth);
 297  
 298          // Now create a category as a child of the newly created category.
 299          $status2 = core_grades_external::create_gradecategory($course->id, 'Test Category 2', ['parentcategoryid' => $record1->id]);
 300          $record2 = $DB->get_record('grade_categories', ['id' => $status2['categoryid']]);
 301          $this->assertEquals($record1->id, $record2->parent);
 302          $this->assertEquals(3, $record2->depth);
 303          // Check the path is correct.
 304          $this->assertEquals('/' . implode('/', [$courseparentcat->id, $record1->id, $record2->id]) . '/', $record2->path);
 305  
 306          // Now create a category with some customised data and check the returns. This customises every value.
 307          $customopts = [
 308              'aggregation' => GRADE_AGGREGATE_MEAN,
 309              'aggregateonlygraded' => 0,
 310              'aggregateoutcomes' => 1,
 311              'droplow' => 1,
 312              'itemname' => 'item',
 313              'iteminfo' => 'info',
 314              'idnumber' => 'idnumber',
 315              'gradetype' => GRADE_TYPE_TEXT,
 316              'grademax' => 5,
 317              'grademin' => 2,
 318              'gradepass' => 3,
 319              'display' => GRADE_DISPLAY_TYPE_LETTER,
 320              // Hack. This must be -2 to use the default setting.
 321              'decimals' => 3,
 322              'hiddenuntil' => time(),
 323              'locktime' => time(),
 324              'weightoverride' => 1,
 325              'aggregationcoef2' => 20,
 326              'parentcategoryid' => $record2->id
 327          ];
 328  
 329          $status3 = core_grades_external::create_gradecategory($course->id, 'Test Category 3', $customopts);
 330          $cat3 = new \grade_category(['courseid' => $course->id, 'id' => $status3['categoryid']], true);
 331          $cat3->load_grade_item();
 332  
 333          // Lets check all of the data is in the right shape.
 334          $this->assertEquals(GRADE_AGGREGATE_MEAN, $cat3->aggregation);
 335          $this->assertEquals(0, $cat3->aggregateonlygraded);
 336          $this->assertEquals(1, $cat3->aggregateoutcomes);
 337          $this->assertEquals(1, $cat3->droplow);
 338          $this->assertEquals('item', $cat3->grade_item->itemname);
 339          $this->assertEquals('info', $cat3->grade_item->iteminfo);
 340          $this->assertEquals('idnumber', $cat3->grade_item->idnumber);
 341          $this->assertEquals(GRADE_TYPE_TEXT, $cat3->grade_item->gradetype);
 342          $this->assertEquals(5, $cat3->grade_item->grademax);
 343          $this->assertEquals(2, $cat3->grade_item->grademin);
 344          $this->assertEquals(3, $cat3->grade_item->gradepass);
 345          $this->assertEquals(GRADE_DISPLAY_TYPE_LETTER, $cat3->grade_item->display);
 346          $this->assertEquals(3, $cat3->grade_item->decimals);
 347          $this->assertGreaterThanOrEqual($cat3->grade_item->hidden, time());
 348          $this->assertGreaterThanOrEqual($cat3->grade_item->locktime, time());
 349          $this->assertEquals(1, $cat3->grade_item->weightoverride);
 350          // Coefficient is converted to percentage.
 351          $this->assertEquals(0.2, $cat3->grade_item->aggregationcoef2);
 352          $this->assertEquals($record2->id, $cat3->parent);
 353      }
 354  
 355  }