Skip to content

Instantly share code, notes, and snippets.

@frumbert
Last active September 11, 2020 12:58
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save frumbert/2d8d5d2729aec8889e1555dd7956887a to your computer and use it in GitHub Desktop.
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)
<?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