weekly release 4.5dev
[moodle.git] / course / tests / targets_test.php
blob6dc0ad89a5ddf138a61583353df26549897ebe8d
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.
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/>.
17 namespace core_course;
19 defined('MOODLE_INTERNAL') || die();
20 global $CFG;
22 require_once($CFG->dirroot . '/completion/criteria/completion_criteria.php');
23 require_once($CFG->dirroot . '/completion/criteria/completion_criteria_activity.php');
24 require_once($CFG->dirroot . '/lib/grade/grade_item.php');
25 require_once($CFG->dirroot . '/lib/grade/grade_grade.php');
26 require_once($CFG->dirroot . '/lib/grade/grade_category.php');
27 require_once($CFG->dirroot . '/lib/grade/constants.php');
29 /**
30 * Unit tests for core targets.
32 * @package core_course
33 * @copyright 2019 Victor Deniz <victor@moodle.com>
34 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36 class targets_test extends \advanced_testcase {
38 /**
39 * Provides course params for the {@link self::test_core_target_course_completion_analysable()} method.
41 * @return array
43 public function analysable_provider() {
45 $now = new \DateTime("now", \core_date::get_server_timezone_object());
46 $year = $now->format('Y');
47 $month = $now->format('m');
49 return [
50 'coursenotyetstarted' => [
51 'params' => [
52 'enablecompletion' => 1,
53 'startdate' => mktime(0, 0, 0, 10, 24, $year + 1)
55 'isvalid' => get_string('coursenotyetstarted', 'course')
57 'coursenostudents' => [
58 'params' => [
59 'enablecompletion' => 1,
60 'startdate' => mktime(0, 0, 0, 10, 24, $year - 2),
61 'enddate' => mktime(0, 0, 0, 10, 24, $year - 1)
63 'isvalid' => get_string('nocoursestudents', 'course')
65 'coursenosections' => [
66 'params' => [
67 'enablecompletion' => 1,
68 'format' => 'social',
69 'students' => true
71 'isvalid' => get_string('nocoursesections', 'course')
73 'coursenoendtime' => [
74 'params' => [
75 'enablecompletion' => 1,
76 'format' => 'topics',
77 'enddate' => 0,
78 'students' => true
80 'isvalid' => get_string('nocourseendtime', 'course')
82 'courseendbeforestart' => [
83 'params' => [
84 'enablecompletion' => 1,
85 'enddate' => mktime(0, 0, 0, 10, 23, $year - 2),
86 'students' => true
88 'isvalid' => get_string('errorendbeforestart', 'course')
90 'coursetoolong' => [
91 'params' => [
92 'enablecompletion' => 1,
93 'startdate' => mktime(0, 0, 0, 10, 24, $year - 2),
94 'enddate' => mktime(0, 0, 0, 10, 23, $year),
95 'students' => true
97 'isvalid' => get_string('coursetoolong', 'course')
99 'coursealreadyfinished' => [
100 'params' => [
101 'enablecompletion' => 1,
102 'startdate' => mktime(0, 0, 0, 10, 24, $year - 2),
103 'enddate' => mktime(0, 0, 0, 10, 23, $year - 1),
104 'students' => true
106 'isvalid' => get_string('coursealreadyfinished', 'course'),
107 'fortraining' => false
109 'coursenotyetfinished' => [
110 'params' => [
111 'enablecompletion' => 1,
112 'startdate' => mktime(0, 0, 0, $month - 1, 24, $year),
113 'enddate' => mktime(0, 0, 0, $month + 2, 23, $year),
114 'students' => true
116 'isvalid' => get_string('coursenotyetfinished', 'course')
118 'coursenocompletion' => [
119 'params' => [
120 'enablecompletion' => 0,
121 'startdate' => mktime(0, 0, 0, $month - 2, 24, $year),
122 'enddate' => mktime(0, 0, 0, $month - 1, 23, $year),
123 'students' => true
125 'isvalid' => get_string('completionnotenabledforcourse', 'completion')
127 'coursehiddentraining' => [
128 'params' => [
129 'enablecompletion' => 1,
130 'startdate' => mktime(0, 0, 0, $month - 1, 24, $year - 1),
131 'enddate' => mktime(0, 0, 0, $month - 1, 23, $year),
132 'students' => true,
133 'visible' => '0',
135 'isvalid' => true,
137 'coursehiddenprediction' => [
138 'params' => [
139 'enablecompletion' => 1,
140 'startdate' => mktime(0, 0, 0, $month - 1, 24, $year),
141 'enddate' => mktime(0, 0, 0, $month - 1, 23, $year + 1),
142 'students' => true,
143 'visible' => '0',
145 'isvalid' => get_string('hiddenfromstudents'),
146 'fortraining' => false
152 * Provides enrolment params for the {@link self::test_core_target_course_completion_samples()} method.
154 * @return array
156 public function sample_provider() {
157 $now = time();
158 return [
159 'enrolmentendbeforecourse' => [
160 'coursestart' => $now,
161 'courseend' => $now + (WEEKSECS * 8),
162 'timestart' => $now,
163 'timeend' => $now - DAYSECS,
164 'isvalidfortraining' => false,
165 'isvalidforprediction' => false
167 'enrolmenttoolong' => [
168 'coursestart' => $now,
169 'courseend' => $now + (WEEKSECS * 8),
170 'timestart' => $now - (YEARSECS + (WEEKSECS * 8)),
171 'timeend' => $now + (WEEKSECS * 8),
172 'isvalidfortraining' => false,
173 'isvalidforprediction' => false
175 'enrolmentstartaftercourse' => [
176 'coursestart' => $now,
177 'courseend' => $now + (WEEKSECS * 8),
178 'timestart' => $now + (WEEKSECS * 9),
179 'timeend' => $now + (WEEKSECS * 10),
180 'isvalidfortraining' => false,
181 'isvalidforprediction' => false
183 'enrolmentstartsafternow' => [
184 'coursestart' => $now,
185 'courseend' => $now + (WEEKSECS * 8),
186 'timestart' => $now + (WEEKSECS * 2),
187 'timeend' => $now + (WEEKSECS * 7),
188 'isvalidfortraining' => false,
189 'isvalidforprediction' => false
191 'enrolmentfinishedbeforenow' => [
192 'coursestart' => $now - (WEEKSECS * 4),
193 'courseend' => $now - (WEEKSECS * 1),
194 'timestart' => $now - (WEEKSECS * 3),
195 'timeend' => $now - (WEEKSECS * 2),
196 'isvalidfortraining' => true,
197 'isvalidforprediction' => false
203 * Provides enrolment params for the {@link self::test_core_target_course_completion_samples()} method.
205 * @return array
207 public function active_during_analysis_time_provider() {
208 $now = time();
210 return [
211 'enrol-after-end' => [
212 'starttime' => $now,
213 'endtime' => $now + WEEKSECS,
214 'timestart' => $now + (WEEKSECS * 2),
215 'timeend' => $now + (WEEKSECS * 3),
216 'nullcalculation' => true,
218 'enrol-before-start' => [
219 'starttime' => $now + (WEEKSECS * 2),
220 'endtime' => $now + (WEEKSECS * 3),
221 'timestart' => $now,
222 'timeend' => $now + WEEKSECS,
223 'nullcalculation' => true,
225 'enrol-active-exact-match' => [
226 'starttime' => $now,
227 'endtime' => $now + (WEEKSECS * 1),
228 'timestart' => $now,
229 'timeend' => $now + (WEEKSECS * 1),
230 'nullcalculation' => false,
232 'enrol-active' => [
233 'starttime' => $now + WEEKSECS,
234 'endtime' => $now + (WEEKSECS * 2),
235 'timestart' => $now,
236 'timeend' => $now + (WEEKSECS * 3),
237 'nullcalculation' => false,
239 'enrol-during-analysis-active-just-for-a-while' => [
240 'starttime' => $now,
241 'endtime' => $now + (WEEKSECS * 10),
242 'timestart' => $now + WEEKSECS,
243 'timeend' => $now + (WEEKSECS * 2),
244 'nullcalculation' => true,
246 'enrol-during-analysis-mostly-active' => [
247 'starttime' => $now,
248 'endtime' => $now + (WEEKSECS * 20),
249 'timestart' => $now + WEEKSECS,
250 'timeend' => $now + (WEEKSECS * 19),
251 'nullcalculation' => false,
253 'enrol-partly-active-starts-before' => [
254 'starttime' => $now + WEEKSECS,
255 'endtime' => $now + (WEEKSECS * 10),
256 'timestart' => $now,
257 'timeend' => $now + (WEEKSECS * 2),
258 'nullcalculation' => true,
260 'enrol-mostly-active-starts-before' => [
261 'starttime' => $now + WEEKSECS,
262 'endtime' => $now + (WEEKSECS * 10),
263 'timestart' => $now,
264 'timeend' => $now + (WEEKSECS * 9),
265 'nullcalculation' => false,
267 'enrol-partly-active-ends-afterwards' => [
268 'starttime' => $now,
269 'endtime' => $now + (WEEKSECS * 10),
270 'timestart' => $now + (WEEKSECS * 9),
271 'timeend' => $now + (WEEKSECS * 11),
272 'nullcalculation' => true,
274 'enrol-mostly-active-ends-afterwards' => [
275 'starttime' => $now,
276 'endtime' => $now + (WEEKSECS * 10),
277 'timestart' => $now + WEEKSECS,
278 'timeend' => $now + (WEEKSECS * 11),
279 'nullcalculation' => false,
281 'enrol-partly-active-no-enrolment-end' => [
282 'starttime' => $now,
283 'endtime' => $now + (WEEKSECS * 10),
284 'timestart' => $now + (WEEKSECS * 9),
285 'timeend' => false,
286 'nullcalculation' => true,
288 'enrol-mostly-active-no-enrolment-end-false' => [
289 'starttime' => $now,
290 'endtime' => $now + (WEEKSECS * 10),
291 'timestart' => $now + WEEKSECS,
292 'timeend' => false,
293 'nullcalculation' => false,
295 'enrol-mostly-active-no-enrolment-end-zero' => [
296 'starttime' => $now,
297 'endtime' => $now + (WEEKSECS * 10),
298 'timestart' => $now + WEEKSECS,
299 'timeend' => 0,
300 'nullcalculation' => false,
302 'enrol-no-enrolment-start-false' => [
303 'starttime' => $now,
304 'endtime' => $now + (WEEKSECS * 10),
305 'timestart' => false,
306 'timeend' => $now + (WEEKSECS * 9),
307 'nullcalculation' => false,
309 'enrol-no-enrolment-start-zero' => [
310 'starttime' => $now,
311 'endtime' => $now + (WEEKSECS * 10),
312 'timestart' => 0,
313 'timeend' => $now + (WEEKSECS * 9),
314 'nullcalculation' => false,
316 'no-start' => [
317 'starttime' => 0,
318 'endtime' => $now + (WEEKSECS * 2),
319 'timestart' => $now + WEEKSECS,
320 'timeend' => $now + (WEEKSECS * 3),
321 'nullcalculation' => false,
323 'no-end' => [
324 'starttime' => $now,
325 'endtime' => 0,
326 'timestart' => $now + (WEEKSECS * 2),
327 'timeend' => $now + (WEEKSECS * 3),
328 'nullcalculation' => false,
334 * Test the conditions of a valid analysable, both common and specific to this target (course_completion).
336 * @dataProvider analysable_provider
337 * @param mixed $courseparams Course data
338 * @param true|string $isvalid True when analysable is valid, string when it is not
339 * @param boolean $fortraining True if the course is for training the model
341 public function test_core_target_course_completion_analysable($courseparams, $isvalid, $fortraining = true) {
342 global $DB;
344 $this->resetAfterTest(true);
346 try {
347 $course = $this->getDataGenerator()->create_course($courseparams);
348 } catch (\moodle_exception $e) {
349 $course = $this->getDataGenerator()->create_course();
350 $courserecord = $courseparams;
351 $courserecord['id'] = $course->id;
352 unset($courserecord['students']);
354 $DB->update_record_raw('course', $courserecord);
355 $course = get_course($course->id);
357 $user = $this->getDataGenerator()->create_user();
359 if (!empty($courseparams['enablecompletion'])) {
360 $assign = $this->getDataGenerator()->create_module('assign', ['course' => $course->id, 'completion' => 1]);
361 $cm = get_coursemodule_from_id('assign', $assign->cmid);
363 $criteriadata = (object) [
364 'id' => $course->id,
365 'criteria_activity' => [
366 $cm->id => 1
369 $criterion = new \completion_criteria_activity();
370 $criterion->update_config($criteriadata);
373 $target = new \core_course\analytics\target\course_completion();
375 // Test valid analysables.
377 if (!empty($courseparams['students'])) {
378 // Enroll user in course.
379 $this->getDataGenerator()->enrol_user($user->id, $course->id);
382 $analysable = new \core_analytics\course($course);
383 $this->assertEquals($isvalid, $target->is_valid_analysable($analysable, $fortraining));
387 * Test the conditions of a valid sample, both common and specific to this target (course_completion).
389 * @dataProvider sample_provider
390 * @param int $coursestart Course start date
391 * @param int $courseend Course end date
392 * @param int $timestart Enrol start date
393 * @param int $timeend Enrol end date
394 * @param boolean $isvalidfortraining True when sample is valid for training, false when it is not
395 * @param boolean $isvalidforprediction True when sample is valid for prediction, false when it is not
397 public function test_core_target_course_completion_samples($coursestart, $courseend, $timestart, $timeend,
398 $isvalidfortraining, $isvalidforprediction) {
400 $this->resetAfterTest(true);
402 $courserecord = new \stdClass();
403 $courserecord->startdate = $coursestart;
404 $courserecord->enddate = $courseend;
406 $user = $this->getDataGenerator()->create_user();
407 $course = $this->getDataGenerator()->create_course($courserecord);
408 $this->getDataGenerator()->enrol_user($user->id, $course->id, null, 'manual', $timestart, $timeend);
410 $target = new \core_course\analytics\target\course_completion();
411 $analyser = new \core\analytics\analyser\student_enrolments(1, $target, [], [], []);
412 $analysable = new \core_analytics\course($course);
414 $class = new \ReflectionClass('\core\analytics\analyser\student_enrolments');
415 $method = $class->getMethod('get_all_samples');
417 list($sampleids, $samplesdata) = $method->invoke($analyser, $analysable);
418 $target->add_sample_data($samplesdata);
419 $sampleid = reset($sampleids);
421 $this->assertEquals($isvalidfortraining, $target->is_valid_sample($sampleid, $analysable, true));
422 $this->assertEquals($isvalidforprediction, $target->is_valid_sample($sampleid, $analysable, false));
426 * Test the conditions of a valid calculation (course_completion).
428 * @dataProvider active_during_analysis_time_provider
429 * @param int $starttime Analysis start time
430 * @param int $endtime Analysis end time
431 * @param int $timestart Enrol start date
432 * @param int $timeend Enrol end date
433 * @param boolean $nullcalculation Whether the calculation should be null or not
435 public function test_core_target_course_completion_active_during_analysis_time($starttime, $endtime, $timestart, $timeend,
436 $nullcalculation) {
438 $this->resetAfterTest(true);
440 $user = $this->getDataGenerator()->create_user();
441 $course = $this->getDataGenerator()->create_course();
442 $this->getDataGenerator()->enrol_user($user->id, $course->id, null, 'manual', $timestart, $timeend);
444 $target = new \core_course\analytics\target\course_completion();
445 $analyser = new \core\analytics\analyser\student_enrolments(1, $target, [], [], []);
446 $analysable = new \core_analytics\course($course);
448 $class = new \ReflectionClass('\core\analytics\analyser\student_enrolments');
449 $method = $class->getMethod('get_all_samples');
451 list($sampleids, $samplesdata) = $method->invoke($analyser, $analysable);
452 $target->add_sample_data($samplesdata);
453 $sampleid = reset($sampleids);
455 $reftarget = new \ReflectionObject($target);
456 $refmethod = $reftarget->getMethod('calculate_sample');
458 if ($nullcalculation) {
459 $this->assertNull($refmethod->invoke($target, $sampleid, $analysable, $starttime, $endtime));
460 } else {
461 $this->assertNotNull($refmethod->invoke($target, $sampleid, $analysable, $starttime, $endtime));
466 * Setup user, framework, competencies and course competencies.
468 protected function setup_competencies_environment() {
469 $this->resetAfterTest(true);
470 $now = time();
471 $this->setAdminUser();
472 $dg = $this->getDataGenerator();
473 $lpg = $dg->get_plugin_generator('core_competency');
475 $course = $dg->create_course(array('startdate' => $now - WEEKSECS, 'enddate' => $now - DAYSECS));
476 $coursenocompetencies = $dg->create_course(array('startdate' => $now - WEEKSECS, 'enddate' => $now - DAYSECS));
478 $u1 = $dg->create_user();
479 $this->getDataGenerator()->enrol_user($u1->id, $course->id);
480 $this->getDataGenerator()->enrol_user($u1->id, $coursenocompetencies->id);
481 $f1 = $lpg->create_framework();
482 $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get('id')));
483 $c2 = $lpg->create_competency(array('competencyframeworkid' => $f1->get('id')));
484 $c3 = $lpg->create_competency(array('competencyframeworkid' => $f1->get('id')));
485 $c4 = $lpg->create_competency(array('competencyframeworkid' => $f1->get('id')));
486 $cc1 = $lpg->create_course_competency(array('competencyid' => $c1->get('id'), 'courseid' => $course->id,
487 'ruleoutcome' => \core_competency\course_competency::OUTCOME_NONE));
488 $cc2 = $lpg->create_course_competency(array('competencyid' => $c2->get('id'), 'courseid' => $course->id,
489 'ruleoutcome' => \core_competency\course_competency::OUTCOME_EVIDENCE));
490 $cc3 = $lpg->create_course_competency(array('competencyid' => $c3->get('id'), 'courseid' => $course->id,
491 'ruleoutcome' => \core_competency\course_competency::OUTCOME_RECOMMEND));
492 $cc4 = $lpg->create_course_competency(array('competencyid' => $c4->get('id'), 'courseid' => $course->id,
493 'ruleoutcome' => \core_competency\course_competency::OUTCOME_COMPLETE));
495 return array(
496 'course' => $course,
497 'coursenocompetencies' => $coursenocompetencies,
498 'user' => $u1,
499 'course_competencies' => array($cc1, $cc2, $cc3, $cc4)
504 * Test the specific conditions of a valid analysable for the course_competencies target.
506 public function test_core_target_course_competencies_analysable() {
508 $data = $this->setup_competencies_environment();
510 $analysable = new \core_analytics\course($data['course']);
511 $target = new \core_course\analytics\target\course_competencies();
513 $this->assertTrue($target->is_valid_analysable($analysable));
515 $analysable = new \core_analytics\course($data['coursenocompetencies']);
516 $this->assertEquals(get_string('nocompetenciesincourse', 'tool_lp'), $target->is_valid_analysable($analysable));
520 * Test the target value calculation.
522 public function test_core_target_course_competencies_calculate() {
524 $data = $this->setup_competencies_environment();
526 $target = new \core_course\analytics\target\course_competencies();
527 $analyser = new \core\analytics\analyser\student_enrolments(1, $target, [], [], []);
528 $analysable = new \core_analytics\course($data['course']);
530 $class = new \ReflectionClass('\core\analytics\analyser\student_enrolments');
531 $method = $class->getMethod('get_all_samples');
533 list($sampleids, $samplesdata) = $method->invoke($analyser, $analysable);
534 $target->add_sample_data($samplesdata);
535 $sampleid = reset($sampleids);
537 $class = new \ReflectionClass('\core_course\analytics\target\course_competencies');
538 $method = $class->getMethod('calculate_sample');
540 // Method calculate_sample() returns 1 when the user has not achieved all the competencies assigned to the course.
541 $this->assertEquals(1, $method->invoke($target, $sampleid, $analysable));
543 // Grading of all the competences assigned to the course, in such way that the user achieves them all.
544 foreach ($data['course_competencies'] as $competency) {
545 \core_competency\api::grade_competency_in_course($data['course']->id, $data['user']->id,
546 $competency->get('competencyid'), 3, 'Unit test');
548 // Method calculate_sample() returns 0 when the user has achieved all the competencies assigned to the course.
549 $this->assertEquals(0, $method->invoke($target, $sampleid, $analysable));
553 * Test the specific conditions of a valid analysable for the course_gradetopass target.
555 public function test_core_target_course_gradetopass_analysable() {
556 global $DB;
558 $this->resetAfterTest(true);
559 $now = time();
561 $dg = $this->getDataGenerator();
563 // Course without grade to pass set.
564 $course1 = $dg->create_course(array('startdate' => $now - WEEKSECS, 'enddate' => $now - DAYSECS));
565 $student1 = $dg->create_user();
566 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
567 $dg->enrol_user($student1->id, $course1->id, $studentrole->id);
569 $analysable = new \core_analytics\course($course1);
570 $target = new \core_course\analytics\target\course_gradetopass();
571 $this->assertEquals(get_string('gradetopassnotset', 'course'), $target->is_valid_analysable($analysable));
573 // Set grade to pass.
574 $courseitem = \grade_item::fetch_course_item($course1->id);
575 $courseitem->gradepass = 50;
576 $DB->update_record('grade_items', $courseitem);
577 // Since the grade to pass value is cached in the target, a new one it is instanciated.
578 $target = new \core_course\analytics\target\course_gradetopass();
579 $this->assertTrue($target->is_valid_analysable($analysable));
584 * Test the target value calculation of the course_gradetopass target.
586 public function test_core_target_course_gradetopass_calculate() {
587 global $DB;
589 $this->resetAfterTest(true);
591 $dg = $this->getDataGenerator();
592 $course1 = $dg->create_course();
593 // Set grade to pass.
594 $student1 = $dg->create_user();
595 $student2 = $dg->create_user();
596 $student3 = $dg->create_user();
597 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
598 $dg->enrol_user($student1->id, $course1->id, $studentrole->id);
599 $dg->enrol_user($student2->id, $course1->id, $studentrole->id);
600 $dg->enrol_user($student3->id, $course1->id, $studentrole->id);
602 // get_all_samples() does not guarantee any order, so let's
603 // explicitly define the expectations here for later comparing.
604 // Expectations format being array($userid => expectation, ...)
605 $expectations = [];
607 $courseitem = \grade_item::fetch_course_item($course1->id);
608 // Student1 (< gradepass) fails, so it's non achieved sample.
609 $courseitem->update_final_grade($student1->id, 30);
610 $expectations[$student1->id] = 1;
612 // Student2 (> gradepass) passes, so it's achieved sample.
613 $courseitem->update_final_grade($student2->id, 60);
614 $expectations[$student2->id] = 0;
616 // Student 3 (has no grade) fails, so it's non achieved sample.
617 $expectations[$student3->id] = 1;
619 $courseitem->gradepass = 50;
620 $DB->update_record('grade_items', $courseitem);
622 $target = new \core_course\analytics\target\course_gradetopass();
623 $analyser = new \core\analytics\analyser\student_enrolments(1, $target, [], [], []);
624 $analysable = new \core_analytics\course($course1);
626 $class = new \ReflectionClass('\core\analytics\analyser\student_enrolments');
627 $method = $class->getMethod('get_all_samples');
629 list($sampleids, $samplesdata) = $method->invoke($analyser, $analysable);
630 $target->add_sample_data($samplesdata);
632 $class = new \ReflectionClass('\core_course\analytics\target\course_gradetopass');
633 $method = $class->getMethod('calculate_sample');
635 // Verify all the expectations are fulfilled.
636 foreach ($sampleids as $sampleid => $key) {
637 $this->assertEquals($expectations[$samplesdata[$key]['user']->id], $method->invoke($target, $sampleid, $analysable));