Файловый менеджер - Редактировать - /home/harasnat/www/mf/completion.tar
Назад
completion_criteria_completion.php 0000604 00000012513 15062070516 0013541 0 ustar 00 <?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/>. /** * Completion data for a specific user, course and critieria * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/completion/data_object.php'); /** * Completion data for a specific user, course and critieria * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class completion_criteria_completion extends data_object { /* @var string Database table that stores completion type criteria */ public $table = 'course_completion_crit_compl'; /* @var array Array of required table fields, must start with 'id'. */ public $required_fields = array('id', 'userid', 'course', 'criteriaid', 'gradefinal', 'rpl', 'unenroled', 'timecompleted'); /* @var array Array of unique fields, used in where clauses */ public $unique_fields = array('userid', 'course', 'criteriaid'); /* @var int User ID */ public $userid; /* @var int course ID */ public $course; /* @var int The id of the course completion criteria this completion references */ public $criteriaid; /* @var float The final grade for the user in the course (if completing a grade criteria) */ public $gradefinal; /* @var string Record of prior learning, leave blank if none */ public $rpl; /* @var int Timestamp of user unenrolment (if completing a unenrol criteria) */ public $unenroled; /* @var int Timestamp of course criteria completion {@link completion_criteria_completion::mark_complete()} */ public $timecompleted; /* @var completion_criterria Associated criteria object */ private $_criteria; /** * Finds and returns a data_object instance based on params. * * @param array $params associative arrays varname=>value * @return data_object instance of data_object or false if none found. */ public static function fetch($params) { return self::fetch_helper('course_completion_crit_compl', __CLASS__, $params); } /** * Finds and returns all data_object instances based on params. * * @param array $params associative arrays varname=>value * @return array array of data_object insatnces or false if none found. */ public static function fetch_all($params) {} /** * Return status of this criteria completion * * @return bool */ public function is_complete() { return (bool) $this->timecompleted; } /** * Mark this criteria complete for the associated user * * This method creates a course_completion_crit_compl record * * @param int $timecompleted Time completed (optional). * @return int id of completion record. */ public function mark_complete($timecompleted = null) { if (empty($timecompleted)) { $timecompleted = time(); } // Create record $this->timecompleted = $timecompleted; // Save record if ($this->id) { $this->update(); } else { $this->insert(); } // Mark course completion record as started (if not already) $cc = array( 'course' => $this->course, 'userid' => $this->userid ); $ccompletion = new completion_completion($cc); $result = $ccompletion->mark_inprogress($this->timecompleted); return $result; } /** * Attach a preloaded criteria object to this object * * @param $criteria object completion_criteria */ public function attach_criteria(completion_criteria $criteria) { $this->_criteria = $criteria; } /** * Return the associated criteria with this completion * If nothing attached, load from the db * * @return completion_criteria */ public function get_criteria() { if (!$this->_criteria) { global $DB; $params = array( 'id' => $this->criteriaid ); $record = $DB->get_record('course_completion_criteria', $params); $this->attach_criteria(completion_criteria::factory((array) $record)); } return $this->_criteria; } /** * Return criteria status text for display in reports {@link completion_criteria::get_status()} * * @return string */ public function get_status() { return $this->_criteria->get_status($this); } } completion_completion.php 0000604 00000017462 15062070516 0011667 0 ustar 00 <?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/>. /** * Course completion status for a particular user/course * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/completion/data_object.php'); /** * Course completion status for a particular user/course * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class completion_completion extends data_object { /* @var string $table Database table name that stores completion information */ public $table = 'course_completions'; /* @var array $required_fields Array of required table fields, must start with 'id'. */ public $required_fields = array('id', 'userid', 'course', 'timeenrolled', 'timestarted', 'timecompleted', 'reaggregate'); /* @var int $userid User ID */ public $userid; /* @var int $course Course ID */ public $course; /* @var int Time of course enrolment {@link completion_completion::mark_enrolled()} */ public $timeenrolled; /** * Time the user started their course completion {@link completion_completion::mark_inprogress()} * @var int */ public $timestarted; /* @var int Timestamp of course completion {@link completion_completion::mark_complete()} */ public $timecompleted; /* @var int Flag to trigger cron aggregation (timestamp) */ public $reaggregate; /** @var float user's course grade. */ public $gradefinal; /** * Finds and returns a data_object instance based on params. * * @param array $params associative arrays varname = >value * @return data_object instance of data_object or false if none found. */ public static function fetch($params) { $cache = cache::make('core', 'coursecompletion'); $key = $params['userid'] . '_' . $params['course']; if ($hit = $cache->get($key)) { return $hit['value']; } $tocache = self::fetch_helper('course_completions', __CLASS__, $params); $cache->set($key, ['value' => $tocache]); return $tocache; } /** * Return status of this completion * * @return bool */ public function is_complete() { return (bool) $this->timecompleted; } /** * Mark this user as started (or enrolled) in this course * * If the user is already marked as started, no change will occur * * @param integer $timeenrolled Time enrolled (optional) * @return int|null id of completion record on successful update. */ public function mark_enrolled($timeenrolled = null) { if ($this->timeenrolled === null) { if ($timeenrolled === null) { $timeenrolled = time(); } $this->timeenrolled = $timeenrolled; } return $this->_save(); } /** * Mark this user as inprogress in this course * * If the user is already marked as inprogress, the time will not be changed * * @param integer $timestarted Time started (optional) * @return int|null id of completion record on successful update. */ public function mark_inprogress($timestarted = null) { $timenow = time(); // Set reaggregate flag $this->reaggregate = $timenow; if (!$this->timestarted) { if (!$timestarted) { $timestarted = $timenow; } $this->timestarted = $timestarted; } return $this->_save(); } /** * Mark this user complete in this course * * This generally happens when the required completion criteria * in the course are complete. * * @param integer $timecomplete Time completed (optional) * @return int|null id of completion record on successful update. */ public function mark_complete($timecomplete = null) { global $USER; // Never change a completion time. if ($this->timecompleted) { return null; } // Use current time if nothing supplied. if (!$timecomplete) { $timecomplete = time(); } // Set time complete. $this->timecompleted = $timecomplete; // Save record. if ($result = $this->_save()) { $data = $this->get_record_data(); \core\event\course_completed::create_from_completion($data)->trigger(); } // Notify user. $course = get_course($data->course); $messagesubject = get_string('coursecompleted', 'completion'); $a = [ 'coursename' => get_course_display_name_for_list($course), 'courselink' => (string) new moodle_url('/course/view.php', array('id' => $course->id)), ]; $messagebody = get_string('coursecompletedmessage', 'completion', $a); $messageplaintext = html_to_text($messagebody); $eventdata = new \core\message\message(); $eventdata->courseid = $course->id; $eventdata->component = 'moodle'; $eventdata->name = 'coursecompleted'; $eventdata->userfrom = core_user::get_noreply_user(); $eventdata->userto = $data->userid; $eventdata->notification = 1; $eventdata->subject = $messagesubject; $eventdata->fullmessage = $messageplaintext; $eventdata->fullmessageformat = FORMAT_HTML; $eventdata->fullmessagehtml = $messagebody; $eventdata->smallmessage = $messageplaintext; if ($courseimage = \core_course\external\course_summary_exporter::get_course_image($course)) { $eventdata->customdata = [ 'notificationpictureurl' => $courseimage, ]; } message_send($eventdata); return $result; } /** * Save course completion status * * This method creates a course_completions record if none exists * @access private * @return int|null id of completion record on successful update. */ private function _save() { if ($this->timeenrolled === null) { $this->timeenrolled = 0; } // Save record if (isset($this->id)) { $success = $this->update(); } else { // Make sure reaggregate field is not null if (!$this->reaggregate) { $this->reaggregate = 0; } // Make sure timestarted is not null if (!$this->timestarted) { $this->timestarted = 0; } $success = $this->insert(); } if ($success) { // Update the cached record. $cache = cache::make('core', 'coursecompletion'); $data = $this->get_record_data(); $key = $data->userid . '_' . $data->course; $cache->set($key, ['value' => $data]); return $this->id; } return null; } } criteria/completion_criteria_activity.php 0000604 00000021047 15062070516 0015030 0 ustar 00 <?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/>. /** * This file contains the activity completion criteria type class and any * supporting functions it may require. * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Course completion critieria - completion on activity completion * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class completion_criteria_activity extends completion_criteria { /* @var int Criteria [COMPLETION_CRITERIA_TYPE_ACTIVITY] */ public $criteriatype = COMPLETION_CRITERIA_TYPE_ACTIVITY; /** * Finds and returns a data_object instance based on params. * * @param array $params associative arrays varname=>value * @return completion_criteria_activity data_object instance or false if none found. */ public static function fetch($params) { $params['criteriatype'] = COMPLETION_CRITERIA_TYPE_ACTIVITY; return self::fetch_helper('course_completion_criteria', __CLASS__, $params); } /** * Add appropriate form elements to the critieria form * * @param moodleform $mform Moodle forms object * @param stdClass $data details of various modules */ public function config_form_display(&$mform, $data = null) { $modnames = get_module_types_names(); $mform->addElement('advcheckbox', 'criteria_activity['.$data->id.']', $modnames[self::get_mod_name($data->module)] . ' - ' . format_string($data->name), null, array('group' => 1)); if ($this->id) { $mform->setDefault('criteria_activity['.$data->id.']', 1); } } /** * Update the criteria information stored in the database * * @param stdClass $data Form data */ public function update_config(&$data) { global $DB; if (!empty($data->criteria_activity) && is_array($data->criteria_activity)) { $this->course = $data->id; // Data comes from advcheckbox, so contains keys for all activities. // A value of 0 is 'not checked' whereas 1 is 'checked'. foreach ($data->criteria_activity as $activity => $val) { // Only update those which are checked. if (!empty($val)) { $module = $DB->get_record('course_modules', array('id' => $activity)); $this->module = self::get_mod_name($module->module); $this->moduleinstance = $activity; $this->id = null; $this->insert(); } } } } /** * Get module instance module type * * @param int $type Module type id * @return string */ public static function get_mod_name($type) { static $types; if (!is_array($types)) { global $DB; $types = $DB->get_records('modules'); } return $types[$type]->name; } /** * Gets the module instance from the database and returns it. * If no module instance exists this function returns false. * * @return stdClass|bool */ public function get_mod_instance() { global $DB; return $DB->get_record_sql( " SELECT m.* FROM {{$this->module}} m INNER JOIN {course_modules} cm ON cm.id = {$this->moduleinstance} AND m.id = cm.instance " ); } /** * Review this criteria and decide if the user has completed * * @param completion_completion $completion The user's completion record * @param bool $mark Optionally set false to not save changes to database * @return bool */ public function review($completion, $mark = true) { global $DB; $course = $DB->get_record('course', array('id' => $completion->course)); $cm = $DB->get_record('course_modules', array('id' => $this->moduleinstance)); $info = new completion_info($course); $data = $info->get_data($cm, false, $completion->userid); // If the activity is complete if (in_array($data->completionstate, array(COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS, COMPLETION_COMPLETE_FAIL))) { if ($mark) { $completion->mark_complete(); } return true; } return false; } /** * Return criteria title for display in reports * * @return string */ public function get_title() { return get_string('activitiescompleted', 'completion'); } /** * Return a more detailed criteria title for display in reports * * @return string */ public function get_title_detailed() { global $DB; $module = $DB->get_record('course_modules', array('id' => $this->moduleinstance)); $activity = $DB->get_record($this->module, array('id' => $module->instance)); return shorten_text(format_string($activity->name, true, array('context' => context_module::instance($module->id)))); } /** * Return criteria type title for display in reports * * @return string */ public function get_type_title() { return get_string('activities', 'completion'); } /** * Find users who have completed this criteria and mark them accordingly */ public function cron() { \core_completion\api::mark_course_completions_activity_criteria(); } /** * Return criteria progress details for display in reports * * @param completion_completion $completion The user's completion record * @return array An array with the following keys: * type, criteria, requirement, status */ public function get_details($completion) { // Get completion info $modinfo = get_fast_modinfo($completion->course); $cm = $modinfo->get_cm($this->moduleinstance); $details = array(); $details['type'] = $this->get_title(); if ($cm->has_view()) { $details['criteria'] = html_writer::link($cm->url, $cm->get_formatted_name()); } else { $details['criteria'] = $cm->get_formatted_name(); } // Build requirements $details['requirement'] = array(); if ($cm->completion == COMPLETION_TRACKING_MANUAL) { $details['requirement'][] = get_string('markingyourselfcomplete', 'completion'); } elseif ($cm->completion == COMPLETION_TRACKING_AUTOMATIC) { if ($cm->completionview) { $modulename = core_text::strtolower(get_string('modulename', $this->module)); $details['requirement'][] = get_string('viewingactivity', 'completion', $modulename); } if (!is_null($cm->completiongradeitemnumber)) { $details['requirement'][] = get_string('achievinggrade', 'completion'); } if ($cm->completionpassgrade) { $details['requirement'][] = get_string('achievingpassinggrade', 'completion'); } } $details['requirement'] = implode(', ', $details['requirement']); $details['status'] = ''; return $details; } /** * Return pix_icon for display in reports. * * @param string $alt The alt text to use for the icon * @param array $attributes html attributes * @return pix_icon */ public function get_icon($alt, array $attributes = null) { return new pix_icon('monologo', $alt, "mod_{$this->module}", $attributes); } } criteria/completion_criteria_date.php 0000604 00000016410 15062070516 0014107 0 ustar 00 <?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/>. /** * This file contains the date criteria type * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Course completion critieria - completion on specified date * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class completion_criteria_date extends completion_criteria { /* @var int Criteria type constant [COMPLETION_CRITERIA_TYPE_DATE] */ public $criteriatype = COMPLETION_CRITERIA_TYPE_DATE; /** * Finds and returns a data_object instance based on params. * * @param array $params associative arrays varname=>value * @return data_object data_object instance or false if none found. */ public static function fetch($params) { $params['criteriatype'] = COMPLETION_CRITERIA_TYPE_DATE; return self::fetch_helper('course_completion_criteria', __CLASS__, $params); } /** * Add appropriate form elements to the critieria form * * @param MoodleQuickForm $mform Moodle forms object * @param stdClass $data not used */ public function config_form_display(&$mform, $data = null) { $mform->addElement('checkbox', 'criteria_date', get_string('enable')); $mform->addElement('date_selector', 'criteria_date_value', get_string('completionondatevalue', 'core_completion')); $mform->disabledIf('criteria_date_value', 'criteria_date'); // If instance of criteria exists if ($this->id) { $mform->setDefault('criteria_date', 1); $mform->setDefault('criteria_date_value', $this->timeend); } else { $mform->setDefault('criteria_date_value', time() + 3600 * 24); } } /** * Update the criteria information stored in the database * * @param stdClass $data Form data */ public function update_config(&$data) { if (!empty($data->criteria_date)) { $this->course = $data->id; $this->timeend = $data->criteria_date_value; $this->insert(); } } /** * Review this criteria and decide if the user has completed * * @param completion_completion $completion The user's completion record * @param bool $mark Optionally set false to not save changes to database * @return bool */ public function review($completion, $mark = true) { // If current time is past timeend if ($this->timeend && $this->timeend < time()) { if ($mark) { $completion->mark_complete(); } return true; } return false; } /** * Return criteria title for display in reports * * @return string */ public function get_title() { return get_string('date'); } /** * Return a more detailed criteria title for display in reports * * @return string */ public function get_title_detailed() { return userdate($this->timeend, '%d-%h-%y'); } /** * Return criteria type title for display in reports * * @return string */ public function get_type_title() { return get_string('date'); } /** * Return criteria status text for display in reports * * @param completion_completion $completion The user's completion record * @return string */ public function get_status($completion) { return $completion->is_complete() ? get_string('yes') : userdate($this->timeend, '%d-%h-%y'); } /** * Find user's who have completed this criteria */ public function cron() { global $DB; // Get all users who match meet this criteria $sql = ' SELECT DISTINCT c.id AS course, cr.timeend AS timeend, cr.id AS criteriaid, ra.userid AS userid FROM {course_completion_criteria} cr INNER JOIN {course} c ON cr.course = c.id INNER JOIN {context} con ON con.instanceid = c.id INNER JOIN {role_assignments} ra ON ra.contextid = con.id LEFT JOIN {course_completion_crit_compl} cc ON cc.criteriaid = cr.id AND cc.userid = ra.userid WHERE cr.criteriatype = '.COMPLETION_CRITERIA_TYPE_DATE.' AND con.contextlevel = '.CONTEXT_COURSE.' AND c.enablecompletion = 1 AND cc.id IS NULL AND cr.timeend < ? '; // Loop through completions, and mark as complete $rs = $DB->get_recordset_sql($sql, array(time())); foreach ($rs as $record) { $completion = new completion_criteria_completion((array) $record, DATA_OBJECT_FETCH_BY_KEY); $completion->mark_complete($record->timeend); } $rs->close(); } /** * Return criteria progress details for display in reports * * @param completion_completion $completion The user's completion record * @return array An array with the following keys: * type, criteria, requirement, status */ public function get_details($completion) { $details = array(); $details['type'] = get_string('datepassed', 'completion'); $details['criteria'] = get_string('remainingenroleduntildate', 'completion'); $details['requirement'] = userdate($this->timeend, '%d %B %Y'); $details['status'] = ''; return $details; } /** * Return pix_icon for display in reports. * * @param string $alt The alt text to use for the icon * @param array $attributes html attributes * @return pix_icon */ public function get_icon($alt, array $attributes = null) { return new pix_icon('i/calendar', $alt, 'moodle', $attributes); } /** * Shift the date when resetting course. * * @param int $courseid the course id * @param int $timeshift number of seconds to shift date * @return boolean was the operation successful? */ public static function update_date($courseid, $timeshift) { if ($criteria = self::fetch(array('course' => $courseid))) { $criteria->timeend = $criteria->timeend + $timeshift; $criteria->update(); } } } criteria/completion_criteria_self.php 0000604 00000011653 15062070516 0014127 0 ustar 00 <?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/>. /** * Course completion critieria - student self marked * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Course completion critieria - student self marked * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class completion_criteria_self extends completion_criteria { /* @var int Criteria type constant [COMPLETION_CRITERIA_TYPE_SELF] */ public $criteriatype = COMPLETION_CRITERIA_TYPE_SELF; /** * Finds and returns a data_object instance based on params. * * @param array $params associative arrays varname=>value * @return data_object data_object instance or false if none found. */ public static function fetch($params) { $params['criteriatype'] = COMPLETION_CRITERIA_TYPE_SELF; return self::fetch_helper('course_completion_criteria', __CLASS__, $params); } /** * Add appropriate form elements to the critieria form * * @param moodleform $mform Moodle forms object * @param stdClass $data Form data */ public function config_form_display(&$mform, $data = null) { $mform->addElement('checkbox', 'criteria_self', get_string('enable')); if ($this->id ) { $mform->setDefault('criteria_self', 1); } } /** * Update the criteria information stored in the database * * @param stdClass $data Form data */ public function update_config(&$data) { if (!empty($data->criteria_self)) { $this->course = $data->id; $this->insert(); } } /** * Mark this criteria as complete * * @param completion_completion $completion The user's completion record */ public function complete($completion) { $this->review($completion, true, true); } /** * Review this criteria and decide if the user has completed * * @param completion_completion $completion The user's completion record * @param bool $mark Optionally set false to not save changes to database * @param bool $is_complete set true to mark activity complete. * @return bool */ public function review($completion, $mark = true, $is_complete = false) { // If we are marking this as complete if ($is_complete && $mark) { $completion->completedself = 1; $completion->mark_complete(); return true; } return $completion->is_complete(); } /** * Return criteria title for display in reports * * @return string */ public function get_title() { return get_string('selfcompletion', 'completion'); } /** * Return a more detailed criteria title for display in reports * * @return string */ public function get_title_detailed() { return $this->get_title(); } /** * Return criteria type title for display in reports * * @return string */ public function get_type_title() { return get_string('self', 'completion'); } /** * Return criteria progress details for display in reports * * @param completion_completion $completion The user's completion record * @return array An array with the following keys: * type, criteria, requirement, status */ public function get_details($completion) { $details = array(); $details['type'] = $this->get_title(); $details['criteria'] = $this->get_title(); $details['requirement'] = get_string('markingyourselfcomplete', 'completion'); $details['status'] = ''; return $details; } /** * Return pix_icon for display in reports. * * @param string $alt The alt text to use for the icon * @param array $attributes html attributes * @return pix_icon */ public function get_icon($alt, array $attributes = null) { return new pix_icon('i/completion_self', $alt, 'moodle', $attributes); } } criteria/completion_criteria.php 0000604 00000021142 15062070516 0013110 0 ustar 00 <?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/>. /** * Course completion criteria * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/completion/data_object.php'); require_once($CFG->dirroot.'/completion/completion_criteria_completion.php'); /** * Self completion criteria type * Criteria type constant, primarily for storing criteria type in the database. */ define('COMPLETION_CRITERIA_TYPE_SELF', 1); /** * Date completion criteria type * Criteria type constant, primarily for storing criteria type in the database. */ define('COMPLETION_CRITERIA_TYPE_DATE', 2); /** * Unenrol completion criteria type * Criteria type constant, primarily for storing criteria type in the database. */ define('COMPLETION_CRITERIA_TYPE_UNENROL', 3); /** * Activity completion criteria type * Criteria type constant, primarily for storing criteria type in the database. */ define('COMPLETION_CRITERIA_TYPE_ACTIVITY', 4); /** * Duration completion criteria type * Criteria type constant, primarily for storing criteria type in the database. */ define('COMPLETION_CRITERIA_TYPE_DURATION', 5); /** * Grade completion criteria type * Criteria type constant, primarily for storing criteria type in the database. */ define('COMPLETION_CRITERIA_TYPE_GRADE', 6); /** * Role completion criteria type * Criteria type constant, primarily for storing criteria type in the database. */ define('COMPLETION_CRITERIA_TYPE_ROLE', 7); /** * Course completion criteria type * Criteria type constant, primarily for storing criteria type in the database. */ define('COMPLETION_CRITERIA_TYPE_COURSE', 8); /** * Criteria type constant to class name mapping. * * This global variable would be improved if it was implemented as a cache. */ global $COMPLETION_CRITERIA_TYPES; $COMPLETION_CRITERIA_TYPES = array( COMPLETION_CRITERIA_TYPE_SELF => 'self', COMPLETION_CRITERIA_TYPE_DATE => 'date', COMPLETION_CRITERIA_TYPE_UNENROL => 'unenrol', COMPLETION_CRITERIA_TYPE_ACTIVITY => 'activity', COMPLETION_CRITERIA_TYPE_DURATION => 'duration', COMPLETION_CRITERIA_TYPE_GRADE => 'grade', COMPLETION_CRITERIA_TYPE_ROLE => 'role', COMPLETION_CRITERIA_TYPE_COURSE => 'course', ); /** * Completion criteria abstract definition * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class completion_criteria extends data_object { /* @var string Database table name that stores completion criteria information */ public $table = 'course_completion_criteria'; /** * Array of required table fields, must start with 'id'. * Defaults to id, course, criteriatype, module, moduleinstane, courseinstance, * enrolperiod, timeend, gradepass, role * @var array */ public $required_fields = array('id', 'course', 'criteriatype', 'module', 'moduleinstance', 'courseinstance', 'enrolperiod', 'timeend', 'gradepass', 'role'); /* @var int Course id */ public $course; /** * Criteria type * One of the COMPLETION_CRITERIA_TYPE_* constants * @var int */ public $criteriatype; /* @var string Module type this criteria relates to (for activity criteria) */ public $module; /* @var int Course module instance id this criteria relates to (for activity criteria) */ public $moduleinstance; /** * Period after enrolment completion will be triggered (for period criteria) * The value here is the number of days as an int. * @var int */ public $enrolperiod; /** * Date of course completion (for date criteria) * This is a timestamp value * @var int */ public $date; /* @var float Passing grade required to complete course (for grade completion) */ public $gradepass; /* @var int Role ID that has the ability to mark a user as complete (for role completion) */ public $role; /** @var string course instance. */ public $courseinstance; /** @var mixed time end. */ public $timeend; /** * Finds and returns all data_object instances based on params. * * @param array $params associative arrays varname=>value * @return array array of data_object insatnces or false if none found. */ public static function fetch_all($params) {} /** * Factory method for creating correct class object * * @param array $params associative arrays varname=>value * @return completion_criteria */ public static function factory($params) { global $CFG, $COMPLETION_CRITERIA_TYPES; if (!isset($params['criteriatype']) || !isset($COMPLETION_CRITERIA_TYPES[$params['criteriatype']])) { throw new \moodle_exception('invalidcriteriatype', 'completion'); } $class = 'completion_criteria_'.$COMPLETION_CRITERIA_TYPES[$params['criteriatype']]; require_once($CFG->dirroot.'/completion/criteria/'.$class.'.php'); return new $class($params, false); } /** * Add appropriate form elements to the critieria form * * @param moodleform $mform Moodle forms object * @param mixed $data optional Any additional data that can be used to set default values in the form * @return void */ abstract public function config_form_display(&$mform, $data = null); /** * Update the criteria information stored in the database * * @param array $data Form data * @return void */ abstract public function update_config(&$data); /** * Review this criteria and decide if the user has completed * * @param object $completion The user's completion record * @param boolean $mark Optionally set false to not save changes to database * @return boolean */ abstract public function review($completion, $mark = true); /** * Return criteria title for display in reports * * @return string */ abstract public function get_title(); /** * Return a more detailed criteria title for display in reports * * @return string */ abstract public function get_title_detailed(); /** * Return criteria type title for display in reports * * @return string */ abstract public function get_type_title(); /** * Return criteria progress details for display in reports * * @param completion_completion $completion The user's completion record * @return array */ abstract public function get_details($completion); /** * Return pix_icon for display in reports. * * @param string $alt The alt text to use for the icon * @param array $attributes html attributes * @return pix_icon */ public function get_icon($alt, array $attributes = null) { global $COMPLETION_CRITERIA_TYPES; $criteriatype = $COMPLETION_CRITERIA_TYPES[$this->criteriatype]; return new pix_icon('i/'.$criteriatype, $alt, 'moodle', $attributes); } /** * Return criteria status text for display in reports * * @param completion_completion $completion The user's completion record * @return string */ public function get_status($completion) { return $completion->is_complete() ? get_string('yes') : get_string('no'); } /** * Return true if the criteria's current status is different to what is sorted * in the database, e.g. pending an update * * @param completion_completion $completion The user's criteria completion record * @return bool */ public function is_pending($completion) { $review = $this->review($completion, false); return $review !== $completion->is_complete(); } } criteria/completion_criteria_unenrol.php 0000604 00000010705 15062070516 0014655 0 ustar 00 <?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/>. /** * Course completion critieria - completion on unenrolment * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Course completion critieria - completion on unenrolment * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class completion_criteria_unenrol extends completion_criteria { /* @var int Criteria type constant [COMPLETION_CRITERIA_TYPE_UNENROL] */ public $criteriatype = COMPLETION_CRITERIA_TYPE_UNENROL; /** * Finds and returns a data_object instance based on params. * * @param array $params associative arrays varname=>value * @return data_object data_object instance or false if none found. */ public static function fetch($params) { $params['criteriatype'] = COMPLETION_CRITERIA_TYPE_UNENROL; return self::fetch_helper('course_completion_criteria', __CLASS__, $params); } /** * Add appropriate form elements to the critieria form * * @param moodleform $mform Moodle forms object * @param stdClass $data Form data */ public function config_form_display(&$mform, $data = null) { $mform->addElement('checkbox', 'criteria_unenrol', get_string('enable')); if ($this->id) { $mform->setDefault('criteria_unenrol', 1); } } /** * Update the criteria information stored in the database * * @param stdClass $data Form data */ public function update_config(&$data) { if (!empty($data->criteria_unenrol)) { $this->course = $data->id; $this->insert(); } } /** * Review this criteria and decide if the user has completed * * @param completion_completion $completion The user's completion record * @param bool $mark Optionally set false to not save changes to database * @return bool */ public function review($completion, $mark = true) { // Check enrolment return false; } /** * Return criteria title for display in reports * * @return string */ public function get_title() { return get_string('unenrol', 'enrol'); } /** * Return a more detailed criteria title for display in reports * * @return string */ public function get_title_detailed() { return $this->get_title(); } /** * Return criteria type title for display in reports * * @return string */ public function get_type_title() { return get_string('unenrol', 'enrol'); } /** * Return criteria progress details for display in reports * * @param completion_completion $completion The user's completion record * @return array An array with the following keys: * type, criteria, requirement, status */ public function get_details($completion) { $details = array(); $details['type'] = get_string('unenrolment', 'completion'); $details['criteria'] = get_string('unenrolment', 'completion'); $details['requirement'] = get_string('unenrolingfromcourse', 'completion'); $details['status'] = ''; return $details; } /** * Return pix_icon for display in reports. * * @param string $alt The alt text to use for the icon * @param array $attributes html attributes * @return pix_icon */ public function get_icon($alt, array $attributes = null) { return new pix_icon('i/user', $alt, 'moodle', $attributes); } } criteria/completion_criteria_grade.php 0000604 00000022264 15062070516 0014260 0 ustar 00 <?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/>. /** * Course completion critieria - completion on achieving course grade * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once $CFG->dirroot.'/grade/lib.php'; require_once $CFG->dirroot.'/grade/querylib.php'; /** * Course completion critieria - completion on achieving course grade * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class completion_criteria_grade extends completion_criteria { /* @var int Criteria type constant [COMPLETION_CRITERIA_TYPE_GRADE] */ public $criteriatype = COMPLETION_CRITERIA_TYPE_GRADE; /** * Finds and returns a data_object instance based on params. * * @param array $params associative array varname => value of various * parameters used to fetch data_object * @return data_object data_object instance or false if none found. */ public static function fetch($params) { $params['criteriatype'] = COMPLETION_CRITERIA_TYPE_GRADE; return self::fetch_helper('course_completion_criteria', __CLASS__, $params); } /** * Add appropriate form elements to the critieria form * * @param moodle_form $mform Moodle forms object * @param stdClass $data containing default values to be set in the form */ public function config_form_display(&$mform, $data = null) { $mform->addElement('checkbox', 'criteria_grade', get_string('enable')); $mform->addElement('text', 'criteria_grade_value', get_string('graderequired', 'completion')); $mform->disabledIf('criteria_grade_value', 'criteria_grade'); $mform->setType('criteria_grade_value', PARAM_RAW); // Uses unformat_float. // Grades are stored in Moodle with 5 decimal points, make sure we do not accidentally round them // when setting the form value. $mform->setDefault('criteria_grade_value', format_float($data, 5)); if ($this->id) { $mform->setDefault('criteria_grade', 1); $mform->setDefault('criteria_grade_value', format_float($this->gradepass, 5)); } } /** * Update the criteria information stored in the database * * @param stdClass $data Form data */ public function update_config(&$data) { if (!empty($data->criteria_grade)) { $formatedgrade = unformat_float($data->criteria_grade_value); // TODO validation if (!empty($formatedgrade) && is_numeric($formatedgrade)) { $this->course = $data->id; $this->gradepass = $formatedgrade; $this->insert(); } } } /** * Get user's course grade in this course * * @param completion_completion $completion an instance of completion_completion class * @return float */ private function get_grade($completion) { $grade = grade_get_course_grade($completion->userid, $this->course); return $grade->grade; } /** * Review this criteria and decide if the user has completed * * @param completion_completion $completion The user's completion record * @param bool $mark Optionally set false to not save changes to database * @return bool */ public function review($completion, $mark = true) { // Get user's course grade $grade = $this->get_grade($completion); // If user's current course grade is higher than the required pass grade if ($this->gradepass && $this->gradepass <= $grade) { if ($mark) { $completion->gradefinal = $grade; $completion->mark_complete(); } return true; } return false; } /** * Return criteria title for display in reports * * @return string */ public function get_title() { return get_string('coursegrade', 'completion'); } /** * Return a more detailed criteria title for display in reports * * @return string */ public function get_title_detailed() { global $CFG; require_once($CFG->libdir . '/gradelib.php'); $decimalpoints = grade_get_setting($this->course, 'decimalpoints', $CFG->grade_decimalpoints); $graderequired = format_float($this->gradepass, $decimalpoints); return get_string('gradexrequired', 'completion', $graderequired); } /** * Return criteria type title for display in reports * * @return string */ public function get_type_title() { return get_string('gradenoun'); } /** * Return criteria status text for display in reports * * @param completion_completion $completion The user's completion record * @return string */ public function get_status($completion) { global $CFG; require_once($CFG->libdir . '/gradelib.php'); $decimalpoints = grade_get_setting($this->course, 'decimalpoints', $CFG->grade_decimalpoints); $grade = $this->get_grade($completion); $graderequired = $this->get_title_detailed(); if ($grade) { $grade = format_float($grade, $decimalpoints); } else { $grade = get_string('nograde'); } return $grade.' ('.$graderequired.')'; } /** * Find user's who have completed this criteria */ public function cron() { global $DB; // Get all users who meet this criteria $sql = ' SELECT DISTINCT c.id AS course, cr.id AS criteriaid, ra.userid AS userid, gg.finalgrade AS gradefinal, gg.timemodified AS timecompleted FROM {course_completion_criteria} cr INNER JOIN {course} c ON cr.course = c.id INNER JOIN {context} con ON con.instanceid = c.id INNER JOIN {role_assignments} ra ON ra.contextid = con.id INNER JOIN {grade_items} gi ON gi.courseid = c.id AND gi.itemtype = \'course\' INNER JOIN {grade_grades} gg ON gg.itemid = gi.id AND gg.userid = ra.userid LEFT JOIN {course_completion_crit_compl} cc ON cc.criteriaid = cr.id AND cc.userid = ra.userid WHERE cr.criteriatype = '.COMPLETION_CRITERIA_TYPE_GRADE.' AND con.contextlevel = '.CONTEXT_COURSE.' AND c.enablecompletion = 1 AND cc.id IS NULL AND gg.finalgrade >= cr.gradepass '; // Loop through completions, and mark as complete $rs = $DB->get_recordset_sql($sql); foreach ($rs as $record) { $completion = new completion_criteria_completion((array) $record, DATA_OBJECT_FETCH_BY_KEY); $completion->mark_complete($record->timecompleted); } $rs->close(); } /** * Return criteria progress details for display in reports * * @param completion_completion $completion The user's completion record * @return array An array with the following keys: * type, criteria, requirement, status */ public function get_details($completion) { global $CFG; require_once($CFG->libdir . '/gradelib.php'); $decimalpoints = grade_get_setting($this->course, 'decimalpoints', $CFG->grade_decimalpoints); $details = array(); $details['type'] = get_string('coursegrade', 'completion'); $details['criteria'] = get_string('graderequired', 'completion'); $details['requirement'] = format_float($this->gradepass, $decimalpoints); $details['status'] = ''; $grade = format_float($this->get_grade($completion), $decimalpoints); if ($grade) { $details['status'] = $grade; } return $details; } /** * Return pix_icon for display in reports. * * @param string $alt The alt text to use for the icon * @param array $attributes html attributes * @return pix_icon */ public function get_icon($alt, array $attributes = null) { return new pix_icon('i/grades', $alt, 'moodle', $attributes); } } criteria/completion_criteria_course.php 0000604 00000016710 15062070516 0014475 0 ustar 00 <?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/>. /** * This file contains the course criteria type. * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Course completion critieria - completion on course completion * * This course completion criteria depends on another course with * completion enabled to be marked as complete for this user * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class completion_criteria_course extends completion_criteria { /* @var int Criteria type constant */ public $criteriatype = COMPLETION_CRITERIA_TYPE_COURSE; /** * Finds and returns a data_object instance based on params. * * @param array $params associative arrays varname=>value * @return data_object instance of data_object or false if none found. */ public static function fetch($params) { $params['criteriatype'] = COMPLETION_CRITERIA_TYPE_COURSE; return self::fetch_helper('course_completion_criteria', __CLASS__, $params); } /** * Add appropriate form elements to the critieria form * * @param moodle_form $mform Moodle forms object * @param stdClass $data data used to define default value of the form */ public function config_form_display(&$mform, $data = null) { global $CFG; $link = "<a href=\"{$CFG->wwwroot}/course/view.php?id={$data->id}\">".s($data->fullname).'</a>'; $mform->addElement('checkbox', 'criteria_course['.$data->id.']', $link); if ($this->id) { $mform->setDefault('criteria_course['.$data->id.']', 1); } } /** * Update the criteria information stored in the database * * @param array $data Form data */ public function update_config(&$data) { if (!empty($data->criteria_course) && is_array($data->criteria_course)) { $this->course = $data->id; foreach ($data->criteria_course as $course) { $this->courseinstance = $course; $this->id = NULL; $this->insert(); } } } /** * Review this criteria and decide if the user has completed * * @param completion_completion $completion The user's completion record * @param bool $mark Optionally set false to not save changes to database * @return bool */ public function review($completion, $mark = true) { global $DB; $course = $DB->get_record('course', array('id' => $this->courseinstance)); $info = new completion_info($course); // If the course is complete if ($info->is_course_complete($completion->userid)) { if ($mark) { $completion->mark_complete(); } return true; } return false; } /** * Return criteria title for display in reports * * @return string */ public function get_title() { return get_string('dependenciescompleted', 'completion'); } /** * Return a more detailed criteria title for display in reports * * @return string */ public function get_title_detailed() { global $DB; $prereq = $DB->get_record('course', array('id' => $this->courseinstance)); $coursecontext = context_course::instance($prereq->id, MUST_EXIST); $fullname = format_string($prereq->fullname, true, array('context' => $coursecontext)); return shorten_text(urldecode($fullname)); } /** * Return criteria type title for display in reports * * @return string */ public function get_type_title() { return get_string('dependencies', 'completion'); } /** * Find user's who have completed this criteria */ public function cron() { global $DB; // Get all users who meet this criteria $sql = " SELECT DISTINCT c.id AS course, cr.id AS criteriaid, ra.userid AS userid, cc.timecompleted AS timecompleted FROM {course_completion_criteria} cr INNER JOIN {course} c ON cr.course = c.id INNER JOIN {context} con ON con.instanceid = c.id INNER JOIN {role_assignments} ra ON ra.contextid = con.id INNER JOIN {course_completions} cc ON cc.course = cr.courseinstance AND cc.userid = ra.userid LEFT JOIN {course_completion_crit_compl} ccc ON ccc.criteriaid = cr.id AND ccc.userid = ra.userid WHERE cr.criteriatype = ".COMPLETION_CRITERIA_TYPE_COURSE." AND con.contextlevel = ".CONTEXT_COURSE." AND c.enablecompletion = 1 AND ccc.id IS NULL AND cc.timecompleted IS NOT NULL "; // Loop through completions, and mark as complete $rs = $DB->get_recordset_sql($sql); foreach ($rs as $record) { $completion = new completion_criteria_completion((array) $record, DATA_OBJECT_FETCH_BY_KEY); $completion->mark_complete($record->timecompleted); } $rs->close(); } /** * Return criteria progress details for display in reports * * @param completion_completion $completion The user's completion record * @return array An array with the following keys: * type, criteria, requirement, status */ public function get_details($completion) { global $CFG, $DB; // Get completion info $course = new stdClass(); $course->id = $completion->course; $info = new completion_info($course); $prereq = $DB->get_record('course', array('id' => $this->courseinstance)); $coursecontext = context_course::instance($prereq->id, MUST_EXIST); $fullname = format_string($prereq->fullname, true, array('context' => $coursecontext)); $prereq_info = new completion_info($prereq); $details = array(); $details['type'] = $this->get_title(); $details['criteria'] = '<a href="'.$CFG->wwwroot.'/course/view.php?id='.$this->courseinstance.'">'.s($fullname).'</a>'; $details['requirement'] = get_string('coursecompleted', 'completion'); $details['status'] = '<a href="'.$CFG->wwwroot.'/blocks/completionstatus/details.php?course='.$this->courseinstance.'">'.get_string('seedetails', 'completion').'</a>'; return $details; } } criteria/completion_criteria_role.php 0000604 00000012143 15062070516 0014132 0 ustar 00 <?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/>. /** * Course completion critieria - marked by role * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Course completion critieria - marked by role * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class completion_criteria_role extends completion_criteria { /* @var int Criteria type constant [COMPLETION_CRITERIA_TYPE_ROLE] */ public $criteriatype = COMPLETION_CRITERIA_TYPE_ROLE; /** * Finds and returns a data_object instance based on params. * * @param array $params associative arrays varname=>value * @return data_object data_object instance or false if none found. */ public static function fetch($params) { $params['criteriatype'] = COMPLETION_CRITERIA_TYPE_ROLE; return self::fetch_helper('course_completion_criteria', __CLASS__, $params); } /** * Add appropriate form elements to the critieria form * * @param MoodleQuickForm $mform Moodle forms object * @param stdClass $data used to set default values of the form */ public function config_form_display(&$mform, $data = null) { $mform->addElement('checkbox', 'criteria_role['.$data->id.']', $this->get_title($data)); if ($this->id) { $mform->setDefault('criteria_role['.$data->id.']', 1); } } /** * Update the criteria information stored in the database * * @param stdClass $data Form data */ public function update_config(&$data) { if (!empty($data->criteria_role) && is_array($data->criteria_role)) { $this->course = $data->id; foreach (array_keys($data->criteria_role) as $role) { $this->role = $role; $this->id = NULL; $this->insert(); } } } /** * Mark this criteria as complete * * @param completion_completion $completion The user's completion record */ public function complete($completion) { $this->review($completion, true, true); } /** * Review this criteria and decide if the user has completed * * @param completion_completion $completion The user's completion record * @param bool $mark Optionally set false to not save changes to database * @param bool $is_complete Set to false if the criteria has been completed just now. * @return bool */ public function review($completion, $mark = true, $is_complete = false) { // If we are marking this as complete if ($is_complete && $mark) { $completion->completedself = 1; $completion->mark_complete(); return true; } return $completion->is_complete(); } /** * Return criteria title for display in reports * * @return string */ public function get_title() { global $DB; $role = $DB->get_record('role', array('id' => $this->role)); if (!$role) { return '['.get_string('roleidnotfound', 'completion', $this->role).']'; } return role_get_name($role, context_course::instance($this->course)); } /** * Return a more detailed criteria title for display in reports * * @return string */ public function get_title_detailed() { return $this->get_title(); } /** * Return criteria type title for display in reports * * @return string */ public function get_type_title() { return get_string('approval', 'completion'); } /** * Return criteria progress details for display in reports * * @param completion_completion $completion The user's completion record * @return array An array with the following keys: * type, criteria, requirement, status */ public function get_details($completion) { $details = array(); $details['type'] = get_string('manualcompletionby', 'completion'); $details['criteria'] = $this->get_title(); $details['requirement'] = get_string('markedcompleteby', 'completion', $details['criteria']); $details['status'] = ''; return $details; } } criteria/completion_criteria_duration.php 0000604 00000023174 15062070516 0015024 0 ustar 00 <?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/>. /** * Course completion critieria - completion after specific duration from course enrolment * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Course completion critieria - completion after specific duration from course enrolment * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class completion_criteria_duration extends completion_criteria { /* @var int Criteria type constant [COMPLETION_CRITERIA_TYPE_DURATION] */ public $criteriatype = COMPLETION_CRITERIA_TYPE_DURATION; /** * Finds and returns a data_object instance based on params. * * @param array $params associative arrays varname=>value * @return data_object data_object instance or false if none found. */ public static function fetch($params) { $params['criteriatype'] = COMPLETION_CRITERIA_TYPE_DURATION; return self::fetch_helper('course_completion_criteria', __CLASS__, $params); } /** * Add appropriate form elements to the critieria form * * @param moodleform $mform Moodle forms object * @param stdClass $data not used */ public function config_form_display(&$mform, $data = null) { $mform->addElement('checkbox', 'criteria_duration', get_string('enable')); // Populate the duration length drop down. $thresholdmenu = array( // We have strings for 1 - 6 days in the core. 86400 => get_string('secondstotime86400', 'core'), 172800 => get_string('secondstotime172800', 'core'), 259200 => get_string('secondstotime259200', 'core'), 345600 => get_string('secondstotime345600', 'core'), 432000 => get_string('secondstotime432000', 'core'), 518400 => get_string('secondstotime518400', 'core'), 518400 => get_string('secondstotime518400', 'core'), ); // Append strings for 7 - 30 days (step by 1 day). for ($i = 7; $i <= 30; $i++) { $seconds = $i * DAYSECS; $thresholdmenu[$seconds] = get_string('numdays', 'core', $i); } // Append strings for 40 - 180 days (step by 10 days). for ($i = 40; $i <= 180; $i = $i + 10) { $seconds = $i * DAYSECS; $thresholdmenu[$seconds] = get_string('numdays', 'core', $i); } // Append string for 1 year. $thresholdmenu[365 * DAYSECS] = get_string('numdays', 'core', 365); $mform->addElement('select', 'criteria_duration_days', get_string('enrolmentdurationlength', 'core_completion'), $thresholdmenu); $mform->disabledIf('criteria_duration_days', 'criteria_duration'); if ($this->id) { $mform->setDefault('criteria_duration', 1); $mform->setDefault('criteria_duration_days', $this->enrolperiod); } } /** * Update the criteria information stored in the database * * @param stdClass $data Form data */ public function update_config(&$data) { if (!empty($data->criteria_duration)) { $this->course = $data->id; $this->enrolperiod = $data->criteria_duration_days; $this->insert(); } } /** * Get the time this user was enroled * * @param completion_completion $completion * @return int */ private function get_timeenrolled($completion) { global $DB; return $DB->get_field_sql(' SELECT eu.timestart FROM {user_enrolments} eu JOIN {enrol} e ON eu.enrolid = e.id WHERE e.courseid = ? AND eu.userid = ?', array($this->course, $completion->userid)); } /** * Review this criteria and decide if the user has completed * * @param completion_completion $completion The user's completion record * @param bool $mark Optionally set false to not save changes to database * @return bool */ public function review($completion, $mark = true) { $timeenrolled = $this->get_timeenrolled($completion); // If duration since enrollment has passed if (!$this->enrolperiod || !$timeenrolled) { return false; } if (time() > ($timeenrolled + $this->enrolperiod)) { if ($mark) { $completion->mark_complete(); } return true; } return false; } /** * Return criteria title for display in reports * * @return string */ public function get_title() { return get_string('enrolmentduration', 'completion'); } /** * Return a more detailed criteria title for display in reports * * @return string */ public function get_title_detailed() { return get_string('xdays', 'completion', ceil($this->enrolperiod / (60 * 60 * 24))); } /** * Return criteria type title for display in reports * * @return string */ public function get_type_title() { return get_string('days', 'completion'); } /** * Return criteria status text for display in reports * * @param completion_completion $completion The user's completion record * @return string */ public function get_status($completion) { $timeenrolled = $this->get_timeenrolled($completion); $timeleft = $timeenrolled + $this->enrolperiod - time(); $enrolperiod = ceil($this->enrolperiod / (60 * 60 * 24)); $daysleft = ceil($timeleft / (60 * 60 * 24)); return get_string('daysoftotal', 'completion', array( 'days' => $daysleft > 0 ? $daysleft : 0, 'total' => $enrolperiod)); } /** * Find user's who have completed this criteria */ public function cron() { global $DB; /* * Get all users who match meet this criteria * * We can safely ignore duplicate enrolments for * a user in a course here as we only care if * one of the enrolments has passed the set * duration. */ $sql = ' SELECT c.id AS course, cr.id AS criteriaid, u.id AS userid, ue.timestart AS otimestart, (ue.timestart + cr.enrolperiod) AS ctimestart, ue.timecreated AS otimeenrolled, (ue.timecreated + cr.enrolperiod) AS ctimeenrolled FROM {user} u INNER JOIN {user_enrolments} ue ON ue.userid = u.id INNER JOIN {enrol} e ON e.id = ue.enrolid INNER JOIN {course} c ON c.id = e.courseid INNER JOIN {course_completion_criteria} cr ON c.id = cr.course LEFT JOIN {course_completion_crit_compl} cc ON cc.criteriaid = cr.id AND cc.userid = u.id WHERE cr.criteriatype = '.COMPLETION_CRITERIA_TYPE_DURATION.' AND c.enablecompletion = 1 AND cc.id IS NULL AND ( ue.timestart > 0 AND ue.timestart + cr.enrolperiod < ? OR ue.timecreated > 0 AND ue.timecreated + cr.enrolperiod < ? ) '; // Loop through completions, and mark as complete $now = time(); $rs = $DB->get_recordset_sql($sql, array($now, $now)); foreach ($rs as $record) { $completion = new completion_criteria_completion((array) $record, DATA_OBJECT_FETCH_BY_KEY); // Use time start if not 0, otherwise use timeenrolled if ($record->otimestart) { $completion->mark_complete($record->ctimestart); } else { $completion->mark_complete($record->ctimeenrolled); } } $rs->close(); } /** * Return criteria progress details for display in reports * * @param completion_completion $completion The user's completion record * @return array An array with the following keys: * type, criteria, requirement, status */ public function get_details($completion) { $details = array(); $details['type'] = get_string('periodpostenrolment', 'completion'); $details['criteria'] = get_string('remainingenroledfortime', 'completion'); $details['requirement'] = get_string('xdays', 'completion', ceil($this->enrolperiod / (60*60*24))); // Get status $timeenrolled = $this->get_timeenrolled($completion); $timepassed = time() - $timeenrolled; $details['status'] = get_string('xdays', 'completion', floor($timepassed / (60*60*24))); return $details; } } data_object.php 0000604 00000031725 15062070516 0007522 0 ustar 00 <?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/>. /** * Course completion critieria aggregation * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Trigger for the new data_object api. * * See data_object::__constructor */ define('DATA_OBJECT_FETCH_BY_KEY', 2); /** * A data abstraction object that holds methods and attributes * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class data_object { /* @var string Table that the class maps to in the database */ public $table; /* @var array Array of required table fields, must start with 'id'. */ public $required_fields = array('id'); /** * Array of optional fields with default values - usually long text information that is not always needed. * If you want to create an instance without optional fields use: new data_object($only_required_fields, false); * @var array */ public $optional_fields = array(); /* @var Array of unique fields, used in where clauses and constructor */ public $unique_fields = array(); /* @var int The primary key */ public $id; /** @var int completed status. */ public $completedself; /** * Constructor. Optionally (and by default) attempts to fetch corresponding row from DB. * * If $fetch is not false, there are a few different things that can happen: * - true: * load corresponding row from the database, using $params as the WHERE clause * * - DATA_OBJECT_FETCH_BY_KEY: * load corresponding row from the database, using only the $id in the WHERE clause (if set), * otherwise using the columns listed in $this->unique_fields. * * - array(): * load corresponding row from the database, using the columns listed in this array * in the WHERE clause * * @param array $params required parameters and their values for this data object * @param mixed $fetch if false, do not attempt to fetch from the database, otherwise see notes */ public function __construct($params = null, $fetch = true) { if (is_object($params)) { throw new coding_exception('data_object params should be in the form of an array, not an object'); } // If no params given, apply defaults for optional fields if (empty($params) || !is_array($params)) { self::set_properties($this, $this->optional_fields); return; } // If fetch is false, do not load from database if ($fetch === false) { self::set_properties($this, $params); return; } // Compose where clause only from fields in unique_fields if ($fetch === DATA_OBJECT_FETCH_BY_KEY && !empty($this->unique_fields)) { if (empty($params['id'])) { $where = array_intersect_key($params, array_flip($this->unique_fields)); } else { $where = array('id' => $params['id']); } // Compose where clause from given field names } else if (is_array($fetch) && !empty($fetch)) { $where = array_intersect_key($params, array_flip($fetch)); // Use entire params array for where clause } else { $where = $params; } // Attempt to load from database if ($data = $this->fetch($where)) { // Apply data from database, then data sent to constructor self::set_properties($this, $data); self::set_properties($this, $params); } else { // Apply defaults for optional fields, then data from constructor self::set_properties($this, $this->optional_fields); self::set_properties($this, $params); } } /** * Makes sure all the optional fields are loaded. * * If id present (==instance exists in db) fetches data from db. * Defaults are used for new instances. */ public function load_optional_fields() { global $DB; foreach ($this->optional_fields as $field=>$default) { if (property_exists($this, $field)) { continue; } if (empty($this->id)) { $this->$field = $default; } else { $this->$field = $DB->get_field($this->table, $field, array('id', $this->id)); } } } /** * Finds and returns a data_object instance based on params. * * This function MUST be overridden by all deriving classes. * * @param array $params associative arrays varname => value * @throws coding_exception This function MUST be overridden * @return data_object instance of data_object or false if none found. */ public static function fetch($params) { throw new coding_exception('fetch() method needs to be overridden in each subclass of data_object'); } /** * Finds and returns all data_object instances based on params. * * This function MUST be overridden by all deriving classes. * * @param array $params associative arrays varname => value * @throws coding_exception This function MUST be overridden * @return array array of data_object instances or false if none found. */ public static function fetch_all($params) { throw new coding_exception('fetch_all() method needs to be overridden in each subclass of data_object'); } /** * Factory method - uses the parameters to retrieve matching instance from the DB. * * @final * @param string $table The table name to fetch from * @param string $classname The class that you want the result instantiated as * @param array $params Any params required to select the desired row * @return object Instance of $classname or false. */ protected static function fetch_helper($table, $classname, $params) { if ($instances = self::fetch_all_helper($table, $classname, $params)) { if (count($instances) > 1) { // we should not tolerate any errors here - problems might appear later throw new \moodle_exception('morethanonerecordinfetch', 'debug'); } return reset($instances); } else { return false; } } /** * Factory method - uses the parameters to retrieve all matching instances from the DB. * * @final * @param string $table The table name to fetch from * @param string $classname The class that you want the result instantiated as * @param array $params Any params required to select the desired row * @return mixed array of object instances or false if not found */ public static function fetch_all_helper($table, $classname, $params) { $instance = new $classname(); $classvars = (array)$instance; $params = (array)$params; $wheresql = array(); $dbparams = array(); foreach ($params as $var=>$value) { if (!in_array($var, $instance->required_fields) and !array_key_exists($var, $instance->optional_fields)) { continue; } if (is_null($value)) { $wheresql[] = " $var IS NULL "; } else { $wheresql[] = " $var = ? "; $dbparams[] = $value; } } if (empty($wheresql)) { $wheresql = ''; } else { $wheresql = implode("AND", $wheresql); } global $DB; if ($datas = $DB->get_records_select($table, $wheresql, $dbparams)) { $result = array(); foreach($datas as $data) { $instance = new $classname(); self::set_properties($instance, $data); $result[$instance->id] = $instance; } return $result; } else { return false; } } /** * Updates this object in the Database, based on its object variables. ID must be set. * * @return bool success */ public function update() { global $DB; if (empty($this->id)) { debugging('Can not update data object, no id!'); return false; } $data = $this->get_record_data(); $DB->update_record($this->table, $data); $this->notify_changed(false); return true; } /** * Deletes this object from the database. * * @return bool success */ public function delete() { global $DB; if (empty($this->id)) { debugging('Can not delete data object, no id!'); return false; } $data = $this->get_record_data(); if ($DB->delete_records($this->table, array('id'=>$this->id))) { $this->notify_changed(true); return true; } else { return false; } } /** * Returns object with fields and values that are defined in database * * @return stdClass */ public function get_record_data() { $data = new stdClass(); foreach ($this as $var=>$value) { if (in_array($var, $this->required_fields) or array_key_exists($var, $this->optional_fields)) { if (is_object($value) or is_array($value)) { debugging("Incorrect property '$var' found when inserting data object"); } else { $data->$var = $value; } } } return $data; } /** * Records this object in the Database, sets its id to the returned value, and returns that value. * If successful this function also fetches the new object data from database and stores it * in object properties. * * @return int PK ID if successful, false otherwise */ public function insert() { global $DB; if (!empty($this->id)) { debugging("Data object already exists!"); return false; } $data = $this->get_record_data(); $this->id = $DB->insert_record($this->table, $data); // set all object properties from real db data $this->update_from_db(); $this->notify_changed(false); return $this->id; } /** * Using this object's id field, fetches the matching record in the DB, and looks at * each variable in turn. If the DB has different data, the db's data is used to update * the object. This is different from the update() function, which acts on the DB record * based on the object. * * @return bool True for success, false otherwise. */ public function update_from_db() { if (empty($this->id)) { debugging("The object could not be used in its state to retrieve a matching record from the DB, because its id field is not set."); return false; } global $DB; if (!$params = $DB->get_record($this->table, array('id' => $this->id))) { debugging("Object with this id:{$this->id} does not exist in table:{$this->table}, can not update from db!"); return false; } self::set_properties($this, $params); return true; } /** * Given an associated array or object, cycles through each key/variable * and assigns the value to the corresponding variable in this object. * * @final * @param data_object $instance * @param array $params */ public static function set_properties(&$instance, $params) { $params = (array) $params; foreach ($params as $var => $value) { if (in_array($var, $instance->required_fields) or array_key_exists($var, $instance->optional_fields)) { $instance->$var = $value; } } } /** * Called immediately after the object data has been inserted, updated, or * deleted in the database. Default does nothing, can be overridden to * hook in special behaviour. * * @param bool $deleted Set this to true if it has been deleted. */ public function notify_changed($deleted) { } } classes/activity_custom_completion.php 0000604 00000015365 15062070516 0014401 0 ustar 00 <?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/>. declare(strict_types = 1); namespace core_completion; use cm_info; use coding_exception; use moodle_exception; /** * Base class for defining an activity module's custom completion rules. * * Class for defining an activity module's custom completion rules and fetching the completion statuses * of the custom completion rules for a given module instance and a user. * * @package core_completion * @copyright 2021 Jun Pataleta <jun@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class activity_custom_completion { /** @var cm_info The course module information object. */ protected $cm; /** @var int The user's ID. */ protected $userid; /** @var array The current state of core completion */ protected $completionstate; /** * activity_custom_completion constructor. * * @param cm_info $cm * @param int $userid * @param array|null $completionstate The current state of the core completion criteria */ public function __construct(cm_info $cm, int $userid, ?array $completionstate = null) { $this->cm = $cm; $this->userid = $userid; $this->completionstate = $completionstate; } /** * Validates that the custom rule is defined by this plugin and is enabled for this activity instance. * * @param string $rule The custom completion rule. */ public function validate_rule(string $rule): void { // Check that this custom completion rule is defined. if (!$this->is_defined($rule)) { throw new coding_exception("Undefined custom completion rule '$rule'"); } // Check that this custom rule is included in the course module's custom completion rules. if (!$this->is_available($rule)) { throw new moodle_exception("Custom completion rule '$rule' is not used by this activity."); } } /** * Whether this module defines this custom rule. * * @param string $rule The custom completion rule. * @return bool */ public function is_defined(string $rule): bool { return in_array($rule, static::get_defined_custom_rules()); } /** * Checks whether the custom completion rule is being used by the activity module instance. * * @param string $rule The custom completion rule. * @return bool */ public function is_available(string $rule): bool { return in_array($rule, $this->get_available_custom_rules()); } /** * Fetches the list of custom completion rules that are being used by this activity module instance. * * @return array */ public function get_available_custom_rules(): array { $rules = static::get_defined_custom_rules(); $availablerules = []; $customdata = (array)$this->cm->customdata; foreach ($rules as $rule) { $customrule = $customdata['customcompletionrules'][$rule] ?? false; if (!empty($customrule)) { $availablerules[] = $rule; } } return $availablerules; } /** * Fetches the overall completion status of this activity instance for a user based on its available custom completion rules. * * @return int The completion state (e.g. COMPLETION_COMPLETE, COMPLETION_INCOMPLETE). */ public function get_overall_completion_state(): int { foreach ($this->get_available_custom_rules() as $rule) { $state = $this->get_state($rule); // Return early if one of the custom completion rules is not yet complete. if ($state == COMPLETION_INCOMPLETE) { return $state; } } // If this was reached, then all custom rules have been marked complete. return COMPLETION_COMPLETE; } /** * Fetches the description for a given custom completion rule. * * @param string $rule The custom completion rule. * @return string */ public function get_custom_rule_description(string $rule): string { $descriptions = $this->get_custom_rule_descriptions(); if (!isset($descriptions[$rule])) { // Lang string not found for this custom completion rule. Just return it. return $rule; } return $descriptions[$rule]; } /** * Show the manual completion or not regardless of the course's showcompletionconditions setting. * Returns false by default for plugins that don't need to override the course's showcompletionconditions setting. * Activity plugins that need to always show manual completion need to override this function. * * @return bool */ public function manual_completion_always_shown(): bool { return false; } /** * Fetches the module's custom completion class implementation if it's available. * * @param string $modname The activity module name. Usually from cm_info::modname. * @return string|null */ public static function get_cm_completion_class(string $modname): ?string { $cmcompletionclass = "mod_{$modname}\\completion\\custom_completion"; if (class_exists($cmcompletionclass) && is_subclass_of($cmcompletionclass, self::class)) { return $cmcompletionclass; } return null; } /** * Fetches the completion state for a given completion rule. * * @param string $rule The completion rule. * @return int The completion state. */ abstract public function get_state(string $rule): int; /** * Fetch the list of custom completion rules that this module defines. * * @return array */ abstract public static function get_defined_custom_rules(): array; /** * Returns an associative array of the descriptions of custom completion rules. * * @return array */ abstract public function get_custom_rule_descriptions(): array; /** * Returns an array of all completion rules, in the order they should be displayed to users. * * @return array */ abstract public function get_sort_order(): array; } classes/external/completion_info_exporter.php 0000604 00000012772 15062070516 0015657 0 ustar 00 <?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/>. declare(strict_types=1); namespace core_completion\external; defined('MOODLE_INTERNAL') || die(); use renderer_base; /** * Completion info exporter * * @package core_completion * @copyright 2021 Dongsheng Cai * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class completion_info_exporter extends \core\external\exporter { /** * @var object $course moodle course object */ private $course; /** * @var object|cm_info $cm course module info */ private $cminfo; /** * @var int $userid user id */ private $userid; /** * Constructor for the completion info exporter. * * @param object $course course object * @param object|cm_info $cm course module info * @param int $userid user id * @param array $related related values */ public function __construct(object $course, object $cm, int $userid, array $related = []) { $this->course = $course; $this->cminfo = \cm_info::create($cm); $this->userid = $userid; parent::__construct([], $related); } /** * Get the additional values to inject while exporting. * * @param renderer_base $output The renderer. * @return array Keys are the property names, values are their values. */ protected function get_other_values(renderer_base $output): array { $cmcompletion = \core_completion\cm_completion_details::get_instance($this->cminfo, $this->userid); $cmcompletiondetails = $cmcompletion->get_details(); $details = []; foreach ($cmcompletiondetails as $rulename => $rulevalue) { $details[] = [ 'rulename' => $rulename, 'rulevalue' => (array)$rulevalue, ]; } return [ 'state' => $cmcompletion->get_overall_completion(), 'timecompleted' => $cmcompletion->get_timemodified(), 'overrideby' => $cmcompletion->overridden_by(), 'valueused' => \core_availability\info::completion_value_used($this->course, $this->cminfo->id), 'hascompletion' => $cmcompletion->has_completion(), 'isautomatic' => $cmcompletion->is_automatic(), 'istrackeduser' => $cmcompletion->is_tracked_user(), 'overallstatus' => $cmcompletion->get_overall_completion(), 'uservisible' => $this->cminfo->uservisible, 'details' => $details, ]; } /** * Return the list of additional properties used only for display. * * @return array Keys are the property names, and value their definition. */ public static function define_other_properties(): array { return [ 'state' => [ 'type' => PARAM_INT, 'description' => 'overall completion state of this course module.', ], 'timecompleted' => [ 'type' => PARAM_INT, 'description' => 'course completion timestamp.', ], 'overrideby' => [ 'type' => PARAM_INT, 'description' => 'user ID that has overridden the completion state of this activity for the user.', 'null' => NULL_ALLOWED, ], 'valueused' => [ 'type' => PARAM_BOOL, 'description' => 'True if module is used in a condition, false otherwise.', ], 'hascompletion' => [ 'type' => PARAM_BOOL, 'description' => 'Whether this activity module has completion enabled.' ], 'isautomatic' => [ 'type' => PARAM_BOOL, 'description' => 'Whether this activity module instance tracks completion automatically.' ], 'istrackeduser' => [ 'type' => PARAM_BOOL, 'description' => 'Checks whether completion is being tracked for this user.' ], 'uservisible' => [ 'type' => PARAM_BOOL, 'description' => 'Whether this activity is visible to user.' ], 'details' => [ 'multiple' => true, 'description' => 'An array of completion details containing the description and status.', 'type' => [ 'rulename' => [ 'type' => PARAM_TEXT, ], 'rulevalue' => [ 'type' => [ 'status' => [ 'type' => PARAM_INT, ], 'description' => [ 'type' => PARAM_TEXT, ] ] ] ] ], ]; } } classes/bulkedit_form.php 0000604 00000013660 15062070516 0011544 0 ustar 00 <?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/>. /** * Bulk edit activity completion form * * @package core_completion * @copyright 2017 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; /** * Bulk edit activity completion form * * @package core_completion * @copyright 2017 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_completion_bulkedit_form extends core_completion_edit_base_form { /** @var cm_info[] list of selected course modules */ protected $cms = []; /** @var array Do not use directly, call $this->get_module_names() */ protected $_modnames = null; /** * Returns list of types of selected modules * * @return array modname=>modfullname */ protected function get_module_names() { if ($this->_modnames !== null) { return $this->_modnames; } $this->_modnames = []; foreach ($this->cms as $cm) { $this->_modnames[$cm->modname] = $cm->modfullname; } return $this->_modnames; } /** * It will return the course module when $cms has only one course module; otherwise, null will be returned. * * @return cm_info|null */ protected function get_cm(): ?cm_info { if (count($this->cms) === 1) { return reset($this->cms); } // If there are multiple modules, so none will be selected. return null; } /** * Returns an instance of component-specific module form for the first selected module * * @return moodleform_mod|null */ protected function get_module_form() { global $CFG, $PAGE; if ($this->_moduleform) { return $this->_moduleform; } $cm = reset($this->cms); $course = $this->course; $modname = $cm->modname; $modmoodleform = "$CFG->dirroot/mod/$modname/mod_form.php"; if (file_exists($modmoodleform)) { require_once($modmoodleform); } else { throw new \moodle_exception('noformdesc'); } list($cmrec, $context, $module, $data, $cw) = get_moduleinfo_data($cm, $course); $data->return = 0; $data->sr = 0; $data->update = $modname; // Initialise the form but discard all JS requirements it adds, our form has already added them. $mformclassname = 'mod_'.$modname.'_mod_form'; $PAGE->start_collecting_javascript_requirements(); $this->_moduleform = new $mformclassname($data, 0, $cmrec, $course); $PAGE->end_collecting_javascript_requirements(); return $this->_moduleform; } /** * Form definition */ public function definition() { $this->cms = $this->_customdata['cms']; $cm = reset($this->cms); // First selected course module. $this->course = $cm->get_course(); $mform = $this->_form; $idx = 0; foreach ($this->cms as $cm) { $mform->addElement('hidden', 'cmid['.$idx.']', $cm->id); $mform->setType('cmid['.$idx.']', PARAM_INT); $idx++; } parent::definition(); $modform = $this->get_module_form(); if ($modform) { // Pre-fill the form with the current completion rules of the first selected module. list($cmrec, $context, $module, $data, $cw) = get_moduleinfo_data($cm->get_course_module_record(), $this->course); $data = (array)$data; $modform->data_preprocessing($data); // Unset fields that will conflict with this form and set data to this form. unset($data['cmid']); unset($data['id']); $this->set_data($data); } } /** * Form validation * * @param array $data array of ("fieldname"=>value) of submitted data * @param array $files array of uploaded files "element_name"=>tmp_file_path * @return array of "element_name"=>"error_description" if there are errors, * or an empty array if everything is OK (true allowed for backwards compatibility too). */ public function validation($data, $files) { global $CFG; $errors = parent::validation($data, $files); // Completion: Don't let them choose automatic completion without turning // on some conditions. if (array_key_exists('completion', $data) && $data['completion'] == COMPLETION_TRACKING_AUTOMATIC && (!empty($data['completionusegrade']) || !empty($data['completionpassgrade']))) { require_once($CFG->libdir.'/gradelib.php'); $moduleswithoutgradeitem = []; foreach ($this->cms as $cm) { $item = grade_item::fetch(array('courseid' => $cm->course, 'itemtype' => 'mod', 'itemmodule' => $cm->modname, 'iteminstance' => $cm->instance, 'itemnumber' => 0)); if (!$item) { $moduleswithoutgradeitem[] = $cm->get_formatted_name(); } } if ($moduleswithoutgradeitem) { $errors['completionusegrade'] = get_string('nogradeitem', 'completion', join(', ', $moduleswithoutgradeitem)); } } return $errors; } } classes/external.php 0000604 00000055677 15062070516 0010556 0 ustar 00 <?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/>. /** * Completion external API * * @package core_completion * @category external * @copyright 2015 Juan Leyva <juan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.9 */ use core_external\external_api; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; defined('MOODLE_INTERNAL') || die; require_once("$CFG->libdir/completionlib.php"); /** * Completion external functions * * @package core_completion * @category external * @copyright 2015 Juan Leyva <juan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.9 */ class core_completion_external extends external_api { /** * Describes the parameters for update_activity_completion_status_manually. * * @return external_function_parameters * @since Moodle 2.9 */ public static function update_activity_completion_status_manually_parameters() { return new external_function_parameters ( array( 'cmid' => new external_value(PARAM_INT, 'course module id'), 'completed' => new external_value(PARAM_BOOL, 'activity completed or not'), ) ); } /** * Update completion status for the current user in an activity, only for activities with manual tracking. * @param int $cmid Course module id * @param bool $completed Activity completed or not * @return array Result and possible warnings * @since Moodle 2.9 * @throws moodle_exception */ public static function update_activity_completion_status_manually($cmid, $completed) { // Validate and normalize parameters. $params = self::validate_parameters(self::update_activity_completion_status_manually_parameters(), array('cmid' => $cmid, 'completed' => $completed)); $cmid = $params['cmid']; $completed = $params['completed']; $warnings = array(); $context = context_module::instance($cmid); self::validate_context($context); require_capability('moodle/course:togglecompletion', $context); list($course, $cm) = get_course_and_cm_from_cmid($cmid); // Set up completion object and check it is enabled. $completion = new completion_info($course); if (!$completion->is_enabled()) { throw new moodle_exception('completionnotenabled', 'completion'); } // Check completion state is manual. if ($cm->completion != COMPLETION_TRACKING_MANUAL) { throw new moodle_exception('cannotmanualctrack', 'error'); } $targetstate = ($completed) ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE; $completion->update_state($cm, $targetstate); $result = array(); $result['status'] = true; $result['warnings'] = $warnings; return $result; } /** * Describes the update_activity_completion_status_manually return value. * * @return external_single_structure * @since Moodle 2.9 */ public static function update_activity_completion_status_manually_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'status, true if success'), 'warnings' => new external_warnings(), ) ); } /** * Describes the parameters for override_activity_completion_status. * * @return external_external_function_parameters * @since Moodle 3.4 */ public static function override_activity_completion_status_parameters() { return new external_function_parameters ( array( 'userid' => new external_value(PARAM_INT, 'user id'), 'cmid' => new external_value(PARAM_INT, 'course module id'), 'newstate' => new external_value(PARAM_INT, 'the new activity completion state'), ) ); } /** * Update completion status for a user in an activity. * @param int $userid User id * @param int $cmid Course module id * @param int $newstate Activity completion * @return array Array containing the current (updated) completion status. * @since Moodle 3.4 * @throws moodle_exception */ public static function override_activity_completion_status($userid, $cmid, $newstate) { // Validate and normalize parameters. $params = self::validate_parameters(self::override_activity_completion_status_parameters(), array('userid' => $userid, 'cmid' => $cmid, 'newstate' => $newstate)); $userid = $params['userid']; $cmid = $params['cmid']; $newstate = $params['newstate']; $context = context_module::instance($cmid); self::validate_context($context); list($course, $cm) = get_course_and_cm_from_cmid($cmid); // Set up completion object and check it is enabled. $completion = new completion_info($course); if (!$completion->is_enabled()) { throw new moodle_exception('completionnotenabled', 'completion'); } // Update completion state and get the new state back. $completion->update_state($cm, $newstate, $userid, true); $completiondata = $completion->get_data($cm, false, $userid); // Return the current state of completion. return [ 'cmid' => $completiondata->coursemoduleid, 'userid' => $completiondata->userid, 'state' => $completiondata->completionstate, 'timecompleted' => $completiondata->timemodified, 'overrideby' => $completiondata->overrideby, 'tracking' => $completion->is_enabled($cm) ]; } /** * Describes the override_activity_completion_status return value. * * @return external_single_structure * @since Moodle 3.4 */ public static function override_activity_completion_status_returns() { return new external_single_structure( array( 'cmid' => new external_value(PARAM_INT, 'The course module id'), 'userid' => new external_value(PARAM_INT, 'The user id to which the completion info belongs'), 'state' => new external_value(PARAM_INT, 'The current completion state.'), 'timecompleted' => new external_value(PARAM_INT, 'time of completion'), 'overrideby' => new external_value(PARAM_INT, 'The user id who has overriden the status, or null'), 'tracking' => new external_value(PARAM_INT, 'type of tracking: 0 means none, 1 manual, 2 automatic'), ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.9 */ public static function get_activities_completion_status_parameters() { return new external_function_parameters( array( 'courseid' => new external_value(PARAM_INT, 'Course ID'), 'userid' => new external_value(PARAM_INT, 'User ID'), ) ); } /** * Get Activities completion status * * @param int $courseid ID of the Course * @param int $userid ID of the User * @return array of activities progress and warnings * @throws moodle_exception * @since Moodle 2.9 * @throws moodle_exception */ public static function get_activities_completion_status($courseid, $userid) { global $CFG, $USER, $PAGE; require_once($CFG->libdir . '/grouplib.php'); $warnings = array(); $arrayparams = array( 'courseid' => $courseid, 'userid' => $userid, ); $params = self::validate_parameters(self::get_activities_completion_status_parameters(), $arrayparams); $course = get_course($params['courseid']); $user = core_user::get_user($params['userid'], '*', MUST_EXIST); core_user::require_active_user($user); $context = context_course::instance($course->id); self::validate_context($context); // Check that current user have permissions to see this user's activities. if ($user->id != $USER->id) { require_capability('report/progress:view', $context); if (!groups_user_groups_visible($course, $user->id)) { // We are not in the same group! throw new moodle_exception('accessdenied', 'admin'); } } $completion = new completion_info($course); $activities = $completion->get_activities(); $results = array(); foreach ($activities as $activity) { // Check if current user has visibility on this activity. if (!$activity->uservisible) { continue; } // Get progress information and state (we must use get_data because it works for all user roles in course). $exporter = new \core_completion\external\completion_info_exporter( $course, $activity, $userid, ); $renderer = $PAGE->get_renderer('core'); $data = (array)$exporter->export($renderer); $results[] = array_merge([ 'cmid' => $activity->id, 'modname' => $activity->modname, 'instance' => $activity->instance, 'tracking' => $activity->completion, ], $data); } $results = array( 'statuses' => $results, 'warnings' => $warnings ); return $results; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.9 */ public static function get_activities_completion_status_returns() { return new external_single_structure( array( 'statuses' => new external_multiple_structure( new external_single_structure( [ 'cmid' => new external_value(PARAM_INT, 'course module ID'), 'modname' => new external_value(PARAM_PLUGIN, 'activity module name'), 'instance' => new external_value(PARAM_INT, 'instance ID'), 'state' => new external_value(PARAM_INT, "Completion state value: 0 means incomplete, 1 complete, 2 complete pass, 3 complete fail" ), 'timecompleted' => new external_value(PARAM_INT, 'timestamp for completed activity'), 'tracking' => new external_value(PARAM_INT, "type of tracking: 0 means none, 1 manual, 2 automatic" ), 'overrideby' => new external_value(PARAM_INT, 'The user id who has overriden the status, or null', VALUE_OPTIONAL), 'valueused' => new external_value(PARAM_BOOL, 'Whether the completion status affects the availability of another activity.', VALUE_OPTIONAL), 'hascompletion' => new external_value(PARAM_BOOL, 'Whether this activity module has completion enabled', VALUE_OPTIONAL), 'isautomatic' => new external_value(PARAM_BOOL, 'Whether this activity module instance tracks completion automatically.', VALUE_OPTIONAL), 'istrackeduser' => new external_value(PARAM_BOOL, 'Whether completion is being tracked for this user.', VALUE_OPTIONAL), 'uservisible' => new external_value(PARAM_BOOL, 'Whether this activity is visible to the user.', VALUE_OPTIONAL), 'details' => new external_multiple_structure( new external_single_structure( [ 'rulename' => new external_value(PARAM_TEXT, 'Rule name'), 'rulevalue' => new external_single_structure( [ 'status' => new external_value(PARAM_INT, 'Completion status'), 'description' => new external_value(PARAM_TEXT, 'Completion description'), ] ) ] ), 'Completion status details', VALUE_DEFAULT, [] ), ], 'Activity' ), 'List of activities status' ), 'warnings' => new external_warnings() ) ); } /** * Returns description of method parameters * * @return external_function_parameters * @since Moodle 2.9 */ public static function get_course_completion_status_parameters() { return new external_function_parameters( array( 'courseid' => new external_value(PARAM_INT, 'Course ID'), 'userid' => new external_value(PARAM_INT, 'User ID'), ) ); } /** * Get Course completion status * * @param int $courseid ID of the Course * @param int $userid ID of the User * @return array of course completion status and warnings * @since Moodle 2.9 * @throws moodle_exception */ public static function get_course_completion_status($courseid, $userid) { global $CFG, $USER; require_once($CFG->libdir . '/grouplib.php'); $warnings = array(); $arrayparams = array( 'courseid' => $courseid, 'userid' => $userid, ); $params = self::validate_parameters(self::get_course_completion_status_parameters(), $arrayparams); $course = get_course($params['courseid']); $user = core_user::get_user($params['userid'], '*', MUST_EXIST); core_user::require_active_user($user); $context = context_course::instance($course->id); self::validate_context($context); // Can current user see user's course completion status? // This check verifies if completion is enabled because $course is mandatory. if (!completion_can_view_data($user->id, $course)) { throw new moodle_exception('cannotviewreport'); } // The previous function doesn't check groups. if ($user->id != $USER->id) { if (!groups_user_groups_visible($course, $user->id)) { // We are not in the same group! throw new moodle_exception('accessdenied', 'admin'); } } $info = new completion_info($course); // Check this user is enroled. if (!$info->is_tracked_user($user->id)) { if ($USER->id == $user->id) { throw new moodle_exception('notenroled', 'completion'); } else { throw new moodle_exception('usernotenroled', 'completion'); } } $completions = $info->get_completions($user->id); if (empty($completions)) { throw new moodle_exception('nocriteriaset', 'completion'); } // Load course completion. $completionparams = array( 'userid' => $user->id, 'course' => $course->id, ); $ccompletion = new completion_completion($completionparams); $completionrows = array(); // Loop through course criteria. foreach ($completions as $completion) { $criteria = $completion->get_criteria(); $completionrow = array(); $completionrow['type'] = $criteria->criteriatype; $completionrow['title'] = $criteria->get_title(); $completionrow['status'] = $completion->get_status(); $completionrow['complete'] = $completion->is_complete(); $completionrow['timecompleted'] = $completion->timecompleted; $completionrow['details'] = $criteria->get_details($completion); $completionrows[] = $completionrow; } $result = array( 'completed' => $info->is_course_complete($user->id), 'aggregation' => $info->get_aggregation_method(), 'completions' => $completionrows ); $results = array( 'completionstatus' => $result, 'warnings' => $warnings ); return $results; } /** * Returns description of method result value * * @return \core_external\external_description * @since Moodle 2.9 */ public static function get_course_completion_status_returns() { return new external_single_structure( array( 'completionstatus' => new external_single_structure( array( 'completed' => new external_value(PARAM_BOOL, 'true if the course is complete, false otherwise'), 'aggregation' => new external_value(PARAM_INT, 'aggregation method 1 means all, 2 means any'), 'completions' => new external_multiple_structure( new external_single_structure( array( 'type' => new external_value(PARAM_INT, 'Completion criteria type'), 'title' => new external_value(PARAM_TEXT, 'Completion criteria Title'), 'status' => new external_value(PARAM_NOTAGS, 'Completion status (Yes/No) a % or number'), 'complete' => new external_value(PARAM_BOOL, 'Completion status (true/false)'), 'timecompleted' => new external_value(PARAM_INT, 'Timestamp for criteria completetion'), 'details' => new external_single_structure( array( 'type' => new external_value(PARAM_TEXT, 'Type description'), 'criteria' => new external_value(PARAM_RAW, 'Criteria description'), 'requirement' => new external_value(PARAM_TEXT, 'Requirement description'), 'status' => new external_value(PARAM_RAW, 'Status description, can be anything'), ), 'details'), ), 'Completions' ), '' ) ), 'Course status' ), 'warnings' => new external_warnings() ), 'Course completion status' ); } /** * Describes the parameters for mark_course_self_completed. * * @return external_function_parameters * @since Moodle 3.0 */ public static function mark_course_self_completed_parameters() { return new external_function_parameters ( array( 'courseid' => new external_value(PARAM_INT, 'Course ID') ) ); } /** * Update the course completion status for the current user (if course self-completion is enabled). * * @param int $courseid Course id * @return array Result and possible warnings * @since Moodle 3.0 * @throws moodle_exception */ public static function mark_course_self_completed($courseid) { global $USER; $warnings = array(); $params = self::validate_parameters(self::mark_course_self_completed_parameters(), array('courseid' => $courseid)); $course = get_course($params['courseid']); $context = context_course::instance($course->id); self::validate_context($context); // Set up completion object and check it is enabled. $completion = new completion_info($course); if (!$completion->is_enabled()) { throw new moodle_exception('completionnotenabled', 'completion'); } if (!$completion->is_tracked_user($USER->id)) { throw new moodle_exception('nottracked', 'completion'); } $completion = $completion->get_completion($USER->id, COMPLETION_CRITERIA_TYPE_SELF); // Self completion criteria not enabled. if (!$completion) { throw new moodle_exception('noselfcompletioncriteria', 'completion'); } // Check if the user has already marked himself as complete. if ($completion->is_complete()) { throw new moodle_exception('useralreadymarkedcomplete', 'completion'); } // Mark the course complete. $completion->mark_complete(); $result = array(); $result['status'] = true; $result['warnings'] = $warnings; return $result; } /** * Describes the mark_course_self_completed return value. * * @return external_single_structure * @since Moodle 3.0 */ public static function mark_course_self_completed_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'status, true if success'), 'warnings' => new external_warnings(), ) ); } } classes/cm_completion_details.php 0000604 00000023721 15062070516 0013252 0 ustar 00 <?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/>. /** * Contains the class for building the user's activity completion details. * * @package core_completion * @copyright 2021 Jun Pataleta <jun@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types = 1); namespace core_completion; use cm_info; use completion_info; /** * Class for building the user's activity completion details. * * @package core_completion * @copyright 2021 Jun Pataleta <jun@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class cm_completion_details { /** @var completion_info The completion info instance for this cm's course. */ protected $completioninfo = null; /** @var object The completion data. */ protected $completiondata = null; /** @var cm_info The course module information. */ protected $cminfo = null; /** @var int The user ID. */ protected $userid = 0; /** @var bool Whether to return automatic completion details. */ protected $returndetails = true; /** @var activity_custom_completion Activity custom completion object. */ protected $cmcompletion = null; /** * Constructor. * * @param completion_info $completioninfo The completion info instance for this cm's course. * @param cm_info $cminfo The course module information. * @param int $userid The user ID. * @param bool $returndetails Whether to return completion details or not. */ public function __construct(completion_info $completioninfo, cm_info $cminfo, int $userid, bool $returndetails = true) { $this->completioninfo = $completioninfo; // We need to pass wholecourse = true here for better performance. All the course's completion data for the current // logged-in user will get in a single query instead of multiple queries and loaded to cache. $this->completiondata = $completioninfo->get_data($cminfo, true, $userid); $this->cminfo = $cminfo; $this->userid = $userid; $this->returndetails = $returndetails; $cmcompletionclass = activity_custom_completion::get_cm_completion_class($this->cminfo->modname); if ($cmcompletionclass) { $this->cmcompletion = new $cmcompletionclass( $this->cminfo, $this->userid, $completioninfo->get_core_completion_state($cminfo, $userid) ); } } /** * Fetches the completion details for a user. * * @return array An array of completion details for a user containing the completion requirement's description and status. * @throws \coding_exception */ public function get_details(): array { if (!$this->is_automatic()) { // No details need to be returned for modules that don't have automatic completion tracking enabled. return []; } if (!$this->returndetails) { // We don't need to return completion details. return []; } $completiondata = $this->completiondata; $hasoverride = !empty($this->overridden_by()); $details = []; // Completion rule: Student must view this activity. if (!empty($this->cminfo->completionview)) { if (!$hasoverride) { $status = COMPLETION_INCOMPLETE; if ($completiondata->viewed == COMPLETION_VIEWED) { $status = COMPLETION_COMPLETE; } } else { $status = $completiondata->completionstate; } $details['completionview'] = (object)[ 'status' => $status, 'description' => get_string('detail_desc:view', 'completion'), ]; } // Completion rule: Student must receive a grade. if (!is_null($this->cminfo->completiongradeitemnumber)) { if (!$hasoverride) { $status = $completiondata->completiongrade ?? COMPLETION_INCOMPLETE; } else { $status = $completiondata->completionstate; } $details['completionusegrade'] = (object)[ 'status' => $status, 'description' => get_string('detail_desc:receivegrade', 'completion'), ]; if (!is_null($this->cminfo->completionpassgrade) && $this->cminfo->completionpassgrade) { $details['completionpassgrade'] = (object)[ 'status' => $completiondata->passgrade ?? COMPLETION_INCOMPLETE, 'description' => get_string('detail_desc:receivepassgrade', 'completion'), ]; } } if ($this->cmcompletion) { if (isset($completiondata->customcompletion)) { foreach ($completiondata->customcompletion as $rule => $status) { $details[$rule] = (object)[ 'status' => !$hasoverride ? $status : $completiondata->completionstate, 'description' => $this->cmcompletion->get_custom_rule_description($rule), ]; } $details = $this->sort_completion_details($details); } } return $details; } /** * Sort completion details in the order specified by the activity's custom completion implementation. * * @param array $details The completion details to be sorted. * @return array * @throws \coding_exception */ protected function sort_completion_details(array $details): array { $sortorder = $this->cmcompletion->get_sort_order(); $sorteddetails = []; foreach ($sortorder as $sortedkey) { if (isset($details[$sortedkey])) { $sorteddetails[$sortedkey] = $details[$sortedkey]; } } // Make sure the sorted list includes all of the conditions that were set. if (count($sorteddetails) < count($details)) { $exceptiontext = get_class($this->cmcompletion) .'::get_sort_order() is missing one or more completion conditions.' . ' All custom and standard conditions that apply to this activity must be listed.'; throw new \coding_exception($exceptiontext); } return $sorteddetails; } /** * Fetches the overall completion state of this course module. * * @return int The overall completion state for this course module. */ public function get_overall_completion(): int { return (int)$this->completiondata->completionstate; } /** * Whether this activity module has completion enabled. * * @return bool */ public function has_completion(): bool { return $this->completioninfo->is_enabled($this->cminfo) != COMPLETION_DISABLED; } /** * Whether this activity module instance tracks completion automatically. * * @return bool */ public function is_automatic(): bool { return $this->cminfo->completion == COMPLETION_TRACKING_AUTOMATIC; } /** * Whether this activity module instance tracks completion manually. * * @return bool */ public function is_manual(): bool { return $this->cminfo->completion == COMPLETION_TRACKING_MANUAL; } /** * Fetches the user ID that has overridden the completion state of this activity for the user. * * @return int|null */ public function overridden_by(): ?int { return isset($this->completiondata->overrideby) ? (int)$this->completiondata->overrideby : null; } /** * Checks whether completion is being tracked for this user. * * @return bool */ public function is_tracked_user(): bool { return $this->completioninfo->is_tracked_user($this->userid); } /** * Determine whether to show the manual completion or not. * * @return bool */ public function show_manual_completion(): bool { global $PAGE; if (!$this->is_manual()) { return false; } if ($PAGE->context->contextlevel == CONTEXT_MODULE) { // Manual completion should always be shown on the activity page. return true; } else { $course = $this->cminfo->get_course(); if ($course->showcompletionconditions == COMPLETION_SHOW_CONDITIONS) { return true; } else if ($this->cmcompletion) { return $this->cmcompletion->manual_completion_always_shown(); } } return false; } /** * Completion state timemodified * * @return int timestamp */ public function get_timemodified(): int { return (int)$this->completiondata->timemodified; } /** * Generates an instance of this class. * * @param cm_info $cminfo The course module info instance. * @param int $userid The user ID that we're fetching completion details for. * @param bool $returndetails Whether to return completion details or not. * @return cm_completion_details */ public static function get_instance(cm_info $cminfo, int $userid, bool $returndetails = true): cm_completion_details { $course = $cminfo->get_course(); $completioninfo = new \completion_info($course); return new self($completioninfo, $cminfo, $userid, $returndetails); } } classes/progress.php 0000604 00000006012 15062070516 0010553 0 ustar 00 <?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/>. /** * Contains class used to return completion progress information. * * @package core_completion * @copyright 2017 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_completion; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/completionlib.php'); /** * Class used to return completion progress information. * * @package core_completion * @copyright 2017 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class progress { /** * Returns the course percentage completed by a certain user, returns null if no completion data is available. * * @param \stdClass $course Moodle course object * @param int $userid The id of the user, 0 for the current user * @return null|float The percentage, or null if completion is not supported in the course, * or there are no activities that support completion. */ public static function get_course_progress_percentage($course, $userid = 0) { global $USER; // Make sure we continue with a valid userid. if (empty($userid)) { $userid = $USER->id; } $completion = new \completion_info($course); // First, let's make sure completion is enabled. if (!$completion->is_enabled()) { return null; } if (!$completion->is_tracked_user($userid)) { return null; } // Before we check how many modules have been completed see if the course has. if ($completion->is_course_complete($userid)) { return 100; } // Get the number of modules that support completion. $modules = $completion->get_activities(); $count = count($modules); if (!$count) { return null; } // Get the number of modules that have been completed. $completed = 0; foreach ($modules as $module) { $data = $completion->get_data($module, true, $userid); if (($data->completionstate == COMPLETION_INCOMPLETE) || ($data->completionstate == COMPLETION_COMPLETE_FAIL)) { $completed += 0; } else { $completed += 1; }; } return ($completed / $count) * 100; } } classes/edit_base_form.php 0000604 00000026255 15062070516 0011664 0 ustar 00 <?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/>. defined('MOODLE_INTERNAL') || die; require_once($CFG->libdir.'/formslib.php'); require_once($CFG->dirroot.'/course/modlib.php'); /** * Base form for changing completion rules. Used in bulk editing activity completion and editing default activity completion * * @package core_completion * @copyright 2017 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class core_completion_edit_base_form extends moodleform { use \core_completion\form\form_trait; /** @var moodleform_mod Do not use directly, call $this->get_module_form() */ protected $_moduleform = null; /** @var bool */ protected $hascustomrules = false; /** @var stdClass */ protected $course; /** * Returns list of types of selected module types * * @return array modname=>modfullname */ abstract protected function get_module_names(); /** * Get the module name. If the form have more than one modules, it will return the first one. * * @return string|null The module name or null if there is no modules associated to this form. */ protected function get_module_name(): ?string { $modnames = $this->get_module_names(); if (empty($modnames)) { return null; } $modnamekeys = array_keys($modnames); return reset($modnamekeys); } /** * Returns true if all selected modules support tracking view. * * @return bool */ protected function support_views() { foreach ($this->get_module_names() as $modname => $modfullname) { if (!plugin_supports('mod', $modname, FEATURE_COMPLETION_TRACKS_VIEWS, false)) { return false; } } return true; } /** * Returns true if all selected modules support grading. * * @return bool */ protected function support_grades() { foreach ($this->get_module_names() as $modname => $modfullname) { if (!plugin_supports('mod', $modname, FEATURE_GRADE_HAS_GRADE, false)) { return false; } } return true; } /** * Returns an instance of component-specific module form for the first selected module * * @return moodleform_mod|null */ abstract protected function get_module_form(); /** * If all selected modules are of the same module type, adds custom completion rules from this module type * * @return array */ protected function add_custom_completion(string $function): array { $modnames = array_keys($this->get_module_names()); if (count($modnames) != 1 || !plugin_supports('mod', $modnames[0], FEATURE_COMPLETION_HAS_RULES, false)) { return [false, []]; } $component = "mod_{$modnames[0]}"; $itemnames = \core_grades\component_gradeitems::get_itemname_mapping_for_component($component); $hascustomrules = count($itemnames) > 1; try { // Add completion rules from the module form to this form. $moduleform = $this->get_module_form(); $moduleform->_form = $this->_form; if ($customcompletionelements = $moduleform->{$function}()) { $hascustomrules = true; foreach ($customcompletionelements as $customcompletionelement) { // Instead of checking for the suffix at the end of the element name, we need to check for its presence // because some modules, like SCORM, are adding things at the end. if (!str_contains($customcompletionelement, $this->get_suffix())) { debugging( 'Custom completion rule ' . $customcompletionelement . ' of module ' . $modnames[0] . ' has wrong suffix and has been removed from the form. This has to be fixed by the developer', DEBUG_DEVELOPER ); $moduleform->_form->removeElement($customcompletionelement); } } } return [$hascustomrules, $customcompletionelements]; } catch (Exception $e) { debugging('Could not add custom completion rule of module ' . $modnames[0] . ' to this form, this has to be fixed by the developer', DEBUG_DEVELOPER); return [$hascustomrules, $customcompletionelements]; } } /** * If all selected modules are of the same module type, adds custom completion rules from this module type * * @return array */ protected function add_completion_rules() { list($hascustomrules, $customcompletionelements) = $this->add_custom_completion('add_completion_rules'); if (!$this->hascustomrules && $hascustomrules) { $this->hascustomrules = true; } $component = "mod_{$this->get_module_name()}"; $itemnames = \core_grades\component_gradeitems::get_itemname_mapping_for_component($component); if (count($itemnames) > 1) { $customcompletionelements[] = 'completiongradeitemnumber'; } return $customcompletionelements; } /** * Checks if at least one of the custom completion rules is enabled * * @param array $data Input data (not yet validated) * @return bool True if one or more rules is enabled, false if none are; * default returns false */ protected function completion_rule_enabled($data) { if ($this->hascustomrules) { return $this->get_module_form()->completion_rule_enabled($data); } return false; } /** * If all selected modules are of the same module type, adds custom completion rules from this module type * * @return array */ public function add_completiongrade_rules(): array { list($hascustomrules, $customcompletionelements) = $this->add_custom_completion('add_completiongrade_rules'); if (!$this->hascustomrules && $hascustomrules) { $this->hascustomrules = true; } return $customcompletionelements; } /** * Returns list of modules that have automatic completion rules that are not shown on this form * (because they are not present in at least one other selected module). * * @return array */ protected function get_modules_with_hidden_rules() { $modnames = $this->get_module_names(); if (count($modnames) <= 1) { // No rules definitions conflicts if there is only one module type. return []; } $conflicts = []; if (!$this->support_views()) { // If we don't display views rule but at least one module supports it - we have conflicts. foreach ($modnames as $modname => $modfullname) { if (empty($conflicts[$modname]) && plugin_supports('mod', $modname, FEATURE_COMPLETION_TRACKS_VIEWS, false)) { $conflicts[$modname] = $modfullname; } } } if (!$this->support_grades()) { // If we don't display grade rule but at least one module supports it - we have conflicts. foreach ($modnames as $modname => $modfullname) { if (empty($conflicts[$modname]) && plugin_supports('mod', $modname, FEATURE_GRADE_HAS_GRADE, false)) { $conflicts[$modname] = $modfullname; } } } foreach ($modnames as $modname => $modfullname) { // We do not display any custom completion rules, find modules that define them and add to conflicts list. if (empty($conflicts[$modname]) && plugin_supports('mod', $modname, FEATURE_COMPLETION_HAS_RULES, false)) { $conflicts[$modname] = $modfullname; } } return $conflicts; } /** * Form definition */ public function definition() { $mform = $this->_form; // Course id. $mform->addElement('hidden', 'id', $this->course->id); $mform->setType('id', PARAM_INT); // Add the completion elements to the form. $this->add_completion_elements( $this->get_module_name(), $this->support_views(), $this->support_grades(), false, $this->course->id ); if ($conflicts = $this->get_modules_with_hidden_rules()) { $mform->addElement('static', 'qwerty', '', get_string('hiddenrules', 'completion', join(', ', $conflicts))); } // Whether to show the cancel button or not in the form. $displaycancel = $this->_customdata['displaycancel'] ?? true; $this->add_action_buttons($displaycancel); } /** * Return the course module of the form, if any. * * @return cm_info|null */ protected function get_cm(): ?cm_info { return null; } /** * Each module which defines definition_after_data() must call this method. */ public function definition_after_data() { $this->definition_after_data_completion($this->get_cm()); } /** * Form validation * * @param array $data array of ("fieldname"=>value) of submitted data * @param array $files array of uploaded files "element_name"=>tmp_file_path * @return array of "element_name"=>"error_description" if there are errors, * or an empty array if everything is OK (true allowed for backwards compatibility too). */ public function validation($data, $files) { $errors = parent::validation($data, $files); // Completion: Check completion fields don't have errors. $errors = array_merge($errors, $this->validate_completion($data)); return $errors; } /** * Returns if this form has custom completion rules. This is only possible if all selected modules have the same * module type and this module type supports custom completion rules * * @return bool */ public function has_custom_completion_rules() { return $this->hascustomrules; } /** * Return submitted data if properly submitted or returns NULL if validation fails or * if there is no submitted data. * * @return object submitted data; NULL if not valid or not submitted or cancelled */ public function get_data() { $data = parent::get_data(); if ($data && $this->hascustomrules) { $this->get_module_form()->data_postprocessing($data); } return $data; } } classes/manager.php 0000604 00000064513 15062070516 0010333 0 ustar 00 <?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/>. /** * Bulk activity completion manager class * * @package core_completion * @category completion * @copyright 2017 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_completion; use core\context; use stdClass; use context_course; use cm_info; use tabobject; use lang_string; use moodle_url; defined('MOODLE_INTERNAL') || die; /** * Bulk activity completion manager class * * @package core_completion * @category completion * @copyright 2017 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class manager { /** * @var int $courseid the course id. */ protected $courseid; /** * manager constructor. * @param int $courseid the course id. */ public function __construct($courseid) { $this->courseid = $courseid; } /** * Returns current course context or system level for $SITE courseid. * * @return context The course based on current courseid or system context. */ protected function get_context(): context { global $SITE; if ($this->courseid && $this->courseid != $SITE->id) { return context_course::instance($this->courseid); } return \context_system::instance(); } /** * Gets the data (context) to be used with the bulkactivitycompletion template. * * @return stdClass data for use with the bulkactivitycompletion template. */ public function get_activities_and_headings() { global $OUTPUT; $moduleinfo = get_fast_modinfo($this->courseid); $sections = $moduleinfo->get_sections(); $data = new stdClass; $data->courseid = $this->courseid; $data->sesskey = sesskey(); $data->helpicon = $OUTPUT->help_icon('bulkcompletiontracking', 'core_completion'); $data->sections = []; foreach ($sections as $sectionnumber => $section) { $sectioninfo = $moduleinfo->get_section_info($sectionnumber); $sectionobject = new stdClass(); $sectionobject->sectionnumber = $sectionnumber; $sectionobject->name = get_section_name($this->courseid, $sectioninfo); $sectionobject->activities = $this->get_activities($section, true); $data->sections[] = $sectionobject; } return $data; } /** * Gets the data (context) to be used with the activityinstance template * * @param array $cmids list of course module ids * @param bool $withcompletiondetails include completion details * @return array */ public function get_activities($cmids, $withcompletiondetails = false) { $moduleinfo = get_fast_modinfo($this->courseid); $activities = []; foreach ($cmids as $cmid) { $mod = $moduleinfo->get_cm($cmid); if (!$mod->uservisible) { continue; } $moduleobject = new stdClass(); $moduleobject->cmid = $cmid; $moduleobject->modname = $mod->get_formatted_name(); $moduleobject->icon = $mod->get_icon_url()->out(); $moduleobject->url = $mod->url; $moduleobject->canmanage = $withcompletiondetails && self::can_edit_bulk_completion($this->courseid, $mod); // Get activity completion information. if ($moduleobject->canmanage) { $moduleobject->completionstatus = $this->get_completion_detail($mod); } else { $moduleobject->completionstatus = ['icon' => null, 'string' => null]; } if (self::can_edit_bulk_completion($this->courseid, $mod)) { $activities[] = $moduleobject; } } return $activities; } /** * Get completion information on the selected module or module type * * @param cm_info|stdClass $mod either instance of cm_info (with 'customcompletionrules' in customdata) or * object with fields ->completion, ->completionview, ->completionexpected, ->completionusegrade * and ->customdata['customcompletionrules'] * @return array */ private function get_completion_detail($mod) { global $OUTPUT; $strings = []; switch ($mod->completion) { case COMPLETION_TRACKING_NONE: $strings['string'] = get_string('none'); break; case COMPLETION_TRACKING_MANUAL: $strings['string'] = get_string('manual', 'completion'); $strings['icon'] = $OUTPUT->pix_icon('i/completion-manual-y', get_string('completion_manual', 'completion')); break; case COMPLETION_TRACKING_AUTOMATIC: $strings['string'] = get_string('withconditions', 'completion'); $strings['icon'] = $OUTPUT->pix_icon('i/completion-auto-y', get_string('completion_automatic', 'completion')); break; default: $strings['string'] = get_string('none'); break; } // Get the descriptions for all the active completion rules for the module. if ($ruledescriptions = $this->get_completion_active_rule_descriptions($mod)) { foreach ($ruledescriptions as $ruledescription) { $strings['string'] .= \html_writer::empty_tag('br') . $ruledescription; } } return $strings; } /** * Get the descriptions for all active conditional completion rules for the current module. * * @param cm_info|stdClass $moduledata either instance of cm_info (with 'customcompletionrules' in customdata) or * object with fields ->completion, ->completionview, ->completionexpected, ->completionusegrade * and ->customdata['customcompletionrules'] * @return array $activeruledescriptions an array of strings describing the active completion rules. */ protected function get_completion_active_rule_descriptions($moduledata) { $activeruledescriptions = []; if ($moduledata->completion == COMPLETION_TRACKING_AUTOMATIC) { // Generate the description strings for the core conditional completion rules (if set). if (!empty($moduledata->completionview)) { $activeruledescriptions[] = get_string('completionview_desc', 'completion'); } if ($moduledata instanceof cm_info && !is_null($moduledata->completiongradeitemnumber) || ($moduledata instanceof stdClass && !empty($moduledata->completionusegrade))) { $description = 'completionusegrade_desc'; if (!empty($moduledata->completionpassgrade)) { $description = 'completionpassgrade_desc'; } $activeruledescriptions[] = get_string($description, 'completion'); } // Now, ask the module to provide descriptions for its custom conditional completion rules. if ($customruledescriptions = component_callback($moduledata->modname, 'get_completion_active_rule_descriptions', [$moduledata])) { $activeruledescriptions = array_merge($activeruledescriptions, $customruledescriptions); } } if ($moduledata->completion != COMPLETION_TRACKING_NONE) { if (!empty($moduledata->completionexpected)) { $activeruledescriptions[] = get_string('completionexpecteddesc', 'completion', userdate($moduledata->completionexpected)); } } return $activeruledescriptions; } /** * Gets the course modules for the current course. * * @param bool $includedefaults Whether the default values should be included or not. * @return stdClass $data containing the modules */ public function get_activities_and_resources(bool $includedefaults = true) { global $DB, $OUTPUT, $CFG; require_once($CFG->dirroot.'/course/lib.php'); // Get enabled activities and resources. $modules = $DB->get_records('modules', ['visible' => 1], 'name ASC'); $data = new stdClass(); $data->courseid = $this->courseid; $data->sesskey = sesskey(); $data->helpicon = $OUTPUT->help_icon('bulkcompletiontracking', 'core_completion'); // Add icon information. $data->modules = array_values($modules); $context = $this->get_context(); $canmanage = has_capability('moodle/course:manageactivities', $context); $course = get_course($this->courseid); foreach ($data->modules as $module) { $module->icon = $OUTPUT->image_url('monologo', $module->name)->out(); $module->formattedname = format_string(get_string('modulename', 'mod_' . $module->name), true, ['context' => $context]); $module->canmanage = $canmanage && course_allowed_module($course, $module->name); if ($includedefaults) { $defaults = self::get_default_completion($course, $module, false); $defaults->modname = $module->name; $module->completionstatus = $this->get_completion_detail($defaults); } } // Order modules by displayed name. $modules = (array) $data->modules; usort($modules, function($a, $b) { return strcmp($a->formattedname, $b->formattedname); }); $data->modules = $modules; return $data; } /** * Checks if current user can edit activity completion * * @param int|stdClass $courseorid * @param \cm_info|null $cm if specified capability for a given coursemodule will be check, * if not specified capability to edit at least one activity is checked. */ public static function can_edit_bulk_completion($courseorid, $cm = null) { if ($cm) { return $cm->uservisible && has_capability('moodle/course:manageactivities', $cm->context); } $coursecontext = context_course::instance(is_object($courseorid) ? $courseorid->id : $courseorid); if (has_capability('moodle/course:manageactivities', $coursecontext)) { return true; } $modinfo = get_fast_modinfo($courseorid); foreach ($modinfo->cms as $mod) { if ($mod->uservisible && has_capability('moodle/course:manageactivities', $mod->context)) { return true; } } return false; } /** * Gets the available completion tabs for the current course and user. * * @deprecated since Moodle 4.0 * @param stdClass|int $courseorid the course object or id. * @return tabobject[] */ public static function get_available_completion_tabs($courseorid) { debugging('get_available_completion_tabs() has been deprecated. Please use ' . 'core_completion\manager::get_available_completion_options() instead.', DEBUG_DEVELOPER); $tabs = []; $courseid = is_object($courseorid) ? $courseorid->id : $courseorid; $coursecontext = context_course::instance($courseid); if (has_capability('moodle/course:update', $coursecontext)) { $tabs[] = new tabobject( 'completion', new moodle_url('/course/completion.php', ['id' => $courseid]), new lang_string('coursecompletion', 'completion') ); } if (has_capability('moodle/course:manageactivities', $coursecontext)) { $tabs[] = new tabobject( 'defaultcompletion', new moodle_url('/course/defaultcompletion.php', ['id' => $courseid]), new lang_string('defaultcompletion', 'completion') ); } if (self::can_edit_bulk_completion($courseorid)) { $tabs[] = new tabobject( 'bulkcompletion', new moodle_url('/course/bulkcompletion.php', ['id' => $courseid]), new lang_string('bulkactivitycompletion', 'completion') ); } return $tabs; } /** * Returns an array with the available completion options (url => name) for the current course and user. * * @param int $courseid The course id. * @return array */ public static function get_available_completion_options(int $courseid): array { $coursecontext = context_course::instance($courseid); $options = []; if (has_capability('moodle/course:update', $coursecontext)) { $completionlink = new moodle_url('/course/completion.php', ['id' => $courseid]); $options[$completionlink->out(false)] = get_string('coursecompletionsettings', 'completion'); } if (has_capability('moodle/course:manageactivities', $coursecontext)) { $defaultcompletionlink = new moodle_url('/course/defaultcompletion.php', ['id' => $courseid]); $options[$defaultcompletionlink->out(false)] = get_string('defaultcompletion', 'completion'); } if (self::can_edit_bulk_completion($courseid)) { $bulkcompletionlink = new moodle_url('/course/bulkcompletion.php', ['id' => $courseid]); $options[$bulkcompletionlink->out(false)] = get_string('bulkactivitycompletion', 'completion'); } return $options; } /** * Applies completion from the bulk edit form to all selected modules * * @param stdClass $data data received from the core_completion_bulkedit_form * @param bool $updateinstances if we need to update the instance tables of the module (i.e. 'assign', 'forum', etc.) - * if no module-specific completion rules were added to the form, update of the module table is not needed. */ public function apply_completion($data, $updateinstances) { $updated = false; $needreset = []; $modinfo = get_fast_modinfo($this->courseid); $cmids = $data->cmid; $data = (array)$data; unset($data['id']); // This is a course id, we don't want to confuse it with cmid or instance id. unset($data['cmid']); unset($data['submitbutton']); foreach ($cmids as $cmid) { $cm = $modinfo->get_cm($cmid); if (self::can_edit_bulk_completion($this->courseid, $cm) && $this->apply_completion_cm($cm, $data, $updateinstances)) { $updated = true; if ($cm->completion != COMPLETION_TRACKING_MANUAL || $data['completion'] != COMPLETION_TRACKING_MANUAL) { // If completion was changed we will need to reset it's state. Exception is when completion was and remains as manual. $needreset[] = $cm->id; } } // Update completion calendar events. $completionexpected = ($data['completionexpected']) ? $data['completionexpected'] : null; \core_completion\api::update_completion_date_event($cm->id, $cm->modname, $cm->instance, $completionexpected); } if ($updated) { // Now that modules are fully updated, also update completion data if required. // This will wipe all user completion data and recalculate it. rebuild_course_cache($this->courseid, true); $modinfo = get_fast_modinfo($this->courseid); $completion = new \completion_info($modinfo->get_course()); foreach ($needreset as $cmid) { $completion->reset_all_state($modinfo->get_cm($cmid)); } // And notify the user of the result. \core\notification::add(get_string('activitycompletionupdated', 'core_completion'), \core\notification::SUCCESS); } } /** * Applies new completion rules to one course module * * @param \cm_info $cm * @param array $data * @param bool $updateinstance if we need to update the instance table of the module (i.e. 'assign', 'forum', etc.) - * if no module-specific completion rules were added to the form, update of the module table is not needed. * @return bool if module was updated */ protected function apply_completion_cm(\cm_info $cm, $data, $updateinstance) { global $DB; $defaults = [ 'completion' => COMPLETION_DISABLED, 'completionview' => COMPLETION_VIEW_NOT_REQUIRED, 'completionexpected' => 0, 'completiongradeitemnumber' => null, 'completionpassgrade' => 0 ]; $data += ['completion' => $cm->completion, 'completionexpected' => $cm->completionexpected, 'completionview' => $cm->completionview]; if ($cm->completion == $data['completion'] && $cm->completion == COMPLETION_TRACKING_NONE) { // If old and new completion are both "none" - no changes are needed. return false; } if ($cm->completion == $data['completion'] && $cm->completion == COMPLETION_TRACKING_NONE && $cm->completionexpected == $data['completionexpected']) { // If old and new completion are both "manual" and completion expected date is not changed - no changes are needed. return false; } if (array_key_exists('completionusegrade', $data)) { // Convert the 'use grade' checkbox into a grade-item number: 0 if checked, null if not. $data['completiongradeitemnumber'] = !empty($data['completionusegrade']) ? 0 : null; unset($data['completionusegrade']); } else { // Completion grade item number is classified in mod_edit forms as 'use grade'. $data['completionusegrade'] = is_null($cm->completiongradeitemnumber) ? 0 : 1; $data['completiongradeitemnumber'] = $cm->completiongradeitemnumber; } // Update module instance table. if ($updateinstance) { $moddata = ['id' => $cm->instance, 'timemodified' => time()] + array_diff_key($data, $defaults); $DB->update_record($cm->modname, $moddata); } // Update course modules table. $cmdata = ['id' => $cm->id, 'timemodified' => time()] + array_intersect_key($data, $defaults); $DB->update_record('course_modules', $cmdata); \core\event\course_module_updated::create_from_cm($cm, $cm->context)->trigger(); // We need to reset completion data for this activity. return true; } /** * Saves default completion from edit form to all selected module types * * @param stdClass $data data received from the core_completion_bulkedit_form * @param bool $updatecustomrules if we need to update the custom rules of the module - * if no module-specific completion rules were added to the form, update of the module table is not needed. * @param string $suffix the suffix to add to the name of the completion rules. */ public function apply_default_completion($data, $updatecustomrules, string $suffix = '') { global $DB; if (!empty($suffix)) { // Fields were renamed to avoid conflicts, but they need to be stored in DB with the original name. $modules = property_exists($data, 'modules') ? $data->modules : null; if ($modules !== null) { unset($data->modules); $data = (array)$data; foreach ($data as $name => $value) { if (str_ends_with($name, $suffix)) { $data[substr($name, 0, strpos($name, $suffix))] = $value; unset($data[$name]); } else if ($name == 'customdata') { $customrules = $value['customcompletionrules']; foreach ($customrules as $rulename => $rulevalue) { if (str_ends_with($rulename, $suffix)) { $customrules[substr($rulename, 0, strpos($rulename, $suffix))] = $rulevalue; unset($customrules[$rulename]); } } $data['customdata'] = $customrules; } } $data = (object)$data; } } $courseid = $data->id; // MDL-72375 Unset the id here, it should not be stored in customrules. unset($data->id); $coursecontext = context_course::instance($courseid); if (!$modids = $data->modids) { return; } $defaults = [ 'completion' => COMPLETION_DISABLED, 'completionview' => COMPLETION_VIEW_NOT_REQUIRED, 'completionexpected' => 0, 'completionusegrade' => 0, 'completionpassgrade' => 0 ]; $data = (array)$data; if (!array_key_exists('completionusegrade', $data)) { $data['completionusegrade'] = 0; } if (!array_key_exists('completionpassgrade', $data)) { $data['completionpassgrade'] = 0; } if ($data['completionusegrade'] == 0) { $data['completionpassgrade'] = 0; } if ($updatecustomrules) { $customdata = array_diff_key($data, $defaults); $data['customrules'] = $customdata ? json_encode($customdata) : null; $defaults['customrules'] = null; } $data = array_intersect_key($data, $defaults); // Get names of the affected modules. list($modidssql, $params) = $DB->get_in_or_equal($modids); $params[] = 1; $modules = $DB->get_records_select_menu('modules', 'id ' . $modidssql . ' and visible = ?', $params, '', 'id, name'); // Get an associative array of [module_id => course_completion_defaults_id]. list($in, $params) = $DB->get_in_or_equal($modids); $params[] = $courseid; $defaultsids = $DB->get_records_select_menu('course_completion_defaults', 'module ' . $in . ' and course = ?', $params, '', 'module, id'); foreach ($modids as $modid) { if (!array_key_exists($modid, $modules)) { continue; } if (isset($defaultsids[$modid])) { $DB->update_record('course_completion_defaults', $data + ['id' => $defaultsids[$modid]]); } else { $defaultsids[$modid] = $DB->insert_record('course_completion_defaults', $data + ['course' => $courseid, 'module' => $modid]); } // Trigger event. \core\event\completion_defaults_updated::create([ 'objectid' => $defaultsids[$modid], 'context' => $coursecontext, 'other' => ['modulename' => $modules[$modid]], ])->trigger(); } // Add notification. \core\notification::add(get_string('defaultcompletionupdated', 'completion'), \core\notification::SUCCESS); } /** * Returns default completion rules for given module type in the given course * * @param stdClass $course * @param stdClass $module * @param bool $flatten if true all module custom completion rules become properties of the same object, * otherwise they can be found as array in ->customdata['customcompletionrules'] * @param string $suffix the suffix to add to the name of the completion rules. * @return stdClass */ public static function get_default_completion($course, $module, $flatten = true, string $suffix = '') { global $DB, $CFG, $SITE; $fields = 'completion, completionview, completionexpected, completionusegrade, completionpassgrade, customrules'; // Check course default completion values. $params = ['course' => $course->id, 'module' => $module->id]; $data = $DB->get_record('course_completion_defaults', $params, $fields); if (!$data && $course->id != $SITE->id) { // If there is no course default completion, check site level default completion values ($SITE->id). $params['course'] = $SITE->id; $data = $DB->get_record('course_completion_defaults', $params, $fields); } if ($data) { if ($data->customrules && ($customrules = @json_decode($data->customrules, true))) { // MDL-72375 This will override activity id for new mods. Skip this field, it is already exposed as courseid. unset($customrules['id']); if ($flatten) { foreach ($customrules as $key => $value) { $data->$key = $value; } } else { $data->customdata['customcompletionrules'] = $customrules; } } unset($data->customrules); } else { $data = new stdClass(); $data->completion = COMPLETION_TRACKING_NONE; } // If the suffix is not empty, the completion rules need to be renamed to avoid conflicts. if (!empty($suffix)) { $data = (array)$data; foreach ($data as $name => $value) { if (str_starts_with($name, 'completion')) { $data[$name . $suffix] = $value; unset($data[$name]); } else if ($name == 'customdata') { $customrules = $value['customcompletionrules']; foreach ($customrules as $rulename => $rulevalue) { if (str_starts_with($rulename, 'completion')) { $customrules[$rulename . $suffix] = $rulevalue; unset($customrules[$rulename]); } } $data['customdata'] = $customrules; } } $data = (object)$data; } return $data; } } classes/defaultedit_form.php 0000604 00000011762 15062070516 0012234 0 ustar 00 <?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/>. /** * Default activity completion form * * @package core_completion * @copyright 2017 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_completion_defaultedit_form extends core_completion_edit_base_form { /** @var array */ protected $modules; /** @var array */ protected $_modnames; public function __construct( $action = null, $customdata = null, $method = 'post', $target = '', $attributes = null, $editable = true, $ajaxformdata = null ) { $this->modules = $customdata['modules']; if ($modname = $this->get_module_name()) { // Set the form suffix to the module name so that the form identifier is unique for each module type. $this->set_suffix('_' . $modname); } parent::__construct($action, $customdata, $method, $target, $attributes, $editable, $ajaxformdata); } /** * Returns list of types of selected modules * * @return array modname=>modfullname */ protected function get_module_names() { if ($this->_modnames !== null) { return $this->_modnames; } $this->_modnames = []; foreach ($this->modules as $module) { $this->_modnames[$module->name] = $module->formattedname; } return $this->_modnames; } /** * Returns an instance of component-specific module form for the first selected module * * @return moodleform_mod|null */ protected function get_module_form() { global $CFG, $PAGE; if ($this->_moduleform) { return $this->_moduleform; } $modnames = array_keys($this->get_module_names()); $modname = $modnames[0]; $course = $this->course; $modmoodleform = "$CFG->dirroot/mod/$modname/mod_form.php"; if (file_exists($modmoodleform)) { require_once($modmoodleform); } else { throw new \moodle_exception('noformdesc'); } list($module, $context, $cw, $cmrec, $data) = prepare_new_moduleinfo_data($course, $modname, 0, $this->get_suffix()); $data->return = 0; $data->sr = 0; $data->add = $modname; // Initialise the form but discard all JS requirements it adds, our form has already added them. $mformclassname = 'mod_'.$modname.'_mod_form'; $PAGE->start_collecting_javascript_requirements(); $this->_moduleform = new $mformclassname($data, 0, $cmrec, $course); $this->_moduleform->set_suffix('_' . $modname); $PAGE->end_collecting_javascript_requirements(); return $this->_moduleform; } /** * Form definition, */ public function definition() { $course = $this->_customdata['course']; $this->course = is_numeric($course) ? get_course($course) : $course; $this->modules = $this->_customdata['modules']; $mform = $this->_form; foreach ($this->modules as $modid => $module) { $mform->addElement('hidden', 'modids['.$modid.']', $modid); $mform->setType('modids['.$modid.']', PARAM_INT); } parent::definition(); $modform = $this->get_module_form(); if ($modform) { $modnames = array_keys($this->get_module_names()); $modname = $modnames[0]; // Pre-fill the form with the current completion rules of the first selected module type. list($module, $context, $cw, $cmrec, $data) = prepare_new_moduleinfo_data( $this->course, $modname, 0, $this->get_suffix() ); $data = (array)$data; $modform->data_preprocessing($data); // Unset fields that will conflict with this form and set data to this form. unset($data['cmid']); unset($data['modids']); unset($data['id']); $this->set_data($data); } } /** * This method has been overridden because the form identifier must be unique for each module type. * Otherwise, the form will display the same data for each module type once it's submitted. */ protected function get_form_identifier() { return parent::get_form_identifier() . $this->get_suffix(); } } classes/form/form_trait.php 0000604 00000054023 15062070516 0012025 0 ustar 00 <?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/>. namespace core_completion\form; use core_grades\component_gradeitems; use cm_info; /** * Completion trait helper, with methods to add completion elements and validate them. * * @package core_completion * @since Moodle 4.3 * @copyright 2023 Sara Arjona (sara@moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ trait form_trait { /** @var string The suffix to be added to the completion elements when creating them (for example, 'completion_assign'). */ protected $suffix = ''; /** * Called during validation. * Override this method to indicate, based on the data, whether a custom completion rule is selected or not. * * @param array $data Input data (not yet validated) * @return bool True if one or more rules are enabled; false if none are. */ abstract protected function completion_rule_enabled($data); /** * Add completion elements to the form and return the list of element ids. * * @return array Array of string IDs of added items, empty array if none */ abstract protected function add_completion_rules(); /** * Get the form associated to this class, where the completion elements will be added. * This method must be overriden by the class using this trait if it doesn't include a _form property. * * @return \MoodleQuickForm * @throws \coding_exception If the class does not have a _form property. */ protected function get_form(): \MoodleQuickForm { if (property_exists($this, '_form')) { return $this->_form; } throw new \coding_exception('This class does not have a _form property. Please, add it or override the get_form() method.'); } /** * Set the suffix to be added to the completion elements when creating them (for example, 'completion_assign'). * * @param string $suffix */ public function set_suffix(string $suffix): void { $this->suffix = $suffix; } /** * Get the suffix to be added to the completion elements when creating them (for example, 'completion_assign'). * * @return string The suffix */ public function get_suffix(): string { return $this->suffix; } /** * Add completion elements to the form. * * @param string|null $modname The module name (for example, 'assign'). If null and form is moodleform_mod, the parameters are * overriden with the expected values from the form. * @param bool $supportviews True if the module supports views and false otherwise. * @param bool $supportgrades True if the module supports grades and false otherwise. * @param bool $rating True if the rating feature is enabled and false otherwise. * @param int|null $courseid Course where to add completion elements. * @throws \coding_exception If the form is not moodleform_mod and $modname is null. */ protected function add_completion_elements( string $modname = null, bool $supportviews = false, bool $supportgrades = false, bool $rating = false, ?int $courseid = null ): void { global $SITE; $mform = $this->get_form(); if ($modname === null) { if ($this instanceof \moodleform_mod) { // By default, all the modules can be initiatized with the same parameters. $modname = $this->_modname; $supportviews = plugin_supports('mod', $modname, FEATURE_COMPLETION_TRACKS_VIEWS, false); $supportgrades = plugin_supports('mod', $modname, FEATURE_GRADE_HAS_GRADE, false); $rating = $this->_features->rating; } else { throw new \coding_exception('You must specify the modname parameter if you are not using a moodleform_mod.'); } } // Unlock button if people have completed it. The button will be removed later in definition_after_data if they haven't. // The unlock buttons don't need suffix because they are only displayed in the module settings page. $mform->addElement('submit', 'unlockcompletion', get_string('unlockcompletion', 'completion')); $mform->registerNoSubmitButton('unlockcompletion'); $mform->addElement('hidden', 'completionunlocked', 0); $mform->setType('completionunlocked', PARAM_INT); $trackingdefault = COMPLETION_TRACKING_NONE; // Get the sufix to add to the completion elements name. $suffix = $this->get_suffix(); $completionel = 'completion' . $suffix; $mform->addElement( 'radio', $completionel, '', get_string('completion_none', 'completion'), COMPLETION_TRACKING_NONE, ['class' => 'left-indented'] ); $mform->addElement( 'radio', $completionel, '', get_string('completion_manual', 'completion'), COMPLETION_TRACKING_MANUAL, ['class' => 'left-indented'] ); $allconditionsel = 'allconditions' . $suffix; $allconditions = $mform->createElement( 'static', $allconditionsel, '', get_string('allconditions', 'completion')); $conditionsgroupel = 'conditionsgroup' . $suffix; $mform->addGroup([$allconditions], $conditionsgroupel, '', null, false); $mform->hideIf($conditionsgroupel, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC); $mform->setType($completionel, PARAM_INT); $mform->setDefault($completionel, COMPLETION_TRACKING_NONE); // Automatic completion once you view it. if ($supportviews) { $completionviewel = 'completionview' . $suffix; $mform->addElement( 'checkbox', $completionviewel, '', get_string('completionview_desc', 'completion') ); $mform->hideIf($completionviewel, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC); // Check by default if automatic completion tracking is set. if ($trackingdefault == COMPLETION_TRACKING_AUTOMATIC) { $mform->setDefault($completionviewel, 1); } } // Automatic completion according to module-specific rules. $customcompletionelements = $this->add_completion_rules(); if (property_exists($this, '_customcompletionelements')) { $this->_customcompletionelements = $customcompletionelements; } if ($customcompletionelements !== null) { foreach ($customcompletionelements as $element) { $mform->hideIf($element, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC); } } // If the activity supports grading, the grade elements must be added. if ($supportgrades) { $this->add_completiongrade_elements($modname, $rating); } $autocompletionpossible = $supportviews || $supportgrades || (count($customcompletionelements) > 0); // Automatic option only appears if possible. if ($autocompletionpossible) { $automatic = $mform->createElement( 'radio', $completionel, '', get_string('completion_automatic', 'completion'), COMPLETION_TRACKING_AUTOMATIC, ['class' => 'left-indented'] ); $mform->insertElementBefore($automatic, $conditionsgroupel); } // Completion expected at particular date? (For progress tracking). // We don't show completion expected at site level default completion. if ($courseid != $SITE->id) { $completionexpectedel = 'completionexpected' . $suffix; $mform->addElement('date_time_selector', $completionexpectedel, get_string('completionexpected', 'completion'), ['optional' => true]); $a = get_string('pluginname', $modname); $mform->addHelpButton($completionexpectedel, 'completionexpected', 'completion', '', false, $a); $mform->hideIf($completionexpectedel, $completionel, 'eq', COMPLETION_TRACKING_NONE); } } /** * Add completion grade elements to the form. * * @param string $modname The name of the module (for example, 'assign'). * @param bool $rating True if the rating feature is enabled and false otherwise. */ protected function add_completiongrade_elements( string $modname, bool $rating = false ): void { $mform = $this->get_form(); // Get the sufix to add to the completion elements name. $suffix = $this->get_suffix(); $completionel = 'completion' . $suffix; $completionelementexists = $mform->elementExists($completionel); $component = "mod_{$modname}"; $itemnames = component_gradeitems::get_itemname_mapping_for_component($component); $indentation = ['parentclass' => 'ml-2']; $receiveagradeel = 'receiveagrade' . $suffix; $completionusegradeel = 'completionusegrade' . $suffix; $completionpassgradeel = 'completionpassgrade' . $suffix; if (count($itemnames) === 1) { // Only one gradeitem in this activity. // We use the completionusegrade field here. $mform->addElement( 'checkbox', $completionusegradeel, '', get_string('completionusegrade_desc', 'completion') ); // Complete if the user has reached any grade. $mform->addElement( 'radio', $completionpassgradeel, null, get_string('completionanygrade_desc', 'completion'), 0, $indentation ); // Complete if the user has reached the pass grade. $mform->addElement( 'radio', $completionpassgradeel, null, get_string('completionpassgrade_desc', 'completion'), 1, $indentation ); $mform->hideIf($completionpassgradeel, $completionusegradeel, 'notchecked'); if ($completionelementexists) { $mform->hideIf($completionpassgradeel, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC); $mform->hideIf($completionusegradeel, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC); } // The disabledIf logic differs between ratings and other grade items due to different field types. if ($rating) { // If using the rating system, there is no grade unless ratings are enabled. $mform->hideIf($completionusegradeel, 'assessed', 'eq', 0); $mform->hideIf($completionusegradeel, 'assessed', 'eq', 0); } else { // All other field types use the '$gradefieldname' field's modgrade_type. $itemnumbers = array_keys($itemnames); $itemnumber = array_shift($itemnumbers); $gradefieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'grade'); $mform->hideIf($completionusegradeel, "{$gradefieldname}[modgrade_type]", 'eq', 'none'); $mform->hideIf($completionusegradeel, "{$gradefieldname}[modgrade_type]", 'eq', 'none'); } } else if (count($itemnames) > 1) { // There are multiple grade items in this activity. // Show them all. $options = []; foreach ($itemnames as $itemnumber => $itemname) { $options[$itemnumber] = get_string("grade_{$itemname}_name", $component); } $group = [$mform->createElement( 'checkbox', $completionusegradeel, null, get_string('completionusegrade_desc', 'completion') )]; $completiongradeitemnumberel = 'completiongradeitemnumber' . $suffix; $group[] =& $mform->createElement( 'select', $completiongradeitemnumberel, '', $options ); $receiveagradegroupel = 'receiveagradegroup' . $suffix; $mform->addGroup($group, $receiveagradegroupel, '', [' '], false); if ($completionelementexists) { $mform->hideIf($completionusegradeel, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC); $mform->hideIf($receiveagradegroupel, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC); } $mform->hideIf($completiongradeitemnumberel, $completionusegradeel, 'notchecked'); // Complete if the user has reached any grade. $mform->addElement( 'radio', $completionpassgradeel, null, get_string('completionanygrade_desc', 'completion'), 0, $indentation ); // Complete if the user has reached the pass grade. $mform->addElement( 'radio', $completionpassgradeel, null, get_string('completionpassgrade_desc', 'completion'), 1, $indentation ); $mform->hideIf($completionpassgradeel, $completionusegradeel, 'notchecked'); if ($completionelementexists) { $mform->hideIf($completiongradeitemnumberel, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC); $mform->hideIf($completionpassgradeel, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC); } } $customgradingelements = $this->add_completiongrade_rules(); if (property_exists($this, '_customcompletionelements')) { $this->_customcompletionelements = array_merge($this->_customcompletionelements, $customgradingelements); } if ($completionelementexists) { foreach ($customgradingelements as $customgradingelement) { $mform->hideIf($customgradingelement, $completionel, 'ne', COMPLETION_TRACKING_AUTOMATIC); } } } /** * Add completion grading elements to the form and return the list of element ids. * * @return array Array of string IDs of added items, empty array if none */ abstract public function add_completiongrade_rules(): array; /** * Perform some extra validation for completion settings. * * @param array $data Array of ["fieldname" => value] of submitted data. * @return array List of ["element_name" => "error_description"] if there are errors or an empty array if everything is OK. */ protected function validate_completion(array $data): array { $errors = []; // Get the sufix to add to the completion elements name. $suffix = $this->get_suffix(); $completionel = 'completion' . $suffix; // Completion: Don't let them choose automatic completion without turning on some conditions. $automaticcompletion = array_key_exists($completionel, $data) && $data[$completionel] == COMPLETION_TRACKING_AUTOMATIC; // Ignore this check when completion settings are locked, as the options are then disabled. // The unlock buttons don't need suffix because they are only displayed in the module settings page. $automaticcompletion = $automaticcompletion && !empty($data['completionunlocked']); if ($automaticcompletion) { // View to complete. $completionviewel = 'completionview' . $suffix; $rulesenabled = !empty($data[$completionviewel]); // Use grade to complete (only one grade item). $completionusegradeel = 'completionusegrade' . $suffix; $completionpassgradeel = 'completionpassgrade' . $suffix; $rulesenabled = $rulesenabled || !empty($data[$completionusegradeel]) || !empty($data[$completionpassgradeel]); // Use grade to complete (specific grade item). $completiongradeitemnumberel = 'completiongradeitemnumber' . $suffix; if (!$rulesenabled && isset($data[$completiongradeitemnumberel])) { $rulesenabled = $data[$completiongradeitemnumberel] != ''; } // Module-specific completion rules. $rulesenabled = $rulesenabled || $this->completion_rule_enabled($data); if (!$rulesenabled) { // No rules are enabled. Can't set automatically completed without rules. $errors[$completionel] = get_string('badautocompletion', 'completion'); } } return $errors; } /** * It should be called from the definition_after_data() to setup the completion settings in the form. * * @param cm_info|null $cm The course module associated to this form. */ protected function definition_after_data_completion(?cm_info $cm = null): void { global $COURSE, $SITE; $mform = $this->get_form(); $completion = new \completion_info($COURSE); // We use $SITE course for site default activity completion, // so users could set default values regardless of whether completion is enabled or not.". if ($completion->is_enabled() || $COURSE->id == $SITE->id) { $suffix = $this->get_suffix(); // If anybody has completed the activity, these options will be 'locked'. // We use $SITE course for site default activity completion, so we don't need any unlock button. $completedcount = (empty($cm) || $COURSE->id == $SITE->id) ? 0 : $completion->count_user_data($cm); $freeze = false; if (!$completedcount) { // The unlock buttons don't need suffix because they are only displayed in the module settings page. if ($mform->elementExists('unlockcompletion')) { $mform->removeElement('unlockcompletion'); } // Automatically set to unlocked. Note: this is necessary in order to make it recalculate completion once // the option is changed, maybe someone has completed it now. if ($mform->elementExists('completionunlocked')) { $mform->getElement('completionunlocked')->setValue(1); } } else { // Has the element been unlocked, either by the button being pressed in this request, or the field already // being set from a previous one? if ($mform->exportValue('unlockcompletion') || $mform->exportValue('completionunlocked')) { // Yes, add in warning text and set the hidden variable. $completedunlockedel = $mform->createElement( 'static', 'completedunlocked', get_string('completedunlocked', 'completion'), get_string('completedunlockedtext', 'completion') ); $mform->insertElementBefore($completedunlockedel, 'unlockcompletion'); $mform->removeElement('unlockcompletion'); $mform->getElement('completionunlocked')->setValue(1); } else { // No, add in the warning text with the count (now we know it) before the unlock button. $completedwarningel = $mform->createElement( 'static', 'completedwarning', get_string('completedwarning', 'completion'), get_string('completedwarningtext', 'completion', $completedcount) ); $mform->insertElementBefore($completedwarningel, 'unlockcompletion'); $freeze = true; } } if ($freeze) { $completionel = 'completion' . $suffix; $mform->freeze($completionel); $completionviewel = 'completionview' . $suffix; if ($mform->elementExists($completionviewel)) { // Don't use hardFreeze or checkbox value gets lost. $mform->freeze($completionviewel); } $completionusegradeel = 'completionusegrade' . $suffix; if ($mform->elementExists($completionusegradeel)) { $mform->freeze($completionusegradeel); } $completionpassgradeel = 'completionpassgrade' . $suffix; if ($mform->elementExists($completionpassgradeel)) { $mform->freeze($completionpassgradeel); // Has the completion pass grade completion criteria been set? If it has, then we shouldn't change // the gradepass field. if ($mform->exportValue($completionpassgradeel)) { $mform->freeze('gradepass'); } } $completiongradeitemnumberel = 'completiongradeitemnumber' . $suffix; if ($mform->elementExists($completiongradeitemnumberel)) { $mform->freeze($completiongradeitemnumberel); } if (property_exists($this, '_customcompletionelements')) { $mform->freeze($this->_customcompletionelements); } } } } } classes/privacy/provider.php 0000604 00000036716 15062070516 0012234 0 ustar 00 <?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/>. /** * Privacy class for requesting user data. * * @package core_completion * @copyright 2018 Adrian Greeve <adrian@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_completion\privacy; defined('MOODLE_INTERNAL') || die(); use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_userlist; use core_privacy\local\request\contextlist; use core_privacy\local\request\transform; use core_privacy\local\request\userlist; require_once($CFG->dirroot . '/comment/lib.php'); /** * Privacy class for requesting user data. * * @package core_completion * @copyright 2018 Adrian Greeve <adrian@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\subsystem\plugin_provider, \core_privacy\local\request\shared_userlist_provider { /** * Returns meta data about this system. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection) : collection { $collection->add_database_table('course_completions', [ 'userid' => 'privacy:metadata:userid', 'course' => 'privacy:metadata:course', 'timeenrolled' => 'privacy:metadata:timeenrolled', 'timestarted' => 'privacy:metadata:timestarted', 'timecompleted' => 'privacy:metadata:timecompleted', 'reaggregate' => 'privacy:metadata:reaggregate' ], 'privacy:metadata:coursesummary'); $collection->add_database_table('course_modules_completion', [ 'userid' => 'privacy:metadata:userid', 'coursemoduleid' => 'privacy:metadata:coursemoduleid', 'completionstate' => 'privacy:metadata:completionstate', 'overrideby' => 'privacy:metadata:overrideby', 'timemodified' => 'privacy:metadata:timemodified' ], 'privacy:metadata:coursemodulesummary'); $collection->add_database_table('course_modules_viewed', [ 'userid' => 'privacy:metadata:userid', 'coursemoduleid' => 'privacy:metadata:coursemoduleid', 'timecreated' => 'privacy:metadata:timecreated', ], 'privacy:metadata:coursemodulesummary'); $collection->add_database_table('course_completion_crit_compl', [ 'userid' => 'privacy:metadata:userid', 'course' => 'privacy:metadata:course', 'gradefinal' => 'privacy:metadata:gradefinal', 'unenroled' => 'privacy:metadata:unenroled', 'timecompleted' => 'privacy:metadata:timecompleted' ], 'privacy:metadata:coursecompletedsummary'); return $collection; } /** * Get join sql to retrieve courses the user is in. * * @param int $userid The user ID * @param string $prefix A unique prefix for these joins. * @param string $joinfield A field to join these tables to. Joins to course ID. * @return array The join, where, and params for this join. */ public static function get_course_completion_join_sql(int $userid, string $prefix, string $joinfield) : array { $cccalias = "{$prefix}_ccc"; // Course completion criteria. $cmcalias = "{$prefix}_cmc"; // Course modules completion. $cmvalias = "{$prefix}_cmv"; // Course modules viewed. $ccccalias = "{$prefix}_cccc"; // Course completion criteria completion. $join = "JOIN {course_completion_criteria} {$cccalias} ON {$joinfield} = {$cccalias}.course LEFT JOIN {course_modules_completion} {$cmcalias} ON {$cccalias}.moduleinstance = {$cmcalias}.coursemoduleid AND {$cmcalias}.userid = :{$prefix}_moduleuserid LEFT JOIN {course_modules_viewed} {$cmvalias} ON {$cccalias}.moduleinstance = {$cmvalias}.coursemoduleid AND {$cmvalias}.userid = :{$prefix}_moduleuserid2 LEFT JOIN {course_completion_crit_compl} {$ccccalias} ON {$ccccalias}.criteriaid = {$cccalias}.id AND {$ccccalias}.userid = :{$prefix}_courseuserid"; $where = "{$cmcalias}.id IS NOT NULL OR {$ccccalias}.id IS NOT NULL OR {$cmvalias}.id IS NOT NULL"; $params = ["{$prefix}_moduleuserid" => $userid, "{$prefix}_moduleuserid2" => $userid, "{$prefix}_courseuserid" => $userid]; return [$join, $where, $params]; } /** * Find users' course completion by context and add to the provided userlist. * * @param userlist $userlist The userlist to add to. */ public static function add_course_completion_users_to_userlist(userlist $userlist) { $context = $userlist->get_context(); if (!$context instanceof \context_course) { return; } $params = ['courseid' => $context->instanceid]; $sql = "SELECT cmc.userid FROM {course} c JOIN {course_completion_criteria} ccc ON ccc.course = c.id JOIN {course_modules_completion} cmc ON cmc.coursemoduleid = ccc.moduleinstance WHERE c.id = :courseid"; $userlist->add_from_sql('userid', $sql, $params); $sql = "SELECT cmv.userid FROM {course} c JOIN {course_completion_criteria} ccc ON ccc.course = c.id JOIN {course_modules_viewed} cmv ON cmv.coursemoduleid = ccc.moduleinstance WHERE c.id = :courseid"; $userlist->add_from_sql('userid', $sql, $params); $sql = "SELECT ccc_compl.userid FROM {course} c JOIN {course_completion_criteria} ccc ON ccc.course = c.id JOIN {course_completion_crit_compl} ccc_compl ON ccc_compl.criteriaid = ccc.id WHERE c.id = :courseid"; $userlist->add_from_sql('userid', $sql, $params); } /** * Returns activity completion information about a user. * * @param \stdClass $user The user to return information about. * @param \stdClass $course The course the user is in. * @param \stdClass $cm Course module information. * @return \stdClass Activity completion information. */ public static function get_activity_completion_info(\stdClass $user, \stdClass $course, $cm) : \stdClass { $completioninfo = new \completion_info($course); $completion = $completioninfo->is_enabled($cm); return ($completion != COMPLETION_TRACKING_NONE) ? $completioninfo->get_data($cm, true, $user->id) : new \stdClass(); } /** * Returns course completion information for a user. * * @param \stdClass $user The user that we are getting completion information for. * @param \stdClass $course The course we are interested in. * @return \stdClass Course completion information. */ public static function get_course_completion_info(\stdClass $user, \stdClass $course) : array { $completioninfo = new \completion_info($course); $completion = $completioninfo->is_enabled(); if ($completion != COMPLETION_ENABLED) { return []; } $coursecomplete = $completioninfo->is_course_complete($user->id); if ($coursecomplete) { $status = get_string('complete'); } else { $criteriacomplete = $completioninfo->count_course_user_data($user->id); $ccompletion = new \completion_completion(['userid' => $user->id, 'course' => $course->id]); if (!$criteriacomplete && !$ccompletion->timestarted) { $status = get_string('notyetstarted', 'completion'); } else { $status = get_string('inprogress', 'completion'); } } $completions = $completioninfo->get_completions($user->id); $overall = get_string('nocriteriaset', 'completion'); if (!empty($completions)) { if ($completioninfo->get_aggregation_method() == COMPLETION_AGGREGATION_ALL) { $overall = get_string('criteriarequiredall', 'completion'); } else { $overall = get_string('criteriarequiredany', 'completion'); } } $coursecompletiondata = [ 'status' => $status, 'required' => $overall, ]; $coursecompletiondata['criteria'] = array_map(function($completion) use ($completioninfo) { $criteria = $completion->get_criteria(); $aggregation = $completioninfo->get_aggregation_method($criteria->criteriatype); $required = ($aggregation == COMPLETION_AGGREGATION_ALL) ? get_string('all', 'completion') : get_string('any', 'completion'); $data = [ 'required' => $required, 'completed' => transform::yesno($completion->is_complete()), 'timecompleted' => isset($completion->timecompleted) ? transform::datetime($completion->timecompleted) : '' ]; $details = $criteria->get_details($completion); $data = array_merge($data, $details); return $data; }, $completions); return $coursecompletiondata; } /** * Delete completion information for users. * * @param \stdClass $user The user. If provided will delete completion information for just this user. Else all users. * @param int $courseid The course id. Provide this if you want course completion and activity completion deleted. * @param int $cmid The course module id. Provide this if you only want activity completion deleted. */ public static function delete_completion(\stdClass $user = null, int $courseid = null, int $cmid = null) { global $DB; if (isset($cmid)) { $params = (isset($user)) ? ['userid' => $user->id, 'coursemoduleid' => $cmid] : ['coursemoduleid' => $cmid]; // Only delete the record for course modules completion. $DB->delete_records('course_modules_completion', $params); return; } if (isset($courseid)) { $usersql = isset($user) ? 'AND cmc.userid = :userid' : ''; $usercmvsql = isset($user) ? 'AND cmv.userid = :userid' : ''; $params = isset($user) ? ['course' => $courseid, 'userid' => $user->id] : ['course' => $courseid]; // Find records relating to course modules. $sql = "SELECT cmc.id FROM {course_completion_criteria} ccc JOIN {course_modules_completion} cmc ON ccc.moduleinstance = cmc.coursemoduleid WHERE ccc.course = :course $usersql"; $recordids = $DB->get_records_sql($sql, $params); $ids = array_keys($recordids); if (!empty($ids)) { list($deletesql, $deleteparams) = $DB->get_in_or_equal($ids); $deletesql = 'id ' . $deletesql; $DB->delete_records_select('course_modules_completion', $deletesql, $deleteparams); } // Find records relating to course modules completion viewed. $sql = "SELECT cmv.id FROM {course_completion_criteria} ccc JOIN {course_modules_viewed} cmv ON ccc.moduleinstance = cmv.coursemoduleid WHERE ccc.course = :course $usercmvsql"; $recordids = $DB->get_records_sql($sql, $params); $ids = array_keys($recordids); if (!empty($ids)) { list($deletesql, $deleteparams) = $DB->get_in_or_equal($ids); $deletesql = 'id ' . $deletesql; $DB->delete_records_select('course_modules_viewed', $deletesql, $deleteparams); } $DB->delete_records('course_completion_crit_compl', $params); $DB->delete_records('course_completions', $params); } } /** * Delete completion information for users within an approved userlist. * * @param approved_userlist $userlist The approved userlist of users to delete completion information for. * @param int $courseid The course id. Provide this if you want course completion and activity completion deleted. * @param int $cmid The course module id. Provide this if you only want activity completion deleted. */ public static function delete_completion_by_approved_userlist(approved_userlist $userlist, int $courseid = null, int $cmid = null) { global $DB; $userids = $userlist->get_userids(); if (empty($userids)) { return; } list($useridsql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED); if (isset($cmid)) { $params['coursemoduleid'] = $cmid; // Only delete the record for course modules completion. $sql = "coursemoduleid = :coursemoduleid AND userid {$useridsql}"; $DB->delete_records_select('course_modules_completion', $sql, $params); $DB->delete_records_select('course_modules_viewed', $sql, $params); return; } if (isset($courseid)) { $params['course'] = $courseid; // Find records relating to course modules. $sql = "SELECT cmc.id FROM {course_completion_criteria} ccc JOIN {course_modules_completion} cmc ON ccc.moduleinstance = cmc.coursemoduleid WHERE ccc.course = :course AND cmc.userid {$useridsql}"; $recordids = $DB->get_records_sql($sql, $params); $ids = array_keys($recordids); if (!empty($ids)) { list($deletesql, $deleteparams) = $DB->get_in_or_equal($ids); $deletesql = 'id ' . $deletesql; $DB->delete_records_select('course_modules_completion', $deletesql, $deleteparams); } // Find records relating to course modules. $sql = "SELECT cmv.id FROM {course_completion_criteria} ccc JOIN {course_modules_viewed} cmv ON ccc.moduleinstance = cmv.coursemoduleid WHERE ccc.course = :course AND cmv.userid {$useridsql}"; $recordids = $DB->get_records_sql($sql, $params); $ids = array_keys($recordids); if (!empty($ids)) { list($deletesql, $deleteparams) = $DB->get_in_or_equal($ids); $deletesql = 'id ' . $deletesql; $DB->delete_records_select('course_modules_viewed', $deletesql, $deleteparams); } $sql = "course = :course AND userid {$useridsql}"; $DB->delete_records_select('course_completion_crit_compl', $sql, $params); $DB->delete_records_select('course_completions', $sql, $params); } } } classes/api.php 0000604 00000020126 15062070516 0007462 0 ustar 00 <?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/>. /** * Contains class containing completion API. * * @package core_completion * @copyright 2017 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_completion; defined('MOODLE_INTERNAL') || die(); /** * Class containing completion API. * * @package core_completion * @copyright 2017 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class api { /** * @var string The completion expected on event. */ const COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED = 'expectcompletionon'; /** * Creates, updates or deletes an event for the expected completion date. * * @param int $cmid The course module id * @param string $modulename The name of the module (eg. assign, quiz) * @param \stdClass|int $instanceorid The instance object or ID. * @param int|null $completionexpectedtime The time completion is expected, null if not set * @return bool */ public static function update_completion_date_event($cmid, $modulename, $instanceorid, $completionexpectedtime) { global $CFG, $DB; // Required for calendar constant CALENDAR_EVENT_TYPE_ACTION. require_once($CFG->dirroot . '/calendar/lib.php'); $instance = null; if (is_object($instanceorid)) { $instance = $instanceorid; } else { $instance = $DB->get_record($modulename, array('id' => $instanceorid), '*', IGNORE_MISSING); } if (!$instance) { return false; } $course = get_course($instance->course); $completion = new \completion_info($course); // Can not create/update an event if completion is disabled. if (!$completion->is_enabled() && $completionexpectedtime !== null) { return true; } // Create the \stdClass we will be using for our language strings. $lang = new \stdClass(); $lang->modulename = get_string('pluginname', $modulename); $lang->instancename = $instance->name; // Create the calendar event. $event = new \stdClass(); $event->type = CALENDAR_EVENT_TYPE_ACTION; $event->eventtype = self::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED; if ($event->id = $DB->get_field('event', 'id', array('modulename' => $modulename, 'instance' => $instance->id, 'eventtype' => $event->eventtype))) { if ($completionexpectedtime !== null) { // Calendar event exists so update it. $event->name = get_string('completionexpectedfor', 'completion', $lang); $event->description = format_module_intro($modulename, $instance, $cmid, false); $event->format = FORMAT_HTML; $event->timestart = $completionexpectedtime; $event->timesort = $completionexpectedtime; $event->visible = instance_is_visible($modulename, $instance); $event->timeduration = 0; $calendarevent = \calendar_event::load($event->id); $calendarevent->update($event, false); } else { // Calendar event is no longer needed. $calendarevent = \calendar_event::load($event->id); $calendarevent->delete(); } } else { // Event doesn't exist so create one. if ($completionexpectedtime !== null) { $event->name = get_string('completionexpectedfor', 'completion', $lang); $event->description = format_module_intro($modulename, $instance, $cmid, false); $event->format = FORMAT_HTML; $event->courseid = $instance->course; $event->groupid = 0; $event->userid = 0; $event->modulename = $modulename; $event->instance = $instance->id; $event->timestart = $completionexpectedtime; $event->timesort = $completionexpectedtime; $event->visible = instance_is_visible($modulename, $instance); $event->timeduration = 0; \calendar_event::create($event, false); } } return true; } /** * Mark users who completed course based on activity criteria. * @param array $userdata If set only marks specified user in given course else checks all courses/users. * @return int Completion record id if $userdata is set, 0 else. * @since Moodle 4.0 */ public static function mark_course_completions_activity_criteria($userdata = null): int { global $DB; // Get all users who meet this criteria $sql = "SELECT DISTINCT c.id AS course, cr.id AS criteriaid, ra.userid AS userid, mc.timemodified AS timecompleted FROM {course_completion_criteria} cr INNER JOIN {course} c ON cr.course = c.id INNER JOIN {context} con ON con.instanceid = c.id INNER JOIN {role_assignments} ra ON ra.contextid = con.id INNER JOIN {course_modules} cm ON cm.id = cr.moduleinstance INNER JOIN {course_modules_completion} mc ON mc.coursemoduleid = cr.moduleinstance AND mc.userid = ra.userid LEFT JOIN {course_completion_crit_compl} cc ON cc.criteriaid = cr.id AND cc.userid = ra.userid WHERE cr.criteriatype = :criteriatype AND con.contextlevel = :contextlevel AND c.enablecompletion = 1 AND cc.id IS NULL AND ( mc.completionstate = :completionstate OR (cm.completionpassgrade = 1 AND mc.completionstate = :completionstatepass1) OR (cm.completionpassgrade = 0 AND (mc.completionstate = :completionstatepass2 OR mc.completionstate = :completionstatefail)) )"; $params = [ 'criteriatype' => COMPLETION_CRITERIA_TYPE_ACTIVITY, 'contextlevel' => CONTEXT_COURSE, 'completionstate' => COMPLETION_COMPLETE, 'completionstatepass1' => COMPLETION_COMPLETE_PASS, 'completionstatepass2' => COMPLETION_COMPLETE_PASS, 'completionstatefail' => COMPLETION_COMPLETE_FAIL ]; if ($userdata) { $params['courseid'] = $userdata['courseid']; $params['userid'] = $userdata['userid']; $sql .= " AND c.id = :courseid AND ra.userid = :userid"; // Mark as complete. $record = $DB->get_record_sql($sql, $params); if ($record) { $completion = new \completion_criteria_completion((array) $record, DATA_OBJECT_FETCH_BY_KEY); $result = $completion->mark_complete($record->timecompleted); return $result; } } else { // Loop through completions, and mark as complete. $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $record) { $completion = new \completion_criteria_completion((array) $record, DATA_OBJECT_FETCH_BY_KEY); $completion->mark_complete($record->timecompleted); } $rs->close(); } return 0; } } upgrade.txt 0000604 00000011044 15062070516 0006732 0 ustar 00 This file describes API changes in /completion/* - completion, information provided here is intended especially for developers. === 4.3 === * A trait class, core_completion/form/form_trait has been added to reuse code for adding and validation completion settings to any form. * New method is_manual() has been added to `core_completion/cm_completion_details` * The method manager::get_activities_and_resources() now has the optional $includedefaults parameter to define whether the default values should be included or not. * The methods manager::apply_default_completion() and manager::get_default_completion() now have the optional $suffix parameter to be added to the name of the completion rules. * The method manager::defaultcompletion() now has the $modules and $form parameters, to specify the modules that have been set through the form and the current form that has been sent. * Support for deprecated `[modname]_get_completion_state` callbacks has been removed, custom completion rules must be implemented by appropriate `mod_[modname]\completion\custom_completion` class instead * $CFG->completiondefault setting has been removed. === 4.0 === * New method mark_course_completions_activity_criteria() has been added to mark course completions instantly. It is based on cron for completion_criteria_activity.php which is refactored to use it as well. * Core now passes an additional parameter to '_get_completion_state'. This is an array representation of the completion results that have already been tested. Currently contains - viewed, usegrade, passgrade. Any plugin that are dependent on these criteria can now check this array instead of retesting it. * The method \completion_criteria_completion::mark_complete() now has the optional $timecompleted parameter to specify when the criteria was completed. * New method get_available_completion_options() has been added in the core_completion\manager class. This method can be used to obtain an array with the available completion options ([url => name]) for the current course and user. * The method get_available_completion_tabs() in the core_completion\manager class has been deprecated because the tabs navigation structure is no longer used in the completion pages. Please use core_completion\manager::get_available_completion_options() instead. === 3.11 === * New Behat steps for activity completion in the behat_completion class: - activity_completion_condition_displayed_as() - Given the "<Completion condition>" completion condition of "<Activity name>" is displayed as "<Status>" - activity_should_have_the_completion_condition() - Given "<Activity name>" should have the "<Condition name>" completion condition - manual_completion_button_displayed_as() - Given the manual completion button of "<Activity name>" is displayed as "<Status>" - the_manual_completion_button_for_activity_should_be_disabled() - Given the manual completion button for "<Activity name>" should be disabled - there_should_be_no_completion_for_activity() - Given there should be no completion information shown for "<Activity name>" - toggle_the_manual_completion_state() - Given I toggle the manual completion state of "<Activity name>" - overridden_manual_completion_button_displayed_as - Given the manual completion button of "<Activity name>" overridden by "<User>" is displayed as "<Status>" - overridden_activity_completion_condition_displayed_as - Given the "<Completion condition>" completion condition of "<Activity name>" overridden by "<User>" is displayed as "<Status>" * *_get_completion_state() callback functions have been deprecated and should no longer be used. Plugins that define custom completion rules must implement the mod_[modname]\completion\custom_completion class that extends the \core_completion\activity_custom_completion base class. === 3.7 === * External function core_completion_external::get_activities_completion_status new returns the following additional field: - valueused (indicates whether the completion state affects the availability of other content) * On the course page, only users with the capability 'moodle/course:isincompletionreports' (students, by default) can now tick the completion checkboxes. Teachers no longer get working checkboxes; tey see slightly different icons that indicate whether completion is enabled for the activity. These are the same icons which have always been shown to teachers before when the enabled the course editing mode. === 2.9 === * A completed and failed activity counts as a completed activity for course completion. completion_aggregation.php 0000604 00000007022 15062070516 0011774 0 ustar 00 <?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/>. /** * Course completion critieria aggregation * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/completion/data_object.php'); /** * Course completion critieria aggregation * * @package core_completion * @category completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes <aaronb@catalyst.net.nz> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class completion_aggregation extends data_object { /* @var string Database table name that stores completion aggregation information */ public $table = 'course_completion_aggr_methd'; /** * Array of required table fields, must start with 'id'. * Defaults to id, course, criteriatype, method, value * @var array */ public $required_fields = array('id', 'course', 'criteriatype', 'method', 'value'); /* @var array Array of unique fields, used in where clauses */ public $unique_fields = array('course', 'criteriatype'); /* @var int Course id */ public $course; /* @var int Criteria type this aggregation method applies to, or NULL for overall course aggregation */ public $criteriatype; /* @var int Aggregation method (COMPLETION_AGGREGATION_* constant) */ public $method; /* @var mixed Method value */ public $value; /** * Finds and returns a data_object instance based on params. * * @param array $params associative arrays varname=>value * @return data_object instance of data_object or false if none found. */ public static function fetch($params) { return self::fetch_helper('course_completion_aggr_methd', __CLASS__, $params); } /** * Finds and returns all data_object instances based on params. * * @param array $params associative arrays varname=>value * @return array array of data_object insatnces or false if none found. */ public static function fetch_all($params) {} /** * Set the aggregation method * * @param int $method One of COMPLETION_AGGREGATION_ALL or COMPLETION_AGGREGATION_ANY */ public function setMethod($method) { $methods = array( COMPLETION_AGGREGATION_ALL, COMPLETION_AGGREGATION_ANY, ); if (in_array($method, $methods)) { $this->method = $method; } else { $this->method = COMPLETION_AGGREGATION_ALL; } } /** * Save aggregation method to database * * @access public * @return boolean */ public function save() { if ($this->id) { return $this->update(); } else { return $this->insert(); } } } tests/generator_test.php 0000604 00000011000 15062070516 0011432 0 ustar 00 <?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/>. namespace core_completion; /** * PHPUnit data generator testcase * * @package core_completion * @category test * @copyright 2023 Amaia Anabitarte <amaia@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_completion_generator */ class generator_test extends \advanced_testcase { /** * Test create_default_completion. * * @dataProvider create_default_completion_provider * * @param int|null|string $course The course to add the default activities conditions to. * @param int|null|string $module The module to add the default activities conditions to. * @param bool $exception Whether an exception is expected or not. * @param int $count The number of default activity completions to be created. * @param int $completion The value for completion setting. * * @covers ::create_default_completion */ public function test_create_default_completion($course, $module, bool $exception, int $count, int $completion = 0) { global $DB; $this->resetAfterTest(true); $generator = $this->getDataGenerator()->get_plugin_generator('core_completion'); $record = [ 'course' => $course, 'module' => $module, 'completion' => $completion, ]; $result = (object) array_merge([ 'completion' => 0, 'completionview' => 0, 'completionusegrade' => 0, 'completionpassgrade' => 0, 'completionexpected' => 0, 'customrules' => '', ], $record); if ($exception) { $this->expectException('moodle_exception'); } $defaultcompletion = $generator->create_default_completion($record); if (!$exception) { foreach ($result as $key => $value) { $this->assertEquals($defaultcompletion->{$key}, $value); } } $this->assertEquals( $count, $DB->count_records('course_completion_defaults', ['course' => $course, 'module' => $module]) ); } /** * Data provider for test_create_default_completion(). * @return array[] */ public function create_default_completion_provider(): array { global $SITE; return [ 'Null course' => [ 'course' => null, 'module' => null, 'exception' => true, 'count' => 0, ], 'Empty course' => [ 'course' => '', 'module' => null, 'exception' => true, 'count' => 0, ], 'Invalid course' => [ 'course' => 0, 'module' => null, 'exception' => true, 'count' => 0, ], 'Null module' => [ 'course' => $SITE->id, 'module' => null, 'exception' => true, 'count' => 0, ], 'Empty module' => [ 'course' => $SITE->id, 'module' => null, 'exception' => true, 'count' => 0, ], 'Invalid module' => [ 'course' => $SITE->id, 'module' => 0, 'exception' => true, 'count' => 0, ], 'Default activity completion: NONE' => [ 'course' => $SITE->id, 'module' => 1, 'exception' => false, 'count' => 1, ], 'Default activity completion: AUTOMATIC' => [ 'course' => $SITE->id, 'module' => 1, 'exception' => false, 'count' => 1, 'completion' => 2, ], ]; } } tests/externallib_test.php 0000604 00000074044 15062070516 0011776 0 ustar 00 <?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/>. namespace core_completion; use core_completion_external; use core_external\external_api; use externallib_advanced_testcase; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * External completion functions unit tests * * @package core_completion * @category external * @copyright 2015 Juan Leyva <juan@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.9 */ class externallib_test extends externallib_advanced_testcase { /** * Test update_activity_completion_status_manually */ public function test_update_activity_completion_status_manually() { global $DB, $CFG; $this->resetAfterTest(true); $CFG->enablecompletion = true; $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), array('completion' => 1)); $cm = get_coursemodule_from_id('data', $data->cmid); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id); $this->setUser($user); $result = core_completion_external::update_activity_completion_status_manually($data->cmid, true); // We need to execute the return values cleaning process to simulate the web service server. $result = external_api::clean_returnvalue( core_completion_external::update_activity_completion_status_manually_returns(), $result); // Check in DB. $this->assertEquals(1, $DB->get_field('course_modules_completion', 'completionstate', array('coursemoduleid' => $data->cmid))); // Check using the API. $completion = new \completion_info($course); $completiondata = $completion->get_data($cm); $this->assertEquals(1, $completiondata->completionstate); $this->assertTrue($result['status']); $result = core_completion_external::update_activity_completion_status_manually($data->cmid, false); // We need to execute the return values cleaning process to simulate the web service server. $result = external_api::clean_returnvalue( core_completion_external::update_activity_completion_status_manually_returns(), $result); $this->assertEquals(0, $DB->get_field('course_modules_completion', 'completionstate', array('coursemoduleid' => $data->cmid))); $completiondata = $completion->get_data($cm); $this->assertEquals(0, $completiondata->completionstate); $this->assertTrue($result['status']); } /** * Test update_activity_completion_status */ public function test_get_activities_completion_status() { global $DB, $CFG, $PAGE; $this->resetAfterTest(true); $CFG->enablecompletion = true; $student = $this->getDataGenerator()->create_user(); $teacher = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1, 'groupmode' => SEPARATEGROUPS, 'groupmodeforce' => 1)); \availability_completion\condition::wipe_static_cache(); $data = $this->getDataGenerator()->create_module('data', ['course' => $course->id], ['completion' => COMPLETION_TRACKING_MANUAL], ); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id], ['completion' => COMPLETION_TRACKING_MANUAL], ); $forumautocompletion = $this->getDataGenerator()->create_module('forum', ['course' => $course->id], ['showdescription' => true, 'completionview' => 1, 'completion' => COMPLETION_TRACKING_AUTOMATIC], ); $availability = '{"op":"&","c":[{"type":"completion","cm":' . $forum->cmid .',"e":1}],"showc":[true]}'; $assign = $this->getDataGenerator()->create_module('assign', ['course' => $course->id], ['availability' => $availability], ); $assignautocompletion = $this->getDataGenerator()->create_module('assign', ['course' => $course->id], [ 'showdescription' => true, 'completionview' => 1, 'completion' => COMPLETION_TRACKING_AUTOMATIC, 'completiongradeitemnumber' => 1, 'completionpassgrade' => 1, ], ); $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id), array('completion' => 1, 'visible' => 0)); $cmdata = get_coursemodule_from_id('data', $data->cmid); $cmforum = get_coursemodule_from_id('forum', $forum->cmid); $cmforumautocompletion = get_coursemodule_from_id('forum', $forumautocompletion->cmid); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); // Teacher and student in different groups initially. groups_add_member($group1->id, $student->id); groups_add_member($group2->id, $teacher->id); $this->setUser($student); // Forum complete. $completion = new \completion_info($course); $completion->update_state($cmforum, COMPLETION_COMPLETE); $result = core_completion_external::get_activities_completion_status($course->id, $student->id); // We need to execute the return values cleaning process to simulate the web service server. $result = external_api::clean_returnvalue( core_completion_external::get_activities_completion_status_returns(), $result); // We added 6 activities, but only 4 with completion enabled and one of those is hidden. $numberofactivities = 6; $numberofhidden = 1; $numberofcompletions = $numberofactivities - $numberofhidden; $numberofstatusstudent = 4; $this->assertCount($numberofstatusstudent, $result['statuses']); $activitiesfound = 0; foreach ($result['statuses'] as $status) { if ($status['cmid'] == $forum->cmid and $status['modname'] == 'forum' and $status['instance'] == $forum->id) { $activitiesfound++; $this->assertEquals(COMPLETION_COMPLETE, $status['state']); $this->assertEquals(COMPLETION_TRACKING_MANUAL, $status['tracking']); $this->assertTrue($status['valueused']); $this->assertTrue($status['hascompletion']); $this->assertFalse($status['isautomatic']); $this->assertTrue($status['istrackeduser']); $this->assertTrue($status['uservisible']); $details = $status['details']; $this->assertCount(0, $details); } else if ($status['cmid'] == $forumautocompletion->cmid) { $activitiesfound++; $this->assertEquals(COMPLETION_INCOMPLETE, $status['state']); $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $status['tracking']); $this->assertFalse($status['valueused']); $this->assertTrue($status['hascompletion']); $this->assertTrue($status['isautomatic']); $this->assertTrue($status['istrackeduser']); $this->assertTrue($status['uservisible']); $details = $status['details']; $this->assertCount(1, $details); $this->assertEquals('completionview', $details[0]['rulename']); $this->assertEquals(0, $details[0]['rulevalue']['status']); } else if ($status['cmid'] == $assignautocompletion->cmid) { $activitiesfound++; $this->assertEquals(COMPLETION_INCOMPLETE, $status['state']); $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $status['tracking']); $this->assertFalse($status['valueused']); $this->assertTrue($status['hascompletion']); $this->assertTrue($status['isautomatic']); $this->assertTrue($status['istrackeduser']); $this->assertTrue($status['uservisible']); $details = $status['details']; $this->assertCount(3, $details); $expecteddetails = [ 'completionview', 'completionusegrade', 'completionpassgrade', ]; foreach ($expecteddetails as $index => $name) { $this->assertEquals($name, $details[$index]['rulename']); $this->assertEquals(0, $details[$index]['rulevalue']['status']); } } else if ($status['cmid'] == $data->cmid and $status['modname'] == 'data' and $status['instance'] == $data->id) { $activitiesfound++; $this->assertEquals(COMPLETION_INCOMPLETE, $status['state']); $this->assertEquals(COMPLETION_TRACKING_MANUAL, $status['tracking']); $this->assertFalse($status['valueused']); $this->assertFalse($status['valueused']); $this->assertTrue($status['hascompletion']); $this->assertFalse($status['isautomatic']); $this->assertTrue($status['istrackeduser']); $this->assertTrue($status['uservisible']); $details = $status['details']; $this->assertCount(0, $details); } } $this->assertEquals(4, $activitiesfound); // Teacher should see students status, they are in different groups but the teacher can access all groups. $this->setUser($teacher); $result = core_completion_external::get_activities_completion_status($course->id, $student->id); // We need to execute the return values cleaning process to simulate the web service server. $result = external_api::clean_returnvalue( core_completion_external::get_activities_completion_status_returns(), $result); $this->assertCount($numberofcompletions, $result['statuses']); // Override status by teacher. $completion->update_state($cmforum, COMPLETION_INCOMPLETE, $student->id, true); $result = core_completion_external::get_activities_completion_status($course->id, $student->id); // We need to execute the return values cleaning process to simulate the web service server. $result = external_api::clean_returnvalue( core_completion_external::get_activities_completion_status_returns(), $result); // Check forum has been overriden by the teacher. foreach ($result['statuses'] as $status) { if ($status['cmid'] == $forum->cmid) { $this->assertEquals(COMPLETION_INCOMPLETE, $status['state']); $this->assertEquals(COMPLETION_TRACKING_MANUAL, $status['tracking']); $this->assertEquals($teacher->id, $status['overrideby']); break; } } // Teacher should see his own completion status. // Forum complete for teacher. $completion = new \completion_info($course); $completion->update_state($cmforum, COMPLETION_COMPLETE); $result = core_completion_external::get_activities_completion_status($course->id, $teacher->id); // We need to execute the return values cleaning process to simulate the web service server. $result = external_api::clean_returnvalue( core_completion_external::get_activities_completion_status_returns(), $result); $this->assertCount($numberofcompletions, $result['statuses']); $activitiesfound = 0; foreach ($result['statuses'] as $status) { if ($status['cmid'] == $forum->cmid and $status['modname'] == 'forum' and $status['instance'] == $forum->id) { $activitiesfound++; $this->assertEquals(COMPLETION_COMPLETE, $status['state']); $this->assertEquals(COMPLETION_TRACKING_MANUAL, $status['tracking']); } else if (in_array($status['cmid'], [$forumautocompletion->cmid, $assignautocompletion->cmid])) { $activitiesfound++; $this->assertEquals(COMPLETION_INCOMPLETE, $status['state']); $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $status['tracking']); } else { $activitiesfound++; $this->assertEquals(COMPLETION_INCOMPLETE, $status['state']); $this->assertEquals(COMPLETION_TRACKING_MANUAL, $status['tracking']); } } $this->assertEquals(5, $activitiesfound); // Change teacher role capabilities (disable access all groups). $context = \context_course::instance($course->id); assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $teacherrole->id, $context); accesslib_clear_all_caches_for_unit_testing(); try { $result = core_completion_external::get_activities_completion_status($course->id, $student->id); $this->fail('Exception expected due to groups permissions.'); } catch (\moodle_exception $e) { $this->assertEquals('accessdenied', $e->errorcode); } // Now add the teacher in the same group. groups_add_member($group1->id, $teacher->id); $result = core_completion_external::get_activities_completion_status($course->id, $student->id); // We need to execute the return values cleaning process to simulate the web service server. $result = external_api::clean_returnvalue( core_completion_external::get_activities_completion_status_returns(), $result); $this->assertCount($numberofcompletions, $result['statuses']); } /** * Test override_activity_completion_status */ public function test_override_activity_completion_status() { global $DB, $CFG; $this->resetAfterTest(true); // Create course with teacher and student enrolled. $CFG->enablecompletion = true; $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); $student = $this->getDataGenerator()->create_user(); $teacher = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', ['shortname' => 'student']); $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); $teacherrole = $DB->get_record('role', ['shortname' => 'teacher']); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); // Create 2 activities, one with manual completion (data), one with automatic completion triggered by viewing it (forum). $data = $this->getDataGenerator()->create_module('data', ['course' => $course->id], ['completion' => 1]); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id], ['completion' => 2, 'completionview' => 1]); $cmdata = get_coursemodule_from_id('data', $data->cmid); $cmforum = get_coursemodule_from_id('forum', $forum->cmid); // Manually complete the data activity as the student. $this->setUser($student); $completion = new \completion_info($course); $completion->update_state($cmdata, COMPLETION_COMPLETE); // Test overriding the status of the manual-completion-activity 'incomplete'. $this->setUser($teacher); $result = core_completion_external::override_activity_completion_status($student->id, $data->cmid, COMPLETION_INCOMPLETE); $result = external_api::clean_returnvalue(core_completion_external::override_activity_completion_status_returns(), $result); $this->assertEquals($result['state'], COMPLETION_INCOMPLETE); $completiondata = $completion->get_data($cmdata, false, $student->id); $this->assertEquals(COMPLETION_INCOMPLETE, $completiondata->completionstate); // Test overriding the status of the manual-completion-activity back to 'complete'. $result = core_completion_external::override_activity_completion_status($student->id, $data->cmid, COMPLETION_COMPLETE); $result = external_api::clean_returnvalue(core_completion_external::override_activity_completion_status_returns(), $result); $this->assertEquals($result['state'], COMPLETION_COMPLETE); $completiondata = $completion->get_data($cmdata, false, $student->id); $this->assertEquals(COMPLETION_COMPLETE, $completiondata->completionstate); // Test overriding the status of the auto-completion-activity to 'complete'. $result = core_completion_external::override_activity_completion_status($student->id, $forum->cmid, COMPLETION_COMPLETE); $result = external_api::clean_returnvalue(core_completion_external::override_activity_completion_status_returns(), $result); $this->assertEquals($result['state'], COMPLETION_COMPLETE); $completionforum = $completion->get_data($cmforum, false, $student->id); $this->assertEquals(COMPLETION_COMPLETE, $completionforum->completionstate); // Test overriding the status of the auto-completion-activity to 'incomplete'. $result = core_completion_external::override_activity_completion_status($student->id, $forum->cmid, COMPLETION_INCOMPLETE); $result = external_api::clean_returnvalue(core_completion_external::override_activity_completion_status_returns(), $result); $this->assertEquals($result['state'], COMPLETION_INCOMPLETE); $completionforum = $completion->get_data($cmforum, false, $student->id); $this->assertEquals(COMPLETION_INCOMPLETE, $completionforum->completionstate); // Test overriding the status of the auto-completion-activity to an invalid state. $this->expectException('moodle_exception'); core_completion_external::override_activity_completion_status($student->id, $forum->cmid, 3); } /** * Test overriding the activity completion status as a user without the capability to do so. */ public function test_override_status_user_without_capability() { global $DB, $CFG; $this->resetAfterTest(true); // Create course with teacher and student enrolled. $CFG->enablecompletion = true; $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); $student = $this->getDataGenerator()->create_user(); $teacher = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', ['shortname' => 'student']); $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); $teacherrole = $DB->get_record('role', ['shortname' => 'teacher']); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); $coursecontext = \context_course::instance($course->id); // Create an activity with automatic completion (a forum). $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id], ['completion' => 2, 'completionview' => 1]); // Test overriding the status of the activity for a user without the capability. $this->setUser($teacher); assign_capability('moodle/course:overridecompletion', CAP_PREVENT, $teacherrole->id, $coursecontext); $this->expectException('required_capability_exception'); core_completion_external::override_activity_completion_status($student->id, $forum->cmid, COMPLETION_COMPLETE); } /** * Test get_course_completion_status */ public function test_get_course_completion_status() { global $DB, $CFG, $COMPLETION_CRITERIA_TYPES; require_once($CFG->dirroot.'/completion/criteria/completion_criteria_self.php'); require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php'); require_once($CFG->dirroot.'/completion/criteria/completion_criteria_unenrol.php'); require_once($CFG->dirroot.'/completion/criteria/completion_criteria_activity.php'); require_once($CFG->dirroot.'/completion/criteria/completion_criteria_duration.php'); require_once($CFG->dirroot.'/completion/criteria/completion_criteria_grade.php'); require_once($CFG->dirroot.'/completion/criteria/completion_criteria_role.php'); require_once($CFG->dirroot.'/completion/criteria/completion_criteria_course.php'); $this->resetAfterTest(true); $CFG->enablecompletion = true; $student = $this->getDataGenerator()->create_user(); $teacher = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1, 'groupmode' => SEPARATEGROUPS, 'groupmodeforce' => 1)); $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), array('completion' => 1)); $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), array('completion' => 1)); $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id)); $cmdata = get_coursemodule_from_id('data', $data->cmid); $cmforum = get_coursemodule_from_id('forum', $forum->cmid); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); // Teacher and student in different groups initially. groups_add_member($group1->id, $student->id); groups_add_member($group2->id, $teacher->id); // Set completion rules. $completion = new \completion_info($course); // Loop through each criteria type and run its update_config() method. $criteriadata = new \stdClass(); $criteriadata->id = $course->id; $criteriadata->criteria_activity = array(); // Some activities. $criteriadata->criteria_activity[$cmdata->id] = 1; $criteriadata->criteria_activity[$cmforum->id] = 1; // In a week criteria date value. $criteriadata->criteria_date_value = time() + WEEKSECS; // Self completion. $criteriadata->criteria_self = 1; foreach ($COMPLETION_CRITERIA_TYPES as $type) { $class = 'completion_criteria_'.$type; $criterion = new $class(); $criterion->update_config($criteriadata); } // Handle overall aggregation. $aggdata = array( 'course' => $course->id, 'criteriatype' => null ); $aggregation = new \completion_aggregation($aggdata); $aggregation->setMethod(COMPLETION_AGGREGATION_ALL); $aggregation->save(); $aggdata['criteriatype'] = COMPLETION_CRITERIA_TYPE_ACTIVITY; $aggregation = new \completion_aggregation($aggdata); $aggregation->setMethod(COMPLETION_AGGREGATION_ALL); $aggregation->save(); $this->setUser($student); $result = core_completion_external::get_course_completion_status($course->id, $student->id); // We need to execute the return values cleaning process to simulate the web service server. $studentresult = external_api::clean_returnvalue( core_completion_external::get_course_completion_status_returns(), $result); // 3 different criteria. $this->assertCount(3, $studentresult['completionstatus']['completions']); $this->assertEquals(COMPLETION_AGGREGATION_ALL, $studentresult['completionstatus']['aggregation']); $this->assertFalse($studentresult['completionstatus']['completed']); $this->assertEquals('No', $studentresult['completionstatus']['completions'][0]['status']); $this->assertEquals('No', $studentresult['completionstatus']['completions'][1]['status']); $this->assertEquals('No', $studentresult['completionstatus']['completions'][2]['status']); // Teacher should see students status, they are in different groups but the teacher can access all groups. $this->setUser($teacher); $result = core_completion_external::get_course_completion_status($course->id, $student->id); // We need to execute the return values cleaning process to simulate the web service server. $teacherresult = external_api::clean_returnvalue( core_completion_external::get_course_completion_status_returns(), $result); $this->assertEquals($studentresult, $teacherresult); // Change teacher role capabilities (disable access al goups). $context = \context_course::instance($course->id); assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $teacherrole->id, $context); accesslib_clear_all_caches_for_unit_testing(); try { $result = core_completion_external::get_course_completion_status($course->id, $student->id); $this->fail('Exception expected due to groups permissions.'); } catch (\moodle_exception $e) { $this->assertEquals('accessdenied', $e->errorcode); } // Now add the teacher in the same group. groups_add_member($group1->id, $teacher->id); $result = core_completion_external::get_course_completion_status($course->id, $student->id); // We need to execute the return values cleaning process to simulate the web service server. $teacherresult = external_api::clean_returnvalue( core_completion_external::get_course_completion_status_returns(), $result); $this->assertEquals($studentresult, $teacherresult); } /** * Test mark_course_self_completed */ public function test_mark_course_self_completed() { global $DB, $CFG; require_once($CFG->dirroot.'/completion/criteria/completion_criteria_self.php'); $this->resetAfterTest(true); $CFG->enablecompletion = true; $student = $this->getDataGenerator()->create_user(); $teacher = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); // Set completion rules. $completion = new \completion_info($course); $criteriadata = new \stdClass(); $criteriadata->id = $course->id; $criteriadata->criteria_activity = array(); // Self completion. $criteriadata->criteria_self = COMPLETION_CRITERIA_TYPE_SELF; $class = 'completion_criteria_self'; $criterion = new $class(); $criterion->update_config($criteriadata); // Handle overall aggregation. $aggdata = array( 'course' => $course->id, 'criteriatype' => null ); $aggregation = new \completion_aggregation($aggdata); $aggregation->setMethod(COMPLETION_AGGREGATION_ALL); $aggregation->save(); $this->setUser($student); $result = core_completion_external::mark_course_self_completed($course->id); // We need to execute the return values cleaning process to simulate the web service server. $result = external_api::clean_returnvalue( core_completion_external::mark_course_self_completed_returns(), $result); // We expect a valid result. $this->assertEquals(true, $result['status']); $result = core_completion_external::get_course_completion_status($course->id, $student->id); // We need to execute the return values cleaning process to simulate the web service server. $result = external_api::clean_returnvalue( core_completion_external::get_course_completion_status_returns(), $result); // Course must be completed. $this->assertEquals(COMPLETION_COMPLETE, $result['completionstatus']['completions'][0]['complete']); try { $result = core_completion_external::mark_course_self_completed($course->id); $this->fail('Exception expected due course already self completed.'); } catch (\moodle_exception $e) { $this->assertEquals('useralreadymarkedcomplete', $e->errorcode); } } } tests/fixtures/legacy_course_completion.mbz 0000604 00000021146 15062070516 0015350 0 ustar 00 � �}M���q�\���hu�XU=h�H;a��{%��EfUQ�E��n2|�I'{X��}X�i�����v�;6"��ɏ*Vw��k�{@##3#3#"#�̈����O�����G��ggI6��d�B���Lf�g�k���M�ض3�̲�C��ʼ �2D[�0�,MØZ$6��V<DIH?\[_�eR|i��w$(����h~�y����W:��y�N����`>����?_}��No?w�S{��\��.��&!�D9֯T��Ҥ�E��ӷԎ4 K�t<�wјQ*;�z�(�a/hV5ׯ�:#�F��o,Y��M�i�$���e�d-���U(JViFWrH\�W�m�1�f��^��4���$�Z���kL����.��J�O{9R��� ��mS���)cU�m{� TZ,I�~�G��v�-84�Q�1��v�^8&+x�G��[Ϡ�S��|λ�L-P�i*Ĺ�O������F�-S��K�/w�GfNZ0E����g~[���=W���ʑ��V��i������ŏ{�q4�ܕmN�o萮2�QN�3=\��O��:���B�~:�g��>�o?�3��C�d?�=t8�e���U��O��:��t�y�������8]��5�V��-��$m)=[h��|��g�k��3�?���Q��7n�w�R'l�#����c���6�gS�̙������wa��a���`)���_8��FR��}����叿7��Ż;�'OV~��� !�i$$![��/8}a��ȇ��M�����e��l�>C�^�f�����B�� ��XQRd���r�l�4���Z��� ��P�zؒba�,��H�H��|�>�~:ȢZ ��=�ʱ��-�Ȫ����UL�"ZEa� ��V�1��[[�fFU(/����vx��<^4,i�b�'��"]�d��ijŻ �x "�6N�q�WY��:��,9^�і�AGh��3����j�̚a�߀qԌ~[F��)�d>,�'WPEɶ��"5�� ����%�^E۾�vئ�< �-jy���#ܒ�= �x�(�(�l�3DО$�@[����Nr &潀��MwD�ؤ�"I�^fK>��|�=�\�Yb0j�� xۯ�D�0�����;�CRTC�r�������q�?��_��?�S����A*ގ#��C]��`�^�0�К��6g��|��4��&�_�gU+�㹰�uJ6S���MD��� ���D+|\���j�uh��rZ拄>�8��U&��+/������(�l�@��J9/PՊ_Z��r��(��kY5���2뾮�=�ԞB|eą����YO�W���W�g<�V�DXE��`��m�u�8Q�`�F�ޟ�!n�u^a2�L�eLC��`�$.�R��n"{�B6��8����i�4��]�z�-�����KH���2�{��K� ��}� ,��-�g�3\?�����?�N�k@�����C�Z���x����<���'��q�,���?����H}��r��ui��t������8,�~]���(�����/�5��}i�������8$��=�ɿ?����?κ��w�u�`�mpcB�n�Tp�1x˩�Y��qH�=��������t�/?�)���l����/�{����b �"���`;��|�����ꄱ���`����H�K�8��;���d2������/I�}�XG.�-0�ސ)4��Q6/�z\.yq�ߝx���&�(��c�ݫkL������[�c/�p�`���$�X��i���O}� ]�$�sLe��M?X<[�ϲ��C|$�8�cEh�$]�#iO �Ա �ɬj�j�HᄢG=T�e��T��P���u��3Ȗ|�sT�L��L�(Y�P��,w�&J�����!n�8�hjӰ���Lbm˸Xϥ�XL�����L�I4�^gt͆-H�gtp+"9��#�֛B~�A��A�@�Іg�\C� ��(����?U �-�sm|H�l�0��8���dF���,~�hD�,�[f&��y����G��v�G�ƫ�9��R%�yu~��L S:�̨�%Ql� sSU_�4 wi�<@�M�m��X�k���d\�I}��4���.�w&�a�S���/m���Yc�ﺣ��z����e���4�c:Q���m|��5�@����ʿ`���L�ؓ��Uo������Z*��˴�U���^c���꺓�;�C�����q@���W������W�9;X�]OUȵ]מ8���]u T�n�@�!3�l�/�w7D˯ޱ��w�^�ݑ0��]�Vq j�U�~���P���ޱ��y���a`�S�<Ͱ#k� ��q�cš�i��q��{PY�V�y�O���/��F&S����L�(��w/�B�D�ϳ&&��H������j��y{ QL�Q\�H���֮��l6o�ޅ�c�$����:� "�M�_�W���qؕ���8d��|�u��0���g�����5����r�#Pw�J�[��q�q���l6�������U��4�c:I:�1��t�33���0��$y��~���tw�tdNW��[�����m�����mg��ϙ:�o�4�}۩�U���8,����t6� i����SG���q��īɿ��A�3�G]��oZ�Ǝr�<D�]�Dm|�3���l2� ����%����r/�1�M��N������I��1��v��ď�ũ�m���7�9�x��t�/�ᑽf\q�/ߤY�N�}�Y≹(�g�� A��C}�#}y�ݒ�_�8�~�fx�x�V��if�QaA]��giQ��"a��� �d�8*���C��J�_�VL / ���.A�,sk��;����&z [ �7ET@SE�A� �����O`,��T�X9�E4�l- V���8��DPF �!��bG���3�a,�@���~F���K yAg�h]��w�nC2Zd ��Oʮ����"@��� �eV�W0�1�������-�s��2�H�b����D��Be�I��2ޕ蠅wi��1RI�+�����jY'D�A�J��Z���5 �~ɪcs@J�I�m�!b���* "�/��;���f�PB��P��U���hAY�����$�2�2�HUy���-H*"� �d4(s�pm�%�c�͑O���ѐ�4��@�� �4Pre�`�" �bM1XЧ��<�?K��� !�r����:A[5�=�GЌ�ʒ��^�)�qx����M��,\��2 *�]b3�1�)�iM")xQ����Qr�$L�Z�R�lxP��X��J� � .�0��dO� (͝�N��&�$֊f�ۀ� h��&�1�̮zB�uI;���4K�h��X���#s�6vb�A�����U�L,��ZYYd�:?J�E��%i���(-T2���k� Wne'�( ����̿������_ơ|&er�X��;��:�F�M��%�H��M�`��dHh��d�x�h�� ��h�0@q����aS�� \��u��=+Z�#��Ϡ��!���4��0�7��j�� �+�@��i�`%W\��3Z: �-D(���-�B �j�q7!����x�; ��u�D��*��d0׆�%l�e� cbŅ 4-䤋e���Lш)L��m�t���-8"��<��]`*`��È���?�`V���ߖ�0e�9�O�@�q���e�z{X$�aءd��r�fb�8�����1��k�.�\����(,1T�&$H3��2�1�w�W\wZ���q�hq����DE��0J-TÀK�2��R�+!�bE�(���:�M�<� �DŽ+{�7��Ĉ`<��l��xL�MX�E_){֤bO�ĺQ3�X�����J�(��*=a$`?(f���a]��� ��`��SR���3Ȇ?�6Q����ߐ1��1�|�w[�ڊ�) �[3 �*1��_Uڄ�:x���W�X 5)�>_�$�^�p�m�]������Y�`B��\� kdHn�D6�A�\1L>� �'�b�:=Cq��M�U�]�>�H֕�^r�#\��0V�*WɎb�6�ٷ�m����D����(Oi�(��T2����gJ�[a ���v�Z�%ق���J� ��*� �Z5��J � #5� ��٫"e� ���?�t9�!�� $<P����"��9w �|��VG1�6xql:���V��"��t�#��n${�/X�_�a�T�i�7f�tM�'ɜy��%���`����&���^ڡ�d������J�潳&��O��#]�qw�� ��.�@~�!P�3 �ť'F�V�Ǫ9^�sΈ5(Q�/�J�b$v��y�x!�)W�3���@�O�e7{�yA��S���_�,�w��n��?�wޔ/�֫m�0��6���:��^�"���H��uK�S ��+u��~ӳ�7�\��2C��ƕ���䜗N�VKFo%��Eh\��6��u�&Y��ء���3��q��� ���O� B%£��A�68�Ŗ$%��8�U�ƃ1���\�*g�,�( �2\=(��-����B4�t�Y!}�E�S�g�u�������V�Xu��&q�������1��1P ������L������P�C�@{ ���/5Ǘ�()��6��6<����yM�Iޤ���M�i�7kÛ5��mx�^�!Ys 9���Bmb6�CM̐�����l�M<��Ӭ�C���*9���� �61�L����ɩp{����R3���>�SZ����n��SKo9v3���E��n�C4�D��z�� V�2��^G��@�Eu|�`8���p��G���p?��<��c�u�k��uX�'h=��r8��`��h1����sT�[W�9�WoH~�(��:��A��ti>���sF]7��t�T&�Bq�v�ao/���'���L�5��9�=��$���v�Bo��tH��F�_g2�&�������Yx�{س8��}�����T� <� �������?���f: �δ��qf�AҠ�_�����|�5G�~z�K�#�z���� ����w�o�0�_(#�iŪ`nv#���M��=C�9��%��P؍��E�#ڑ{��0�;Hf'���O����?���Q�_}�|:�a;�!�7�?zl�7��&���vR�n�h�o��M�߾=������_�}'E����͊��3|q!Ra� ���0S�#ʎdh �_l3���ذP>�GUnxe7XH�S&�2�M�G�vFxo����&Zo�~W�AY���Gv_F���M�$~b}ga���"<n:;"��w����aĥJ7n�ܴͶ�Ϝ}���7�x�N#|��Y�� ��O;y��c0��!�<�����K@E$]p�< 4��Kj�)7&Z�����@���fMa����xۮv!H�JL6��,c(dK>,ۖQ��D��#L��j&G���I���k�T0�)<�Qk���f��� ��X8K�)��� ��4X�R��p���Շ�G j����P�j���� �!���Y)� ڒ87.� �~���8 ��v◞�:Bf��23�4�˝<��?�_I�-P]E��}�Bum�B}�i�8$���V@��*��t�R����z\3*�I���t�g�;/��q�u�Ϟ��I��?�!�B��(��COڦi�% ޗ����M�����1�� ��_�O�೯�[����wh �In��1n��ZDs8x�'�e�g4�$��ɵ���C�ᗖ,�窴D�9�fk,L1�3�|�5��Z�rV��Tȕ�s�Mh��iA��wQ�q�( ��y���D�&Ա�x��EzO?�Y$���mm��C���#?>>fiZ,6E����ƹu����u�ד�/���6|�L�*t�qale�I�~C��b6�9`�I��B� |Jg�v@fd>�B�Vmk�Z�5~�yD閬�25� �e�-;Q�k0�ev�v�nu[ҍ�^�vx�+���r��@��+�:S<�`~�5�3^�|'68�����ء�=�=ޒe8�L��v��Nݥ��W���kxw�}Kl)?��ݺ�&� >��#���BbNX��BN?РT�D�C��x� ��� ���h�v�YkI�I�@���d�o�%@�:ĽV۞p�ՌDEL��W�D ����M�0�h�>-t��-�ĩ�|SﳂhC#{��� =k8���~�6e����n4��C���@�9:�$��O���S��'�{��) ��'p�'�$��O���~*@%���NU�4KB����uS\4B��ҠK��y'�tM}g�a� Z#1}��m-� ���<�U���z�T��S�H��A�Q2�| �6��>G�̜�h�9G�$I��m�q>K/�G��Z(�lu��m��n��ye�Ag����"�-[:d�LU�l:N�]]=�J�O�t놳4͜A��b�!����X^UP�Aۭ<D�]�J�T�*��'oV��͖��Z�5�lF��v/^��~��D/;���ғ�+��M��ο�����N{_!��7��"�l#|G8�8�y-ܛ�W2�n?z��2���}%���{-�ۛ�W2��~�N^�������ߏ^���ooz_6��Qd1>h�O����ӟ+�3��<v���W?���������?$�``�(�����/ݥ1���s*�gȿ?�&���'�=���6�4�#�Y��t@��������A�Q�_����~9؝�ad��N�aPL���$�ЬZ���4n��U`���=jA��+4^��ɠyvܫNP�����ߧ�yQ��e���OQ��1�Wd���tbw�.}�Ǚ5�8��$���Oj��&���ӑI��;c(�3�����&��&>����H�Jo\�;�����|p�"u�)Z��W� ��������i���H]�/��ɬ�{�9���ARo���w��X���^i���O�yHFv_���{Ǹ����� s;����d�Q����p���B���b��ӗ��S��]�����"u���Q��F��m�.���N����a��l��1�?g �o��N�:.�_�������x�k�4�o;u����(�C�g��a�����q�D���&��7D�����4�o;u����� ���g��h�}�������?���H]�9���ߨ�I����S��_��7�$=����d��>���M���t���H]�q�o�3H������;�$=�� ���31'����x�������� ����H��W��m@WQ� �Q��%1��; � �1TT�z��;��f����q��t{w��8�n�&ԁ��u%ԕ�%y�p��?I�;;��y�Q¯Nf4�!K��� �p���7J ���/� ��[���nXPE��X �g�'��H�T� #�%kJ�B4/A��e�YlIR�:�����97b#\D��bC���fF�y������=�~�6 C"�����|��n4� /fQ,�� �Ҁ��]�Rz{�@&��gQ^Tt�IM�Ė�@�Ճ��4��'��(�n<�y�i�s�O�n�^P�K�g�bC��)�W�`-� ��/L�A� �J�~�D��e����t�P�Ճ�2�@08H�����Z1��Te�њ�,^���g�0���ע�#�y�D�^2;����H��-�, ��X�m#F������ ���e[����j�r$1��=���շ�B��h�wUPW�市 5��ⶮ@�52z�� q��F�:��kIog�U;{�vڛƕ���>��I9����5�3�H��;�|���:uw'{����~8u�n�I&��+�E�>���}^2!���N��<��ȩ",:Y E���^fV+��^�#;���i�$n�&��Id��&y�6�~��?��w��<�.���/�����]�x��{B&����!���Yc�]��]�O{�%�x���Wj����Kf�/��q�6j��P{]��܅��j����dV��dI��@�'���zo�����w���wp��������9�xWh���l�h=��e����k\��1n�E���$�m��;�z���7�? ��Ι����v]�Ҟ~�:�_��W���)�_Yh琤�J�8�0� �8Snn{4�%�pn;$����ҿ��Kw鯼��ɻ�B�,�D6�Rh�(s�H-Ƕ��$�� +@�¦�>M��.~�*�A+�+R�E�'}Z�5`�b����xX|OY>�����9�U����%�� [Lv�<�z ���$ʘ�Ljё�[k:�Vt��̼�r�{s'�m��ي�����q�*K�����;~����c?�h\���c??�����^�=r4^X�������z ���V��4�� ��&��Y��4p�̧��r>���L�pp�r^8����u���w������wp�G�П�=��Q��u8�1��SN� �� Z��� ^ tests/fixtures/completion_creation.php 0000604 00000010517 15062070516 0014327 0 ustar 00 <?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/>. /** * Trait for course completion creation in unit tests * * @package core_completion * @category test * @copyright 2018 Adrian Greeve <adriangreeve.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); 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 . '/completion/criteria/completion_criteria_role.php'); /** * Trait for unit tests and completion. * * @copyright 2018 Adrian Greeve <adriangreeve.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ trait completion_creation { /** @var stdClass The course object. */ public $course; /** @var context The course context object. */ public $coursecontext; /** @var stdClass The course module object */ public $cm; /** * Create completion information. */ public function create_course_completion() { $this->resetAfterTest(); $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); $coursecontext = context_course::instance($course->id); $assign = $this->getDataGenerator()->create_module('assign', ['course' => $course->id, 'completion' => 1]); $modulecontext = context_module::instance($assign->cmid); $cm = get_coursemodule_from_id('assign', $assign->cmid); // Set completion rules. $completion = new \completion_info($course); $criteriadata = (object) [ 'id' => $course->id, 'criteria_activity' => [ $cm->id => 1 ] ]; $criterion = new \completion_criteria_activity(); $criterion->update_config($criteriadata); $criteriadata = (object) [ 'id' => $course->id, 'criteria_role' => [3 => 3] ]; $criterion = new \completion_criteria_role(); $criterion->update_config($criteriadata); // Handle overall aggregation. $aggdata = array( 'course' => $course->id, 'criteriatype' => COMPLETION_CRITERIA_TYPE_ACTIVITY ); $aggregation = new \completion_aggregation($aggdata); $aggregation->setMethod(COMPLETION_AGGREGATION_ALL); $aggregation->save(); $aggdata['criteriatype'] = COMPLETION_CRITERIA_TYPE_ROLE; $aggregation = new \completion_aggregation($aggdata); $aggregation->setMethod(COMPLETION_AGGREGATION_ANY); $aggregation->save(); // Set variables for access in tests. $this->course = $course; $this->coursecontext = $coursecontext; $this->cm = $cm; } /** * Complete some of the course completion criteria. * * @param stdClass $user The user object * @param bool $modulecompletion If true will complete the activity module completion thing. */ public function complete_course($user, $modulecompletion = true) { $this->getDataGenerator()->enrol_user($user->id, $this->course->id, 'student'); $completion = new \completion_info($this->course); $criteriacompletions = $completion->get_completions($user->id, COMPLETION_CRITERIA_TYPE_ROLE); $criteria = completion_criteria::factory(['id' => 3, 'criteriatype' => COMPLETION_CRITERIA_TYPE_ROLE]); foreach ($criteriacompletions as $ccompletion) { $criteria->complete($ccompletion); } if ($modulecompletion) { // Set activity as complete. $completion->update_state($this->cm, COMPLETION_COMPLETE, $user->id); } } } tests/activity_custom_completion_test.php 0000604 00000014142 15062070516 0015135 0 ustar 00 <?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/>. declare(strict_types = 1); namespace core_completion; use advanced_testcase; use coding_exception; use moodle_exception; use PHPUnit\Framework\MockObject\MockObject; /** * Class for unit testing core_completion/activity_custom_completion. * * @package core_completion * @copyright 2021 Jun Pataleta <jun@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class activity_custom_completion_test extends advanced_testcase { /** * Fetches a mocked activity_custom_completion instance. * * @param string[] $methods List of methods to mock. * @return activity_custom_completion|MockObject */ protected function setup_mock(array $methods) { return $this->getMockBuilder(activity_custom_completion::class) ->disableOriginalConstructor() ->onlyMethods($methods) ->getMockForAbstractClass(); } /** * Data provider for test_get_overall_completion_state(). */ public function overall_completion_state_provider(): array { global $CFG; require_once($CFG->libdir . '/completionlib.php'); return [ 'First incomplete, second complete' => [ ['completionsubmit', 'completioncreate'], [COMPLETION_INCOMPLETE, COMPLETION_COMPLETE], 1, COMPLETION_INCOMPLETE ], 'First complete, second incomplete' => [ ['completionsubmit', 'completioncreate'], [COMPLETION_COMPLETE, COMPLETION_INCOMPLETE], 2, COMPLETION_INCOMPLETE ], 'All complete' => [ ['completionsubmit', 'completioncreate'], [COMPLETION_COMPLETE, COMPLETION_COMPLETE], 2, COMPLETION_COMPLETE ], 'No rules' => [ [], [], 0, COMPLETION_COMPLETE ], ]; } /** * Test for \core_completion\activity_custom_completion::get_overall_completion_state(). * * @dataProvider overall_completion_state_provider * @param string[] $rules The custom completion rules. * @param int[] $rulestates The completion states of these custom completion rules. * @param int $invokecount Expected invoke count of get_state(). * @param int $state The expected overall completion state */ public function test_get_overall_completion_state(array $rules, array $rulestates, int $invokecount, int $state) { $stub = $this->setup_mock([ 'get_available_custom_rules', 'get_state', ]); // Mock activity_custom_completion's get_available_custom_rules() method. $stub->expects($this->once()) ->method('get_available_custom_rules') ->willReturn($rules); // Mock activity_custom_completion's get_state() method. if ($invokecount > 0) { $stub->expects($this->exactly($invokecount)) ->method('get_state') ->withConsecutive( [$rules[0]], [$rules[1]] ) ->willReturn($rulestates[0], $rulestates[1]); } else { $stub->expects($this->never()) ->method('get_state'); } $this->assertEquals($state, $stub->get_overall_completion_state()); } /** * Data provider for test_validate_rule(). * * @return array[] */ public function validate_rule_provider() { return [ 'Not defined' => [ false, true, coding_exception::class ], 'Not available' => [ true, false, moodle_exception::class ], 'Defined and available' => [ true, true, null ], ]; } /** * Test for validate_rule() * * @dataProvider validate_rule_provider * @param bool $defined is_defined()'s mocked return value. * @param bool $available is_available()'s mocked return value. * @param string|null $expectedexception Expected expectation class name. */ public function test_validate_rule(bool $defined, bool $available, ?string $expectedexception) { $stub = $this->setup_mock([ 'is_defined', 'is_available' ]); // Mock activity_custom_completion's is_defined() method. $stub->expects($this->any()) ->method('is_defined') ->willReturn($defined); // Mock activity_custom_completion's is_available() method. $stub->expects($this->any()) ->method('is_available') ->willReturn($available); if ($expectedexception) { $this->expectException($expectedexception); } $stub->validate_rule('customcompletionrule'); } /** * Test for is_available(). */ public function test_is_available() { $stub = $this->setup_mock([ 'get_available_custom_rules', ]); // Mock activity_custom_completion's get_available_custom_rules() method. $stub->expects($this->any()) ->method('get_available_custom_rules') ->willReturn(['rule1', 'rule2']); // Rule is available. $this->assertTrue($stub->is_available('rule1')); // Rule is not available. $this->assertFalse($stub->is_available('rule')); } } tests/generator/lib.php 0000604 00000004211 15062070516 0011147 0 ustar 00 <?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/>. /** * Completion test generator * * @package core_completion * @copyright 2023 Amaia Anabitarte <amaia@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_completion_generator extends component_generator_base { /** * Create default completion * * @param array|stdClass $record * @return stdClass */ public function create_default_completion($record): stdClass { global $DB; $record = (array) $record; if (!array_key_exists('course', $record) || !is_numeric($record['course'])) { throw new moodle_exception('courserequired'); } if (!$DB->get_record('course', ['id' => $record['course']])) { throw new moodle_exception('invalidcourseid'); } if (!array_key_exists('module', $record) || !is_numeric($record['module'])) { throw new moodle_exception('modulerequired'); } if (!$DB->get_record('modules', ['id' => $record['module']])) { throw new moodle_exception('invalidmoduleid'); } $record = (object) array_merge([ 'completion' => 0, 'completionview' => 0, 'completionusegrade' => 0, 'completionpassgrade' => 0, 'completionexpected' => 0, 'customrules' => '', ], $record); $record->id = $DB->insert_record('course_completion_defaults', $record); return $record; } } tests/generator/behat_core_completion_generator.php 0000604 00000003507 15062070516 0017002 0 ustar 00 <?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/>. /** * Completion test generator for Behat * * @package core_completion * @copyright 2023 Amaia Anabitarte <amaia@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_core_completion_generator extends behat_generator_base { /** * Get a list of the entities that can be created for completion * * @return array[] */ protected function get_creatable_entities(): array { return [ 'Course defaults' => [ 'singular' => 'Course default', 'datagenerator' => 'default_completion', 'required' => [ 'course', 'module', ], 'switchids' => [ 'course' => 'course', 'module' => 'module', ], ], ]; } /** * Look up module ID from given name * * @param string $name * @return int */ protected function get_module_id(string $name): int { global $DB; return (int) $DB->get_field('modules', 'id', ['name' => $name], MUST_EXIST); } } tests/privacy/provider_test.php 0000604 00000023214 15062070516 0012765 0 ustar 00 <?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/>. /** * Unit Tests for the request helper. * * @package core_completion * @category test * @copyright 2018 Adrian Greeve <adriangreeve.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_completion\privacy; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/completion/tests/fixtures/completion_creation.php'); /** * Tests for the \core_completion API's provider functionality. * * @copyright 2018 Adrian Greeve <adriangreeve.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider_test extends \core_privacy\tests\provider_testcase { use \completion_creation; /** * Test joining course completion data to an sql statement. */ public function test_get_course_completion_join_sql() { global $DB; $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->create_course_completion(); $this->complete_course($user, false); list($join, $where, $params) = \core_completion\privacy\provider::get_course_completion_join_sql($user->id, 'comp', 'c.id'); $sql = "SELECT DISTINCT c.id FROM {course} c {$join} WHERE {$where}"; $records = $DB->get_records_sql($sql, $params); $data = array_shift($records); $this->assertEquals($this->course->id, $data->id); } /** * Test fetching users' course completion by context and adding to a userlist. */ public function test_add_course_completion_users_to_userlist() { $this->resetAfterTest(); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); // User1 and user2 complete course. $this->create_course_completion(); $this->complete_course($user1); $this->complete_course($user2); // User3 is enrolled but has not completed course. $this->getDataGenerator()->enrol_user($user3->id, $this->course->id, 'student'); $userlist = new \core_privacy\local\request\userlist($this->coursecontext, 'test'); \core_completion\privacy\provider::add_course_completion_users_to_userlist($userlist); // Ensure only users that have course completion are returned. $expected = [$user1->id, $user2->id]; $actual = $userlist->get_userids(); sort($expected); sort($actual); $this->assertCount(2, $actual); $this->assertEquals($expected, $actual); } /** * Test getting course completion information. */ public function test_get_course_completion_info() { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->create_course_completion(); $this->complete_course($user); $coursecompletion = \core_completion\privacy\provider::get_course_completion_info($user, $this->course); $this->assertEquals('Complete', $coursecompletion['status']); $this->assertCount(2, $coursecompletion['criteria']); } /** * Test getting activity completion information. */ public function test_get_activity_completion_info() { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->create_course_completion(); $this->complete_course($user); $activitycompletion = \core_completion\privacy\provider::get_activity_completion_info($user, $this->course, $this->cm); $this->assertEquals($user->id, $activitycompletion->userid); $this->assertEquals($this->cm->id, $activitycompletion->coursemoduleid); $this->assertEquals(1, $activitycompletion->completionstate); } /** * Test deleting activity completion information for a user. */ public function test_delete_completion_activity_user() { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->create_course_completion(); $this->complete_course($user); \core_completion\privacy\provider::delete_completion($user, null, $this->cm->id); $activitycompletion = \core_completion\privacy\provider::get_activity_completion_info($user, $this->course, $this->cm); $this->assertEquals(0, $activitycompletion->completionstate); } /** * Test deleting course completion information. */ public function test_delete_completion_course() { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $this->create_course_completion(); $this->complete_course($user); \core_completion\privacy\provider::delete_completion(null, $this->course->id); $coursecompletion = \core_completion\privacy\provider::get_course_completion_info($user, $this->course); foreach ($coursecompletion['criteria'] as $criterion) { $this->assertEquals('No', $criterion['completed']); } } /** * Test deleting course completion information by approved userlist. */ public function test_delete_completion_by_approved_userlist() { $this->resetAfterTest(); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); $user4 = $this->getDataGenerator()->create_user(); $this->create_course_completion(); $this->complete_course($user1); $this->complete_course($user2); $this->complete_course($user3); $this->complete_course($user4); // Prepare approved userlist (context/component are irrelevant for this test). $approveduserids = [$user1->id, $user3->id]; $userlist = new \core_privacy\local\request\approved_userlist($this->coursecontext, 'completion', $approveduserids); // Test deleting activity completion information only affects approved userlist. \core_completion\privacy\provider::delete_completion_by_approved_userlist( $userlist, null, $this->cm->id); $activitycompletion1 = \core_completion\privacy\provider::get_activity_completion_info($user1, $this->course, $this->cm); $this->assertEquals(0, $activitycompletion1->completionstate); $activitycompletion2 = \core_completion\privacy\provider::get_activity_completion_info($user2, $this->course, $this->cm); $this->assertNotEquals(0, $activitycompletion2->completionstate); $activitycompletion3 = \core_completion\privacy\provider::get_activity_completion_info($user3, $this->course, $this->cm); $this->assertEquals(0, $activitycompletion3->completionstate); $activitycompletion4 = \core_completion\privacy\provider::get_activity_completion_info($user4, $this->course, $this->cm); $this->assertNotEquals(0, $activitycompletion4->completionstate); // Prepare different approved userlist (context/component are irrelevant for this test). $approveduserids = [$user2->id, $user4->id]; $userlist = new \core_privacy\local\request\approved_userlist($this->coursecontext, 'completion', $approveduserids); // Test deleting course completion information only affects approved userlist. \core_completion\privacy\provider::delete_completion_by_approved_userlist($userlist, $this->course->id); $coursecompletion1 = \core_completion\privacy\provider::get_course_completion_info($user1, $this->course); $hasno = array_search('No', $coursecompletion1['criteria'], true); $this->assertFalse($hasno); $coursecompletion2 = \core_completion\privacy\provider::get_course_completion_info($user2, $this->course); $hasyes = array_search('Yes', $coursecompletion2['criteria'], true); $this->assertFalse($hasyes); $coursecompletion3 = \core_completion\privacy\provider::get_course_completion_info($user3, $this->course); $hasno = array_search('No', $coursecompletion3['criteria'], true); $this->assertFalse($hasno); $coursecompletion4 = \core_completion\privacy\provider::get_course_completion_info($user4, $this->course); $hasyes = array_search('Yes', $coursecompletion4['criteria'], true); $this->assertFalse($hasyes); } /** * Test getting course completion information with completion disabled. */ public function test_get_course_completion_info_completion_disabled() { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(['enablecompletion' => 0]); $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student'); $coursecompletion = \core_completion\privacy\provider::get_course_completion_info($user, $course); $this->assertTrue(is_array($coursecompletion)); $this->assertEmpty($coursecompletion); } } tests/behat/site_default_activity_completion.feature 0000604 00000011077 15062070516 0017167 0 ustar 00 @core @core_completion Feature: Allow admins to edit the default activity completion rules at site level. In order to set the activity completion defaults for new activities As an admin I need to be able to edit the completion rules for a group of activities at site level. Background: Given the following "courses" exist: | fullname | shortname | category | enablecompletion | | Course 1 | C1 | 0 | 1 | And I log in as "admin" @javascript Scenario: Default activity completion rules with no site or course default completion Given the following "activity" exists: | activity | assign | | course | C1 | | name | Test assignment one | | completion | 1 | And I am on "Course 1" course homepage with editing mode on When I add a "Assignment" to section "0" And I expand all fieldsets # Completion tracking 0 = Do not indicate activity completion. Then the field "None" matches value "1" # Default values don't affect existing activities. But I am on the "Test assignment one" "assign activity editing" page And I navigate to "Settings" in current page administration And I expand all fieldsets And the field "Students must manually mark the activity as done" matches value "1" And the field "None" matches value "0" @javascript Scenario: Default activity completion rules with site default completion but with no course default completion Given the following "activity" exists: | activity | assign | | course | C1 | | name | Test assignment one | | completion | 0 | And the following "core_completion > Course default" exist: | course | module | completion | completionview | completionusegrade | completionsubmit | | Acceptance test site | assign | 2 | 0 | 1 | 1 | And I am on "Course 1" course homepage with editing mode on When I add a "Assignment" to section "0" And I expand all fieldsets Then the field "Add requirements" matches value "1" And the field "completionview" matches value "0" And the field "completionusegrade" matches value "1" And the field "completionsubmit" matches value "1" # Default values don't affect existing activities. But I am on the "Test assignment one" "assign activity editing" page And I navigate to "Settings" in current page administration And I expand all fieldsets And the field "Add requirements" matches value "0" And the field "None" matches value "1" @javascript Scenario: Default activity completion rules with site default completion and course default completion Given the following "activity" exists: | activity | assign | | course | C1 | | name | Test assignment one | | completion | 0 | And the following "core_completion > Course defaults" exist: | course | module | completion | completionview | completionusegrade | completionsubmit | | Acceptance test site | assign | 2 | 0 | 1 | 1 | | C1 | assign | 2 | 1 | 0 | 1 | And I am on "Course 1" course homepage with editing mode on When I add a "Assignment" to section "0" And I expand all fieldsets Then the field "Add requirements" matches value "1" And the field "completionview" matches value "1" And the field "completionusegrade" matches value "0" And the field "completionsubmit" matches value "1" # Default values don't affect existing activities. But I am on the "Test assignment one" "assign activity editing" page And I navigate to "Settings" in current page administration And I expand all fieldsets And the field "Add requirements" matches value "0" And the field "None" matches value "1" Scenario: Navigate to site default activity completion Given I navigate to "Courses > Default settings > Default activity completion" in site administration When I should see "Default activity completion" Then I should see "These are the default completion conditions for activities in all courses." And the following config values are set as admin: | enablecompletion | 0 | And I navigate to "Courses > Default settings" in site administration And I should not see "Default activity completion" tests/behat/completion_other_courses.feature 0000604 00000002760 15062070516 0015466 0 ustar 00 @core @core_completion Feature: Set completion of other courses as criteria for completion of current course In order to set completion of other courses as criteria for completion of current course As a user I want to select the prerequisite courses in completion settings Background: Given the following "courses" exist: | fullname | shortname | category | enablecompletion | | Course 1 | C1 | 0 | 1 | | Course 2 | C2 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | student1 | Student | One | student1@example.com | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | @javascript Scenario: Set completion of prerequisite course as completion criteria of current course When I log in as "admin" And I am on "Course 1" course homepage with editing mode on And I navigate to "Course completion" in current page administration And I click on "Condition: Completion of other courses" "link" And I set the field "Courses available" to "Course 2" And I press "Save changes" And I add the "Course completion status" block And I click on "View course report" "link" in the "Course completion status" "block" Then I should see "Course 2" in the "completion-progress" "table" And I should see "Student One" in the "completion-progress" "table" tests/behat/passgrade_completion_criteria_gradeitem_visibility.feature 0000604 00000017072 15062070516 0022727 0 ustar 00 @core @core_completion @javascript Feature: Students will be shown relevant completion state based on grade item visibility. In order to understand completion states of course modules As a student I need to see relevant completion information for various combination of activity passgrade settings Background: Given the following "courses" exist: | fullname | shortname | category | enablecompletion | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | Frist | teacher1@example.com | | student1 | Student | First | student1@example.com | | student2 | Student | Second | student2@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | And the following "activity" exists: | activity | assign | | course | C1 | | name | Test assignment name | | assignsubmission_onlinetext_enabled | 1 | | assignsubmission_file_enabled | 0 | | completion | 2 | | completionpassgrade | 1 | | completionusegrade | 1 | | gradepass | 50 | And I am on the "Course 1" course page logged in as teacher1 And "Student First" user has not completed "Test assignment name" activity And I am on the "Course 1" course page logged in as student1 And the "Receive a grade" completion condition of "Test assignment name" is displayed as "todo" And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "todo" Scenario: Passing grade and receive a grade completions for visible grade item (passgrade completion enabled) Given the following "grade grades" exist: | gradeitem | user | grade | | Test assignment name | student1 | 21.00 | | Test assignment name | student2 | 50.00 | And I am on the "Course 1" course page logged in as teacher1 And "Student First" user has completed "Test assignment name" activity And "Student Second" user has completed "Test assignment name" activity When I am on the "Course 1" course page logged in as student1 And the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "failed" And I am on the "Course 1" course page logged in as student2 Then the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "done" Scenario: Passing grade and receive a grade completions for hidden grade item (passgrade completion enabled) Given I am on the "Course 1" "grades > gradebook setup" page logged in as "teacher1" And I hide the grade item "Test assignment name" of type "gradeitem" on "setup" page And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I give the grade "21" to the user "Student First" for the grade item "Test assignment name" And I give the grade "50" to the user "Student Second" for the grade item "Test assignment name" And I press "Save changes" And I am on "Course 1" course homepage And "Student First" user has not completed "Test assignment name" activity And "Student Second" user has completed "Test assignment name" activity And I am on the "Course 1" course page logged in as student1 And the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "todo" And I am on the "Course 1" course page logged in as student2 And the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "done" Scenario: Receive a grade completion for visible grade item (passgrade completion disabled) Given I am on the "Test assignment name" "assign activity editing" page logged in as teacher1 And I set the following fields to these values: | completionpassgrade | 0 | And I press "Save and display" And I am on the "Course 1" course page logged in as student1 And the "Receive a grade" completion condition of "Test assignment name" is displayed as "todo" And I should not see "Receive a passing grade" And I am on the "Course 1" "grades > Grader report > View" page logged in as "teacher1" And I turn editing mode on And I give the grade "21" to the user "Student First" for the grade item "Test assignment name" And I give the grade "50" to the user "Student Second" for the grade item "Test assignment name" And I press "Save changes" And I am on "Course 1" course homepage And "Student First" user has completed "Test assignment name" activity And "Student Second" user has completed "Test assignment name" activity When I am on the "Course 1" course page logged in as student1 # Once MDL-75582 is fixed "failed" should be changed to "done" And the "Receive a grade" completion condition of "Test assignment name" is displayed as "failed" And I should not see "Receive a passing grade" And I am on the "Course 1" course page logged in as student2 Then the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" Scenario: Receive a grade completion for hidden grade item (passgrade completion disabled) Given I am on the "Test assignment name" "assign activity editing" page logged in as teacher1 And I set the following fields to these values: | completionpassgrade | 0 | And I press "Save and display" And I am on the "Course 1" course page logged in as student1 And the "Receive a grade" completion condition of "Test assignment name" is displayed as "todo" And I should not see "Receive a passing grade" And I am on the "Course 1" "grades > gradebook setup" page logged in as "teacher1" And I hide the grade item "Test assignment name" of type "gradeitem" on "setup" page And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I give the grade "21" to the user "Student First" for the grade item "Test assignment name" And I give the grade "50" to the user "Student Second" for the grade item "Test assignment name" And I press "Save changes" And I am on "Course 1" course homepage And "Student First" user has completed "Test assignment name" activity And "Student Second" user has completed "Test assignment name" activity When I am on the "Course 1" course page logged in as student1 Then the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" And I should not see "Receive a passing grade" And I am on the "Course 1" course page logged in as student2 And the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" tests/behat/activity_completion_criteria.feature 0000604 00000017611 15062070516 0016321 0 ustar 00 @core @core_completion Feature: Allow to mark course as completed without cron for activity completion criteria In order for students to see instant course completion updates I need to be able update completion state without cron Background: Given the following "courses" exist: | fullname | shortname | category | | Completion course | CC1 | 0 | And the following "users" exist: | username | firstname | lastname | email | | student1 | Student | First | student1@example.com | | student2 | Student | Second | student2@example.com | | teacher1 | Teacher | First | teacher1@example.com | And the following "course enrolments" exist: | user | course | role | | student1 | CC1 | student | | student2 | CC1 | student | | teacher1 | CC1 | editingteacher | And the following "activity" exists: | activity | assign | | course | CC1 | | name | Test assignment name | | idnumber | assign1 | And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | completionstatus | Course | CC1 | course-view-* | side-pre | And I am on the "Completion course" "course editing" page logged in as admin And I expand all fieldsets And I set the field "Enable completion tracking" to "Yes" And I click on "Save and display" "button" And I am on the "Test assignment name" "assign activity editing" page And I click on "Expand all" "link" in the "region-main" "region" And I set the field "Add requirements" to "1" And I set the field "completionusegrade" to "1" And I press "Save and return to course" And I navigate to "Course completion" in current page administration And I expand all fieldsets And I set the field "Assignment - Test assignment name" to "1" And I press "Save changes" @javascript Scenario: Update course completion when student marks activity as complete Given I am on the "Test assignment name" "assign activity editing" page logged in as teacher1 And I click on "Expand all" "link" in the "region-main" "region" And I set the field "Students must manually mark the activity as done" to "1" And I press "Save and return to course" When I am on the "Completion course" course page logged in as student1 And I should see "Status: Not yet started" And I press "Mark as done" And I wait until "Done" "button" exists And "Mark as done" "button" should not exist And I reload the page Then I should see "Status: Complete" @javascript Scenario: Update course completion when teacher grades a single assignment Given I am on the "Test assignment name" "assign activity" page logged in as teacher1 And I follow "View all submissions" And I click on "Grade" "link" in the "student1@example.com" "table_row" And I set the field "Grade out of 100" to "40" And I click on "Save changes" "button" And I am on "Completion course" course homepage When I am on the "Completion course" course page logged in as student1 Then I should see "Status: Complete" @javascript Scenario: Update course completion with multiple activity criteria Given the following "activity" exists: | activity | assign | | course | CC1 | | name | Test assignment name2 | | idnumber | assign2 | And I am on the "Test assignment name2" "assign activity editing" page logged in as admin And I click on "Expand all" "link" in the "region-main" "region" And I set the field "Add requirements" to "1" And I set the field "completionusegrade" to "1" And I press "Save and return to course" And I navigate to "Course completion" in current page administration And I should see "Course completion settings" in the "tertiary-navigation" "region" And I expand all fieldsets And I set the field "Assignment - Test assignment name" to "1" And I set the field "Assignment - Test assignment name2" to "1" And I press "Save changes" And I am on the "Test assignment name" "assign activity" page And I follow "View all submissions" And I click on "Grade" "link" in the "student1@example.com" "table_row" And I set the field "Grade out of 100" to "40" And I click on "Save changes" "button" And I am on the "Completion course" course page logged in as student1 And I should see "Status: In progress" And I am on the "Test assignment name2" "assign activity" page logged in as teacher1 And I follow "View all submissions" And I click on "Grade" "link" in the "student1@example.com" "table_row" And I set the field "Grade out of 100" to "40" And I click on "Save changes" "button" When I am on the "Completion course" course page logged in as student1 Then I should see "Status: Complete" @javascript Scenario: Course completion should not be updated when teacher grades assignment on course grader report page Given I am on the "Completion course" "grades > Grader report > View" page logged in as "teacher1" And I turn editing mode on And I give the grade "57" to the user "Student First" for the grade item "Test assignment name" And I press "Save changes" When I am on the "Completion course" course page logged in as student1 Then I should see "Status: Pending" And I run the scheduled task "core\task\completion_regular_task" And I wait "1" seconds And I run the scheduled task "core\task\completion_regular_task" And I reload the page And I should see "Status: Complete" @javascript Scenario: Course completion should not be updated when teacher grades assignment on activity grader report page Given I am on the "Completion course" "grades > Single View > View" page logged in as "teacher1" And I click on "Users" "link" in the ".page-toggler" "css_element" And I turn editing mode on And I click on "Student First" in the "user" search widget And I set the field "Override for Test assignment name" to "1" When I set the following fields to these values: | Grade for Test assignment name | 10.00 | | Feedback for Test assignment name | test data | And I press "Save" When I am on the "Completion course" course page logged in as student1 And I should see "Status: Pending" And I run the scheduled task "core\task\completion_regular_task" And I wait "1" seconds And I run the scheduled task "core\task\completion_regular_task" And I reload the page Then I should see "Status: Complete" @javascript @_file_upload Scenario: Course completion should not be updated when teacher imports grades with csv file Given I am on the "Completion course" course page logged in as teacher1 And I navigate to "CSV file" import page in the course gradebook And I upload "lib/tests/fixtures/upload_grades.csv" file to "File" filemanager And I press "Upload grades" And I set the field "Map to" to "Email address" And I set the field "Test assignment name" to "Assignment: Test assignment name" And I press "Upload grades" And I press "Continue" And I should see "10.00" in the "Student First" "table_row" And I am on the "Completion course" course page logged in as student1 And I should see "Status: Pending" When I run the scheduled task "core\task\completion_regular_task" And I wait "1" seconds And I run the scheduled task "core\task\completion_regular_task" And I reload the page Then I should see "Status: Complete" tests/behat/bulk_edit_activity_completion.feature 0000604 00000013746 15062070516 0016466 0 ustar 00 @core @core_completion @javascript Feature: Allow teachers to bulk edit activity completion rules in a course. In order to avoid editing single activities As a teacher I need to be able to edit the completion rules for a group of activities. Background: Given the following "courses" exist: | fullname | shortname | category | enablecompletion | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | Frist | teacher1@example.com | | student1 | Student | First | student1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | course | idnumber | name | intro | grade | | assign | C1 | a1 | Test assignment one | Submit something! | 30 | | assign | C1 | a2 | Test assignment two | Submit something! | 10 | | assign | C1 | a3 | Test assignment three | Submit something! | 15 | | assign | C1 | a4 | Test assignment four | Submit nothing! | 15 | And I log out # Given I am a teacher in a course with completion tracking enabled and activities present. # When I bulk edit activity completion rules for activities of the same kind. # Then the completion rules should be updated for all selected activities. Scenario: Bulk edit activity completion rules Given I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on When I navigate to "Course completion" in current page administration And I set the field "Course completion tertiary navigation" to "Bulk edit activity completion" And I click on "Test assignment one" "checkbox" And I click on "Test assignment two" "checkbox" And I click on "Edit" "button" And I should see "The changes will affect the following 2 activities or resources:" And I set the following fields to these values: | Add requirements | 1 | | View the activity | 1 | | Make a submission | 1 | | Receive a grade | 1 | And I click on "Save changes" "button" Then I should see "Changes saved" And I should see "With conditions" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment one']]" "xpath_element" And I should see "View the activity" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment one']]" "xpath_element" And I should see "Receive a grade" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment one']]" "xpath_element" And I should see "Make a submission" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment one']]" "xpath_element" And I should not see "Completion expected on" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment one']]" "xpath_element" And I should see "With conditions" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment two']]" "xpath_element" And I should see "View the activity" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment two']]" "xpath_element" And I should see "Receive a grade" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment two']]" "xpath_element" And I should see "Make a submission" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment two']]" "xpath_element" And I should not see "Completion expected on" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment two']]" "xpath_element" # Same conditions as above, # However if completionpassgrade is set, only the completionpassgrade detail should be shown. # It is implied requires grade is selected as it passgrade is dependent on it. Scenario: Bulk edit passing grade completion Given I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on When I navigate to "Course completion" in current page administration And I set the field "Course completion tertiary navigation" to "Bulk edit activity completion" And I click on "Test assignment one" "checkbox" And I click on "Test assignment two" "checkbox" And I click on "Edit" "button" And I should see "The changes will affect the following 2 activities or resources:" And I set the field "Add requirements" to "1" And I should see "Make a submission" And I set the field "Receive a grade" to "1" And I set the field "Passing grade" to "1" And I click on "Save changes" "button" Then I should see "Changes saved" And I should see "With conditions" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment one']]" "xpath_element" And I should see "Passing grade" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment one']]" "xpath_element" And I should not see "Completion expected on" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment one']]" "xpath_element" And I should see "With conditions" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment two']]" "xpath_element" And I should see "Passing grade" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment two']]" "xpath_element" And I should not see "Completion expected on" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Test assignment two']]" "xpath_element" tests/behat/completion_no_calendar_capabilities.feature 0000604 00000003445 15062070516 0017561 0 ustar 00 @core @core_completion Feature: Completion with no calendar capabilites In order to allow work effectively As a teacher I need to be able to create activities with completion enabled without calendar capabilities Background: Given the following "courses" exist: | fullname | shortname | category | groupmode | enablecompletion | | Course 1 | C1 | 0 | 1 | 1 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "activity" exists: | activity | forum | | course | C1 | | idnumber | 00001 | | name | Test forum name | | completion | 2 | And I am on the "Test forum name" "forum activity editing" page logged in as admin And I set the following fields to these values: | id_completionexpected_enabled | 1 | | id_completionexpected_day | 1 | | id_completionexpected_month | 1 | | id_completionexpected_year | 2017 | And I press "Save and return to course" And I am on the "Course 1" "permissions" page And I override the system permissions of "Teacher" role with: | capability | permission | | moodle/calendar:manageentries | Prohibit | Scenario: Editing completion date When I am on the "Test forum name" "forum activity editing" page logged in as teacher1 And I set the following fields to these values: | id_completionexpected_year | 2018 | And I press "Save and return to course" Then I should see "Test forum name" tests/behat/completion_course_page_display.feature 0000604 00000006211 15062070516 0016616 0 ustar 00 @core @core_completion @javascript Feature: Show activity completion status or activity completion configuration on the course page In order to understand the configuration or status of an activity's completion As a user I need to see the appropriate completion information for each activity in the course homepage Background: Given the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | First | teacher1@example.com | | teacher2 | Teacher | Second | teacher2@example.com | | student1 | Student | First | student1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher2 | C1 | teacher | | student1 | C1 | student | And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And I navigate to "Settings" in current page administration And I set the following fields to these values: | Enable completion tracking | Yes | And I press "Save and display" And the following "activities" exist: | activity | course | idnumber | name | intro | completion | completionview | completionexpected | | assign | C1 | assign1 | Test assignment name | Test assignment description | 2 | 1 | 0 | | quiz | C1 | quiz1 | Test quiz name | Test quiz description | 0 | 0 | 0 | | forum | C1 | forum1 | Test forum name | | 1 | 0 | 0 | Scenario: Show completion status to students Given I am on the "Course 1" course page logged in as student1 And the manual completion button of "Test forum name" is displayed as "Mark as done" And the "View" completion condition of "Test assignment name" is displayed as "todo" And there should be no completion information shown for "Test quiz name" Scenario: Show completion configuration to editing teachers Given I am on the "Course 1" course page logged in as teacher1 And "Test forum name" should have the "Mark as done" completion condition And "Test assignment name" should have the "View" completion condition And there should be no completion information shown for "Test quiz name" And I am on "Course 1" course homepage with editing mode on And "Test forum name" should have the "Mark as done" completion condition And "Test assignment name" should have the "View" completion condition And there should be no completion information shown for "Test quiz name" Scenario: Show completion configuration to non-editing teachers Given I am on the "Course 1" course page logged in as teacher2 And "Test forum name" should have the "Mark as done" completion condition And "Test assignment name" should have the "View" completion condition And there should be no completion information shown for "Test quiz name" tests/behat/enable_completion_on_pass_grade.feature 0000604 00000005242 15062070516 0016712 0 ustar 00 @core @core_completion @javascript Feature: Students will be marked as completed if they have achieved a passing grade. Background: Given the following "courses" exist: | fullname | shortname | category | enablecompletion | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | Frist | teacher1@example.com | | student1 | Student | First | student1@example.com | | student2 | Student | Second | student2@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | And the following "activity" exists: | idnumber | a1 | | activity | assign | | course | C1 | | name | Test assignment name | | intro | Submit your online text | | assignsubmission_onlinetext_enabled | 1 | | assignsubmission_file_enabled | 0 | | completion | 2 | | completionpassgrade | 1 | | completionusegrade | 1 | | gradepass | 50 | And I am on the "Course 1" course page logged in as teacher1 And "Student First" user has not completed "Test assignment name" activity Scenario: Passing grade completion Given I am on the "Course 1" "grades > Grader report > View" page And I turn editing mode on And I give the grade "21" to the user "Student First" for the grade item "Test assignment name" And I give the grade "50" to the user "Student Second" for the grade item "Test assignment name" And I press "Save changes" When I am on the "Course 1" course page logged in as student1 Then the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "failed" And I am on the "Course 1" course page logged in as student2 And the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "done" tests/behat/restrict_section_availability.feature 0000604 00000011030 15062070516 0016454 0 ustar 00 @core @core_completion Feature: Restrict sections availability through completion or grade conditions In order to control section's contents access through activities completion or grade condition As a teacher I need to restrict sections availability using different conditions Background: Given the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | Frist | teacher1@example.com | | student1 | Student | First | student1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | course | section | name | intro | assignsubmission_onlinetext_enabled | assignsubmission_file_enabled | submissiondrafts | content | | assign | C1 | 1 | Grade assignment | Grade this assignment to revoke restriction on restricted assignment | 1 | 0 | 0 | | | page | C1 | 2 | Test page name | Restricted section page resource, till grades in Grade assignment is at least 20% | | | | Test page contents | @javascript Scenario: Show section greyed-out to student when completion condition is not satisfied Given I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And I navigate to "Settings" in current page administration And I set the following fields to these values: | Enable completion tracking | Yes | And I press "Save and display" And the following "activities" exist: | activity | course | section | intro | completion | idnumber | | label | C1 | 1 | Test label | 1 | 1 | When I edit the section "2" And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" And I set the following fields to these values: | cm | Test label | | Required completion status | must be marked complete | And I press "Save changes" And I am on the "Course 1" course page logged in as "student1" Then I should see "Not available unless: The activity Test label is marked complete" And I should not see "Test page name" And I toggle the manual completion state of "Test label" And I should see "Test page name" And I should not see "Not available unless: The activity Test label is marked complete" @javascript Scenario: Show section greyed-out to student when grade condition is not satisfied Given I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And I edit the section "2" And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Grade" "button" in the "Add restriction..." "dialogue" And I set the following fields to these values: | id | Grade assignment | | min | 1 | | minval | 20 | And I press "Save changes" When I am on the "Course 1" course page logged in as "student1" Then I should see "Not available unless: You achieve higher than a certain score in Grade assignment" And "Test page name" activity should be hidden And I am on the "Grade assignment" "assign activity" page And I press "Add submission" And I set the following fields to these values: | Online text | I'm the student submission | And I press "Save changes" And I should see "Submitted for grading" And I log out And I am on the "Grade assignment" "assign activity" page logged in as teacher1 And I follow "View all submissions" And I click on "Grade" "link" in the "Student First" "table_row" And I set the following fields to these values: | Grade | 21 | And I press "Save changes" And I follow "Edit settings" And I am on the "Course 1" Course page logged in as student1 And "Test page name" activity should be visible And I should not see "Not available unless: You achieve higher than a certain score in Grade assignment" tests/behat/teacher_manual_completion.feature 0000604 00000003743 15062070516 0015554 0 ustar 00 @core @core_completion Feature: Allow teachers to manually mark users as complete when configured In order for teachers to mark students as complete As a teacher I need to be able to use the completion report mark complete functionality Scenario: Mark a student as complete using the completion report Given the following "courses" exist: | fullname | shortname | category | enablecompletion | | Completion course | CC1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | student1 | Student | First | student1@example.com | | teacher1 | Teacher | First | teacher1@example.com | And the following "course enrolments" exist: | user | course | role | | student1 | CC1 | student | | teacher1 | CC1 | editingteacher | And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | completionstatus | Course | CC1 | course-view-* | side-pre | And I am on the "Completion course" course page logged in as admin And I navigate to "Course completion" in current page administration And I set the field "Teacher" to "1" And I press "Save changes" And I am on the "Completion course" course page logged in as student1 And I should see "Status: Not yet started" When I am on the "Completion course" course page logged in as teacher1 And I follow "View course report" And I should see "Student First" And I follow "Click to mark user complete" # Running completion task just after clicking sometimes fail, as record # should be created before the task runs. And I wait "1" seconds And I run the scheduled task "core\task\completion_regular_task" And I am on site homepage Then I am on the "Completion course" course page logged in as student1 And I should see "Status: Complete" tests/behat/restrict_activity_by_grade.feature 0000604 00000005611 15062070516 0015756 0 ustar 00 @core @core_completion Feature: Restrict activity availability through grade conditions In order to control activity access through grade condition As a teacher I need to set grade condition to restrict activity access @javascript Scenario: Show activity greyed-out to students when grade condition is not satisfied Given the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | Frist | teacher1@example.com | | student1 | Student | First | student1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | course | activity | idnumber | name | assignsubmission_onlinetext_enabled | assignsubmission_file_enabled | submissiondrafts | | C1 | assign | Grade assignment | Grade assignment | 1 | 0 | 0 | | C1 | page | Grade page | Test page name | | | | # Adding the page like this because id_availableform_enabled needs to be clicked to trigger the action. And I am on the "Test page name" "page activity editing" page logged in as "teacher1" And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Grade" "button" in the "Add restriction..." "dialogue" And I click on "min" "checkbox" And I set the following fields to these values: | id | Grade assignment | | minval | 20 | And I press "Save and return to course" When I am on the "Course 1" course page logged in as student1 Then I should see "Not available unless: You achieve higher than a certain score in Grade assignment" And I should see "Test page name" And "Test page name" "link" should not exist in the "region-main" "region" And I am on the "Grade assignment" "assign activity" page And I press "Add submission" And I set the following fields to these values: | Online text | I'm the student submission | And I press "Save changes" And I should see "Submitted for grading" And I am on the "Grade assignment" "assign activity" page logged in as teacher1 And I follow "View all submissions" And I click on "Grade" "link" in the "Student First" "table_row" And I set the following fields to these values: | Grade | 21 | And I press "Save changes" And I am on the "Course 1" course page logged in as student1 And "Test page name" activity should be visible And I should not see "Not available unless: You achieve higher than a certain score in Grade assignment" tests/behat/enable_completion_on_view_and_grade.feature 0000604 00000010507 15062070516 0017540 0 ustar 00 @core @core_completion @javascript Feature: Students will be marked as completed and pass/fail if they have viewed an activity and achieved a grade. Background: Given the following "courses" exist: | fullname | shortname | category | enablecompletion | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | Frist | teacher1@example.com | | student1 | Student | First | student1@example.com | | student2 | Student | Second | student2@example.com | | student3 | Student | Third | student3@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | | student3 | C1 | student | And the following "activity" exists: | activity | assign | | course | C1 | | idnumber | a1 | | name | Test assignment name | | assignsubmission_onlinetext_enabled | 1 | | assignsubmission_file_enabled | 0 | | completion | 2 | | completionview | 1 | | completionusegrade | 1 | | gradepass | 50 | | completionpassgrade | 1 | And I am on the "Course 1" course page logged in as teacher1 And "Student First" user has not completed "Test assignment name" activity And I am on the "Test assignment name" "assign activity" page logged in as student2 And I am on the "Test assignment name" "assign activity" page logged in as student1 Scenario: Confirm completion (incomplete/pass/fail) are set correctly Given the following "grade grades" exist: | gradeitem | user | grade | | Test assignment name | student1 | 21.00 | | Test assignment name | student2 | 50.00 | | Test assignment name | student3 | 30.00 | When I am on "Course 1" course homepage Then the "View" completion condition of "Test assignment name" is displayed as "done" And the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "failed" And I am on the "Course 1" course page logged in as student2 And the "View" completion condition of "Test assignment name" is displayed as "done" And the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "done" And I am on the "Course 1" course page logged in as student3 And the "View" completion condition of "Test assignment name" is displayed as "todo" And the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "failed" @javascript Scenario: Keep current view completion condition when the teacher does the action 'Unlock completion settings'. Given the following "grade grades" exist: | gradeitem | user | grade | | Test assignment name | student1 | 21.00 | | Test assignment name | student2 | 50.00 | And I am on the "Test assignment name" "assign activity editing" page logged in as teacher1 And I expand all fieldsets And I press "Unlock completion settings" And I expand all fieldsets And I should see "Completion options unlocked" And I click on "Save and display" "button" When I am on the "Course 1" course page logged in as student1 Then the "View" completion condition of "Test assignment name" is displayed as "done" And I am on the "Course 1" course page logged in as student2 And the "View" completion condition of "Test assignment name" is displayed as "done" tests/behat/custom_completion_display_conditions.feature 0000604 00000011230 15062070516 0020062 0 ustar 00 @core @core_completion Feature: Allow teachers to edit the visibility of completion conditions in a course In order to show students the course completion conditions in a course As a teacher I need to be able to edit completion conditions settings Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | enablecompletion | showcompletionconditions | | Course 1 | C1 | 1 | 1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | course | idnumber | name | completion | completionsubmit | | choice | C1 | c1m | Test choice manual| 1 | 0 | | choice | C1 | c1a | Test choice auto | 2 | 1 | @javascript Scenario: Completion condition displaying for manual and auto completion Given I log in as "teacher1" When I am on "Course 1" course homepage # The manual completion "Mark as done" criteria should displayed in the dropdown in the course homepage. Then "Test choice manual" should have the "Mark as done" completion condition And I follow "Test choice manual" # The manual completion toggle button should be displayed in activity view. And the manual completion button for "Test choice manual" should be disabled # Automatic completion conditions should be displayed on both activity view page and course homepage if show completion conditions is enabled. And I am on "Course 1" course homepage And "Test choice auto" should have the "Make a choice" completion condition And I follow "Test choice auto" And "Test choice auto" should have the "Make a choice" completion condition Scenario: Completion condition displaying setting can be disabled at course level Given I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And I navigate to "Settings" in current page administration When I set the following fields to these values: | Show activity completion conditions | No | And I click on "Save and display" "button" # Automatic completion conditions should not be displayed on the course homepage if show completion conditions is disabled. And there should be no completion information shown for "Test choice auto" # Completion conditions are always shown in the module's view page. And I follow "Test choice auto" Then "Test choice auto" should have the "Make a choice" completion condition # The manual completion toggle button should not be displayed in the course homepage when completion is disabled. And I am on "Course 1" course homepage And the manual completion button for "Test choice manual" should not exist # The manual completion toggle button should always be displayed in the activity view. And I follow "Test choice manual" And the manual completion button for "Test choice manual" should be disabled Scenario Outline: Default showcompletionconditions value in course form on course creation Given I log in as "admin" And I navigate to "Courses > Default settings > Course default settings" in site administration And I set the field "Show activity completion conditions" to "<siteshowcompletion>" And I press "Save changes" When I navigate to "Courses > Add a new course" in site administration Then the field "showcompletionconditions" matches value "<expected>" Examples: | siteshowcompletion | expected | | Yes | Yes | | No | No | Scenario Outline: Default showcompletionconditions displayed when editing a course with disabled completion tracking Given I log in as "admin" And I navigate to "Courses > Default settings > Course default settings" in site administration And I set the field "Show activity completion conditions" to "<siteshowcompletion>" And I press "Save changes" And I am on "Course 1" course homepage with editing mode on And I navigate to "Settings" in current page administration And I set the field "Enable completion tracking" to "No" And I press "Save and display" And I navigate to "Settings" in current page administration Then the field "Show activity completion conditions" matches value "<expected>" Examples: | siteshowcompletion | expected | | Yes | Yes | | No | No | tests/behat/restrict_activity_by_date.feature 0000604 00000005540 15062070516 0015612 0 ustar 00 @core @core_completion Feature: Restrict activity availability through date conditions In order to control activity access through date condition As a teacher I need to set allow access dates to restrict activity access Background: Given the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | Frist | teacher1@example.com | | student1 | Student | First | student1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activity" exists: | activity | assign | | course | C1 | | section | 1 | | name | Test assignment 1 | | intro | This assignment is restricted by date | | assignsubmission_onlinetext_enabled | 1 | | assignsubmission_file_enabled | 0 | And I am on the "Test assignment 1" "assign activity" page logged in as "teacher1" And I navigate to "Settings" in current page administration And I expand all fieldsets @javascript Scenario: Show activity greyed-out to students when available from date is in future Given I click on "Add restriction..." "button" And I click on "Date" "button" in the "Add restriction..." "dialogue" And I set the following fields to these values: | x[day] | 31 | | x[month] | 12 | | x[year] | 2037 | And I press "Save and return to course" And I log out When I am on the "Course 1" course page logged in as student1 Then I should see "Available from 31 December 2037" And "Test assignment 1" "link" should not exist in the "page" "region" @javascript Scenario: Show activity hidden to students when available until date is in past Given I click on "Add restriction..." "button" And I click on "Date" "button" in the "Add restriction..." "dialogue" And I set the following fields to these values: | x[day] | 1 | | x[month] | 2 | | x[year] | 2013 | | Direction | until | # Click eye icon to hide it when not available. And I click on ".availability-item .availability-eye img" "css_element" And I press "Save and return to course" And I log out When I am on the "Course 1" course page logged in as student1 Then I should not see "Test assignment 1" in the "page" "region" tests/behat/behat_completion.php 0000604 00000050204 15062070516 0013015 0 ustar 00 <?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/>. /** * Completion steps definitions. * * @package core_completion * @category test * @copyright 2013 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. require_once(__DIR__ . '/../../../lib/behat/behat_base.php'); use Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException; /** * Steps definitions to deal with course and activities completion. * * @package core_completion * @category test * @copyright 2013 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_completion extends behat_base { /** * Checks that the specified user has completed the specified activity of the current course. * * @Then /^"(?P<user_fullname_string>(?:[^"]|\\")*)" user has completed "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/ * @param string $userfullname * @param string $activityname */ public function user_has_completed_activity($userfullname, $activityname) { // Will throw an exception if the element can not be hovered. $titleliteral = $userfullname . ", " . $activityname . ": Completed"; $xpath = "//table[@id='completion-progress']"; $this->execute("behat_completion::go_to_the_current_course_activity_completion_report"); $this->execute("behat_general::should_exist_in_the", array($titleliteral, "icon", $xpath, "xpath_element") ); } /** * Checks that the specified user has not completed the specified activity of the current course. * * @Then /^"(?P<user_fullname_string>(?:[^"]|\\")*)" user has not completed "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/ * @param string $userfullname * @param string $activityname */ public function user_has_not_completed_activity($userfullname, $activityname) { // Will throw an exception if the element can not be hovered. $titleliteral = $userfullname . ", " . $activityname . ": Not completed"; $xpath = "//table[@id='completion-progress']"; $this->execute("behat_completion::go_to_the_current_course_activity_completion_report"); $this->execute("behat_general::should_exist_in_the", array($titleliteral, "icon", $xpath, "xpath_element") ); } /** * Goes to the current course activity completion report. * * @Given /^I go to the current course activity completion report$/ */ public function go_to_the_current_course_activity_completion_report() { $completionnode = get_string('pluginname', 'report_progress'); $reportsnode = get_string('reports'); $this->execute("behat_navigation::i_navigate_to_in_current_page_administration", $reportsnode); $this->execute("behat_general::i_click_on_in_the", [$completionnode, "link", "region-main", "region"]); } /** * Toggles completion tracking for course being in the course page. * * @When /^completion tracking is "(?P<completion_status_string>Enabled|Disabled)" in current course$/ * @param string $completionstatus The status, enabled or disabled. */ public function completion_is_toggled_in_course($completionstatus) { $toggle = strtolower($completionstatus) == 'enabled' ? get_string('yes') : get_string('no'); // Go to course editing. $this->execute("behat_general::click_link", get_string('settings')); // Expand all the form fields. $this->execute("behat_forms::i_expand_all_fieldsets"); // Enable completion. $this->execute("behat_forms::i_set_the_field_to", array(get_string('enablecompletion', 'completion'), $toggle)); // Save course settings. $this->execute("behat_forms::press_button", get_string('savechangesanddisplay')); } /** * Checks if the activity with specified name is maked as complete. * * @Given /^the "(?P<activityname_string>(?:[^"]|\\")*)" "(?P<activitytype_string>(?:[^"]|\\")*)" activity with "(manual|auto)" completion should be marked as complete$/ */ public function activity_marked_as_complete($activityname, $activitytype, $completiontype) { if ($completiontype == "manual") { $imgalttext = get_string("completion-alt-manual-y", 'core_completion', $activityname); } else { $imgalttext = get_string("completion-alt-auto-y", 'core_completion', $activityname); } $activityxpath = "//li[contains(concat(' ', @class, ' '), ' modtype_" . strtolower($activitytype) . " ')]"; $activityxpath .= "[descendant::*[contains(text(), '" . $activityname . "')]]"; $this->execute("behat_general::should_exist_in_the", array($imgalttext, "icon", $activityxpath, "xpath_element") ); } /** * Checks if the activity with specified name is maked as complete. * * @Given /^the "(?P<activityname_string>(?:[^"]|\\")*)" "(?P<activitytype_string>(?:[^"]|\\")*)" activity with "(manual|auto)" completion should be marked as not complete$/ */ public function activity_marked_as_not_complete($activityname, $activitytype, $completiontype) { if ($completiontype == "manual") { $imgalttext = get_string("completion-alt-manual-n", 'core_completion', $activityname); } else { $imgalttext = get_string("completion-alt-auto-n", 'core_completion', $activityname); } $activityxpath = "//li[contains(concat(' ', @class, ' '), ' modtype_" . strtolower($activitytype) . " ')]"; $activityxpath .= "[descendant::*[contains(text(), '" . $activityname . "')]]"; $this->execute("behat_general::should_exist_in_the", array($imgalttext, "icon", $activityxpath, "xpath_element") ); } /** * Checks if the activity with specified name is maked as complete. * * @When the :conditionname completion condition of :activityname is displayed as :completionstatus * @param string $conditionname The completion condition text. * @param string $activityname The activity name. * @param string $completionstatus The completion status. Must be either of the following: 'todo', 'done', 'failed'. */ public function activity_completion_condition_displayed_as(string $conditionname, string $activityname, string $completionstatus): void { if (!in_array($completionstatus, ['todo', 'done', 'failed'])) { throw new coding_exception('Invalid completion status. It must be of type "todo", "done", or "failed".'); } $text = get_string("completion_automatic:$completionstatus", 'core_course'); $conditionslistlabel = get_string('completionrequirements', 'core_course', $activityname); $selector = "div[aria-label='$conditionslistlabel']"; try { // If there is a dropdown, open it. $dropdownnode = $this->find('css', $selector . ' .dropdown-menu'); if (!$dropdownnode->hasClass('show')) { $params = ["button.dropdown-toggle", "css_element", $selector, "css_element"]; $this->execute("behat_general::i_click_on_in_the", $params); } } catch (ElementNotFoundException $e) { // If the dropdown does not exist, we are in the activity page, all good. } $xpath = "//div[@aria-label='$conditionslistlabel']//span[text()='$conditionname']/.."; $this->execute("behat_general::assert_element_contains_text", [$text, $xpath, "xpath_element"]); } /** * Checks if the activity with specified name is maked as complete. * * @When the :conditionname completion condition of :activityname overridden by :username is displayed as :completionstatus * @param string $conditionname The completion condition text. * @param string $activityname The activity name. * @param string $username The full name of the user overriding the student's activity completion. * @param string $completionstatus The override completion status. Must be either of the following: 'todo', 'done'. */ public function overridden_activity_completion_condition_displayed_as(string $conditionname, string $activityname, string $username, string $completionstatus): void { if (!in_array($completionstatus, ['todo', 'done'])) { throw new coding_exception('Invalid override completion status. It must be of type "todo" or "done".'); } $conditionlabel = get_string('completion_setby:auto:' . $completionstatus, 'core_course', (object)[ 'condition' => $conditionname, 'setby' => $username, ]); $conditionbadge = "div[aria-label='$conditionlabel']"; $conditionslistlabel = get_string('completionrequirements', 'core_course', $activityname); $completionconditions = "div[aria-label='$conditionslistlabel']"; $params = [$conditionbadge, 'css_element', $completionconditions, 'css_element']; $this->execute("behat_general::should_exist_in_the", $params); } /** * Checks the manual completion state of an activity. * * @Given /^the manual completion button of "(?P<activityname>(?:[^"]|\\")*)" is displayed as "(?P<completionstatus>(?:[^"]|\\")*)"$/ * @param string $activityname The activity name. * @param string $completionstatus The completion status shown on the manual completion button. * Must be either 'Mark as done' or 'Done'. */ public function manual_completion_button_displayed_as(string $activityname, string $completionstatus): void { if (!in_array($completionstatus, ['Mark as done', 'Done'])) { throw new coding_exception('Invalid completion status. It must be "Mark as done" or "Done".'); } $langstringkey = $completionstatus === 'Done' ? 'done' : 'markdone'; $conditionslistlabel = get_string('completion_manual:aria:' . $langstringkey, 'core_course', $activityname); $selector = "button[aria-label='$conditionslistlabel']"; $this->execute("behat_general::assert_element_contains_text", [$completionstatus, $selector, "css_element"]); } /** * Checks the manual completion state of an activity. * * @Given /^the manual completion button of "(?P<activityname>(?:[^"]|\\")*)" overridden by "(?P<username>(?:[^"]|\\")*)" is displayed as "(?P<completionstatus>(?:[^"]|\\")*)"$/ * @param string $activityname The activity name. * @param string $username The full name of the user overriding the student's activity completion. * @param string $completionstatus The completion status shown on the manual completion button. * Must be either 'Mark as done' or 'Done'. */ public function overridden_manual_completion_button_displayed_as(string $activityname, string $username, string $completionstatus): void { if (!in_array($completionstatus, ['Mark as done', 'Done'])) { throw new coding_exception('Invalid completion status. It must be "Mark as done" or "Done".'); } $langstringkey = $completionstatus === 'Done' ? 'done' : 'markdone'; $conditionslistlabel = get_string('completion_setby:manual:' . $langstringkey, 'core_course', (object)[ 'activityname' => $activityname, 'setby' => $username, ]); $selector = "button[aria-label='$conditionslistlabel']"; $this->execute("behat_general::assert_element_contains_text", [$completionstatus, $selector, "css_element"]); } /** * Toggles the manual completion button for a given activity. * * @Given /^I toggle the manual completion state of "(?P<activityname>(?:[^"]|\\")*)"$/ * @param string $activityname The activity name. */ public function toggle_the_manual_completion_state(string $activityname): void { $selector = "button[data-action=toggle-manual-completion][data-activityname='{$activityname}']"; $this->execute("behat_general::i_click_on", [$selector, "css_element"]); } /** * Check that the activity does show completion information. * * @Given /^there should be no completion information shown for "(?P<activityname>(?:[^"]|\\")*)"$/ * @param string $activityname The activity name. */ public function there_should_be_no_completion_for_activity(string $activityname): void { $containerselector = "div[data-region=activity-information][data-activityname='$activityname']"; try { $this->find('css_element', $containerselector); } catch (ElementNotFoundException $e) { // If activity information container does not exist (activity dates not shown, completion info not shown), all good. return; } // Otherwise, ensure that the completion information does not exist. $elementselector = "div[data-region=completion-info]"; $params = [$elementselector, "css_element", $containerselector, "css_element"]; $this->execute("behat_general::should_not_exist_in_the", $params); } /** * Check that the manual completion button for the activity is disabled. * * @Given /^the manual completion button for "(?P<activityname>(?:[^"]|\\")*)" should be disabled$/ * @param string $activityname The activity name. */ public function the_manual_completion_button_for_activity_should_be_disabled(string $activityname): void { $selector = "div[data-region='activity-information'][data-activityname='$activityname'] button"; $params = [$selector, "css_element"]; $this->execute("behat_general::the_element_should_be_disabled", $params); } /** * Check that the manual completion button for the activity does not exist. * * @Given /^the manual completion button for "(?P<activityname>(?:[^"]|\\")*)" should not exist/ * @param string $activityname The activity name. */ public function the_manual_completion_button_for_activity_should_not_exist(string $activityname): void { $selector = "div[data-region=activity-information][data-activityname='$activityname'] button"; $params = [$selector, "css_element"]; $this->execute('behat_general::should_not_exist', $params); } /** * Check that the manual completion button for the activity exists. * * @Given /^the manual completion button for "(?P<activityname>(?:[^"]|\\")*)" should exist/ * @param string $activityname The activity name. */ public function the_manual_completion_button_for_activity_should_exist(string $activityname): void { $selector = "div[data-region=activity-information][data-activityname='$activityname'] button"; $params = [$selector, "css_element"]; $this->execute('behat_general::should_exist', $params); } /** * Check that the activity has the given automatic completion condition. * * @When :activityname should have the :conditionname completion condition * @param string $activityname The activity name. * @param string $conditionname The automatic condition name. */ public function activity_should_have_the_completion_condition(string $activityname, string $conditionname): void { $containerselector = "div[data-region=activity-information][data-activityname='$activityname']"; try { // If there is a dropdown, open it. $dropdownnode = $this->find('css', $containerselector . ' .dropdown-menu'); if (!$dropdownnode->hasClass('show')) { $params = ["button.dropdown-toggle", "css_element", $containerselector, "css_element"]; $this->execute("behat_general::i_click_on_in_the", $params); } } catch (ElementNotFoundException $e) { // If the dropdown does not exist, we are in the activity page, all good. } $params = [$conditionname, $containerselector, 'css_element']; $this->execute("behat_general::assert_element_contains_text", $params); } /** * Checks if the activity with specified name shows a information completion checkbox (i.e. showing the completion tracking * configuration). * * @Given /^the "(?P<activityname_string>(?:[^"]|\\")*)" "(?P<activitytype_string>(?:[^"]|\\")*)" activity with "(manual|auto)" completion shows a configuration completion checkbox/ * @param string $activityname The activity name. * @param string $activitytype The activity type. * @param string $completiontype The completion type. */ public function activity_has_configuration_completion_checkbox($activityname, $activitytype, $completiontype) { if ($completiontype == "manual") { $imgname = 'i/completion-manual-enabled'; } else { $imgname = 'i/completion-auto-enabled'; } $iconxpath = "//li[contains(concat(' ', @class, ' '), ' modtype_" . strtolower($activitytype) . " ')]"; $iconxpath .= "[descendant::*[contains(text(), '" . $activityname . "')]]"; $iconxpath .= "/descendant::div[@class='actions']/descendant::img[contains(@src, 'i/completion-')]"; $this->execute("behat_general::the_attribute_of_should_contain", array("src", $iconxpath, "xpath_element", $imgname) ); } /** * Checks if the activity with specified name shows a tracking completion checkbox (i.e. showing my completion tracking status) * * @Given /^the "(?P<activityname_string>(?:[^"]|\\")*)" "(?P<activitytype_string>(?:[^"]|\\")*)" activity with "(manual|auto)" completion shows a status completion checkbox/ * @param string $activityname The activity name. * @param string $activitytype The activity type. * @param string $completiontype The completion type. */ public function activity_has_status_completion_checkbox($activityname, $activitytype, $completiontype) { if ($completiontype == "manual") { $imgname = 'i/completion-manual-'; } else { $imgname = 'i/completion-auto-'; } $iconxpath = "//li[contains(concat(' ', @class, ' '), ' modtype_" . strtolower($activitytype) . " ')]"; $iconxpath .= "[descendant::*[contains(text(), '" . $activityname . "')]]"; $iconxpath .= "/descendant::div[@class='actions']/descendant::img[contains(@src, 'i/completion-')]"; $this->execute("behat_general::the_attribute_of_should_contain", array("src", $iconxpath, "xpath_element", $imgname) ); $this->execute("behat_general::the_attribute_of_should_not_contain", array("src", $iconxpath, "xpath_element", '-enabled') ); } /** * Checks if the activity with specified name does not show any completion checkbox. * * @Given /^the "(?P<activityname_string>(?:[^"]|\\")*)" "(?P<activitytype_string>(?:[^"]|\\")*)" activity does not show any completion checkbox/ * @param string $activityname The activity name. * @param string $activitytype The activity type. */ public function activity_has_not_any_completion_checkbox($activityname, $activitytype) { $iconxpath = "//li[contains(concat(' ', @class, ' '), ' modtype_" . strtolower($activitytype) . " ')]"; $iconxpath .= "[descendant::*[contains(text(), '" . $activityname . "')]]"; $iconxpath .= "/descendant::img[contains(@src, 'i/completion-')]"; $this->execute("behat_general::should_not_exist", array($iconxpath, "xpath_element") ); } } tests/behat/backup_restore_completion.feature 0000604 00000006344 15062070516 0015614 0 ustar 00 @core @core_completion Feature: Backup and restore the activity with the completion Background: Given the following "courses" exist: | fullname | shortname | category | enablecompletion | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | student1 | Student | First | student1@example.com | | student2 | Student | Second | student2@example.com | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | student2 | C1 | student | And the following "activity" exists: | activity | assign | | course | C1 | | idnumber | a1 | | name | Test assignment name | | intro | Submit your online text | | assignsubmission_onlinetext_enabled | 1 | | assignsubmission_file_enabled | 0 | | completion | 2 | | completionview | 1 | | completionusegrade | 1 | | gradepass | 50 | | completionpassgrade | 1 | And I am on the "Test assignment name" "assign activity" page logged in as student1 And I log out @javascript @_file_upload Scenario: Restore the legacy assignment with completion condition. Given I am on the "Course 1" "restore" page logged in as "admin" And I press "Manage backup files" And I upload "completion/tests/fixtures/legacy_course_completion.mbz" file to "Files" filemanager And I press "Save changes" And I restore "legacy_course_completion.mbz" backup into a new course using this options: | Schema | Course name | Course 2 | | Schema | Course short name | C2 | When I am on the "Course 2" course page logged in as student1 Then the "View" completion condition of "Test assignment name" is displayed as "done" And I am on the "Course 2" course page logged in as student2 And the "View" completion condition of "Test assignment name" is displayed as "todo" @javascript @_file_upload Scenario: Backup and restore the assignment with the viewed and not-viewed completion condition Given I am on the "Course 1" course page logged in as admin And I backup "Course 1" course using this options: | Confirmation | Filename | test_backup.mbz | And I restore "test_backup.mbz" backup into a new course using this options: | Schema | Course name | Course 2 | | Schema | Course short name | C2 | When I am on the "Course 2" course page logged in as student1 Then the "View" completion condition of "Test assignment name" is displayed as "done" And I am on the "Course 2" course page logged in as student2 And the "View" completion condition of "Test assignment name" is displayed as "todo" tests/behat/default_activity_completion.feature 0000604 00000030245 15062070516 0016141 0 ustar 00 @core @core_completion @javascript Feature: Allow teachers to edit the default activity completion rules in a course. In order to set the activity completion defaults for new activities As a teacher I need to be able to edit the completion rules for a group of activities. Background: Given the following "courses" exist: | fullname | shortname | category | enablecompletion | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | Frist | teacher1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | # Given I am a teacher in a course with completion tracking enabled and activities present. # When I edit activity completion defaults for activity types. # Then the completion rule defaults should apply only to activities created from that point onwards. Scenario: Edit default activity completion rules for assignment Given the following "activity" exists: | activity | assign | | course | C1 | | name | Test assignment one | | completion | 0 | And I am on the "Course 1" course page logged in as teacher1 When I navigate to "Course completion" in current page administration And I set the field "Course completion tertiary navigation" to "Default activity completion" And I click on "Expand Assignment" "button" And I set the following fields to these values: | Add requirements | 1 | | completionview_assign | 0 | | completionusegrade_assign | 1 | | completionsubmit_assign | 1 | And I should not see "Cancel" in the "[data-region='activitycompletion-forum']" "css_element" And I click on "Save changes" "button" in the "[data-region='activitycompletion-assign']" "css_element" Then I should see "Changes saved" And I am on "Course 1" course homepage with editing mode on And I press "Add an activity or resource" And I click on "Add a new Assignment" "link" in the "Add an activity or resource" "dialogue" And I expand all fieldsets # Completion tracking 2 = Add requirements. And the field "Add requirements" matches value "1" And the field "completionview" matches value "0" And the field "completionusegrade" matches value "1" And the field "completionsubmit" matches value "1" But I am on the "Test assignment one" Activity page And I navigate to "Settings" in current page administration And I expand all fieldsets # Completion tracking 0 = Do not indicate activity completion. And the field "None" matches value "1" Scenario: Edit default activity completion rules for forum Given the following "activity" exists: | activity | forum | | course | C1 | | name | Test forum one | | completion | 0 | And I am on the "Course 1" course page logged in as teacher1 When I navigate to "Course completion" in current page administration And I set the field "Course completion tertiary navigation" to "Default activity completion" And I click on "Expand Forum" "button" And I set the following fields to these values: # 2 = Add requeriments. | id_completion_forum_2 | 1 | | completionview_forum | 0 | # 0 = Rating. | completionusegrade_forum | 1 | | completiongradeitemnumber_forum | 0 | # 1 = Passing grade. | completionpassgrade_forum | 1 | | completionpostsenabled_forum | 1 | | completionposts_forum | 2 | | completionrepliesenabled_forum | 1 | | completionreplies_forum | 3 | And I click on "Save changes" "button" in the "[data-region='activitycompletion-forum']" "css_element" Then I should see "Changes saved" And I am on "Course 1" course homepage with editing mode on And I press "Add an activity or resource" And I click on "Add a new Forum" "link" in the "Add an activity or resource" "dialogue" And I expand all fieldsets # Completion tracking 2 = Add requirements. And the field "Add requirements" matches value "1" And the field "completionview" matches value "0" # Value 0 for completiongradeitemnumber is "Rating". And the field "completiongradeitemnumber" matches value "0" And the field "completionpassgrade" matches value "1" And the field "completionpostsenabled" matches value "1" And the field "completionposts" matches value "2" And the field "completionrepliesenabled" matches value "1" And the field "completionreplies" matches value "3" But I am on the "Test forum one" Activity page And I navigate to "Settings" in current page administration And I expand all fieldsets # None checked = Do not indicate activity completion. And the field "None" matches value "1" Scenario: Edit default activity completion rules for glossary Given the following "activity" exists: | activity | glossary | | course | C1 | | name | Test glossary one | | completion | 0 | And I am on the "Course 1" course page logged in as teacher1 When I navigate to "Course completion" in current page administration And I set the field "Course completion tertiary navigation" to "Default activity completion" And I click on "Expand Glossary" "button" And I set the following fields to these values: # Add requirements = 2. | id_completion_glossary_2 | 1 | | completionview_glossary | 0 | | completionusegrade_glossary | 1 | | completionentriesenabled_glossary | 1 | | completionentries_glossary | 2 | And I click on "Save changes" "button" in the "[data-region='activitycompletion-glossary']" "css_element" Then I should see "Changes saved" And I am on "Course 1" course homepage with editing mode on And I press "Add an activity or resource" And I click on "Add a new Glossary" "link" in the "Add an activity or resource" "dialogue" And I expand all fieldsets # Completion tracking 2 = Add requirements. And the field "Add requirements" matches value "1" And the field "completionview" matches value "0" And the field "completionusegrade" matches value "1" And the field "completionentriesenabled" matches value "1" And the field "completionentries" matches value "2" But I am on the "Test glossary one" Activity page And I navigate to "Settings" in current page administration And I expand all fieldsets # Completion tracking 0 = Do not indicate activity completion. And the field "None" matches value "1" Scenario: Edit default activity completion rules for several activities Given I am on the "Course 1" course page logged in as teacher1 When I navigate to "Course completion" in current page administration And I set the field "Course completion tertiary navigation" to "Default activity completion" And I click on "Expand Assignment" "button" And I set the following fields to these values: | id_completion_assign_2 | 1 | | completionview_assign | 0 | | completionusegrade_assign | 0 | | completionsubmit_assign | 1 | And I click on "Save changes" "button" in the "[data-region='activitycompletion-assign']" "css_element" And I should see "Changes saved" And I click on "Expand Forum" "button" And I set the following fields to these values: | id_completion_forum_2 | 1 | | completionview_forum | 0 | | completionpostsenabled_forum | 1 | | completionposts_forum | 3 | | completiondiscussionsenabled_forum | 0 | | completionrepliesenabled_forum | 0 | And I click on "Save changes" "button" in the "[data-region='activitycompletion-forum']" "css_element" And I should see "Changes saved" And I click on "Expand SCORM package" "button" And I set the following fields to these values: | id_completion_scorm_2 | 1 | | completionview_scorm | 0 | | completionscoredisabled | 1 | | completionscorerequired_scorm | 3 | | completionstatusrequired_scorm[2] | 1 | | completionstatusrequired_scorm[4] | 0 | | completionstatusallscos_scorm | 1 | And I click on "Save changes" "button" in the "[data-region='activitycompletion-scorm']" "css_element" And I should see "Changes saved" And I click on "Expand Book" "button" And I set the following fields to these values: | completion_book | Do not indicate activity completion | And I click on "Save changes" "button" in the "[data-region='activitycompletion-book']" "css_element" And I should see "Changes saved" And I click on "Expand Chat" "button" And I set the following fields to these values: # Students must manually mark the activity as done = 1. | id_completion_chat_1 | 1 | And I click on "Save changes" "button" in the "[data-region='activitycompletion-chat']" "css_element" And I should see "Changes saved" # Change current page and go back to "Default activity completion", to confirm the form values have been saved properly. And I set the field "Course completion tertiary navigation" to "Course completion settings" And I set the field "Course completion tertiary navigation" to "Default activity completion" Then the field "id_completion_chat_1" matches value "1" # Check that the rules for book, assignment and forum are still the same. And I click on "Expand Book" "button" And the field "id_completion_book_0" matches value "1" And I click on "Expand Assignment" "button" And the field "id_completion_assign_2" matches value "1" And the field "completionview_assign" matches value "0" And the field "completionusegrade_assign" matches value "0" And the field "completionsubmit_assign" matches value "1" And the field "id_completion_forum_2" matches value "1" And the field "completionview_forum" matches value "0" And the field "completionpostsenabled_forum" matches value "1" And the field "completionposts_forum" matches value "3" And the field "completiondiscussionsenabled_forum" matches value "0" And the field "completionrepliesenabled_forum" matches value "0" And the field "id_completion_scorm_2" matches value "1" And the field "completionview_scorm" matches value "0" And the field "completionscorerequired_scorm" matches value "3" And the field "completionstatusrequired_scorm[2]" matches value "1" And the field "completionstatusrequired_scorm[4]" matches value "0" And the field "completionstatusallscos_scorm" matches value "1" Scenario: Edit default activity completion without rules for automatic completion Given I am on the "Course 1" course page logged in as teacher1 When I navigate to "Course completion" in current page administration And I set the field "Course completion tertiary navigation" to "Default activity completion" And I click on "Expand Assignment" "button" And I set the following fields to these values: | id_completion_assign_2 | 1 | | completionview_assign | 0 | | completionusegrade_assign | 0 | | completionsubmit_assign | 0 | And I click on "Save changes" "button" in the "[data-region='activitycompletion-assign']" "css_element" Then I should see "You must select at least one condition" And I should not see "Changes saved" Scenario: Activities in Default activity completion are ordered alphabetically Given I am on the "Course 1" course page logged in as teacher1 When I navigate to "Course completion" in current page administration And I set the field "Course completion tertiary navigation" to "Default activity completion" Then "Survey" "text" should appear before "Text and media area" "text" tests/behat/enable_manual_complete_mark.feature 0000604 00000003456 15062070516 0016041 0 ustar 00 @core @core_completion Feature: Allow students to manually mark an activity as complete In order to let students decide when an activity is completed As a teacher I need to allow students to mark activities as completed @javascript Scenario: Mark an activity as completed Given the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | Frist | teacher1@example.com | | student1 | Student | First | student1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activity" exists: | activity | forum | | course | C1 | | name | Test forum name | And I am on the "Course 1" course page logged in as teacher1 And I navigate to "Settings" in current page administration And I set the following fields to these values: | Enable completion tracking | Yes | And I press "Save and display" And I am on the "Test forum name" "forum activity editing" page And I set the following fields to these values: | Students must manually mark the activity as done | 1 | And I press "Save and return to course" And "Student First" user has not completed "Test forum name" activity And I am on the "Course 1" course page logged in as student1 When I toggle the manual completion state of "Test forum name" Then the manual completion button of "Test forum name" is displayed as "Done" And I am on the "Course 1" course page logged in as teacher1 And "Student First" user has completed "Test forum name" activity tests/behat/course_completion_activity_criteria.feature 0000604 00000023073 15062070516 0017700 0 ustar 00 @block @block_completionstatus @core_completion @javascript Feature: Course completion state should match completion criteria In order to understand the configuration or status of an course's completion As a user I need to see the appropriate completion information on course and dashboard pages Background: Given the following "users" exist: | username | firstname | lastname | email | idnumber | | teacher1 | Teacher | 1 | teacher1@example.com | T1 | | teacher2 | Teacher | 2 | teacher1@example.com | T2 | | student1 | Student | 1 | student1@example.com | S1 | And the following "courses" exist: | fullname | shortname | category | enablecompletion | showcompletionconditions | | Course 1 | C1 | 0 | 1 | 1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher2 | C1 | teacher | | student1 | C1 | student | And the following "activity" exists: | activity | assign | | course | C1 | | name | Test assignment name | | completion | 1 | | assignsubmission_onlinetext_enabled | 1 | | grade[modgrade_type] | Point | | grade[modgrade_point] | 100 | And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | completionstatus | Course | C1 | course-view-* | side-pre | And I am on the "Course 1" course page logged in as teacher1 And I navigate to "Course completion" in current page administration And I click on "Condition: Activity completion" "link" And I set the field "Assignment - Test assignment name" to "1" And I press "Save changes" And I am on the "Test assignment name" "assign activity editing" page And I set the following fields to these values: | Add requirements | 1 | | Receive a grade | 1 | | Passing grade | 1 | | gradepass | 70 | And I press "Save and return to course" Scenario: Completion status show match completion criteria when passgrage condition is set. Given I am on the "Course 1" course page logged in as "student1" And the "Receive a grade" completion condition of "Test assignment name" is displayed as "todo" And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "todo" And I should see "Status: Not yet started" in the "Course completion status" "block" And I am on the "Test assignment name" "assign activity" page And I press "Add submission" And I set the following fields to these values: | Online text | I'm the student1 submission | And I press "Save changes" And I press "Submit assignment" And I press "Continue" And I am on the "Test assignment name" "assign activity" page logged in as teacher1 And I follow "View all submissions" And I click on "Grade" "link" in the "Student 1" "table_row" And I set the following fields to these values: | Grade out of 100 | 50.0 | And I press "Save changes" And I am on the "Course 1" course page And I navigate to "Reports" in current page administration And I click on "Activity completion" "link" And "Student 1, Test assignment name: Completed (did not achieve pass grade)" "icon" should exist in the "Student 1" "table_row" And I navigate to "Reports" in current page administration And I click on "Course completion" "link" in the "region-main" "region" And "Student 1, Test assignment name: Completed (did not achieve pass grade)" "icon" should exist in the "Student 1" "table_row" And "Student 1, Course complete: Not completed" "icon" should exist in the "Student 1" "table_row" When I am on the "Course 1" course page logged in as "student1" And I should see "Status: Pending" in the "Course completion status" "block" And the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "failed" And I am on the "My courses" page And I should not see "100%" in the "Course overview" "block" And I am on the "Test assignment name" "assign activity" page logged in as teacher1 And I follow "View all submissions" And I click on "Grade" "link" in the "Student 1" "table_row" And I set the following fields to these values: | Grade out of 100 | 75.0 | And I press "Save changes" And I am on the "Course 1" course page And I navigate to "Reports" in current page administration And I click on "Activity completion" "link" And "Student 1, Test assignment name: Completed (achieved pass grade)" "icon" should exist in the "Student 1" "table_row" And I navigate to "Reports" in current page administration And I click on "Course completion" "link" in the "region-main" "region" And "Student 1, Test assignment name: Completed (achieved pass grade)" "icon" should exist in the "Student 1" "table_row" And "Student 1, Course complete: Completed" "icon" should exist in the "Student 1" "table_row" And I am on the "Course 1" course page logged in as "student1" And I should see "Status: Complete" in the "Course completion status" "block" And the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "done" And I am on the "My courses" page Then I should see "100%" in the "Course overview" "block" Scenario: Completion status show match completion criteria when passgrage condition is not set. Given I am on the "Test assignment name" "assign activity editing" page logged in as teacher1 And I set the following fields to these values: | Add requirements | 1 | | completionusegrade | 1 | | completionpassgrade | 0 | | gradepass | 70 | And I press "Save and return to course" And I am on the "Course 1" course page logged in as "student1" And the "Receive a grade" completion condition of "Test assignment name" is displayed as "todo" And I should see "Status: Not yet started" in the "Course completion status" "block" And I am on the "Test assignment name" "assign activity" page And I press "Add submission" And I set the following fields to these values: | Online text | I'm the student1 submission | And I press "Save changes" And I press "Submit assignment" And I press "Continue" And I am on the "Test assignment name" "assign activity" page logged in as teacher1 And I follow "View all submissions" And I click on "Grade" "link" in the "Student 1" "table_row" And I set the following fields to these values: | Grade out of 100 | 50.0 | And I press "Save changes" And I am on the "Course 1" course page And I navigate to "Reports" in current page administration And I click on "Activity completion" "link" And "Student 1, Test assignment name: Completed (did not achieve pass grade)" "icon" should exist in the "Student 1" "table_row" And I navigate to "Reports" in current page administration And I click on "Course completion" "link" in the "region-main" "region" And "Student 1, Test assignment name: Completed (did not achieve pass grade)" "icon" should exist in the "Student 1" "table_row" And "Student 1, Course complete: Completed" "icon" should exist in the "Student 1" "table_row" When I am on the "Course 1" course page logged in as "student1" And I should see "Status: Complete" in the "Course completion status" "block" # Once MDL-75582 is fixed "failed" should be changed to "done" And the "Receive a grade" completion condition of "Test assignment name" is displayed as "failed" And I am on the "My courses" page And I should see "100%" in the "Course overview" "block" And I am on the "Test assignment name" "assign activity" page logged in as teacher1 And I follow "View all submissions" And I click on "Grade" "link" in the "Student 1" "table_row" And I set the following fields to these values: | Grade out of 100 | 75.0 | And I press "Save changes" And I am on the "Course 1" course page And I navigate to "Reports" in current page administration And I click on "Activity completion" "link" And "Student 1, Test assignment name: Completed (achieved pass grade)" "icon" should exist in the "Student 1" "table_row" And I navigate to "Reports" in current page administration And I click on "Course completion" "link" in the "region-main" "region" And "Student 1, Test assignment name: Completed (achieved pass grade)" "icon" should exist in the "Student 1" "table_row" And "Student 1, Course complete: Completed" "icon" should exist in the "Student 1" "table_row" And I am on the "Course 1" course page logged in as "student1" And I should see "Status: Complete" in the "Course completion status" "block" And the "Receive a grade" completion condition of "Test assignment name" is displayed as "done" And I am on the "My courses" page Then I should see "100%" in the "Course overview" "block" tests/capabilities_test.php 0000604 00000004123 15062070516 0012105 0 ustar 00 <?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/>. namespace core_completion; /** * Tests that completion works without requiring unnecessary capabilities. * * @package core_completion * @copyright 2018 University of Nottingham * @author Neill Magill <neill.magill@nottingham.ac.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class capabilities_test extends \advanced_testcase { /** * A user who does not have capabilities to add events to the calendar should be able to create activities. */ public function test_creation_with_no_calendar_capabilities() { $this->resetAfterTest(); $course = self::getDataGenerator()->create_course(['enablecompletion' => 1]); $context = \context_course::instance($course->id); $user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher'); $roleid = self::getDataGenerator()->create_role(); self::getDataGenerator()->role_assign($roleid, $user->id, $context->id); assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true); $generator = self::getDataGenerator()->get_plugin_generator('mod_forum'); // Create an instance as a user without the calendar capabilities. $this->setUser($user); $params = array( 'course' => $course->id, 'completionexpected' => time() + 2000, ); $generator->create_instance($params); } } tests/cm_completion_details_test.php 0000604 00000046771 15062070516 0014030 0 ustar 00 <?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/>. /** * Contains unit tests for core_completion/cm_completion_details. * * @package core_completion * @copyright 2021 Jun Pataleta <jun@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ declare(strict_types = 1); namespace core_completion; use advanced_testcase; use cm_info; use completion_info; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->libdir . '/completionlib.php'); /** * Class for unit testing core_completion/cm_completion_details. * * @package core_completion * @copyright 2021 Jun Pataleta <jun@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \core_completion\cm_completion_details */ class cm_completion_details_test extends advanced_testcase { /** @var completion_info A completion object. */ protected $completioninfo = null; /** * Fetches a mocked cm_completion_details instance. * * @param int|null $completion The completion tracking mode for the module. * @param array $completionoptions Completion options (e.g. completionview, completionusegrade, etc.) * @param object $mockcompletiondata Mock data to be returned by get_data. * @param string $modname The modname to set in the cm if a specific one is required. * @return cm_completion_details */ protected function setup_data(?int $completion, array $completionoptions = [], object $mockcompletiondata = null, $modname = 'somenonexistentmod'): cm_completion_details { if (is_null($completion)) { $completion = COMPLETION_TRACKING_AUTOMATIC; } // Mock a completion_info instance so we can simply mock the returns of completion_info::get_data() later. $this->completioninfo = $this->getMockBuilder(completion_info::class) ->disableOriginalConstructor() ->getMock(); // Mock return of completion_info's is_enabled() method to match the expected completion tracking for the module. $this->completioninfo->expects($this->any()) ->method('is_enabled') ->willReturn($completion); if (!empty($mockcompletiondata)) { $this->completioninfo->expects($this->any()) ->method('get_data') ->willReturn($mockcompletiondata); } // Build a mock cm_info instance. $mockcminfo = $this->getMockBuilder(cm_info::class) ->disableOriginalConstructor() ->onlyMethods(['__get']) ->getMock(); // Mock the return of the magic getter method when fetching the cm_info object's customdata and instance values. $mockcminfo->expects($this->any()) ->method('__get') ->will($this->returnValueMap([ ['completion', $completion], ['instance', 1], ['modname', $modname], ['completionview', $completionoptions['completionview'] ?? COMPLETION_VIEW_NOT_REQUIRED], ['completiongradeitemnumber', $completionoptions['completionusegrade'] ?? null], ['completionpassgrade', $completionoptions['completionpassgrade'] ?? null], ])); return new cm_completion_details($this->completioninfo, $mockcminfo, 2); } /** * Provides data for test_has_completion(). * * @return array[] */ public function has_completion_provider(): array { return [ 'Automatic' => [ COMPLETION_TRACKING_AUTOMATIC, true ], 'Manual' => [ COMPLETION_TRACKING_MANUAL, true ], 'None' => [ COMPLETION_TRACKING_NONE, false ], ]; } /** * Test for has_completion(). * * @covers ::has_completion * @dataProvider has_completion_provider * @param int $completion The completion tracking mode. * @param bool $expectedresult Expected result. */ public function test_has_completion(int $completion, bool $expectedresult) { $cmcompletion = $this->setup_data($completion); $this->assertEquals($expectedresult, $cmcompletion->has_completion()); } /** * Provides data for test_is_automatic(). * * @return array[] */ public function is_automatic_provider(): array { return [ 'Automatic' => [ COMPLETION_TRACKING_AUTOMATIC, true ], 'Manual' => [ COMPLETION_TRACKING_MANUAL, false ], 'None' => [ COMPLETION_TRACKING_NONE, false ], ]; } /** * Test for is_available(). * * @covers ::is_automatic * @dataProvider is_automatic_provider * @param int $completion The completion tracking mode. * @param bool $expectedresult Expected result. */ public function test_is_automatic(int $completion, bool $expectedresult) { $cmcompletion = $this->setup_data($completion); $this->assertEquals($expectedresult, $cmcompletion->is_automatic()); } /** * Provides data for test_is_manual(). * * @return array[] */ public function is_manual_provider(): array { return [ 'Automatic' => [ COMPLETION_TRACKING_AUTOMATIC, false ], 'Manual' => [ COMPLETION_TRACKING_MANUAL, true ], 'None' => [ COMPLETION_TRACKING_NONE, false ], ]; } /** * Test for is_manual(). * * @covers ::is_manual * @dataProvider is_manual_provider * @param int $completion The completion tracking mode. * @param bool $expectedresult Expected result. */ public function test_is_manual(int $completion, bool $expectedresult) { $cmcompletion = $this->setup_data($completion); $this->assertEquals($expectedresult, $cmcompletion->is_manual()); } /** * Data provider for test_get_overall_completion(). * @return array[] */ public function overall_completion_provider(): array { return [ 'Complete' => [COMPLETION_COMPLETE], 'Incomplete' => [COMPLETION_INCOMPLETE], ]; } /** * Test for get_overall_completion(). * * @covers ::get_overall_completion * @dataProvider overall_completion_provider * @param int $state */ public function test_get_overall_completion(int $state) { $completiondata = (object)['completionstate' => $state]; $cmcompletion = $this->setup_data(COMPLETION_TRACKING_AUTOMATIC, [], $completiondata); $this->assertEquals($state, $cmcompletion->get_overall_completion()); } /** * Data provider for test_get_details(). * @return array[] */ public function get_details_provider() { return [ 'No completion tracking' => [ COMPLETION_TRACKING_NONE, null, null, null, [] ], 'Manual completion tracking' => [ COMPLETION_TRACKING_MANUAL, null, null, null, [] ], 'Automatic, require view, not viewed' => [ COMPLETION_TRACKING_AUTOMATIC, COMPLETION_INCOMPLETE, null, null, [ 'completionview' => (object)[ 'status' => COMPLETION_INCOMPLETE, 'description' => get_string('detail_desc:view', 'completion'), ] ] ], 'Automatic, require view, viewed' => [ COMPLETION_TRACKING_AUTOMATIC, COMPLETION_COMPLETE, null, null, [ 'completionview' => (object)[ 'status' => COMPLETION_COMPLETE, 'description' => get_string('detail_desc:view', 'completion'), ] ] ], 'Automatic, require grade, incomplete' => [ COMPLETION_TRACKING_AUTOMATIC, null, COMPLETION_INCOMPLETE, null, [ 'completionusegrade' => (object)[ 'status' => COMPLETION_INCOMPLETE, 'description' => get_string('detail_desc:receivegrade', 'completion'), ] ] ], 'Automatic, require grade, complete' => [ COMPLETION_TRACKING_AUTOMATIC, null, COMPLETION_COMPLETE, null, [ 'completionusegrade' => (object)[ 'status' => COMPLETION_COMPLETE, 'description' => get_string('detail_desc:receivegrade', 'completion'), ] ] ], 'Automatic, require view (complete) and grade (incomplete)' => [ COMPLETION_TRACKING_AUTOMATIC, COMPLETION_COMPLETE, COMPLETION_INCOMPLETE, null, [ 'completionview' => (object)[ 'status' => COMPLETION_COMPLETE, 'description' => get_string('detail_desc:view', 'completion'), ], 'completionusegrade' => (object)[ 'status' => COMPLETION_INCOMPLETE, 'description' => get_string('detail_desc:receivegrade', 'completion'), ] ] ], 'Automatic, require view (incomplete) and grade (complete)' => [ COMPLETION_TRACKING_AUTOMATIC, COMPLETION_INCOMPLETE, COMPLETION_COMPLETE, null, [ 'completionview' => (object)[ 'status' => COMPLETION_INCOMPLETE, 'description' => get_string('detail_desc:view', 'completion'), ], 'completionusegrade' => (object)[ 'status' => COMPLETION_COMPLETE, 'description' => get_string('detail_desc:receivegrade', 'completion'), ] ] ], 'Automatic, require grade, require pass grade, complete' => [ COMPLETION_TRACKING_AUTOMATIC, null, COMPLETION_COMPLETE, COMPLETION_COMPLETE, [ 'completionusegrade' => (object)[ 'status' => COMPLETION_COMPLETE, 'description' => get_string('detail_desc:receivegrade', 'completion'), ], 'completionpassgrade' => (object)[ 'status' => COMPLETION_COMPLETE, 'description' => get_string('detail_desc:receivepassgrade', 'completion'), ], ] ], 'Automatic, require grade, require pass grade, incomplete' => [ COMPLETION_TRACKING_AUTOMATIC, null, COMPLETION_COMPLETE, COMPLETION_INCOMPLETE, [ 'completionusegrade' => (object)[ 'status' => COMPLETION_COMPLETE, 'description' => get_string('detail_desc:receivegrade', 'completion'), ], 'completionpassgrade' => (object)[ 'status' => COMPLETION_INCOMPLETE, 'description' => get_string('detail_desc:receivepassgrade', 'completion'), ], ] ], 'Automatic, require view (complete), require grade(complete), require pass grade(complete)' => [ COMPLETION_TRACKING_AUTOMATIC, COMPLETION_COMPLETE, COMPLETION_COMPLETE, COMPLETION_COMPLETE, [ 'completionview' => (object)[ 'status' => COMPLETION_COMPLETE, 'description' => get_string('detail_desc:view', 'completion'), ], 'completionusegrade' => (object)[ 'status' => COMPLETION_COMPLETE, 'description' => get_string('detail_desc:receivegrade', 'completion'), ], 'completionpassgrade' => (object)[ 'status' => COMPLETION_COMPLETE, 'description' => get_string('detail_desc:receivepassgrade', 'completion'), ], ] ], 'Automatic, require view (incomplete), require grade(complete), require pass grade(complete)' => [ COMPLETION_TRACKING_AUTOMATIC, COMPLETION_INCOMPLETE, COMPLETION_COMPLETE, COMPLETION_COMPLETE, [ 'completionview' => (object)[ 'status' => COMPLETION_INCOMPLETE, 'description' => get_string('detail_desc:view', 'completion'), ], 'completionusegrade' => (object)[ 'status' => COMPLETION_COMPLETE, 'description' => get_string('detail_desc:receivegrade', 'completion'), ], 'completionpassgrade' => (object)[ 'status' => COMPLETION_COMPLETE, 'description' => get_string('detail_desc:receivepassgrade', 'completion'), ], ] ], ]; } /** * Test for \core_completion\cm_completion_details::get_details(). * * @covers ::get_details * @dataProvider get_details_provider * @param int $completion The completion tracking mode. * @param int|null $completionview Completion status of the "view" completion condition. * @param int|null $completiongrade Completion status of the "must receive grade" completion condition. * @param int|null $completionpassgrade Completion status of the "must receive passing grade" completion condition. * @param array $expecteddetails Expected completion details returned by get_details(). */ public function test_get_details(int $completion, ?int $completionview, ?int $completiongrade, ?int $completionpassgrade, array $expecteddetails) { $options = []; $getdatareturn = (object)[ 'viewed' => $completionview, 'completiongrade' => $completiongrade, 'passgrade' => $completionpassgrade, ]; if (!is_null($completionview)) { $options['completionview'] = true; } if (!is_null($completiongrade)) { $options['completionusegrade'] = true; } if (!is_null($completionpassgrade)) { $options['completionpassgrade'] = true; } $cmcompletion = $this->setup_data($completion, $options, $getdatareturn); $this->assertEquals($expecteddetails, $cmcompletion->get_details()); } /** * Data provider for test_get_details_custom_order(). * @return array[] */ public function get_details_custom_order_provider() { return [ 'Custom and view/grade standard conditions, view first and grade last' => [ true, true, [ 'completionsubmit' => true, ], 'assign', ['completionview', 'completionsubmit', 'completionusegrade'], ], 'Custom and view/grade standard conditions, grade not last' => [ true, true, [ 'completionminattempts' => 2, 'completionusegrade' => 50, 'completionpassorattemptsexhausted' => 1, ], 'quiz', ['completionview', 'completionminattempts', 'completionusegrade', 'completionpassorattemptsexhausted'], ], 'Custom and grade standard conditions only, no view condition' => [ false, true, [ 'completionsubmit' => true, ], 'assign', ['completionsubmit', 'completionusegrade'], ], 'Custom and view standard conditions only, no grade condition' => [ true, false, [ 'completionsubmit' => true ], 'assign', ['completionview', 'completionsubmit'], ], 'View and grade conditions only, activity with no custom conditions' => [ true, true, [ 'completionview' => true, 'completionusegrade' => true ], 'workshop', ['completionview', 'completionusegrade'], ], 'View condition only, activity with no custom conditions' => [ true, false, [ 'completionview' => true, ], 'workshop', ['completionview'], ], ]; } /** * Test custom sort order is functioning in \core_completion\cm_completion_details::get_details(). * * @covers ::get_details * @dataProvider get_details_custom_order_provider * @param bool $completionview Completion status of the "view" completion condition. * @param bool $completiongrade Completion status of the "must receive grade" completion condition. * @param array $customcompletionrules Custom completion requirements, along with their values. * @param string $modname The name of the module having data fetched. * @param array $expectedorder The expected order of completion conditions returned about the module. */ public function test_get_details_custom_order(bool $completionview, bool $completiongrade, array $customcompletionrules, string $modname, array $expectedorder) { $options['customcompletion'] = []; $customcompletiondata = []; if ($completionview) { $options['completionview'] = true; } if ($completiongrade) { $options['completionusegrade'] = true; } // Set up the completion rules for the completion info. foreach ($customcompletionrules as $customtype => $isenabled) { $customcompletiondata[$customtype] = COMPLETION_COMPLETE; } $getdatareturn = (object)[ 'viewed' => $completionview ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE, 'completiongrade' => $completiongrade ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE, 'customcompletion' => $customcompletiondata, ]; $cmcompletion = $this->setup_data(COMPLETION_TRACKING_AUTOMATIC, $options, $getdatareturn, $modname); $this->completioninfo->expects($this->any()) ->method('get_data') ->willReturn($getdatareturn); $fetcheddetails = $cmcompletion->get_details(); // Check the expected number of items are returned, and sorted in the correct order. $this->assertCount(count($expectedorder), $fetcheddetails); $this->assertTrue((array_keys($fetcheddetails) === $expectedorder)); } } tests/bulk_update_test.php 0000604 00000032422 15062070516 0011756 0 ustar 00 <?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/>. namespace core_completion; use core_completion_bulkedit_form; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->libdir . '/completionlib.php'); /** * External completion functions unit tests * * @package core_completion * @copyright 2017 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class bulk_update_test extends \advanced_testcase { /** * Provider for test_bulk_form_submit_single * @return array */ public function bulk_form_submit_single_provider() { return [ 'assign-1' => ['assign', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionsubmit' => 1]], 'assign-2' => ['assign', ['completion' => COMPLETION_TRACKING_MANUAL]], 'book-1' => ['book', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1]], 'book-2' => ['book', ['completion' => COMPLETION_TRACKING_MANUAL]], 'chat-1' => ['chat', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1]], 'chat-2' => ['chat', ['completion' => COMPLETION_TRACKING_MANUAL]], 'choice-1' => ['choice', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionsubmit' => 1]], 'choice-2' => ['choice', ['completion' => COMPLETION_TRACKING_MANUAL]], 'data-1' => ['data', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1]], 'data-2' => ['data', ['completion' => COMPLETION_TRACKING_MANUAL]], 'data-3' => ['data', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1, 'completionentries' => 3, 'completionentriesenabled' => 1], ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1, 'completionentries' => 3]], 'feedback-1' => ['feedback', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 0, 'completionsubmit' => 1]], 'feedback-2' => ['feedback', ['completion' => COMPLETION_TRACKING_MANUAL]], 'folder-1' => ['folder', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1]], 'folder-2' => ['folder', ['completion' => COMPLETION_TRACKING_MANUAL]], 'forum-1' => ['forum', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completiondiscussions' => 1, 'completiondiscussionsenabled' => 1], ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completiondiscussions' => 1]], 'forum-2' => ['forum', ['completion' => COMPLETION_TRACKING_MANUAL]], 'glossary-1' => ['glossary', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1, 'completionentries' => 3, 'completionentriesenabled' => 1], ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1, 'completionentries' => 3]], 'glossary-2' => ['glossary', ['completion' => COMPLETION_TRACKING_MANUAL]], 'imscp-1' => ['imscp', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1]], 'imscp-2' => ['imscp', ['completion' => COMPLETION_TRACKING_MANUAL]], 'label-1' => ['label', ['completion' => COMPLETION_TRACKING_MANUAL]], 'lesson-1' => ['lesson', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionendreached' => 1]], 'lesson-2' => ['lesson', ['completion' => COMPLETION_TRACKING_MANUAL]], 'lti-1' => ['lti', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1]], 'lti-2' => ['lti', ['completion' => COMPLETION_TRACKING_MANUAL]], 'page-1' => ['page', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1]], 'page-2' => ['page', ['completion' => COMPLETION_TRACKING_MANUAL]], 'quiz-1' => ['quiz', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionpassgrade' => 1]], 'quiz-2' => ['quiz', ['completion' => COMPLETION_TRACKING_MANUAL]], 'resource-1' => ['resource', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1]], 'resource-2' => ['resource', ['completion' => COMPLETION_TRACKING_MANUAL]], 'scorm-1' => ['scorm', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionscorerequired' => 1, 'completionstatusrequired' => [2 => 'passed']], ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionscorerequired' => 1, 'completionstatusrequired' => 2]], 'scorm-2' => ['scorm', ['completion' => COMPLETION_TRACKING_MANUAL]], 'survey-1' => ['survey', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionsubmit' => 1]], 'survey-2' => ['survey', ['completion' => COMPLETION_TRACKING_MANUAL]], 'url-1' => ['url', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1]], 'url-2' => ['url', ['completion' => COMPLETION_TRACKING_MANUAL]], 'wiki-1' => ['wiki', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1]], 'wiki-2' => ['wiki', ['completion' => COMPLETION_TRACKING_MANUAL]], 'workshop-1' => ['workshop', ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1]], 'workshop-2' => ['workshop', ['completion' => COMPLETION_TRACKING_MANUAL]], ]; } /** * Creates an instance of bulk edit completion form for one activity, validates and saves it * * @dataProvider bulk_form_submit_single_provider * @param string $modname * @param array $submitdata data to use in mock form submit * @param array|null $validatedata data to validate the */ public function test_bulk_form_submit_single($modname, $submitdata, $validatedata = null) { global $DB; if ($validatedata === null) { $validatedata = $submitdata; } $this->resetAfterTest(); $this->setAdminUser(); list($course, $cms) = $this->create_course_and_modules([$modname]); // Submit the bulk completion form with the provided data and make sure it returns the same data. core_completion_bulkedit_form::mock_submit(['id' => $course->id, 'cmid' => array_keys($cms)] + $submitdata, []); $form = new core_completion_bulkedit_form(null, ['cms' => $cms]); $this->assertTrue($form->is_validated()); $data = $form->get_data(); foreach ($validatedata as $key => $value) { $this->assertEquals($value, $data->$key); } // Apply completion rules to the modules. $manager = new manager($course->id); $manager->apply_completion($data, $form->has_custom_completion_rules()); // Make sure either course_modules or instance table was respectfully updated. $cm = reset($cms); $cmrec = $DB->get_record('course_modules', ['id' => $cm->id]); $instancerec = $DB->get_record($modname, ['id' => $cm->instance]); foreach ($validatedata as $key => $value) { if (property_exists($cmrec, $key)) { $this->assertEquals($value, $cmrec->$key); } else { $this->assertEquals($value, $instancerec->$key); } } } /** * Creates a course and the number of modules * @param array $modulenames * @return array array of two elements - course and list of cm_info objects */ protected function create_course_and_modules($modulenames) { global $CFG, $PAGE; $CFG->enablecompletion = true; $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1], ['createsections' => true]); $PAGE->set_course($course); $cmids = []; foreach ($modulenames as $modname) { $module = $this->getDataGenerator()->create_module($modname, ['course' => $course->id]); $cmids[] = $module->cmid; } $modinfo = get_fast_modinfo($course); $cms = []; foreach ($cmids as $cmid) { $cms[$cmid] = $modinfo->get_cm($cmid); } return [$course, $cms]; } /** * Provider for test_bulk_form_submit_multiple * @return array */ public function bulk_form_submit_multiple_provider() { return [ 'Several modules with the same module type (choice)' => [ [ 'modulenames' => ['choice', 'choice', 'choice'], 'submitdata' => ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionsubmit' => 1], 'validatedata' => ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionsubmit' => 1], 'cmdata' => ['completion' => COMPLETION_TRACKING_AUTOMATIC], 'instancedata' => [['completionsubmit' => 1], ['completionsubmit' => 1], ['completionsubmit' => 1]] ] ], 'Several modules with different module type' => [ [ 'modulenames' => ['choice', 'forum'], 'submitdata' => ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1], 'validatedata' => ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1], 'cmdata' => ['completion' => COMPLETION_TRACKING_AUTOMATIC], 'instancedata' => null ] ], 'Setting manual completion (completionview shoud be ignored)' => [ [ 'modulenames' => ['scorm', 'forum', 'label', 'assign'], 'submitdata' => ['completion' => COMPLETION_TRACKING_MANUAL, 'completionview' => 1], 'validatedata' => [], 'cmdata' => ['completion' => COMPLETION_TRACKING_MANUAL, 'completionview' => 0], 'instancedata' => null ] ], 'If at least one module does not support completionsubmit it can\'t be set' => [ [ 'modulenames' => ['survey', 'wiki'], 'submitdata' => ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1, 'completionsubmit' => 1], 'validatedata' => [], 'cmdata' => ['completion' => COMPLETION_TRACKING_AUTOMATIC, 'completionview' => 1], 'instancedata' => [['completionsubmit' => 0], []] ] ] ]; } /** * Use bulk completion edit for updating multiple modules * * @dataProvider bulk_form_submit_multiple_provider * @param array $providerdata */ public function test_bulk_form_submit_multiple($providerdata) { global $DB; $modulenames = $providerdata['modulenames']; $submitdata = $providerdata['submitdata']; $validatedata = $providerdata['validatedata']; $cmdata = $providerdata['cmdata']; $instancedata = $providerdata['instancedata']; $this->resetAfterTest(); $this->setAdminUser(); list($course, $cms) = $this->create_course_and_modules($modulenames); // Submit the bulk completion form with the provided data and make sure it returns the same data. core_completion_bulkedit_form::mock_submit(['id' => $course->id, 'cmid' => array_keys($cms)] + $submitdata, []); $form = new core_completion_bulkedit_form(null, ['cms' => $cms]); $this->assertTrue($form->is_validated()); $data = $form->get_data(); foreach ($validatedata as $key => $value) { $this->assertEquals($value, $data->$key); } // Apply completion rules to the modules. $manager = new manager($course->id); $manager->apply_completion($data, $form->has_custom_completion_rules()); // Make sure either course_modules or instance table was respectfully updated. $cnt = 0; foreach ($cms as $cm) { $cmrec = $DB->get_record('course_modules', ['id' => $cm->id]); $instancerec = $DB->get_record($cm->modname, ['id' => $cm->instance]); foreach ($cmdata as $key => $value) { $this->assertEquals($value, $cmrec->$key, 'Error asserting that value for the field ' . $key.' ' . $cmrec->$key . ' matches expected value ' . $value); } if ($instancedata) { foreach ($instancedata[$cnt] as $key => $value) { $this->assertEquals($value, $instancerec->$key, 'Error asserting that value for the field ' . $key . ' '. $instancerec->$key . ' matches expected value ' . $value); } } $cnt++; } } } tests/api_test.php 0000604 00000037647 15062070516 0010246 0 ustar 00 <?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/>. namespace core_completion; /** * Test completion API. * * @package core_completion * @category test * @copyright 2017 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class api_test extends \advanced_testcase { /** * Test setup. */ public function setUp(): void { $this->resetAfterTest(); } public function test_update_completion_date_event() { global $CFG, $DB; $this->setAdminUser(); // Create a course. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); // Create an assign activity. $time = time(); $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id)); // Create the completion event. $CFG->enablecompletion = true; \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign, $time); // Check that there is now an event in the database. $events = $DB->get_records('event'); $this->assertCount(1, $events); // Get the event. $event = reset($events); // Confirm the event is correct. $this->assertEquals('assign', $event->modulename); $this->assertEquals($assign->id, $event->instance); $this->assertEquals(CALENDAR_EVENT_TYPE_ACTION, $event->type); $this->assertEquals(\core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED, $event->eventtype); $this->assertEquals($time, $event->timestart); $this->assertEquals($time, $event->timesort); require_once($CFG->dirroot . '/course/lib.php'); // Delete the module. course_delete_module($assign->cmid); // Check we don't get a failure when called on a deleted module. \core_completion\api::update_completion_date_event($assign->cmid, 'assign', null, $time); } public function test_update_completion_date_event_update() { global $CFG, $DB; $this->setAdminUser(); // Create a course. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); // Create an assign activity. $time = time(); $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id)); // Create the event. $CFG->enablecompletion = true; \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign, $time); // Call it again, but this time with a different time. \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign, $time + DAYSECS); // Check that there is still only one event in the database. $events = $DB->get_records('event'); $this->assertCount(1, $events); // Get the event. $event = reset($events); // Confirm that the event has been updated. $this->assertEquals('assign', $event->modulename); $this->assertEquals($assign->id, $event->instance); $this->assertEquals(CALENDAR_EVENT_TYPE_ACTION, $event->type); $this->assertEquals(\core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED, $event->eventtype); $this->assertEquals($time + DAYSECS, $event->timestart); $this->assertEquals($time + DAYSECS, $event->timesort); } public function test_update_completion_date_event_delete() { global $CFG, $DB; $this->setAdminUser(); // Create a course. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); // Create an assign activity. $time = time(); $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id)); // Create the event. $CFG->enablecompletion = true; \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign, $time); // Call it again, but the time specified as null. \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign, null); // Check that there is no event in the database. $this->assertEquals(0, $DB->count_records('event')); } public function test_update_completion_date_event_completion_disabled() { global $CFG, $DB; $this->setAdminUser(); // Create a course. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); // Create an assign activity. $time = time(); $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id)); // Try and create the completion event with completion disabled. $CFG->enablecompletion = false; \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign, $time); // Check that there is no event in the database. $this->assertEquals(0, $DB->count_records('event')); } public function test_update_completion_date_event_update_completion_disabled() { global $CFG, $DB; $this->setAdminUser(); // Create a course. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); // Create an assign activity. $time = time(); $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id)); // Create the completion event. $CFG->enablecompletion = true; \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign, $time); // Disable completion. $CFG->enablecompletion = false; // Try and update the completion date. \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign, $time + DAYSECS); // Check that there is an event in the database. $events = $DB->get_records('event'); $this->assertCount(1, $events); // Get the event. $event = reset($events); // Confirm the event has not changed. $this->assertEquals('assign', $event->modulename); $this->assertEquals($assign->id, $event->instance); $this->assertEquals(CALENDAR_EVENT_TYPE_ACTION, $event->type); $this->assertEquals(\core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED, $event->eventtype); $this->assertEquals($time, $event->timestart); $this->assertEquals($time, $event->timesort); } public function test_update_completion_date_event_delete_completion_disabled() { global $CFG, $DB; $this->setAdminUser(); // Create a course. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); // Create an assign activity. $time = time(); $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id)); // Create the completion event. $CFG->enablecompletion = true; \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign, $time); // Disable completion. $CFG->enablecompletion = false; // Should still be able to delete completion events even when completion is disabled. \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign, null); // Check that there is now no event in the database. $this->assertEquals(0, $DB->count_records('event')); } /** * Test for mark_course_completions_activity_criteria(). */ public function test_mark_course_completions_activity_criteria() { global $DB, $CFG; require_once($CFG->dirroot.'/completion/criteria/completion_criteria_activity.php'); $this->resetAfterTest(true); $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); $student1 = $this->getDataGenerator()->create_user(); $student2 = $this->getDataGenerator()->create_user(); $teacher = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); $this->getDataGenerator()->enrol_user($student1->id, $course->id, $studentrole->id); $this->getDataGenerator()->enrol_user($student2->id, $course->id, $studentrole->id); $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), array('completion' => 1)); $cmdata = get_coursemodule_from_id('data', $data->cmid); $cm = get_coursemodule_from_instance('data', $data->id); $c = new \completion_info($course); // Add activity completion criteria. $criteriadata = new \stdClass(); $criteriadata->id = $course->id; $criteriadata->criteria_activity = array(); // Some activities. $criteriadata->criteria_activity[$cmdata->id] = 1; $criterion = new \completion_criteria_activity(); $criterion->update_config($criteriadata); $this->setUser($teacher); // Mark activity complete for both users. $completion = new \stdClass(); $completion->coursemoduleid = $cm->id; $completion->completionstate = COMPLETION_COMPLETE; $completion->timemodified = time(); $completion->viewed = COMPLETION_NOT_VIEWED; $completion->overrideby = null; $completion->id = 0; $completion->userid = $student1->id; $c->internal_set_data($cm, $completion, true); $completion->id = 0; $completion->userid = $student2->id; $c->internal_set_data($cm, $completion, true); // Run instant course completions for student1. Only student1 will be marked as completed a course. $userdata = ['userid' => $student1->id, 'courseid' => $course->id]; $actual = $DB->get_records('course_completions'); $this->assertEmpty($actual); $coursecompletionid = \core_completion\api::mark_course_completions_activity_criteria($userdata); $actual = $DB->get_records('course_completions'); $this->assertEquals(reset($actual)->id, $coursecompletionid); $this->assertEquals(1, count($actual)); $this->assertEquals($student1->id, reset($actual)->userid); // Run course completions cron. Both students will be marked as completed a course. $coursecompletionid = \core_completion\api::mark_course_completions_activity_criteria(); $this->assertEquals(0, $coursecompletionid); $actual = $DB->get_records('course_completions'); $students = [$student1->id, $student2->id]; $this->assertEquals(2, count($actual)); $this->assertContains(reset($actual)->userid, $students); $this->assertContains(end($actual)->userid, $students); } /** * Test for mark_course_completions_activity_criteria() with different completionpassgrade settings. * @covers ::mark_course_completions_activity_criteria */ public function test_mark_course_completions_activity_criteria_completion_states() { global $DB, $CFG; require_once($CFG->dirroot . '/completion/criteria/completion_criteria_activity.php'); $this->resetAfterTest(true); $courses[] = $this->getDataGenerator()->create_course(['shortname' => 'completionpassgradenotset', 'enablecompletion' => 1]); $courses[] = $this->getDataGenerator()->create_course(['shortname' => 'completionpassgradeset', 'enablecompletion' => 1]); $student1 = $this->getDataGenerator()->create_user(); $student2 = $this->getDataGenerator()->create_user(); $teacher = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); foreach ($courses as $course) { $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); $this->getDataGenerator()->enrol_user($student1->id, $course->id, $studentrole->id); $this->getDataGenerator()->enrol_user($student2->id, $course->id, $studentrole->id); $completioncriteria = [ 'completionusegrade' => 1, 'gradepass' => 50 ]; if ($course->shortname == 'completionpassgradeset') { $completioncriteria['completionpassgrade'] = 1; } /** @var \mod_assign_generator $assigngenerator */ $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $assign = $assigngenerator->create_instance([ 'course' => $course->id, 'completion' => COMPLETION_ENABLED, ] + $completioncriteria); $cmassing = get_coursemodule_from_id('assign', $assign->cmid); $cm = get_coursemodule_from_instance('assign', $assign->id); $c = new \completion_info($course); // Add activity completion criteria. $criteriadata = new \stdClass(); $criteriadata->id = $course->id; $criteriadata->criteria_activity = array(); // Some activities. $criteriadata->criteria_activity[$cmassing->id] = 1; $criterion = new \completion_criteria_activity(); $criterion->update_config($criteriadata); $this->setUser($teacher); // Mark user completions. $completion = new \stdClass(); $completion->coursemoduleid = $cm->id; $completion->timemodified = time(); $completion->viewed = COMPLETION_NOT_VIEWED; $completion->overrideby = null; // Student1 achieved passgrade. $completion->id = 0; $completion->completionstate = COMPLETION_COMPLETE_PASS; $completion->userid = $student1->id; $c->internal_set_data($cm, $completion, true); // Student2 has not achieved passgrade. $completion->id = 0; $completion->completionstate = COMPLETION_COMPLETE_FAIL; $completion->userid = $student2->id; $c->internal_set_data($cm, $completion, true); $actual = $DB->get_records('course_completions', ['course' => $course->id]); $this->assertEmpty($actual); // Run course completions cron. $coursecompletionid = \core_completion\api::mark_course_completions_activity_criteria(); $this->assertEquals(0, $coursecompletionid); $actual = $DB->get_records('course_completions', ['course' => $course->id]); if ($course->shortname == 'completionpassgradeset') { // Only student1 has completed a course. $this->assertEquals(1, count($actual)); $this->assertEquals($student1->id, reset($actual)->userid); } else { // Both students completed a course. $students = [$student1->id, $student2->id]; $this->assertEquals(2, count($actual)); $this->assertContains(reset($actual)->userid, $students); $this->assertContains(end($actual)->userid, $students); } } } } tests/progress_test.php 0000604 00000030232 15062070516 0011320 0 ustar 00 <?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/>. namespace core_completion; use completion_completion; /** * Test completion progress API. * * @package core_completion * @category test * @copyright 2017 Mark Nelson <markn@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class progress_test extends \advanced_testcase { /** * Test setup. */ public function setUp(): void { global $CFG; $CFG->enablecompletion = true; $this->resetAfterTest(); } /** * Tests that the course progress percentage is returned correctly when we have only activity completion. */ public function test_course_progress_percentage_with_just_activities() { global $DB; // Add a course that supports completion. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); // Enrol a user in the course. $user = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id); // Add four activities that use completion. $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id), array('completion' => 1)); $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), array('completion' => 1)); $this->getDataGenerator()->create_module('forum', array('course' => $course->id), array('completion' => 1)); $this->getDataGenerator()->create_module('forum', array('course' => $course->id), array('completion' => 1)); // Add an activity that does *not* use completion. $this->getDataGenerator()->create_module('assign', array('course' => $course->id)); // Mark two of them as completed for a user. $cmassign = get_coursemodule_from_id('assign', $assign->cmid); $cmdata = get_coursemodule_from_id('data', $data->cmid); $completion = new \completion_info($course); $completion->update_state($cmassign, COMPLETION_COMPLETE, $user->id); $completion->update_state($cmdata, COMPLETION_COMPLETE, $user->id); // Check we have received valid data. // Note - only 4 out of the 5 activities support completion, and the user has completed 2 of those. $this->assertEquals('50', \core_completion\progress::get_course_progress_percentage($course, $user->id)); } /** * Tests that the course progress percentage is returned correctly when we have a course and activity completion. */ public function test_course_progress_percentage_with_activities_and_course() { global $DB; // Add a course that supports completion. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); // Enrol a user in the course. $user = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id); // Add four activities that use completion. $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id), array('completion' => 1)); $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), array('completion' => 1)); $this->getDataGenerator()->create_module('forum', array('course' => $course->id), array('completion' => 1)); $this->getDataGenerator()->create_module('forum', array('course' => $course->id), array('completion' => 1)); // Add an activity that does *not* use completion. $this->getDataGenerator()->create_module('assign', array('course' => $course->id)); // Mark two of them as completed for a user. $cmassign = get_coursemodule_from_id('assign', $assign->cmid); $cmdata = get_coursemodule_from_id('data', $data->cmid); $completion = new \completion_info($course); $completion->update_state($cmassign, COMPLETION_COMPLETE, $user->id); $completion->update_state($cmdata, COMPLETION_COMPLETE, $user->id); // Now, mark the course as completed. $ccompletion = new completion_completion(array('course' => $course->id, 'userid' => $user->id)); $ccompletion->mark_complete(); // Check we have received valid data. // The course completion takes priority, so should return 100. $this->assertEquals('100', \core_completion\progress::get_course_progress_percentage($course, $user->id)); } /** * Tests that the course progress percentage is returned correctly for various grade to pass settings * * @covers \core_completion\progress::get_course_progress_percentage. */ public function test_course_progress_percentage_completion_state() { global $DB, $CFG; require_once("{$CFG->dirroot}/completion/criteria/completion_criteria_activity.php"); // Add a course that supports completion. $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); // Enrol a user in the course. $teacher = $this->getDataGenerator()->create_user(); $user = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', ['shortname' => 'student']); $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); // Add three activities that use completion. /** @var \mod_assign_generator $assigngenerator */ $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); $assign['passgragepassed'] = $assigngenerator->create_instance([ 'course' => $course->id, 'completion' => COMPLETION_ENABLED, 'completionusegrade' => 1, 'gradepass' => 50, 'completionpassgrade' => 1 ]); $assign['passgragefailed'] = $assigngenerator->create_instance([ 'course' => $course->id, 'completion' => COMPLETION_ENABLED, 'completionusegrade' => 1, 'gradepass' => 50, 'completionpassgrade' => 1 ]); $assign['passgragenotused'] = $assigngenerator->create_instance([ 'course' => $course->id, 'completion' => COMPLETION_ENABLED, 'completionusegrade' => 1, 'gradepass' => 50, ]); $assign['nograde'] = $assigngenerator->create_instance([ 'course' => $course->id, 'completion' => COMPLETION_ENABLED, ]); $c = new \completion_info($course); foreach ($assign as $item) { $cmassing = get_coursemodule_from_id('assign', $item->cmid); // Add activity completion criteria. $criteriadata = new \stdClass(); $criteriadata->id = $course->id; $criteriadata->criteria_activity = []; // Some activities. $criteriadata->criteria_activity[$cmassing->id] = 1; $criterion = new \completion_criteria_activity(); $criterion->update_config($criteriadata); } $this->setUser($teacher); foreach ($assign as $key => $item) { $cm = get_coursemodule_from_instance('assign', $item->id); // Mark user completions. $completion = new \stdClass(); $completion->coursemoduleid = $cm->id; $completion->timemodified = time(); $completion->viewed = COMPLETION_NOT_VIEWED; $completion->overrideby = null; if ($key == 'passgragepassed') { $completion->id = 0; $completion->completionstate = COMPLETION_COMPLETE_PASS; $completion->userid = $user->id; $c->internal_set_data($cm, $completion, true); } else if ($key == 'passgragefailed') { $completion->id = 0; $completion->completionstate = COMPLETION_COMPLETE_FAIL; $completion->userid = $user->id; $c->internal_set_data($cm, $completion, true); } else if ($key == 'passgragenotused') { $completion->id = 0; $completion->completionstate = COMPLETION_COMPLETE; $completion->userid = $user->id; $c->internal_set_data($cm, $completion, true); } else if ($key == 'nograde') { $completion->id = 0; $completion->completionstate = COMPLETION_COMPLETE; $completion->userid = $user->id; $c->internal_set_data($cm, $completion, true); } } // Run course completions cron. \core_completion\api::mark_course_completions_activity_criteria(); // Check we have received valid data. // Only assign2 is not completed. $this->assertEquals('75', \core_completion\progress::get_course_progress_percentage($course, $user->id)); } /** * Tests that the course progress returns null when the course does not support it. */ public function test_course_progress_course_not_using_completion() { // Create a course that does not use completion. $course = $this->getDataGenerator()->create_course(); // Check that the result was null. $this->assertNull(\core_completion\progress::get_course_progress_percentage($course)); } /** * Tests that the course progress returns null when there are no activities that support it. */ public function test_course_progress_no_activities_using_completion() { // Create a course that does support completion. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); // Add an activity that does *not* support completion. $this->getDataGenerator()->create_module('assign', array('course' => $course->id)); // Check that the result was null. $this->assertNull(\core_completion\progress::get_course_progress_percentage($course)); } /** * Tests that the course progress returns null for a not tracked for completion user in a course. */ public function test_course_progress_not_tracked_user() { global $DB; // Add a course that supports completion. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); // Enrol a user in the course. $user = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id); // Now, mark the course as completed. $ccompletion = new completion_completion(array('course' => $course->id, 'userid' => $user->id)); $ccompletion->mark_complete(); // The course completion should return 100. $this->assertEquals('100', \core_completion\progress::get_course_progress_percentage($course, $user->id)); // Now make the user's role to be not tracked for completion. unassign_capability('moodle/course:isincompletionreports', $studentrole->id); // Check that the result is null now. $this->assertNull(\core_completion\progress::get_course_progress_percentage($course, $user->id)); } } tests/coverage.php 0000604 00000003177 15062070516 0010220 0 ustar 00 <?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/>. defined('MOODLE_INTERNAL') || die(); /** * Coverage information for the core_completion. * * @package core * @category phpunit * @copyright 2022 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Coverage information for the core subsystem. * * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ return new class extends phpunit_coverage_info { /** @var array The list of folders relative to the plugin root to include in coverage generation. */ protected $includelistfolders = [ 'criteria', ]; /** @var array The list of files relative to the plugin root to include in coverage generation. */ protected $includelistfiles = [ 'completion_aggregation.php', 'completion_completion.php', 'completion_criteria_completion.php', 'data_object.php' ]; }; tests/completion_criteria_test.php 0000604 00000025413 15062070516 0013514 0 ustar 00 <?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/>. namespace core_completion; /** * Test completion criteria. * * @package core_completion * @category test * @copyright 2021 Mikhail Golenkov <mikhailgolenkov@catalyst-au.net> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class completion_criteria_test extends \advanced_testcase { /** * Test setup. */ public function setUp(): void { global $CFG; require_once($CFG->dirroot.'/completion/criteria/completion_criteria_course.php'); require_once($CFG->dirroot.'/completion/criteria/completion_criteria_activity.php'); require_once($CFG->dirroot.'/completion/criteria/completion_criteria_duration.php'); require_once($CFG->dirroot.'/completion/criteria/completion_criteria_grade.php'); require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php'); $this->setAdminUser(); $this->resetAfterTest(); } /** * Test that activity completion dates are used when activity criteria is marked as completed. */ public function test_completion_criteria_activity(): void { global $DB; $timestarted = time(); // Create a course, an activity and enrol a user. $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); $assign = $this->getDataGenerator()->create_module('assign', ['course' => $course->id], ['completion' => 1]); $user = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', ['shortname' => 'student']); $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id); // Set completion criteria and mark the user to complete the criteria. $criteriadata = (object) [ 'id' => $course->id, 'criteria_activity' => [$assign->cmid => 1], ]; $criterion = new \completion_criteria_activity(); $criterion->update_config($criteriadata); $cmassign = get_coursemodule_from_id('assign', $assign->cmid); $completion = new \completion_info($course); $completion->update_state($cmassign, COMPLETION_COMPLETE, $user->id); // Completion criteria for the user is supposed to be marked as completed at now(). $result = \core_completion_external::get_activities_completion_status($course->id, $user->id); $actual = reset($result['statuses']); $this->assertEquals(1, $actual['state']); $this->assertGreaterThanOrEqual($timestarted, $actual['timecompleted']); // And the whole course is marked as completed at now(). $ccompletion = new \completion_completion(['userid' => $user->id, 'course' => $course->id]); $this->assertGreaterThanOrEqual($timestarted, $ccompletion->timecompleted); $this->assertTrue($ccompletion->is_complete()); } /** * Test that enrolment timestart are used when duration criteria is marked as completed. */ public function test_completion_criteria_duration_timestart(): void { global $DB; $timestarted = 1610000000; $durationperiod = DAYSECS; // Create a course and users. $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); $user = $this->getDataGenerator()->create_and_enrol($course, 'student', null, 'manual', $timestarted); // Set completion criteria. $criteriadata = (object) [ 'id' => $course->id, 'criteria_duration' => 1, 'criteria_duration_days' => $durationperiod, ]; $criterion = new \completion_criteria_duration(); $criterion->update_config($criteriadata); // Run completion scheduled task. $task = new \core\task\completion_regular_task(); $this->expectOutputRegex("/Marking complete/"); $task->execute(); // Hopefully, some day MDL-33320 will be fixed and all these sleeps // and double cron calls in behat and unit tests will be removed. sleep(1); $task->execute(); // The course for User is supposed to be marked as completed at $timestarted + $durationperiod. $ccompletion = new \completion_completion(['userid' => $user->id, 'course' => $course->id]); $this->assertEquals($timestarted + $durationperiod, $ccompletion->timecompleted); $this->assertTrue($ccompletion->is_complete()); } /** * Test that enrolment timecreated are used when duration criteria is marked as completed. */ public function test_completion_criteria_duration_timecreated(): void { global $DB; $timecreated = 1620000000; $durationperiod = DAYSECS; // Create a course and users. $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); // Create and enrol user with an empty time start, but update the record like it was created at $timecreated. $user = $this->getDataGenerator()->create_and_enrol($course); $DB->set_field('user_enrolments', 'timecreated', $timecreated, ['userid' => $user->id]); // Set completion criteria. $criteriadata = (object) [ 'id' => $course->id, 'criteria_duration' => 1, 'criteria_duration_days' => $durationperiod, ]; $criterion = new \completion_criteria_duration(); $criterion->update_config($criteriadata); // Run completion scheduled task. $task = new \core\task\completion_regular_task(); $this->expectOutputRegex("/Marking complete/"); $task->execute(); // Hopefully, some day MDL-33320 will be fixed and all these sleeps // and double cron calls in behat and unit tests will be removed. sleep(1); $task->execute(); // The course for user is supposed to be marked as completed at $timecreated + $durationperiod. $ccompletion = new \completion_completion(['userid' => $user->id, 'course' => $course->id]); $this->assertEquals($timecreated + $durationperiod, $ccompletion->timecompleted); $this->assertTrue($ccompletion->is_complete()); } /** * Test that criteria date is used as a course completion date. */ public function test_completion_criteria_date(): void { global $DB; $timeend = 1610000000; // Create a course and enrol a user. $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); $user = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', ['shortname' => 'student']); $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id); // Set completion criteria. $criteriadata = (object) [ 'id' => $course->id, 'criteria_date' => 1, 'criteria_date_value' => $timeend, ]; $criterion = new \completion_criteria_date(); $criterion->update_config($criteriadata); // Run completion scheduled task. $task = new \core\task\completion_regular_task(); $this->expectOutputRegex("/Marking complete/"); $task->execute(); // Hopefully, some day MDL-33320 will be fixed and all these sleeps // and double cron calls in behat and unit tests will be removed. sleep(1); $task->execute(); // The course is supposed to be marked as completed at $timeend. $ccompletion = new \completion_completion(['userid' => $user->id, 'course' => $course->id]); $this->assertEquals($timeend, $ccompletion->timecompleted); $this->assertTrue($ccompletion->is_complete()); } /** * Test that grade timemodified is used when grade criteria is marked as completed. */ public function test_completion_criteria_grade(): void { global $DB; $timegraded = 1610000000; // Create a course and enrol a couple of users. $course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', ['shortname' => 'student']); $this->getDataGenerator()->enrol_user($user1->id, $course->id, $studentrole->id); $this->getDataGenerator()->enrol_user($user2->id, $course->id, $studentrole->id); // Set completion criteria. $criteriadata = (object) [ 'id' => $course->id, 'criteria_grade' => 1, 'criteria_grade_value' => 66, ]; $criterion = new \completion_criteria_grade(); $criterion->update_config($criteriadata); $coursegradeitem = \grade_item::fetch_course_item($course->id); // Grade User 1 with a passing grade. $grade1 = new \grade_grade(); $grade1->itemid = $coursegradeitem->id; $grade1->timemodified = $timegraded; $grade1->userid = $user1->id; $grade1->finalgrade = 80; $grade1->insert(); // Grade User 2 with a non-passing grade. $grade2 = new \grade_grade(); $grade2->itemid = $coursegradeitem->id; $grade2->timemodified = $timegraded; $grade2->userid = $user2->id; $grade2->finalgrade = 40; $grade2->insert(); // Run completion scheduled task. $task = new \core\task\completion_regular_task(); $this->expectOutputRegex("/Marking complete/"); $task->execute(); // Hopefully, some day MDL-33320 will be fixed and all these sleeps // and double cron calls in behat and unit tests will be removed. sleep(1); $task->execute(); // The course for User 1 is supposed to be marked as completed when the user was graded. $ccompletion = new \completion_completion(['userid' => $user1->id, 'course' => $course->id]); $this->assertEquals($timegraded, $ccompletion->timecompleted); $this->assertTrue($ccompletion->is_complete()); // The course for User 2 is supposed to be marked as not completed. $ccompletion = new \completion_completion(['userid' => $user2->id, 'course' => $course->id]); $this->assertFalse($ccompletion->is_complete()); } }
| ver. 1.4 |
Github
|
.
| PHP 8.1.33 | Генерация страницы: 0 |
proxy
|
phpinfo
|
Настройка