Commit a39918da authored by Eiz Eddin Al Katrib's avatar Eiz Eddin Al Katrib Committed by Jake Dallimore
Browse files

MDL-37361 completion: Enabled overriding activity completion status

parent d8e9a23c
......@@ -114,6 +114,126 @@ class core_completion_external extends external_api {
);
}
/**
* Describes the parameters for override_activity_completion_status.
*
* @return external_external_function_parameters
* @since Moodle 3.1
*/
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 Result and possible warnings
* @since Moodle 3.1
* @throws moodle_exception
*/
public static function override_activity_completion_status($userid, $cmid, $newstate) {
global $OUTPUT, $DB, $USER;
// 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'];
$warnings = array();
$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.
$completion->update_state($cm, $newstate, $userid, true);
// Get activity completion data.
$completiondata = $completion->get_data($cm, false, $userid);
$state = $completiondata->completionstate;
$overrideby = $completiondata->overrideby;
$date = userdate($completiondata->timemodified);
// Work out how it corresponds to an icon.
switch($state) {
case COMPLETION_INCOMPLETE :
$completiontype = 'n'.($overrideby ? '-override' : '');
break;
case COMPLETION_COMPLETE :
$completiontype = 'y'.($overrideby ? '-override' : '');
break;
case COMPLETION_COMPLETE_PASS :
$completiontype = 'pass';
break;
case COMPLETION_COMPLETE_FAIL :
$completiontype = 'fail';
break;
}
$completionicon = 'completion-'.
($cm->completion == COMPLETION_TRACKING_AUTOMATIC ? 'auto' : 'manual').
'-'.$completiontype;
$overridebyuser = $DB->get_record('user', array('id' => $USER->id), '*', MUST_EXIST);
$describe = get_string('completion-' . $completiontype, 'completion', fullname($overridebyuser));
$user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
$a = new StdClass;
$a->state = $describe;
$a->date = $date;
$a->user = fullname($user);
$a->activity = $cm->id;
$fulldescribe = get_string('progress-title', 'completion', $a);
$img = '<img src="'.$OUTPUT->pix_url('i/'.$completionicon).
'" alt="'.s($describe).'" title="'.s($fulldescribe).'" />';
// Set data values for next completion change.
$otherstate = ($state == COMPLETION_COMPLETE) ? COMPLETION_INCOMPLETE : COMPLETION_COMPLETE;
$changecompl = $userid . '-' . $cmid . '-' . $otherstate;
$result = array();
$result['status'] = true;
$result['warnings'] = $warnings;
$result['changecompl'] = $changecompl;
$result['img'] = $img;
return $result;
}
/**
* Describes the override_activity_completion_status return value.
*
* @return external_single_structure
* @since Moodle 3.1
*/
public static function override_activity_completion_status_returns() {
return new external_single_structure(
array(
'status' => new external_value(PARAM_BOOL, 'Status, true if success'),
'warnings' => new external_warnings(),
'changecompl' => new external_value(PARAM_ALPHANUMEXT, 'The new completion change data'),
'img' => new external_value(PARAM_RAW, 'Image element to replace existing one'),
)
);
}
/**
* Returns description of method parameters
*
......
......@@ -39,6 +39,7 @@ $string['aggregationmethod'] = 'Aggregation method';
$string['all'] = 'All';
$string['any'] = 'Any';
$string['approval'] = 'Approval';
$string['areyousureoverridecompletion'] = 'Are you sure you want to override the current completion state of this activity for this user and mark it "{$a}"?';
$string['badautocompletion'] = 'When you select automatic completion, you must also enable at least one requirement (below).';
$string['bulkactivitycompletion'] = 'Bulk edit activity completion';
$string['bulkactivitydetail'] = 'Select the activities you wish to bulk edit.';
......@@ -67,8 +68,10 @@ $string['completion-alt-manual-n'] = 'Not completed: {$a}. Select to mark as com
$string['completion-alt-manual-y'] = 'Completed: {$a}. Select to mark as not complete.';
$string['completion-fail'] = 'Completed (did not achieve pass grade)';
$string['completion-n'] = 'Not completed';
$string['completion-n-override'] = 'Not completed (overrride by {$a})';
$string['completion-pass'] = 'Completed (achieved pass grade)';
$string['completion-y'] = 'Completed';
$string['completion-y-override'] = 'Completed (overrride by {$a})';
$string['completion_automatic'] = 'Show activity as complete when conditions are met';
$string['completion_help'] = 'If enabled, activity completion is tracked, either manually or automatically, based on certain conditions. Multiple conditions may be set if desired. If so, the activity will only be considered complete when ALL conditions are met.
......
......@@ -172,6 +172,7 @@ $string['course:managegroups'] = 'Manage groups';
$string['course:managescales'] = 'Manage scales';
$string['course:markcomplete'] = 'Mark users as complete in course completion';
$string['course:movesections'] = 'Move sections';
$string['course:overridecompletion'] = 'Override activity completion status';
$string['course:publish'] = 'Publish a course';
$string['course:renameroles'] = 'Rename roles';
$string['course:request'] = 'Request new courses';
......
......@@ -66,8 +66,13 @@ class course_module_completion_updated extends base {
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' updated the completion state for the course module with id '$this->contextinstanceid' " .
"for the user with id '$this->relateduserid'.";
if (isset($this->other['overrideby']) && $this->other['overrideby']) {
return "The user with id '{$this->userid}' overrode the completion state to '{$this->other['completionstate']}' ".
"for the course module with id '{$this->contextinstanceid}' for the user with id '{$this->relateduserid}'.";
} else {
return "The user with id '{$this->userid}' updated the completion state for the course module with id " .
"'{$this->contextinstanceid}' for the user with id '{$this->relateduserid}'.";
}
}
/**
......
......@@ -548,9 +548,10 @@ class completion_info {
* result. For manual events, COMPLETION_COMPLETE or COMPLETION_INCOMPLETE
* must be used; these directly set the specified state.
* @param int $userid User ID to be updated. Default 0 = current user
* @param bool $override Whether manually overriding the existing completion state.
* @return void
*/
public function update_state($cm, $possibleresult=COMPLETION_UNKNOWN, $userid=0) {
public function update_state($cm, $possibleresult=COMPLETION_UNKNOWN, $userid=0, $override = false) {
global $USER;
// Do nothing if completion is not enabled for that activity
......@@ -569,8 +570,9 @@ class completion_info {
return;
}
if ($cm->completion == COMPLETION_TRACKING_MANUAL) {
// For manual tracking we set the result directly
if ($cm->completion == COMPLETION_TRACKING_MANUAL || $override) {
// For manual tracking, or if overriding the completion state manually,
// we set the result directly.
switch($possibleresult) {
case COMPLETION_COMPLETE:
case COMPLETION_INCOMPLETE:
......@@ -581,14 +583,22 @@ class completion_info {
}
} else {
// Automatic tracking; get new state
$newstate = $this->internal_get_state($cm, $userid, $current);
// Automatic tracking.
if ($current->overrideby) {
// If the current completion state has been set by override, do nothing
// as we don't want it to be changed automatically.
return;
} else {
// Get new state.
$newstate = $this->internal_get_state($cm, $userid, $current);
}
}
// If changed, update
if ($newstate != $current->completionstate) {
$current->completionstate = $newstate;
$current->timemodified = time();
$current->overrideby = $override ? $USER->id : null;
$this->internal_set_data($cm, $current);
}
}
......@@ -958,6 +968,7 @@ class completion_info {
$data['userid'] = $userid;
$data['completionstate'] = 0;
$data['viewed'] = 0;
$data['overrideby'] = null;
$data['timemodified'] = 0;
}
$cacheddata[$othercm->id] = $data;
......@@ -980,6 +991,7 @@ class completion_info {
$data['userid'] = $userid;
$data['completionstate'] = 0;
$data['viewed'] = 0;
$data['overrideby'] = null;
$data['timemodified'] = 0;
}
......@@ -1047,7 +1059,9 @@ class completion_info {
'context' => $cmcontext,
'relateduserid' => $data->userid,
'other' => array(
'relateduserid' => $data->userid
'relateduserid' => $data->userid,
'overrideby' => $data->overrideby,
'completionstate' => $data->completionstate
)
));
$event->add_record_snapshot('course_modules_completion', $data);
......
......@@ -1929,6 +1929,15 @@ $capabilities = array(
'manager' => CAP_ALLOW
)
),
'moodle/course:overridecompletion' => array(
'captype' => 'write',
'contextlevel' => CONTEXT_COURSE,
'archetypes' => array(
'teacher' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW,
'manager' => CAP_ALLOW
)
),
'moodle/community:add' => array(
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
......
......@@ -322,6 +322,7 @@
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="ID of user who has (or hasn't) completed the activity."/>
<FIELD NAME="completionstate" TYPE="int" LENGTH="1" NOTNULL="true" SEQUENCE="false" COMMENT="Whether or not the user has completed the activity. Available states: 0 = not completed [if there's no row in this table, that also counts as 0] 1 = completed 2 = completed, show passed 3 = completed, show failed"/>
<FIELD NAME="viewed" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Tracks whether or not this activity has been viewed. NULL = we are not tracking viewed for this activity 0 = not viewed 1 = viewed"/>
<FIELD NAME="overrideby" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Tracks whether this completion state has been set manually to override a previous state."/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Time at which the completion state last changed."/>
</FIELDS>
<KEYS>
......
......@@ -276,6 +276,14 @@ $functions = array(
'type' => 'write',
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
),
'core_completion_override_activity_completion_status' => array(
'classname' => 'core_completion_external',
'methodname' => 'override_activity_completion_status',
'description' => 'Update completion status for a user in an activity by overriding it.',
'type' => 'write',
'capabilities' => 'moodle/course:overridecompletion',
'ajax' => true,
),
'core_course_create_categories' => array(
'classname' => 'core_course_external',
'methodname' => 'create_categories',
......
......@@ -2601,5 +2601,19 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2017092900.00);
}
if ($oldversion < 2017100600.01) {
// Define field override to be added to course_modules_completion.
$table = new xmldb_table('course_modules_completion');
$field = new xmldb_field('overrideby', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'viewed');
// Conditionally launch add field override.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Main savepoint reached.
upgrade_main_savepoint(true, 2017100600.01);
}
return true;
}
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M10 0v2H6V0h4zm1 2h1c1.1 0 2 .9 2 2v1h2V2c0-1.1-.9-2-2-2h-3v2zm5 4h-2v4h2V6zm-2 5v1c0 1.1-.9 2-2 2h-1v2h3c1.1 0 2-.9 2-2v-3h-2zm-4 5v-2H6v2h4zm-5-2H4c-1.1 0-2-.9-2-2v-1H0v3c0 1.1.9 2 2 2h3v-2zm-5-4h2V6H0v4zm2-5V4c0-1.1.9-2 2-2h1V0H2C.9 0 0 .9 0 2v3h2z" fill="#FF2727"/></svg>
\ No newline at end of file
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M10 0v2H6V0h4zm1 2h1c.4 0 .8.1 1.1.3.3-.3.8-.4 1.2-.4.5 0 1 .2 1.4.6l.3.3V2c0-1.1-.9-2-2-2h-3v2zm3 8h2V6.4l-2 2V10zm0 1v1c0 1.1-.9 2-2 2h-1v2h3c1.1 0 2-.9 2-2v-3h-2zm-4 5v-2H6v2h4zm-5-2H4c-1.1 0-2-.9-2-2v-1H0v3c0 1.1.9 2 2 2h3v-2zm-5-4h2V6H0v4zm2-5V4c0-1.1.9-2 2-2h1V0H2C.9 0 0 .9 0 2v3h2z" fill="#FF2727"/><path d="M15.7 3.9l-.7-.7c-.4-.4-1-.4-1.4 0l-6 6L5.4 7c-.4-.3-1-.3-1.4 0l-.7.7c-.4.4-.4 1 0 1.4l3.6 3.6c.4.4 1 .4 1.4 0l7.4-7.4c.4-.4.4-1 0-1.4z" fill="#76A1F0"/></svg>
\ No newline at end of file
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M14 0H2C.9 0 0 .9 0 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V2c0-1.1-.9-2-2-2zm0 12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h8c1.1 0 2 .9 2 2v8z" fill="#FF2727"/></svg>
\ No newline at end of file
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M14 8.4V12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h8c.4 0 .8.1 1.1.3.3-.3.8-.4 1.2-.4.5 0 1 .2 1.4.6l.3.3V2c0-1.1-.9-2-2-2H2C.9 0 0 .9 0 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V6.4l-2 2z" fill="#FF2727"/><path d="M15.7 3.9l-.7-.7c-.4-.4-1-.4-1.4 0l-6 6L5.4 7c-.4-.3-1-.3-1.4 0l-.7.7c-.4.4-.4 1 0 1.4l3.6 3.6c.4.4 1 .4 1.4 0l7.4-7.4c.4-.4.4-1 0-1.4z" fill="#76A1F0"/></svg>
\ No newline at end of file
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
// 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/>.
/**
* AMD module to handle overriding activity completion status.
*
* @module report_progress/completion_override
* @package report_progress
* @copyright 2016 onwards Eiz Eddin Al Katrib <eiz@barasoft.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.1
*/
define(['jquery', 'core/ajax', 'core/str', 'core/notification'],
function($, ajax, str, notification) {
return /** @alias module:report_progress/completion_override */ {
/**
* Change the activity completion state.
*
* @method change
*/
update: function() {
$('#completion-progress a.changecompl').on('click', function(e) {
e.preventDefault();
var el = $(this);
var changecompl = el.data('changecompl');
var changecomplfields = changecompl.split('-');
var userid = changecomplfields[0];
var cmid = changecomplfields[1];
var newstate = changecomplfields[2];
var newstatestr = (newstate == 1) ? 'completion-y' : 'completion-n';
str.get_strings([
{key: newstatestr, component: 'completion'}
]).done(function(strings) {
str.get_strings([
{key: 'confirm', component: 'moodle'},
{key: 'areyousureoverridecompletion', component: 'completion', param: strings[0]},
{key: 'yes', component: 'moodle'},
{key: 'cancel', component: 'moodle'}
]).done(function(strings) {
notification.confirm(
strings[0], // Confirm.
strings[1], // Message.
strings[2], // Yes.
strings[3], // Cancel.
function() {
el.append('<div class="ajaxworking" />');
var promise = ajax.call([{
methodname: 'core_completion_override_activity_completion_status',
args: {
userid: userid, cmid: cmid, newstate: newstate
}
}]);
promise[0].then(function(results) {
el.data('changecompl', results.changecompl);
el.attr('data-changecompl', results.changecompl);
el.children("img").replaceWith(results.img);
$('.ajaxworking').remove();
}).fail(notification.exception);
}
);
}).fail(notification.exception);
}).fail(notification.exception);
});
}
};
});
......@@ -51,6 +51,9 @@ $sifirst = optional_param('sifirst', 'all', PARAM_NOTAGS);
$silast = optional_param('silast', 'all', PARAM_NOTAGS);
$start = optional_param('start', 0, PARAM_INT);
// Action.
$changecompl = optional_param('changecompl', '', PARAM_ALPHANUMEXT);
// Whether to show extra user identity information
$extrafields = get_extra_user_fields($context);
$leftcols = 1 + count($extrafields);
......@@ -74,6 +77,12 @@ if ($format !== '') {
if ($start !== 0) {
$url->param('start', $start);
}
if ($sifirst !== 'all') {
$url->param('sifirst', $sifirst);
}
if ($silast !== 'all') {
$url->param('silast', $silast);
}
$PAGE->set_url($url);
$PAGE->set_pagelayout('report');
......@@ -94,6 +103,20 @@ $reportsurl = $CFG->wwwroot.'/course/report.php?id='.$course->id;
$completion = new completion_info($course);
$activities = $completion->get_activities();
if ($changecompl) {
if ($changecompl) {
require_capability('moodle/course:overridecompletion', $context);
require_sesskey();
list($userid, $cmid, $newstate) = preg_split('/-/', $changecompl, 3);
// Make sure the activity and user are tracked.
if (isset($activities[$cmid]) &&
$completion->get_num_tracked_users('u.id = :userid', array('userid' => (int)$userid), $group)) {
$completion->update_state($activities[$cmid], $newstate, $userid, true);
}
redirect($PAGE->url);
}
}
if ($sifirst !== 'all') {
set_user_preference('ifirst', $sifirst);
}
......@@ -173,6 +196,7 @@ if ($csv && $grandtotal && count($activities)>0) { // Only show CSV if there are
$PAGE->set_title($strcompletion);
$PAGE->set_heading($course->fullname);
echo $OUTPUT->header();
$PAGE->requires->js_call_amd('report_progress/completion_override', 'update');
// Handle groups (if enabled)
groups_print_course_menu($course,$CFG->wwwroot.'/report/progress/?course='.$course->id);
......@@ -363,25 +387,40 @@ foreach($progress as $user) {
if (array_key_exists($activity->id,$user->progress)) {
$thisprogress=$user->progress[$activity->id];
$state=$thisprogress->completionstate;
$overrideby = $thisprogress->overrideby;
$date=userdate($thisprogress->timemodified);
} else {
$state=COMPLETION_INCOMPLETE;
$overrideby = 0;
$date='';
}
// Work out how it corresponds to an icon
switch($state) {
case COMPLETION_INCOMPLETE : $completiontype='n'; break;
case COMPLETION_COMPLETE : $completiontype='y'; break;
case COMPLETION_COMPLETE_PASS : $completiontype='pass'; break;
case COMPLETION_COMPLETE_FAIL : $completiontype='fail'; break;
case COMPLETION_INCOMPLETE :
$completiontype = 'n'.($overrideby ? '-override' : '');
break;
case COMPLETION_COMPLETE :
$completiontype = 'y'.($overrideby ? '-override' : '');
break;
case COMPLETION_COMPLETE_PASS :
$completiontype = 'pass';
break;
case COMPLETION_COMPLETE_FAIL :
$completiontype = 'fail';
break;
}
$completionicon='completion-'.
($activity->completion==COMPLETION_TRACKING_AUTOMATIC ? 'auto' : 'manual').
'-'.$completiontype;
$describe = get_string('completion-' . $completiontype, 'completion');
if ($overrideby) {
$overridebyuser = $DB->get_record('user', array('id' => $overrideby), '*', MUST_EXIST);
$describe = get_string('completion-' . $completiontype, 'completion', fullname($overridebyuser));
} else {
$describe = get_string('completion-' . $completiontype, 'completion');
}
$a=new StdClass;
$a->state=$describe;
$a->date=$date;
......@@ -392,8 +431,19 @@ foreach($progress as $user) {
if ($csv) {
print $sep.csv_quote($describe).$sep.csv_quote($date);
} else {
$celltext = '<img src="'.$OUTPUT->image_url('i/'.$completionicon).
'" alt="'.s($describe).'" title="'.s($fulldescribe).'" />';
if (has_capability('moodle/course:overridecompletion', $context) &&
$state != COMPLETION_COMPLETE_PASS && $state != COMPLETION_COMPLETE_FAIL) {
$newstate = ($state == COMPLETION_COMPLETE) ? COMPLETION_INCOMPLETE : COMPLETION_COMPLETE;
$changecompl = $user->id . '-' . $activity->id . '-' . $newstate;
$url = new moodle_url($PAGE->url, array('sesskey' => sesskey(),
'changecompl' => $changecompl));
$celltext = html_writer::link($url, $celltext, array('class' => 'changecompl',
'data-changecompl' => $changecompl));
}
print '<td class="completion-progresscell '.$formattedactivities[$activity->id]->datepassedclass.'">'.
$OUTPUT->pix_icon('i/' . $completionicon, $fulldescribe) . '</td>';
$celltext . '</td>';
}
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment