Created
March 2, 2026 22:49
-
-
Save frumbert/0209003586df96262e7c5c8ede2c09d9 to your computer and use it in GitHub Desktop.
MOODLE 5 - Script to reset a course including completion for a single user (deletes existing data and completions, resets caches, leaves their enrolment alone)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * This admin-only page deletes user activity in a course and resets completion, caches | |
| * expected to live somewhere like | |
| * /public/local/somefolder/index.php | |
| */ | |
| require(dirname(dirname(dirname(__FILE__))).'/config.php'); // adjust as needed | |
| require_once($CFG->libdir.'/adminlib.php'); | |
| admin_externalpage_setup('upgradesettings'); // dump it somwhere that exists in the admin page structure | |
| define('ROOT','/1'); // adjust as required | |
| // isadmin checks logon, permission, session etc. | |
| if (is_siteadmin()) { | |
| $courseid = optional_param('courseid', 0, PARAM_INT); | |
| $userid = optional_param('userid', 0, PARAM_INT); | |
| $action = optional_param('action', 'next', PARAM_ALPHA); | |
| echo $OUTPUT->header(); | |
| // i'm going to not bother with mform and just take the naive approach | |
| $root = $DB->get_record('course_categories',['path'=>ROOT],'id',MUST_EXIST); | |
| $categories = $DB->get_records('course_categories',['parent'=>$root->id,'visible'=>1]); | |
| echo "<form method='post'><input type='hidden' name='sesskey' value='" . sesskey() . "'>"; | |
| // dump out visible courses grouped by category (that have completion enabled) | |
| echo "<p><label>Select a course :<select name='courseid'>"; | |
| foreach ($categories as $category) { | |
| echo "<optgroup label='" . $category->name . "'>"; | |
| $courses = $DB->get_records('course',['category'=>$category->id,'visible'=>1,'enablecompletion'=>1], 'fullname'); | |
| foreach ($courses as $course) { | |
| echo "<option value='" . $course->id . "'", ($course->id==$courseid) ? " selected" : "", ">", $course->fullname, " <", $course->idnumber, "></option>"; | |
| } | |
| echo "</optgroup>"; | |
| } | |
| echo "</select></label></p>"; | |
| // when a course is selected, go find the enrolled users and list them (grouped here by auth method, but adjust as required) | |
| if ($courseid > 0) { | |
| $users = $DB->get_records_sql("SELECT id, auth, concat(lastname, ', ', firstname, ' <', email, '>') as name | |
| FROM {user} | |
| WHERE id IN ( | |
| SELECT userid FROM {user_enrolments} WHERE enrolid IN ( | |
| SELECT id from {enrol} WHERE courseid = ? AND status = 0 | |
| ) | |
| ) | |
| AND deleted = 0 AND confirmed = 1 | |
| ORDER BY auth, lastname, firstname", [$courseid]); | |
| echo "<p><label>Select a user<select name='userid'>"; | |
| echo "<option value='0'>Select a user</option>"; | |
| $currentauth = null; | |
| foreach ($users as $user){ | |
| if ($user->auth !== $currentauth) { | |
| if ($currentauth !== null) { | |
| echo "</optgroup>"; | |
| } | |
| echo "<optgroup label='" . $user->auth . "'>"; | |
| $currentauth = $user->auth; | |
| } | |
| echo "<option value='" . $user->id . "'", ($user->id==$userid) ? " selected" : "", ">", $user->name, "</option>"; | |
| } | |
| if ($currentauth !== null) { | |
| echo "</optgroup>"; | |
| } | |
| echo "</select></label></p>"; | |
| } | |
| // validate the action | |
| if ($courseid > 0 && $userid > 0 && ($action !== 'confirm' && $action !== 'sql')) $action = 'ready'; | |
| // action router | |
| switch ($action) { | |
| case "confirm": | |
| delete_user_completion_for_a_course($courseid, $userid); | |
| reset_course_completion_cache_for_user($userid, $courseid); | |
| grade_regrade_final_grades($courseid); | |
| echo "<h1>Completed!</h1>"; | |
| echo "<p><input type='submit' name='action' value='start over'></p>"; | |
| break; | |
| case "ready": | |
| echo "<p>There is no undo here. Please confirm you are ready to do this.</p>"; | |
| echo "<p><input type='submit' name='action' value='confirm'></p>"; | |
| break; | |
| default: | |
| echo "<p><input type='submit' name='action' value='".$action."'></p>"; | |
| } | |
| echo "<p><a href='index.php'>Start over</a></p>"; | |
| echo "</form>"; | |
| echo $OUTPUT->footer(); | |
| } else { | |
| redirect($CFG->wwwroot); | |
| } | |
| // rebuild the cache AND create new blank completion records so the user starts afresh | |
| // you have to recreate the blank completion records for some reason (to do with caching I think) | |
| function reset_course_completion_cache_for_user($userid, $courseid) { | |
| // make sure the user cache is empty | |
| $completioncache = cache::make('core', 'completion'); | |
| $cachekey = "{$userid}_{$courseid}"; | |
| $completioncache->delete($cachekey); | |
| // $completioncache->delete($userid); | |
| // now create new completion records | |
| $course = get_course($courseid); | |
| $completion = new completion_info($course); | |
| $modinfo = get_fast_modinfo($courseid); | |
| $cms = $modinfo->get_cms(); | |
| foreach ($cms as $cm) { | |
| if ($completion->is_enabled($cm)) { | |
| $data = new stdClass(); | |
| $data->id = 0; | |
| $data->userid = $userid; | |
| $data->coursemoduleid = $cm->id; | |
| $data->completionstate = COMPLETION_INCOMPLETE; | |
| $data->timemodified = time(); | |
| $data->viewed = COMPLETION_NOT_VIEWED; | |
| $data->overrideby = null; | |
| $completion->internal_set_data($cm, $data); | |
| } | |
| } | |
| // now rebuild the course cache | |
| rebuild_course_cache($courseid); | |
| } | |
| // delete the table data for activities | |
| function delete_user_completion_for_a_course($courseid, $userid) { | |
| global $DB, $CFG; | |
| $DBManager = $DB->get_manager(); | |
| // $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); | |
| // $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST); | |
| // 1. Delete core completion records --------------------------------------------------------------- | |
| $DB->delete_records('course_completions', array('course' => $courseid, 'userid' => $userid)); | |
| $DB->delete_records('course_completion_crit_compl', array('course' => $courseid, 'userid' => $userid)); | |
| $DB->delete_records_select('course_modules_completion', | |
| 'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=?) AND userid=?', | |
| array($courseid, $userid)); | |
| // 2. Delete SCORM tracking data --------------------------------------------------------------- | |
| if ($scorms = $DB->get_records('scorm', array('course' => $courseid))) { | |
| require_once($CFG->dirroot . '/mod/scorm/locallib.php'); | |
| foreach ($scorms as $scorm) { | |
| // This function deletes all attempts for a user in a scorm activity. | |
| scorm_delete_tracks($scorm->id, null, $userid); | |
| } | |
| } | |
| // 3. Delete Quiz attempts --------------------------------------------------------------- | |
| require_once($CFG->dirroot . '/mod/quiz/locallib.php'); | |
| if ($attempts = $DB->get_records('quiz_attempts', array('userid' => $userid, 'quiz' => $quizid))) { | |
| foreach ($attempts as $attempt) { | |
| question_engine::delete_questions_usage_by_activity($attempt->uniqueid); | |
| $DB->delete_records('quiz_attempts', array('id' => $attempt->id)); | |
| } | |
| } | |
| // 4. Delete other module data | |
| /* follow either pattern: | |
| 1. If there's a locallib.php with a method for deleting a user record, include and use that | |
| 2. otherwise just delete the record from the db directly | |
| */ | |
| // Assignment --------------------------------------------------------------- | |
| // I didn't need to do this one .. ask an AI to fill it in | |
| // Choice --------------------------------------------------------------- | |
| $DB->delete_records_select('choice_answers', | |
| 'choiceid IN (SELECT id FROM {choice} WHERE course=?) AND userid=?', | |
| array($courseid, $userid)); | |
| // Lesson --------------------------------------------------------------- | |
| $DB->delete_records_select('lesson_attempts', | |
| 'lessonid IN (SELECT id FROM {lesson} WHERE course=?) AND userid=?', | |
| array($courseid, $userid)); | |
| $DB->delete_records_select('lesson_grades', | |
| 'lessonid IN (SELECT id FROM {lesson} WHERE course=?) AND userid=?', | |
| array($courseid, $userid)); | |
| // Certificate --------------------------------------------------------------- | |
| if ($DBManager->table_exists('certificate_issues')) { | |
| $DB->delete_records_select('certificate_issues', | |
| 'certificateid IN (SELECT id FROM mdl_certificate WHERE course=?) AND userid=?', | |
| array($courseid, $userid)); | |
| } | |
| // Supervideo --------------------------------------------------------------- | |
| if ($DBManager->table_exists('supervideo_view')) { | |
| $DB->delete_records_select('supervideo_view', | |
| 'cm_id IN (SELECT id FROM {course_modules} WHERE course=? AND module IN (SELECT id FROM {modules} WHERE name=?)) AND user_id=?', | |
| array($courseid, 'supervideo', $userid)); | |
| } | |
| // Questionnaire --------------------------------------------------------------- | |
| if ($DBManager->table_exists('questionnaire')) { | |
| if ($questionnaires = $DB->get_records('questionnaire', ['course' => $courseid])) { | |
| require_once($CFG->dirroot . '/mod/questionnaire/locallib.php'); | |
| foreach ($questionnaires as $questionnaire) { | |
| // get all responses for the user. | |
| if ($responseids = questionnaire_get_user_responses($questionnaire->id, $userid)) { | |
| foreach ($responseids as $responseid) { | |
| // Delete each response using the plugin's API function. | |
| questionnaire_delete_response($responseid); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment