Current Path : C:/xampp/htdocs/moodle/course/classes/analytics/target/ |
Current File : C:/xampp/htdocs/moodle/course/classes/analytics/target/course_enrolments.php |
<?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Base class for targets whose analysable is a course using user enrolments as samples. * * @package core_course * @copyright 2019 Victor Deniz <victor@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_course\analytics\target; defined('MOODLE_INTERNAL') || die(); /** * Base class for targets whose analysable is a course using user enrolments as samples. * * @package core_course * @copyright 2019 Victor Deniz <victor@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class course_enrolments extends \core_analytics\local\target\binary { /** * @var string */ const MESSAGE_ACTION_NAME = 'studentmessage'; /** * @var float */ const ENROL_ACTIVE_PERCENT_REQUIRED = 0.7; /** * Students in the course. * @var int[] */ protected $students; /** * Returns the analyser class that should be used along with this target. * * @return string The full class name as a string */ public function get_analyser_class() { return '\core\analytics\analyser\student_enrolments'; } /** * Only past stuff. * * @param \core_analytics\local\time_splitting\base $timesplitting * @return bool */ public function can_use_timesplitting(\core_analytics\local\time_splitting\base $timesplitting): bool { return ($timesplitting instanceof \core_analytics\local\time_splitting\before_now); } /** * Overwritten to show a simpler language string. * * @param int $modelid * @param \context $context * @return string */ public function get_insight_subject(int $modelid, \context $context) { return get_string('studentsatriskincourse', 'course', $context->get_context_name(false)); } /** * Returns the body message for the insight. * * @param \context $context * @param string $contextname * @param \stdClass $user * @param \moodle_url $insighturl * @return string[] The plain text message and the HTML message */ public function get_insight_body(\context $context, string $contextname, \stdClass $user, \moodle_url $insighturl): array { global $OUTPUT; $a = (object)['coursename' => $contextname, 'userfirstname' => $user->firstname]; $fullmessage = get_string('studentsatriskinfomessage', 'course', $a) . PHP_EOL . PHP_EOL . $insighturl->out(false); $fullmessagehtml = $OUTPUT->render_from_template('core_analytics/insight_info_message', ['url' => $insighturl->out(false), 'insightinfomessage' => get_string('studentsatriskinfomessage', 'course', $a)] ); return [$fullmessage, $fullmessagehtml]; } /** * Discards courses that are not yet ready to be used for training or prediction. * * @param \core_analytics\analysable $course * @param bool $fortraining * @return true|string */ public function is_valid_analysable(\core_analytics\analysable $course, $fortraining = true) { if (!$course->was_started()) { return get_string('coursenotyetstarted', 'course'); } if (!$fortraining && !$course->get_course_data()->visible) { return get_string('hiddenfromstudents'); } if (!$this->students = $course->get_students()) { return get_string('nocoursestudents', 'course'); } if (!course_format_uses_sections($course->get_course_data()->format)) { // We can not split activities in time ranges. return get_string('nocoursesections', 'course'); } if ($course->get_end() == 0) { // We require time end to be set. return get_string('nocourseendtime', 'course'); } if ($course->get_end() < $course->get_start()) { return get_string('errorendbeforestart', 'course'); } // A course that lasts longer than 1 year probably have wrong start or end dates. if ($course->get_end() - $course->get_start() > (YEARSECS + (WEEKSECS * 4))) { return get_string('coursetoolong', 'course'); } // Finished courses can not be used to get predictions. if (!$fortraining && $course->is_finished()) { return get_string('coursealreadyfinished', 'course'); } if ($fortraining) { // Ongoing courses data can not be used to train. if (!$course->is_finished()) { return get_string('coursenotyetfinished', 'course'); } } return true; } /** * Discard student enrolments that are invalid. * * Note that this method assumes that the target is only interested in enrolments that are/were active * between the current course start and end times. Targets interested in predicting students at risk before * their enrolment start and targets interested in getting predictions for students whose enrolment already * finished should overwrite this method as these students are discarded by this method. * * @param int $sampleid * @param \core_analytics\analysable $course * @param bool $fortraining * @return bool */ public function is_valid_sample($sampleid, \core_analytics\analysable $course, $fortraining = true) { $now = time(); $userenrol = $this->retrieve('user_enrolments', $sampleid); if ($userenrol->timeend && $course->get_start() > $userenrol->timeend) { // Discard enrolments which time end is prior to the course start. This should get rid of // old user enrolments that remain on the course. return false; } $limit = $course->get_start() - (YEARSECS + (WEEKSECS * 4)); if (($userenrol->timestart && $userenrol->timestart < $limit) || (!$userenrol->timestart && $userenrol->timecreated < $limit)) { // Following what we do in is_valid_analysable, we will discard enrolments that last more than 1 academic year // because they have incorrect start and end dates or because they are reused along multiple years // without removing previous academic years students. This may not be very accurate because some courses // can last just some months, but it is better than nothing. return false; } if ($course->get_end()) { if (($userenrol->timestart && $userenrol->timestart > $course->get_end()) || (!$userenrol->timestart && $userenrol->timecreated > $course->get_end())) { // Discard user enrolments that start after the analysable official end. return false; } } if ($now < $userenrol->timestart && $userenrol->timestart) { // Discard enrolments whose start date is after now (no need to check timecreated > $now :P). return false; } if (!$fortraining && $userenrol->timeend && $userenrol->timeend < $now) { // We don't want to generate predictions for finished enrolments. return false; } return true; } /** * prediction_actions * * @param \core_analytics\prediction $prediction * @param bool $includedetailsaction * @param bool $isinsightuser * @return \core_analytics\prediction_action[] */ public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false, $isinsightuser = false) { $actions = array(); $sampledata = $prediction->get_sample_data(); $studentid = $sampledata['user']->id; // View outline report. $url = new \moodle_url('/report/outline/user.php', array('id' => $studentid, 'course' => $sampledata['course']->id, 'mode' => 'outline')); $pix = new \pix_icon('i/report', get_string('outlinereport')); $actions[] = new \core_analytics\prediction_action('viewoutlinereport', $prediction, $url, $pix, get_string('outlinereport'), false, ['target' => '_blank']); return array_merge(parent::prediction_actions($prediction, $includedetailsaction, $isinsightuser), $actions); } /** * Suggested bulk actions for a user. * * @param \core_analytics\prediction[] $predictions List of predictions suitable for the bulk actions to use. * @return \core_analytics\bulk_action[] The list of bulk actions. */ public function bulk_actions(array $predictions) { $actions = []; $userids = []; foreach ($predictions as $prediction) { $sampledata = $prediction->get_sample_data(); $userid = $sampledata['user']->id; // Indexed by prediction id because we want the predictionid-userid // mapping later when sending the message. $userids[$prediction->get_prediction_data()->id] = $userid; } // Send a message for all the students. $attrs = array( 'data-bulk-sendmessage' => '1', 'data-prediction-to-user-id' => json_encode($userids) ); $actions[] = new \core_analytics\bulk_action(self::MESSAGE_ACTION_NAME, new \moodle_url(''), new \pix_icon('t/message', get_string('sendmessage', 'message')), get_string('sendmessage', 'message'), true, $attrs); return array_merge($actions, parent::bulk_actions($predictions)); } /** * Adds the JS required to run the bulk actions. */ public function add_bulk_actions_js() { global $PAGE; $PAGE->requires->js_call_amd('report_insights/message_users', 'init', ['.insights-bulk-actions', self::MESSAGE_ACTION_NAME]); parent::add_bulk_actions_js(); } /** * Is/was this user enrolment active during most of the analysis interval? * * This method discards enrolments that were not active during most of the analysis interval. It is * important to discard these enrolments because the indicator calculations can lead to misleading * results. * * Note that this method assumes that the target is interested in enrolments that are/were active * during the analysis interval. Targets interested in predicting students at risk before * their enrolment start should not call this method. Similarly, targets interested in getting * predictions for students whose enrolment already finished should not call this method either. * * @param int $sampleid The id of the sample that is being calculated * @param int $starttime The analysis interval start time * @param int $endtime The analysis interval end time * @return bool */ protected function enrolment_active_during_analysis_time(int $sampleid, int $starttime, int $endtime) { $userenrol = $this->retrieve('user_enrolments', $sampleid); if (!empty($userenrol->timestart)) { $enrolstart = $userenrol->timestart; } else { // This is always set. $enrolstart = $userenrol->timecreated; } if (!empty($userenrol->timeend)) { $enrolend = $userenrol->timeend; } else { // Default to tre end of the world. $enrolend = PHP_INT_MAX; } if ($endtime && $endtime < $enrolstart) { /* The enrolment starts/ed after the analysis end time. * |=========| |----------| * A start A end E start E end */ return false; } if ($starttime && $enrolend < $starttime) { /* The enrolment finishes/ed before the analysis start time. * |---------| |==========| * E start E end A start A end */ return false; } // Now we want to discard enrolments that were not active for most of the analysis interval. We // need both a $starttime and an $endtime to calculate this. if (!$starttime) { // Early return. Nothing to discard if there is no start. return true; } if (!$endtime) { // We can not calculate in relative terms (percent) how far from the analysis start time // this enrolment start is/was. return true; } if ($enrolstart < $starttime && $endtime < $enrolend) { /* The enrolment is active during all the analysis time. * |-----------------------------| * |========| * E start A start A end E end */ return true; } // If we reach this point is because the enrolment is only active for a portion of the analysis interval. // Therefore, we check that it was active for most of the analysis interval, a self::ENROL_ACTIVE_PERCENT_REQUIRED. if ($starttime <= $enrolstart && $enrolend <= $endtime) { /* |=============================| * |--------| * A start E start E end A end */ $activeenrolduration = $enrolend - $enrolstart; } else if ($enrolstart <= $starttime && $enrolend <= $endtime) { /* |===================| * |------------------| * E start A start E end A end */ $activeenrolduration = $enrolend - $starttime; } else if ($starttime <= $enrolstart && $endtime <= $enrolend) { /* |===================| * |------------------| * A start E start A end E end */ $activeenrolduration = $endtime - $enrolstart; } $analysisduration = $endtime - $starttime; if (floatval($activeenrolduration) / floatval($analysisduration) < self::ENROL_ACTIVE_PERCENT_REQUIRED) { // The student was not enroled in the course for most of the analysis interval. return false; } // We happily return true if the enrolment was active for more than self::ENROL_ACTIVE_PERCENT_REQUIRED of // the analysis interval. return true; } }