Last active
September 11, 2020 12:58
-
-
Save frumbert/2d8d5d2729aec8889e1555dd7956887a to your computer and use it in GitHub Desktop.
Block code for hacking scorm and completion data in Moodle (incomplete file, just the important function bits)
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 | |
// there's more to this file than this: | |
public function get_content() | |
{ | |
global $CFG, $DB, $USER; | |
if (!is_siteadmin()) { | |
return null; | |
} | |
if ($this->content !== NULL) { | |
return $this->content; | |
} | |
if (empty($this->instance)) { | |
return ''; | |
} | |
$context = context_course::instance($this->page->course->id); | |
$html = ''; | |
$ul_closed = false; | |
if ($this->page->course->id !== SITEID) { | |
$html .= html_writer::start_tag('ul', array('class' => 'timshax-tools-link')); | |
$grader_user_id = optional_param('user', -1, PARAM_INT); | |
if ($CFG->enablecompletion) { | |
$forcecron = optional_param('forcecompletioncron', 0, PARAM_BOOL); | |
$forcecompletionaggrmethd = optional_param('forcecompletionaggrmethd', 0, PARAM_INT); | |
$forceactivitycompletion = optional_param('forceactivitycompletion', 0, PARAM_INT); | |
/* | |
* Tim: 20171003 | |
* Run the completion cron internal process right now. | |
*/ | |
if ($forcecron) { | |
ob_start(); | |
require_once($CFG->dirroot.'/completion/cron.php'); | |
completion_cron_criteria(); | |
completion_cron_completions(); | |
completion_cron_criteria(); | |
completion_cron_completions(); | |
if (ob_get_contents()) { ob_end_clean(); } | |
} | |
/* | |
* Tim: 20171003 | |
* Toggle the completion method for a course from ALL rules to ANY rules (or back again) | |
* Because of the "unlock" button and its tendency to delete everything | |
*/ | |
if ($forcecompletionaggrmethd > 0) { | |
if ($crit_obj = $DB->get_record('course_completion_aggr_methd', array('course'=> $this->page->course->id, 'criteriatype' => null))) { | |
$crit_obj->method = $forcecompletionaggrmethd; | |
$DB->update_record('course_completion_aggr_methd', $crit_obj); | |
} | |
} | |
/* | |
* Tim: 20171003 | |
* For when you've manually editing a score and set the grade, and update_state() won't cut the mustard for some reason, | |
* take the current (scorm) activity / user and mark it as complete if it's not already. | |
* Creates a {course_modules_completion} record if required. | |
* | |
* Let me know if you find a larger hack/violation of core Moodle than this! | |
*/ | |
if ($forceactivitycompletion > 0 && $grader_user_id > 0) { | |
$completion_info = new completion_info($this->page->course); | |
$completion_info->set_module_viewed($this->page->cm, $grader_user_id); | |
$current_info = $completion_info->get_data($this->page->cm, false, $grader_user_id); | |
if (COMPLETION_INCOMPLETE === $current_info->completionstate) { | |
$current_info->completionstate = COMPLETION_COMPLETE; | |
$current_info->timemodified = time(); | |
$completion_info->internal_set_data($this->page->cm, $current_info); | |
} | |
} | |
/* | |
* Draw a link to run the completion cron | |
*/ | |
$link = new moodle_url($this->page->url); | |
$link->param('forcecompletioncron',1); | |
$cronlink = $link; | |
$html .= html_writer::tag('li', html_writer::tag('a', 'Run completion cron now <i class="fa fa-thumbs-up" aria-hidden="true" title="Pretty safe to do"></i>', array('href' => $cronlink))); | |
/* | |
* does this course have manual completion? check the criteria and offer a toggle | |
*/ | |
if ($DB->count_records('course_completion_criteria', array("course" => $this->page->course->id, "criteriatype" => 7)) > 0) { | |
$crit = (int) $DB->get_field('course_completion_aggr_methd', 'method', array('course'=> $this->page->course->id, 'criteriatype' => null)); | |
$switchlink = new moodle_url($this->page->url); | |
if ($crit === COMPLETION_AGGREGATION_ALL) { | |
$switchlink->param('forcecompletionaggrmethd', COMPLETION_AGGREGATION_ANY); | |
$html .= html_writer::tag('li', 'Completion requirements: ALL (' . html_writer::tag('a', 'Switch to ANY <i class="fa fa-exclamation-triangle" aria-hidden="true" title="Dangerous, have a backup"></i>', array('href' => $switchlink, 'onclick' => 'return window.confirm("This will change LIVE completion records. This is dangerous and must only be used as a last resort after backing up the database and crossing your fingers. Are you sure?");')) . ')'); | |
} elseif ($crit === COMPLETION_AGGREGATION_ANY) { | |
$switchlink->param('forcecompletionaggrmethd', COMPLETION_AGGREGATION_ALL); | |
$html .= html_writer::tag('li', 'Completion requirements: ANY (' . html_writer::tag('a', 'Switch to ALL <i class="fa fa-exclamation-triangle" aria-hidden="true" title="Dangerous, do a backup"></i>', array('href' => $switchlink, 'onclick' => 'return window.confirm("This will change LIVE completion records. This is dangerous and must only be used as a last resort after backing up the database and hiding under the desk. Are you sure?");')) . ')'); | |
} | |
} | |
} | |
// $is_grader_tool = ($this->page->url->get_path() === '/grade/report/grader/index.php'); | |
// if ($is_grader_tool && $grader_user_id > 0) { | |
// } | |
$is_scorm_tool = ($this->page->url->get_path() === '/mod/scorm/report/userreporttracks.php' || $this->page->url->get_path() === '/mod/scorm/report/userreport.php'); | |
if ($is_scorm_tool && null !== $this->page->url->get_param('user')) { | |
$post_sesskey = optional_param('sesskey', '', PARAM_RAW); | |
$user_id = (int) $this->page->url->get_param('user'); | |
$scorm_id = (int) $this->page->url->get_param('id'); | |
$attempt = (int) $this->page->url->get_param('attempt'); | |
$scoid = (int) $this->page->url->get_param('scoid'); | |
$reload_page = false; | |
$reload_log = []; | |
// form post updates the scorm record and logs the change | |
if (!empty($post_sesskey) && $post_sesskey === sesskey()) { | |
if ($row = $DB->get_record("scorm_scoes_track", array("scoid"=>$scoid,"userid"=>$user_id,"element"=>"cmi.core.exit"))) { | |
$update_scorm_data_exit_status = optional_param('update_scorm_data_exit_status', '', PARAM_ALPHA); | |
if ($update_scorm_data_exit_status === "" || $update_scorm_data_exit_status === "suspend") { | |
if ($row->value !== $update_scorm_data_exit_status) { | |
$reload_log[] = "Exit changed from '" . $row->value . "' to '" . $update_scorm_data_exit_status . "'"; | |
$row->value = $update_scorm_data_exit_status; | |
$DB->update_record("scorm_scoes_track", $row); | |
$reload_page = true; | |
} | |
} | |
} | |
if ($row = $DB->get_record("scorm_scoes_track", array("scoid"=>$scoid,"userid"=>$user_id,"element"=>"cmi.core.lesson_status"))) { | |
$update_scorm_data_lesson_status = optional_param('update_scorm_data_lesson_status', 'incomplete', PARAM_ALPHA); | |
if (in_array($update_scorm_data_lesson_status, ["not attempted","incomplete","completed","passed","failed"])) { | |
if ($row->value !== $update_scorm_data_lesson_status) { | |
$reload_log[] = "Exit changed from '" . $row->value . "' to '" . $update_scorm_data_lesson_status . "'"; | |
$row->value = $update_scorm_data_lesson_status; | |
$DB->update_record("scorm_scoes_track", $row); | |
$reload_page = true; | |
} | |
} | |
} | |
$update_scorm_data_lesson_score = optional_param('update_scorm_data_lesson_score', -1, PARAM_INT); | |
if ($update_scorm_data_lesson_score > -1) { | |
if ($DB->count_records("scorm_scoes_track", array("scoid"=>$scoid,"userid"=>$user_id,"element"=>"cmi.core.score.raw")) === 0) { | |
$record = new stdClass(); | |
$record->userid = $user_id; | |
$record->scormid = $scorm_id; | |
$record->scoid = $scoid; | |
$record->attempt = $attempt; | |
$record->element = "cmi.core.score.min"; | |
$record->value = 0; | |
$record->timemodified = time(); | |
$DB->insert_record("scorm_scoes_track", $record); | |
$record->element = "cmi.core.score.max"; | |
$record->value = 100; | |
$DB->insert_record("scorm_scoes_track", $record); | |
$record->element = "cmi.core.score.raw"; | |
$record->value = $update_scorm_data_lesson_score; | |
$DB->insert_record("scorm_scoes_track", $record); | |
$reload_page = true; | |
$reload_log[] = "Inserted cmi.core.score.*"; | |
} elseif ($row = $DB->get_record("scorm_scoes_track", array("scoid"=>$scoid,"userid"=>$user_id,"element"=>"cmi.core.score.raw"))) { | |
if ((int) $row->value !== (int) $update_scorm_data_lesson_score) { | |
$reload_log[] = "Score changed from '" . $row->value . "' to '" . $update_scorm_data_lesson_score . "'"; | |
$row->value = $update_scorm_data_lesson_score; | |
$DB->update_record("scorm_scoes_track", $row); | |
$reload_page = true; | |
} | |
} | |
} | |
if ($reload_page === true) { | |
$reload_log[] = "by " . fullname($USER, true) . " (userid=" . $USER->id . ")"; | |
$record = new stdClass(); | |
$record->userid = $user_id; | |
$record->scormid = $scorm_id; | |
$record->scoid = $scoid; | |
$record->attempt = $attempt; | |
$record->element = "x.timshax.block.edit." . uniqid(); | |
$record->value = implode(', ', $reload_log); | |
$record->timemodified = time(); | |
$DB->insert_record("scorm_scoes_track", $record); | |
if (ob_get_contents()) ob_end_clean(); | |
redirect($this->page->url); | |
die(); | |
} | |
} | |
/* | |
* Draw a link to open the grades screen with editing turned on | |
*/ | |
$user_obj = core_user::get_user($user_id, 'firstname, lastname'); | |
$editgradelink = new moodle_url("/grade/report/grader/index.php", array("plugin"=>"grader", "id"=>$this->page->course->id, "sesskey"=>sesskey(),"sifirst"=>mb_substr($user_obj->firstname, 0, 1, 'utf-8'),"silast"=>mb_substr($user_obj->lastname, 0, 1, 'utf-8'),"edit"=>1)); | |
$html .= html_writer::tag('li', html_writer::tag('a', 'Edit grades for user <i class="fa fa-frown-o" aria-hidden="true" title="Pretty safe, but frowned apon a bit"></i>', array('href' => $editgradelink))); | |
/* | |
* Draw a link that forces the completion state for this activity (überhax) | |
*/ | |
$link = new moodle_url($this->page->url); | |
$link->param('forceactivitycompletion',1); | |
$html .= html_writer::tag('li', html_writer::tag('a', 'Force this activity to be complete <i class="fa fa-exclamation-triangle" aria-hidden="true" title="Try not to do this"></i>', array('href' => $link))); | |
/* | |
* IMPORTANT. To make completions take effect, you have to remove the cached values. Can't see how this is done programatically. | |
*/ | |
$link = html_writer::tag('a', 'purge caches', array("href" => new moodle_url("/admin/purgecaches.php"))); | |
$html .= html_writer::tag('li', "Remember to <i>$link</i> when you are done. <i class='fa fa-thumbs-up' aria-hidden='true' title='Pretty safe to do'></i>", array("style"=>"list-style-type:square")); | |
/* | |
* ok, before we start drawing tables, close the list | |
*/ | |
$html .= html_writer::end_tag('ul'); | |
$ul_closed = true; | |
// find and report on the current scorm track activity for this user/attempt | |
$tracks = $DB->get_records_sql( | |
" | |
SELECT | |
id, | |
element, | |
value | |
FROM | |
{scorm_scoes_track} | |
WHERE | |
scoid = ? | |
AND userid = ? | |
AND element IN | |
( | |
'cmi.core.lesson_status', | |
'cmi.completion_status', | |
'cmi.success_status', | |
'cmi.core.score.raw', | |
'cmi.score.raw', | |
'cmi.exit', | |
'cmi.core.exit' | |
) | |
", | |
array($scoid, $user_id) | |
); | |
$update_scorm_data_exit_status = ""; | |
$update_scorm_data_lesson_status = ""; | |
$update_scorm_data_lesson_score = ""; | |
foreach ($tracks as $track) { | |
if ($track->element === 'cmi.exit' || $track->element === 'cmi.core.exit') { | |
$update_scorm_data_exit_status = $track->value; | |
} elseif ($track->element === 'cmi.core.lesson_status' || $track->element === 'cmi.completion_status' || $track->element === 'cmi.success_status') { | |
$update_scorm_data_lesson_status = $track->value; | |
} elseif ($track->element === 'cmi.score.raw' || $track->element === 'cmi.core.score.raw') { | |
$update_scorm_data_lesson_score = $track->value; | |
} | |
} | |
$rows = []; | |
$row = new html_table_row(); | |
$row->cells[0] = new html_table_cell('<b>Scorm Data Editor</b>'); | |
$row->cells[0]->colspan = 2; | |
$row->cells[0]->header = true; | |
$rows[] = $row; | |
$row = new html_table_row(); | |
$row->cells[0] = new html_table_cell('<b>core-exit:</b>'); | |
$row->cells[1] = new html_table_cell('<input type="text" size="15" name="update_scorm_data_exit_status" value="' . $update_scorm_data_exit_status . '" placeholder="empty" />'); | |
$rows[] = $row; | |
$row = new html_table_row(); | |
$row->cells[0] = new html_table_cell('<b>lesson-status:</b>'); | |
$row->cells[1] = new html_table_cell('<input type="text" size="15" name="update_scorm_data_lesson_status" value="' . $update_scorm_data_lesson_status . '" placeholder="not-attempted, incomplete, completed" />'); | |
$rows[] = $row; | |
$row = new html_table_row(); | |
$row->cells[0] = new html_table_cell('<b>score-raw:</b>'); | |
$row->cells[1] = new html_table_cell('<input type="number" min="0" max="100" step="1" name="update_scorm_data_lesson_score" value="' . $update_scorm_data_lesson_score . '" />'); | |
$rows[] = $row; | |
$row = new html_table_row(); | |
$row->cells[0] = new html_table_cell(''); | |
$row->cells[1] = new html_table_cell('<button type="submit">Update</button> <i class="fa fa-frown-o" aria-hidden="true" title="Pretty safe, but frowned apon a bit"></i>'); | |
$rows[] = $row; | |
if ($modlog = $DB->get_records_sql("SELECT value, timemodified FROM {scorm_scoes_track} WHERE scoid = ? AND userid = ? AND element LIKE 'x.timshax.block.edit.%' ORDER BY timemodified DESC", array($scoid, $user_id))) { | |
$tr = new html_table_row(); | |
$tr->cells[0] = new html_table_cell('<b>Changes History</b>'); | |
$tr->cells[0]->colspan = 2; | |
$tr->cells[0]->header = true; | |
$rows[] = $tr; | |
foreach ($modlog as $row) { | |
$tr = new html_table_row(); | |
$tr->cells[0] = new html_table_cell($row->value); | |
$tr->cells[0]->colspan = 2; | |
$tr->cells[0]->attributes["title"] = userdate($row->timemodified); | |
$rows[] = $tr; | |
} | |
}; | |
$table = new html_table(); | |
$table->width = '100%'; | |
$table->data = $rows; | |
$rows = []; | |
$html .= '<form method="post" action="' . new moodle_url($this->page->url) . '">'; | |
$html .= '<input type="hidden" name="sesskey" value="' . sesskey() . '" />'; | |
$html .= html_writer::table($table); | |
$html .= '</form>'; | |
// dump out the completion block except in the context of the selected user (avoids log-in-as just to check it) | |
$info = new completion_info($this->page->course); | |
if ($info->is_tracked_user($user_id)) { | |
$completions = $info->get_completions($user_id); | |
// Generate markup for criteria statuses. | |
$data = ''; | |
// For aggregating activity completion. | |
$activities = array(); | |
$activities_complete = 0; | |
// For aggregating course prerequisites. | |
$prerequisites = array(); | |
$prerequisites_complete = 0; | |
// Flag to set if current completion data is inconsistent with what is stored in the database. | |
$pending_update = false; | |
// Loop through course criteria. | |
foreach ($completions as $completion) { | |
$criteria = $completion->get_criteria(); | |
$complete = $completion->is_complete(); | |
if (!$pending_update && $criteria->is_pending($completion)) { | |
$pending_update = true; | |
} | |
// Activities are a special case, so cache them and leave them till last. | |
if ($criteria->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) { | |
$activities[$criteria->moduleinstance] = $complete; | |
if ($complete) { | |
$activities_complete++; | |
} | |
continue; | |
} | |
// Prerequisites are also a special case, so cache them and leave them till last. | |
if ($criteria->criteriatype == COMPLETION_CRITERIA_TYPE_COURSE) { | |
$prerequisites[$criteria->courseinstance] = $complete; | |
if ($complete) { | |
$prerequisites_complete++; | |
} | |
continue; | |
} | |
$row = new html_table_row(); | |
$row->cells[0] = new html_table_cell($criteria->get_title()); | |
$row->cells[1] = new html_table_cell($completion->get_status()); | |
$row->cells[1]->style = 'text-align: right;'; | |
$srows[] = $row; | |
} | |
// Aggregate activities. | |
if (!empty($activities)) { | |
$a = new stdClass(); | |
$a->first = $activities_complete; | |
$a->second = count($activities); | |
$row = new html_table_row(); | |
$row->cells[0] = new html_table_cell(get_string('activitiescompleted', 'completion')); | |
$row->cells[1] = new html_table_cell(get_string('firstofsecond', 'block_completionstatus', $a)); | |
$row->cells[1]->style = 'text-align: right;'; | |
$srows[] = $row; | |
} | |
// Aggregate prerequisites. | |
if (!empty($prerequisites)) { | |
$a = new stdClass(); | |
$a->first = $prerequisites_complete; | |
$a->second = count($prerequisites); | |
$row = new html_table_row(); | |
$row->cells[0] = new html_table_cell(get_string('dependenciescompleted', 'completion')); | |
$row->cells[1] = new html_table_cell(get_string('firstofsecond', 'block_completionstatus', $a)); | |
$row->cells[1]->style = 'text-align: right;'; | |
$prows[] = $row; | |
$srows = array_merge($prows, $srows); | |
} | |
// Display completion status. | |
$table = new html_table(); | |
$table->width = '100%'; | |
$table->attributes = array('style'=>'font-size: 90%;', 'class'=>''); | |
$row = new html_table_row(); | |
$content = html_writer::tag('b', 'Completion status: '); | |
// $content .= html_writer::empty_tag('br'); | |
// Is course complete? | |
$coursecomplete = $info->is_course_complete($user_id); | |
// Load course completion. | |
$params = array( | |
'userid' => $user_id, | |
'course' => $this->page->course->id | |
); | |
$ccompletion = new completion_completion($params); | |
// Has this user completed any criteria? | |
$criteriacomplete = $info->count_course_user_data($user_id); | |
if ($pending_update) { | |
$content .= html_writer::tag('i', get_string('pending', 'completion')); | |
} else if ($coursecomplete) { | |
$content .= get_string('complete'); | |
} else if (!$criteriacomplete && !$ccompletion->timestarted) { | |
$content .= html_writer::tag('i', get_string('notyetstarted', 'completion')); | |
} else { | |
$content .= html_writer::tag('i', get_string('inprogress', 'completion')); | |
} | |
$row->cells[0] = new html_table_cell($content); | |
$row->cells[0]->colspan = '2'; | |
$rows[] = $row; | |
$row = new html_table_row(); | |
$content = ""; | |
// Get overall aggregation method. | |
$overall = $info->get_aggregation_method(); | |
if ($overall == COMPLETION_AGGREGATION_ALL) { | |
$content .= get_string('criteriarequiredall', 'completion'); | |
} else { | |
$content .= get_string('criteriarequiredany', 'completion'); | |
} | |
$content .= ':'; | |
$row->cells[0] = new html_table_cell($content); | |
$row->cells[0]->colspan = '2'; | |
$rows[] = $row; | |
$row = new html_table_row(); | |
$row->cells[0] = new html_table_cell(html_writer::tag('b', get_string('requiredcriteria', 'completion'))); | |
$row->cells[1] = new html_table_cell(html_writer::tag('b', get_string('status'))); | |
$row->cells[1]->style = 'text-align: right;'; | |
$rows[] = $row; | |
// Array merge $rows and $data here. | |
$rows = array_merge($rows, $srows); | |
$table->data = $rows; | |
$html .= "<p>Below is the completion status of the selected user.</p>"; | |
$html .= html_writer::table($table); | |
} | |
} | |
if (!$ul_closed) { | |
$html .= html_writer::end_tag('ul'); | |
} | |
} | |
$this->content = new stdClass(); | |
$this->content->text = $html; | |
$this->content->footer = ''; | |
return $this->content; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment