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 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_competency;
  18  
  19  /**
  20   * Competency ruleoutcome override grade tests
  21   *
  22   * @package    core_competency
  23   * @copyright  2022 Matthew Hilton <matthewhilton@catalyst-au.net>
  24   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  25   */
  26  class competency_override_test extends \advanced_testcase {
  27  
  28      public function setUp(): void {
  29          $this->resetAfterTest(true);
  30          $this->setAdminUser();
  31          $dg = $this->getDataGenerator();
  32          $lpg = $dg->get_plugin_generator('core_competency');
  33  
  34          // Create user in course.
  35          $c1 = $dg->create_course((object) ['enablecompletion' => true]);
  36          $u1 = $dg->create_user();
  37          $dg->enrol_user($u1->id, $c1->id);
  38  
  39          // Create framework with three values.
  40          $scale = $dg->create_scale(["scale" => "not,partially,fully"]);
  41          $scaleconfiguration = json_encode([
  42              ['scaleid' => $scale->id],
  43              ['id' => 1, 'scaledefault' => 1, 'proficient' => 1]
  44          ]);
  45          $framework = $lpg->create_framework([
  46              'scaleid' => $scale->id,
  47              'scaleconfiguration' => $scaleconfiguration
  48          ]);
  49  
  50          $plan = $lpg->create_plan(['userid' => $u1->id]);
  51  
  52          $comp1 = $lpg->create_competency([
  53              'competencyframeworkid' => $framework->get('id'),
  54              'scaleid' => $scale->id,
  55              'scaleconfiguration' => $scaleconfiguration
  56          ]);
  57  
  58          $comp2 = $lpg->create_competency([
  59              'competencyframeworkid' => $framework->get('id'),
  60              'scaleid' => $scale->id,
  61              'scaleconfiguration' => $scaleconfiguration
  62          ]);
  63  
  64          api::add_competency_to_plan($plan->get('id'), $comp1->get('id'));
  65          api::add_competency_to_plan($plan->get('id'), $comp2->get('id'));
  66  
  67          $lpg->create_course_competency([
  68              'courseid' => $c1->id,
  69              'competencyid' => $comp1->get('id'),
  70              'ruleoutcome' => \core_competency\course_competency::OUTCOME_COMPLETE,
  71          ]);
  72  
  73          $lpg->create_course_competency([
  74              'courseid' => $c1->id,
  75              'competencyid' => $comp2->get('id'),
  76              'ruleoutcome' => \core_competency\course_competency::OUTCOME_COMPLETE,
  77          ]);
  78  
  79          $label = $dg->create_module('label', ['course' => $c1, 'completion' => COMPLETION_VIEWED, 'completionview' => 1]);
  80          $cm = get_coursemodule_from_instance('label', $label->id);
  81          $completion = new \completion_info($c1);
  82          $this->assertEquals(COMPLETION_ENABLED, $completion->is_enabled($cm));
  83  
  84          // Link course module with the competency and setup a rule to complete the competency when the module is completed.
  85          api::add_competency_to_course_module($cm, $comp1->get('id'));
  86          api::add_competency_to_course_module($cm, $comp2->get('id'));
  87  
  88          $coursemodulecomps = api::list_course_module_competencies_in_course_module($cm);
  89          $this->assertCount(2, $coursemodulecomps);
  90          api::set_course_module_competency_ruleoutcome($coursemodulecomps[0], \core_competency\course_competency::OUTCOME_COMPLETE);
  91          api::set_course_module_competency_ruleoutcome($coursemodulecomps[1], \core_competency\course_competency::OUTCOME_COMPLETE);
  92  
  93          $this->course = $c1;
  94          $this->user = $u1;
  95          $this->scale = $scale;
  96          $this->framework = $framework;
  97          $this->plan = $plan;
  98          $this->comp1 = $comp1;
  99          $this->comp2 = $comp2;
 100          $this->cm = $cm;
 101          $this->completion = new \completion_info($c1);
 102          $this->context = \context_course::instance($this->course->id);
 103      }
 104  
 105      /**
 106       * Test ruleoutcome overridegrade is correctly applied when coursemodule completion is processed.
 107       *
 108       * @covers \core_competency\api::set_course_module_competency_ruleoutcome
 109       */
 110      public function test_ruleoutcome_overridegrade(): void {
 111          // Initially the competency (and hence all the child competencies) should not be complete for the user.
 112          [$coursecomp, $plancomp, $usercomp] = $this->get_related_competencies($this->comp1->get('id'));
 113          $this->assertEquals(0, $plancomp->usercompetency->get('grade'));
 114          $this->assertEquals(0, $usercomp->get('grade'));
 115          $this->assertEquals(0, $coursecomp->get('grade'));
 116  
 117          [$coursecomp2, $plancomp2, $usercomp2] = $this->get_related_competencies($this->comp2->get('id'));
 118          $this->assertEquals(0, $plancomp2->usercompetency->get('grade'));
 119          $this->assertEquals(0, $usercomp2->get('grade'));
 120          $this->assertEquals(0, $coursecomp2->get('grade'));
 121  
 122          // Update the course module completion state to complete and trigger a competency update.
 123          $data = $this->completion->get_data($this->cm, false, $this->user->id);
 124          $data->completionstate = COMPLETION_COMPLETE;
 125          $data->timemodified = time();
 126          $this->completion->internal_set_data($this->cm, $data);
 127  
 128          // Comptency should now be complete for user, plan, and course now that the course module is completed.
 129          [$coursecomp, $plancomp, $usercomp] = $this->get_related_competencies($this->comp1->get('id'));
 130          $this->assertEquals(1, $plancomp->usercompetency->get('grade'));
 131          $this->assertEquals(1, $usercomp->get('grade'));
 132          $this->assertEquals(1, $coursecomp->get('grade'));
 133  
 134          [$coursecomp2, $plancomp2, $usercomp2] = $this->get_related_competencies($this->comp2->get('id'));
 135          $this->assertEquals(1, $plancomp2->usercompetency->get('grade'));
 136          $this->assertEquals(1, $usercomp2->get('grade'));
 137          $this->assertEquals(1, $coursecomp2->get('grade'));
 138  
 139          // Change the competency completion for the user by adding evidence.
 140          api::add_evidence($this->user->id, $this->comp1, $this->context,
 141              evidence::ACTION_OVERRIDE, 'commentincontext', 'core', null, false, null, 2);
 142          api::add_evidence($this->user->id, $this->comp2, $this->context,
 143              evidence::ACTION_OVERRIDE, 'commentincontext', 'core', null, false, null, 2);
 144  
 145          // After adding evidence, the competencies should now reflect the new grade value.
 146          [$coursecomp, $plancomp, $usercomp] = $this->get_related_competencies($this->comp1->get('id'));
 147          $this->assertEquals(2, $plancomp->usercompetency->get('grade'));
 148          $this->assertEquals(2, $usercomp->get('grade'));
 149          $this->assertEquals(2, $coursecomp->get('grade'));
 150  
 151          [$coursecomp2, $plancomp2, $usercomp2] = $this->get_related_competencies($this->comp2->get('id'));
 152          $this->assertEquals(2, $plancomp2->usercompetency->get('grade'));
 153          $this->assertEquals(2, $usercomp2->get('grade'));
 154          $this->assertEquals(2, $coursecomp2->get('grade'));
 155  
 156          // Update the course module competency to incomplete. This will not change the competency status.
 157          $data = $this->completion->get_data($this->cm, false, $this->user->id);
 158          $data->completionstate = COMPLETION_INCOMPLETE;
 159          $data->timemodified = time();
 160          $this->completion->internal_set_data($this->cm, $data);
 161  
 162          [$coursecomp, $plancomp, $usercomp] = $this->get_related_competencies($this->comp1->get('id'));
 163          $this->assertEquals(2, $plancomp->usercompetency->get('grade'));
 164          $this->assertEquals(2, $usercomp->get('grade'));
 165          $this->assertEquals(2, $coursecomp->get('grade'));
 166  
 167          [$coursecomp2, $plancomp2, $usercomp2] = $this->get_related_competencies($this->comp2->get('id'));
 168          $this->assertEquals(2, $plancomp2->usercompetency->get('grade'));
 169          $this->assertEquals(2, $usercomp2->get('grade'));
 170          $this->assertEquals(2, $coursecomp2->get('grade'));
 171  
 172          // Re-complete the course module, so that it attempts to re-complete the competencies.
 173          $data = $this->completion->get_data($this->cm, false, $this->user->id);
 174          $data->completionstate = COMPLETION_COMPLETE;
 175          $data->timemodified = time();
 176          $this->completion->internal_set_data($this->cm, $data);
 177  
 178          // By default, this will not override the existing grade, so it should remain the same as before.
 179          [$coursecomp, $plancomp, $usercomp] = $this->get_related_competencies($this->comp1->get('id'));
 180          $this->assertEquals(2, $plancomp->usercompetency->get('grade'));
 181          $this->assertEquals(2, $usercomp->get('grade'));
 182          $this->assertEquals(2, $coursecomp->get('grade'));
 183  
 184          [$coursecomp2, $plancomp2, $usercomp2] = $this->get_related_competencies($this->comp2->get('id'));
 185          $this->assertEquals(2, $plancomp2->usercompetency->get('grade'));
 186          $this->assertEquals(2, $usercomp2->get('grade'));
 187          $this->assertEquals(2, $coursecomp2->get('grade'));
 188  
 189          // Update the completion rule for only competency 1 to $overridegrade = true.
 190          $coursemodulecomps = api::list_course_module_competencies_in_course_module($this->cm);
 191          api::set_course_module_competency_ruleoutcome($coursemodulecomps[0], \core_competency\course_competency::OUTCOME_COMPLETE,
 192              true);
 193  
 194          // Mark as incomplete then re-complete the course module.
 195          $data = $this->completion->get_data($this->cm, false, $this->user->id);
 196          $data->completionstate = COMPLETION_INCOMPLETE;
 197          $data->timemodified = time();
 198          $this->completion->internal_set_data($this->cm, $data);
 199  
 200          $data = $this->completion->get_data($this->cm, false, $this->user->id);
 201          $data->completionstate = COMPLETION_COMPLETE;
 202          $data->timemodified = time();
 203          $this->completion->internal_set_data($this->cm, $data);
 204  
 205          // Because the rule is now set to override existing grades, the grade should have now updated as per the ruleoutcome.
 206          // However the second competency didn't have this rule set, so it will not be overriden.
 207          [$coursecomp, $plancomp, $usercomp] = $this->get_related_competencies($this->comp1->get('id'));
 208          $this->assertEquals(1, $plancomp->usercompetency->get('grade'));
 209          $this->assertEquals(1, $usercomp->get('grade'));
 210          $this->assertEquals(1, $coursecomp->get('grade'));
 211  
 212          [$coursecomp2, $plancomp2, $usercomp2] = $this->get_related_competencies($this->comp2->get('id'));
 213          $this->assertEquals(2, $plancomp2->usercompetency->get('grade'));
 214          $this->assertEquals(2, $usercomp2->get('grade'));
 215          $this->assertEquals(2, $coursecomp2->get('grade'));
 216  
 217          // If competency 2 is changed now to override and re-completed, it will update the same as competency 1.
 218          api::set_course_module_competency_ruleoutcome($coursemodulecomps[1], \core_competency\course_competency::OUTCOME_COMPLETE,
 219              true);
 220  
 221          $data = $this->completion->get_data($this->cm, false, $this->user->id);
 222          $data->completionstate = COMPLETION_INCOMPLETE;
 223          $data->timemodified = time();
 224          $this->completion->internal_set_data($this->cm, $data);
 225  
 226          $data = $this->completion->get_data($this->cm, false, $this->user->id);
 227          $data->completionstate = COMPLETION_COMPLETE;
 228          $data->timemodified = time();
 229          $this->completion->internal_set_data($this->cm, $data);
 230  
 231          // Now both the competencies have $overridegrade = true,
 232          // they should both reflect the ruleoutcome after the completion above was processed.
 233          [$coursecomp, $plancomp, $usercomp] = $this->get_related_competencies($this->comp1->get('id'));
 234          $this->assertEquals(1, $plancomp->usercompetency->get('grade'));
 235          $this->assertEquals(1, $usercomp->get('grade'));
 236          $this->assertEquals(1, $coursecomp->get('grade'));
 237  
 238          [$coursecomp2, $plancomp2, $usercomp2] = $this->get_related_competencies($this->comp2->get('id'));
 239          $this->assertEquals(1, $plancomp2->usercompetency->get('grade'));
 240          $this->assertEquals(1, $usercomp2->get('grade'));
 241          $this->assertEquals(1, $coursecomp2->get('grade'));
 242      }
 243  
 244      /**
 245       * Test competency backup and restore correctly restores the ruleoutcome overridegrade value.
 246       *
 247       * @covers \core_competency\api::set_course_module_competency_ruleoutcome
 248       */
 249      public function test_override_backup_restore(): void {
 250          global $CFG;
 251          require_once($CFG->dirroot . '/course/externallib.php');
 252  
 253          // Set one to override grade and another to not override grade.
 254          $coursemodulecomps = api::list_course_module_competencies_in_course_module($this->cm);
 255          api::set_course_module_competency_ruleoutcome($coursemodulecomps[0], \core_competency\course_competency::OUTCOME_COMPLETE,
 256              false);
 257          api::set_course_module_competency_ruleoutcome($coursemodulecomps[1], \core_competency\course_competency::OUTCOME_COMPLETE,
 258              true);
 259  
 260          // Duplicate the course (backup and restore).
 261          $duplicated = \core_course_external::duplicate_course($this->course->id, 'test', 'test', $this->course->category);
 262  
 263          // Get the new course modules.
 264          $newcoursemodules = get_coursemodules_in_course('label', $duplicated['id']);
 265          $this->assertCount(1, $newcoursemodules);
 266          $cm = array_pop($newcoursemodules);
 267  
 268          // Get the comeptencies for this cm.
 269          $newcoursemodulecomps = api::list_course_module_competencies_in_course_module($cm);
 270          $this->assertCount(2, $newcoursemodulecomps);
 271  
 272          // Ensure the override grade settings are restored properly.
 273          $this->assertEquals($coursemodulecomps[0]->get('overridegrade'), $newcoursemodulecomps[0]->get('overridegrade'));
 274          $this->assertEquals($coursemodulecomps[1]->get('overridegrade'), $newcoursemodulecomps[1]->get('overridegrade'));
 275      }
 276  
 277      /**
 278       * Gets the course, user and plan competency for the given competency ID
 279       *
 280       * @param int $compid ID of the competency.
 281       * @return array array containing the three related competencies
 282       */
 283      private function get_related_competencies(int $compid): array {
 284          $coursecomp = api::get_user_competency_in_course($this->course->id, $this->user->id, $compid);
 285          $usercomp = api::get_user_competency($this->user->id, $compid);
 286          $plancomp = api::get_plan_competency($this->plan, $compid);
 287          return [$coursecomp, $plancomp, $usercomp];
 288      }
 289  }