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