From 1f23f1412e900e97be661bc1522c34a391f38f42 Mon Sep 17 00:00:00 2001 From: Victor Deniz Falcon Date: Mon, 8 Apr 2019 10:17:02 +0100 Subject: [PATCH] MDL-65176 analytics: target for students who don't get the minimum grade Added new target to predict which students are at risk of not getting the minimum grade to pass a course. --- lang/en/course.php | 1 + lang/en/moodle.php | 4 + .../analytics/target/course_gradetopass.php | 181 +++++++++++++++++++++ lib/tests/targets_test.php | 98 +++++++++++ version.php | 2 +- 5 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 lib/classes/analytics/target/course_gradetopass.php diff --git a/lang/en/course.php b/lang/en/course.php index aba28b38c76..16e50800c97 100644 --- a/lang/en/course.php +++ b/lang/en/course.php @@ -36,6 +36,7 @@ $string['customfield_visibletoall'] = 'Everyone'; $string['customfield_visibletoteachers'] = 'Teachers'; $string['customfieldsettings'] = 'Settings for course custom fields'; $string['favourite'] = 'Starred course'; +$string['gradetopassnotset'] = 'This course does not have a grade to pass set. It may be set in the grade item of the course (Gradebook setup).'; $string['privacy:perpage'] = 'The number of courses to show per page.'; $string['privacy:completionpath'] = 'Course completion'; $string['privacy:favouritespath'] = 'Course starred information'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 5fb1e314289..e1a351ffcf4 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1976,6 +1976,8 @@ $string['target:coursecompetencies'] = 'Students at risk of not achieving the co $string['target:coursecompetencies_help'] = 'This target describes whether a student is at risk of not achieving the competencies assigned to a course. This target considers that all competencies assigned to the course must be achieved by the end of the course.'; $string['target:coursedropout'] = 'Students at risk of dropping out'; $string['target:coursedropout_help'] = 'This target describes whether the student is considered at risk of dropping out.'; +$string['target:coursegradetopass'] = 'Students at risk of not getting the minimum grade to pass the course.'; +$string['target:coursegradetopass_help'] = 'This target describes whether the student is at risk of not getting the minimum grade to pass the course.'; $string['target:noteachingactivity'] = 'No teaching'; $string['target:noteachingactivity_help'] = 'This target describes whether courses due to start in the coming week will have teaching activity.'; $string['targetlabelstudentcompletionno'] = 'Student who is likely to meet the course completion conditions'; @@ -1984,6 +1986,8 @@ $string['targetlabelstudentcompetenciesno'] = 'Student who is likely to achieve $string['targetlabelstudentcompetenciesyes'] = 'Student at risk of not achieving the competencies assigned to a course'; $string['targetlabelstudentdropoutyes'] = 'Student at risk of dropping out'; $string['targetlabelstudentdropoutno'] = 'Not at risk'; +$string['targetlabelstudentgradetopassno'] = 'Student who is likely to meet the minimum grade to pass the course.'; +$string['targetlabelstudentgradetopassyes'] = 'Student at risk of not meeting the minimum grade to pass the course.'; $string['targetlabelteachingyes'] = 'Users with teaching capabilities have access to the course'; $string['targetlabelteachingno'] = 'No teaching'; $string['targetrole'] = 'Target role'; diff --git a/lib/classes/analytics/target/course_gradetopass.php b/lib/classes/analytics/target/course_gradetopass.php new file mode 100644 index 00000000000..a530cd565a9 --- /dev/null +++ b/lib/classes/analytics/target/course_gradetopass.php @@ -0,0 +1,181 @@ +. + +/** + * Getting the minimum grade to pass target. + * + * @package core + * @copyright 2019 Victor Deniz + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\analytics\target; + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Getting the minimum grade to pass target. + * + * @package core + * @copyright 2019 Victor Deniz + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_gradetopass extends \core\analytics\target\course_enrolments { + + /** + * Courses grades to pass. + * @var mixed[] + */ + protected $coursesgradetopass = array(); + + /** + * Courses grades. + * @var mixed[] + */ + protected $coursesgrades = array(); + + /** + * Returns the grade to pass a course. + * + * Save the value in $coursesgradetopass array to prevent new accesses to the database. + * + * @param int $courseid The course id. + * @return array The courseitem id and the required grade to pass the course. + */ + protected function get_course_gradetopass($courseid) { + if (!isset($this->coursesgradetopass[$courseid])) { + // Get course grade_item. + $courseitem = \grade_item::fetch_course_item($courseid); + + $ci = array(); + $ci['courseitemid'] = $courseitem->id; + + if ($courseitem->gradetype == GRADE_TYPE_VALUE && grade_floats_different($courseitem->gradepass, 0.0)) { + $ci['gradetopass'] = $courseitem->gradepass; + } else { + $ci['gradetopass'] = null; + } + $this->coursesgradetopass[$courseid] = $ci; + } + + return $this->coursesgradetopass[$courseid]; + } + + /** + * Returns the grade of a user in a course. + * + * Saves the grades of all course users in $coursesgrades array to prevent new accesses to the database. + * + * @param int $courseitemid The course item id. + * @param int $userid the user whose grade is requested. + * @return array The courseitem id and the required grade to pass the course. + */ + protected function get_user_grade($courseitemid, $userid) { + // If the user grade for this course is not available, get all the grades for the course. + if (!isset($this->coursesgrades[$courseitemid])) { + // Ony a course is cached to avoid high memory usage. + unset($this->coursesgrades); + $gg = new \grade_grade(null, false); + $usersgrades = $gg->fetch_all(array('itemid' => $courseitemid)); + + if ($usersgrades) { + foreach ($usersgrades as $ug) { + $this->coursesgrades[$courseitemid][$ug->userid] = $ug->finalgrade; + } + } + } + + if (!isset($this->coursesgrades[$courseitemid][$userid])) { + $this->coursesgrades[$courseitemid][$userid] = null; + } + + return $this->coursesgrades[$courseitemid][$userid]; + } + + /** + * Returns the name. + * + * If there is a corresponding '_help' string this will be shown as well. + * + * @return \lang_string + */ + public static function get_name() : \lang_string { + return new \lang_string('target:coursegradetopass'); + } + + /** + * Returns descriptions for each of the values the target calculation can return. + * + * @return string[] + */ + protected static function classes_description() { + return array( + get_string('targetlabelstudentgradetopassno'), + get_string('targetlabelstudentgradetopassyes') + ); + } + + /** + * Discards courses that are not yet ready to be used for training or prediction. + * + * Only courses with "value" grade type and grade to pass set are valid. + * + * @param \core_analytics\analysable $course + * @param bool $fortraining + * @return true|string + */ + public function is_valid_analysable(\core_analytics\analysable $course, $fortraining = true) { + $isvalid = parent::is_valid_analysable($course, $fortraining); + + if (is_string($isvalid)) { + return $isvalid; + } + + $courseitem = $this->get_course_gradetopass ($course->get_id()); + if (is_null($courseitem['gradetopass'])) { + return get_string('gradetopassnotset', 'course'); + } + + return true; + } + + /** + * The user's grade in the course sets the target value. + * + * @param int $sampleid + * @param \core_analytics\analysable $course + * @param int $starttime + * @param int $endtime + * @return float 0 -> course grade to pass achieved, 1 -> course grade to pass not achieved + */ + protected function calculate_sample($sampleid, \core_analytics\analysable $course, $starttime = false, $endtime = false) { + + $userenrol = $this->retrieve('user_enrolments', $sampleid); + + // Get course grade to pass. + $courseitem = $this->get_course_gradetopass($course->get_id()); + + // Get the user grade. + $usergrade = $this->get_user_grade($courseitem['courseitemid'], $userenrol->userid); + + if ($usergrade >= $courseitem['gradetopass']) { + return 0; + } + + return 1; + } +} diff --git a/lib/tests/targets_test.php b/lib/tests/targets_test.php index e40e486aee8..025073126ba 100644 --- a/lib/tests/targets_test.php +++ b/lib/tests/targets_test.php @@ -28,6 +28,10 @@ global $CFG; require_once($CFG->dirroot . '/completion/criteria/completion_criteria.php'); require_once($CFG->dirroot . '/completion/criteria/completion_criteria_activity.php'); +require_once($CFG->dirroot . '/lib/grade/grade_item.php'); +require_once($CFG->dirroot . '/lib/grade/grade_grade.php'); +require_once($CFG->dirroot . '/lib/grade/grade_category.php'); +require_once($CFG->dirroot . '/lib/grade/constants.php'); /** * Unit tests for core targets. @@ -341,4 +345,98 @@ class core_analytics_targets_testcase extends advanced_testcase { // Method calculate_sample() returns 0 when the user has achieved all the competencies assigned to the course. $this->assertEquals(0, $method->invoke($target, $sampleid, $analysable)); } + + /** + * Test the specific conditions of a valid analysable for the course_gradetopass target. + */ + public function test_core_target_course_gradetopass_analysable() { + global $DB; + + $this->resetAfterTest(true); + $now = time(); + + $dg = $this->getDataGenerator(); + + // Course without grade to pass set. + $course1 = $dg->create_course(array('startdate' => $now - WEEKSECS, 'enddate' => $now - DAYSECS)); + $student1 = $dg->create_user(); + $studentrole = $DB->get_record('role', array('shortname' => 'student')); + $dg->enrol_user($student1->id, $course1->id, $studentrole->id); + + $analysable = new \core_analytics\course($course1); + $target = new \core\analytics\target\course_gradetopass(); + $this->assertEquals(get_string('gradetopassnotset', 'course'), $target->is_valid_analysable($analysable)); + + // Set grade to pass. + $courseitem = grade_item::fetch_course_item($course1->id); + $courseitem->gradepass = 50; + $DB->update_record('grade_items', $courseitem); + // Since the grade to pass value is cached in the target, a new one it is instanciated. + $target = new \core\analytics\target\course_gradetopass(); + $this->assertTrue($target->is_valid_analysable($analysable)); + + } + + /** + * Test the target value calculation of the course_gradetopass target. + */ + public function test_core_target_course_gradetopass_calculate() { + global $DB; + + $this->resetAfterTest(true); + + $dg = $this->getDataGenerator(); + $course1 = $dg->create_course(); + // Set grade to pass. + $student1 = $dg->create_user(); + $student2 = $dg->create_user(); + $student3 = $dg->create_user(); + $studentrole = $DB->get_record('role', array('shortname' => 'student')); + $dg->enrol_user($student1->id, $course1->id, $studentrole->id); + $dg->enrol_user($student2->id, $course1->id, $studentrole->id); + $dg->enrol_user($student3->id, $course1->id, $studentrole->id); + + $courseitem = grade_item::fetch_course_item($course1->id); + // Student1 fails. + $courseitem->update_final_grade($student1->id, 30); + // Student2 pass. + $courseitem->update_final_grade($student2->id, 60); + // Student 3 has no grade. + + $courseitem->gradepass = 50; + $DB->update_record('grade_items', $courseitem); + + $target = new \core\analytics\target\course_gradetopass(); + $analyser = new \core\analytics\analyser\student_enrolments(1, $target, [], [], []); + $analysable = new \core_analytics\course($course1); + + $class = new ReflectionClass('\core\analytics\analyser\student_enrolments'); + $method = $class->getMethod('get_all_samples'); + $method->setAccessible(true); + + list($sampleids, $samplesdata) = $method->invoke($analyser, $analysable); + $target->add_sample_data($samplesdata); + + // Users in array $sampleids are sorted by user id, so student1 is the first sample. + $sampleid = reset($sampleids); + + $class = new ReflectionClass('\core\analytics\target\course_gradetopass'); + $method = $class->getMethod('calculate_sample'); + $method->setAccessible(true); + + // Method calculate_sample() returns 1 when the user has not successfully graded to pass the course. + $this->assertEquals(1, $method->invoke($target, $sampleid, $analysable)); + + // Student2. + $sampleid = next($sampleids); + + // Method calculate_sample() returns 0 when the user has successfully graded to pass the course. + $this->assertEquals(0, $method->invoke($target, $sampleid, $analysable)); + + // Student3. + $sampleid = next($sampleids); + + // Method calculate_sample() returns 1 when the user has not been graded. + $this->assertEquals(1, $method->invoke($target, $sampleid, $analysable)); + } } diff --git a/version.php b/version.php index 08fe4374e63..5017af85b91 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2019040600.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2019040600.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. -- 2.11.4.GIT