Skip to content

Instantly share code, notes, and snippets.

@frumbert
Created March 2, 2026 22:49
Show Gist options
  • Select an option

  • Save frumbert/0209003586df96262e7c5c8ede2c09d9 to your computer and use it in GitHub Desktop.

Select an option

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)
<?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, " &lt;", $course->idnumber, "&gt;</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, ' &lt;', email, '&gt;') 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