Merge branch 'MDL-37361-master-revised' of https://github.com/snake/moodle
Conflicts: lib/db/upgrade.php version.php
@ -114,6 +114,86 @@ class core_completion_external extends external_api {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the parameters for override_activity_completion_status.
|
||||
*
|
||||
* @return external_external_function_parameters
|
||||
* @since Moodle 3.4
|
||||
*/
|
||||
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 Array containing the current (updated) completion status.
|
||||
* @since Moodle 3.4
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public static function override_activity_completion_status($userid, $cmid, $newstate) {
|
||||
// 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'];
|
||||
|
||||
$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 and get the new state back.
|
||||
$completion->update_state($cm, $newstate, $userid, true);
|
||||
$completiondata = $completion->get_data($cm, false, $userid);
|
||||
|
||||
// Return the current state of completion.
|
||||
return [
|
||||
'cmid' => $completiondata->coursemoduleid,
|
||||
'userid' => $completiondata->userid,
|
||||
'state' => $completiondata->completionstate,
|
||||
'timecompleted' => $completiondata->timemodified,
|
||||
'overrideby' => $completiondata->overrideby,
|
||||
'tracking' => $completion->is_enabled($cm)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the override_activity_completion_status return value.
|
||||
*
|
||||
* @return external_single_structure
|
||||
* @since Moodle 3.4
|
||||
*/
|
||||
public static function override_activity_completion_status_returns() {
|
||||
|
||||
return new external_single_structure(
|
||||
array(
|
||||
'cmid' => new external_value(PARAM_INT, 'The course module id'),
|
||||
'userid' => new external_value(PARAM_INT, 'The user id to which the completion info belongs'),
|
||||
'state' => new external_value(PARAM_INT, 'The current completion state.'),
|
||||
'timecompleted' => new external_value(PARAM_INT, 'time of completion'),
|
||||
'overrideby' => new external_value(PARAM_INT, 'The user id who has overriden the status, or null'),
|
||||
'tracking' => new external_value(PARAM_INT, 'type of tracking:
|
||||
0 means none, 1 manual, 2 automatic'),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns description of method parameters
|
||||
*
|
||||
|
@ -186,6 +186,83 @@ class core_completion_externallib_testcase extends externallib_advanced_testcase
|
||||
$this->assertCount(3, $result['statuses']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test override_activity_completion_status
|
||||
*/
|
||||
public function test_override_activity_completion_status() {
|
||||
global $DB, $CFG;
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
// Create course with teacher and student enrolled.
|
||||
$CFG->enablecompletion = true;
|
||||
$course = $this->getDataGenerator()->create_course(['enablecompletion' => 1]);
|
||||
$student = $this->getDataGenerator()->create_user();
|
||||
$teacher = $this->getDataGenerator()->create_user();
|
||||
$studentrole = $DB->get_record('role', array('shortname' => 'student'));
|
||||
$this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
|
||||
$teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
|
||||
$this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
|
||||
// Create 2 activities, one with manual completion (data), one with automatic completion triggered by viewiung it (forum).
|
||||
$data = $this->getDataGenerator()->create_module('data', ['course' => $course->id], ['completion' => 1]);
|
||||
$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id],
|
||||
['completion' => 2, 'completionview' => 1]);
|
||||
$cmdata = get_coursemodule_from_id('data', $data->cmid);
|
||||
$cmforum = get_coursemodule_from_id('forum', $forum->cmid);
|
||||
|
||||
// Manually complete the data activity as the student.
|
||||
$this->setUser($student);
|
||||
$completion = new completion_info($course);
|
||||
$completion->update_state($cmdata, COMPLETION_COMPLETE);
|
||||
|
||||
// Test overriding the status of the manual-completion-activity 'incomplete'.
|
||||
$this->setUser($teacher);
|
||||
$result = core_completion_external::override_activity_completion_status($student->id, $data->cmid, COMPLETION_INCOMPLETE);
|
||||
$result = external_api::clean_returnvalue(core_completion_external::override_activity_completion_status_returns(), $result);
|
||||
$this->assertEquals($result['state'], COMPLETION_INCOMPLETE);
|
||||
$completiondata = $completion->get_data($cmdata, false, $student->id);
|
||||
$this->assertEquals(COMPLETION_INCOMPLETE, $completiondata->completionstate);
|
||||
|
||||
// Test overriding the status of the manual-completion-activity back to 'complete'.
|
||||
$result = core_completion_external::override_activity_completion_status($student->id, $data->cmid, COMPLETION_COMPLETE);
|
||||
$result = external_api::clean_returnvalue(core_completion_external::override_activity_completion_status_returns(), $result);
|
||||
$this->assertEquals($result['state'], COMPLETION_COMPLETE);
|
||||
$completiondata = $completion->get_data($cmdata, false, $student->id);
|
||||
$this->assertEquals(COMPLETION_COMPLETE, $completiondata->completionstate);
|
||||
|
||||
// Test overriding the status of the auto-completion-activity to 'complete'.
|
||||
$result = core_completion_external::override_activity_completion_status($student->id, $forum->cmid, COMPLETION_COMPLETE);
|
||||
$result = external_api::clean_returnvalue(core_completion_external::override_activity_completion_status_returns(), $result);
|
||||
$this->assertEquals($result['state'], COMPLETION_COMPLETE);
|
||||
$completionforum = $completion->get_data($cmforum, false, $student->id);
|
||||
$this->assertEquals(COMPLETION_COMPLETE, $completionforum->completionstate);
|
||||
|
||||
// Test overriding the status of the auto-completion-activity to 'incomplete'.
|
||||
$result = core_completion_external::override_activity_completion_status($student->id, $forum->cmid, COMPLETION_INCOMPLETE);
|
||||
$result = external_api::clean_returnvalue(core_completion_external::override_activity_completion_status_returns(), $result);
|
||||
$this->assertEquals($result['state'], COMPLETION_INCOMPLETE);
|
||||
$completionforum = $completion->get_data($cmforum, false, $student->id);
|
||||
$this->assertEquals(COMPLETION_INCOMPLETE, $completionforum->completionstate);
|
||||
|
||||
// Test overriding the status of the auto-completion-activity to an invalid state. It should remain incomplete.
|
||||
$this->expectException('moodle_exception');
|
||||
$result = core_completion_external::override_activity_completion_status($student->id, $forum->cmid, 3);
|
||||
$result = external_api::clean_returnvalue(core_completion_external::override_activity_completion_status_returns(), $result);
|
||||
$this->assertEquals($result['state'], COMPLETION_INCOMPLETE);
|
||||
$completionforum = $completion->get_data($cmforum, false, $student->id);
|
||||
$this->assertEquals(COMPLETION_INCOMPLETE, $completionforum->completionstate);
|
||||
|
||||
// Test overriding the status of the auto-completion-activity for a user without capabilities. It should remain incomplete.
|
||||
$this->expectException('moodle_exception');
|
||||
unassign_capability('moodle/course:overridecompletion', $teacherrole->id, $coursecontext);
|
||||
$result = core_completion_external::override_activity_completion_status($student->id, $forum->cmid, 1);
|
||||
$result = external_api::clean_returnvalue(core_completion_external::override_activity_completion_status_returns(), $result);
|
||||
$this->assertEquals($result['state'], COMPLETION_INCOMPLETE);
|
||||
$completionforum = $completion->get_data($cmforum, false, $student->id);
|
||||
$this->assertEquals(COMPLETION_INCOMPLETE, $completionforum->completionstate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_course_completion_status
|
||||
*/
|
||||
|
@ -456,7 +456,7 @@ class core_course_renderer extends plugin_renderer_base {
|
||||
* @return string
|
||||
*/
|
||||
public function course_section_cm_completion($course, &$completioninfo, cm_info $mod, $displayoptions = array()) {
|
||||
global $CFG;
|
||||
global $CFG, $DB;
|
||||
$output = '';
|
||||
if (!empty($displayoptions['hidecompletion']) || !isloggedin() || isguestuser() || !$mod->uservisible) {
|
||||
return $output;
|
||||
@ -485,16 +485,20 @@ class core_course_renderer extends plugin_renderer_base {
|
||||
} else if ($completion == COMPLETION_TRACKING_MANUAL) {
|
||||
switch($completiondata->completionstate) {
|
||||
case COMPLETION_INCOMPLETE:
|
||||
$completionicon = 'manual-n'; break;
|
||||
$completionicon = 'manual-n' . ($completiondata->overrideby ? '-override' : '');
|
||||
break;
|
||||
case COMPLETION_COMPLETE:
|
||||
$completionicon = 'manual-y'; break;
|
||||
$completionicon = 'manual-y' . ($completiondata->overrideby ? '-override' : '');
|
||||
break;
|
||||
}
|
||||
} else { // Automatic
|
||||
switch($completiondata->completionstate) {
|
||||
case COMPLETION_INCOMPLETE:
|
||||
$completionicon = 'auto-n'; break;
|
||||
$completionicon = 'auto-n' . ($completiondata->overrideby ? '-override' : '');
|
||||
break;
|
||||
case COMPLETION_COMPLETE:
|
||||
$completionicon = 'auto-y'; break;
|
||||
$completionicon = 'auto-y' . ($completiondata->overrideby ? '-override' : '');
|
||||
break;
|
||||
case COMPLETION_COMPLETE_PASS:
|
||||
$completionicon = 'auto-pass'; break;
|
||||
case COMPLETION_COMPLETE_FAIL:
|
||||
@ -503,7 +507,15 @@ class core_course_renderer extends plugin_renderer_base {
|
||||
}
|
||||
if ($completionicon) {
|
||||
$formattedname = $mod->get_formatted_name();
|
||||
$imgalt = get_string('completion-alt-' . $completionicon, 'completion', $formattedname);
|
||||
if ($completiondata->overrideby) {
|
||||
$args = new stdClass();
|
||||
$args->modname = $formattedname;
|
||||
$overridebyuser = \core_user::get_user($completiondata->overrideby, '*', MUST_EXIST);
|
||||
$args->overrideuser = fullname($overridebyuser);
|
||||
$imgalt = get_string('completion-alt-' . $completionicon, 'completion', $args);
|
||||
} else {
|
||||
$imgalt = get_string('completion-alt-' . $completionicon, 'completion', $formattedname);
|
||||
}
|
||||
|
||||
if ($this->page->user_is_editing()) {
|
||||
// When editing, the icon is just an image.
|
||||
@ -512,7 +524,6 @@ class core_course_renderer extends plugin_renderer_base {
|
||||
$output .= html_writer::tag('span', $this->output->render($completionpixicon),
|
||||
array('class' => 'autocompletion'));
|
||||
} else if ($completion == COMPLETION_TRACKING_MANUAL) {
|
||||
$imgtitle = get_string('completion-title-' . $completionicon, 'completion', $formattedname);
|
||||
$newstate =
|
||||
$completiondata->completionstate == COMPLETION_COMPLETE
|
||||
? COMPLETION_INCOMPLETE
|
||||
|
@ -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.';
|
||||
@ -60,15 +61,21 @@ $string['completion'] = 'Completion tracking';
|
||||
$string['completion-alt-auto-enabled'] = 'The system marks this item complete according to conditions: {$a}';
|
||||
$string['completion-alt-auto-fail'] = 'Completed: {$a} (did not achieve pass grade)';
|
||||
$string['completion-alt-auto-n'] = 'Not completed: {$a}';
|
||||
$string['completion-alt-auto-n-override'] = 'Not completed: {$a->modname} (set by {$a->overrideuser})';
|
||||
$string['completion-alt-auto-pass'] = 'Completed: {$a} (achieved pass grade)';
|
||||
$string['completion-alt-auto-y'] = 'Completed: {$a}';
|
||||
$string['completion-alt-auto-y-override'] = 'Completed: {$a->modname} (set by {$a->overrideuser})';
|
||||
$string['completion-alt-manual-enabled'] = 'Students can manually mark this item complete: {$a}';
|
||||
$string['completion-alt-manual-n'] = 'Not completed: {$a}. Select to mark as complete.';
|
||||
$string['completion-alt-manual-n-override'] = 'Not completed: {$a->modname} (set by {$a->overrideuser}). Select to mark as complete.';
|
||||
$string['completion-alt-manual-y'] = 'Completed: {$a}. Select to mark as not complete.';
|
||||
$string['completion-alt-manual-y-override'] = 'Completed: {$a->modname} (set by {$a->overrideuser}). 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 (set by {$a})';
|
||||
$string['completion-pass'] = 'Completed (achieved pass grade)';
|
||||
$string['completion-y'] = 'Completed';
|
||||
$string['completion-y-override'] = 'Completed (set 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}'.";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -122,6 +127,7 @@ class course_module_completion_updated extends base {
|
||||
public static function get_other_mapping() {
|
||||
$othermapped = array();
|
||||
$othermapped['relateduserid'] = array('db' => 'user', 'restore' => 'user');
|
||||
$othermapped['overrideby'] = array('db' => 'user', 'restore' => 'user');
|
||||
|
||||
return $othermapped;
|
||||
}
|
||||
|
@ -519,6 +519,16 @@ class completion_info {
|
||||
return $ccompletion->is_complete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the supplied user can override the activity completion statuses within the current course.
|
||||
*
|
||||
* @param stdClass $user The user object.
|
||||
* @return bool True if the user can override, false otherwise.
|
||||
*/
|
||||
public function user_can_override_completion($user) {
|
||||
return has_capability('moodle/course:overridecompletion', context_course::instance($this->course_id), $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates (if necessary) the completion state of activity $cm for the given
|
||||
* user.
|
||||
@ -548,9 +558,11 @@ 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
|
||||
* @throws moodle_exception if trying to override without permission.
|
||||
*/
|
||||
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
|
||||
@ -558,6 +570,14 @@ class completion_info {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're processing an override and the current user isn't allowed to do so, then throw an exception.
|
||||
if ($override) {
|
||||
if (!$this->user_can_override_completion($USER)) {
|
||||
throw new required_capability_exception(context_course::instance($this->course_id),
|
||||
'moodle/course:overridecompletion', 'nopermission', '');
|
||||
}
|
||||
}
|
||||
|
||||
// Get current value of completion state and do nothing if it's same as
|
||||
// the possible result of this change. If the change is to COMPLETE and the
|
||||
// current value is one of the COMPLETE_xx subtypes, ignore that as well
|
||||
@ -569,8 +589,17 @@ class completion_info {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($cm->completion == COMPLETION_TRACKING_MANUAL) {
|
||||
// For manual tracking we set the result directly
|
||||
// For auto tracking, if the status is overridden to 'COMPLETION_COMPLETE', then disallow further changes,
|
||||
// unless processing another override.
|
||||
// Basically, we want those activities which have been overridden to COMPLETE to hold state, and those which have been
|
||||
// overridden to INCOMPLETE to still be processed by normal completion triggers.
|
||||
if ($cm->completion == COMPLETION_TRACKING_AUTOMATIC && !is_null($current->overrideby)
|
||||
&& $current->completionstate == COMPLETION_COMPLETE && !$override) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For manual tracking, or if overriding the completion state, we set the state directly.
|
||||
if ($cm->completion == COMPLETION_TRACKING_MANUAL || $override) {
|
||||
switch($possibleresult) {
|
||||
case COMPLETION_COMPLETE:
|
||||
case COMPLETION_INCOMPLETE:
|
||||
@ -581,7 +610,6 @@ class completion_info {
|
||||
}
|
||||
|
||||
} else {
|
||||
// Automatic tracking; get new state
|
||||
$newstate = $this->internal_get_state($cm, $userid, $current);
|
||||
}
|
||||
|
||||
@ -589,6 +617,7 @@ class completion_info {
|
||||
if ($newstate != $current->completionstate) {
|
||||
$current->completionstate = $newstate;
|
||||
$current->timemodified = time();
|
||||
$current->overrideby = $override ? $USER->id : null;
|
||||
$this->internal_set_data($cm, $current);
|
||||
}
|
||||
}
|
||||
@ -698,8 +727,9 @@ class completion_info {
|
||||
// Get current completion state
|
||||
$data = $this->get_data($cm, false, $userid);
|
||||
|
||||
// If we already viewed it, don't do anything
|
||||
if ($data->viewed == COMPLETION_VIEWED) {
|
||||
// If we already viewed it, don't do anything unless the completion status is overridden.
|
||||
// If the completion status is overridden, then we need to allow this 'view' to trigger automatic completion again.
|
||||
if ($data->viewed == COMPLETION_VIEWED && empty($data->overrideby)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -958,6 +988,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 +1011,7 @@ class completion_info {
|
||||
$data['userid'] = $userid;
|
||||
$data['completionstate'] = 0;
|
||||
$data['viewed'] = 0;
|
||||
$data['overrideby'] = null;
|
||||
$data['timemodified'] = 0;
|
||||
}
|
||||
|
||||
@ -1047,7 +1079,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);
|
||||
|
@ -1933,6 +1933,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',
|
||||
|
@ -2664,5 +2664,19 @@ function xmldb_main_upgrade($oldversion) {
|
||||
upgrade_main_savepoint(true, 2017101000.00);
|
||||
}
|
||||
|
||||
if ($oldversion < 2017101000.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, 2017101000.01);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -128,10 +128,10 @@ class core_completionlib_testcase extends advanced_testcase {
|
||||
$this->mock_setup();
|
||||
|
||||
$mockbuilder = $this->getMockBuilder('completion_info');
|
||||
$mockbuilder->setMethods(array('is_enabled', 'get_data', 'internal_get_state', 'internal_set_data'));
|
||||
$mockbuilder->setMethods(array('is_enabled', 'get_data', 'internal_get_state', 'internal_set_data',
|
||||
'user_can_override_completion'));
|
||||
$mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
|
||||
$c = $mockbuilder->getMock();
|
||||
|
||||
$cm = (object)array('id'=>13, 'course'=>42);
|
||||
|
||||
// Not enabled, should do nothing.
|
||||
@ -142,7 +142,7 @@ class core_completionlib_testcase extends advanced_testcase {
|
||||
$c->update_state($cm);
|
||||
|
||||
// Enabled, but current state is same as possible result, do nothing.
|
||||
$current = (object)array('completionstate'=>COMPLETION_COMPLETE);
|
||||
$current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
|
||||
$c->expects($this->at(0))
|
||||
->method('is_enabled')
|
||||
->with($cm)
|
||||
@ -200,7 +200,7 @@ class core_completionlib_testcase extends advanced_testcase {
|
||||
|
||||
// Auto, change state.
|
||||
$cm = (object)array('id'=>13, 'course'=>42, 'completion'=>COMPLETION_TRACKING_AUTOMATIC);
|
||||
$current = (object)array('completionstate'=>COMPLETION_COMPLETE);
|
||||
$current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
|
||||
$c->expects($this->at(0))
|
||||
->method('is_enabled')
|
||||
->with($cm)
|
||||
@ -221,6 +221,107 @@ class core_completionlib_testcase extends advanced_testcase {
|
||||
->method('internal_set_data')
|
||||
->with($cm, $comparewith);
|
||||
$c->update_state($cm, COMPLETION_COMPLETE_PASS);
|
||||
|
||||
// Manual tracking, change state by overriding it manually.
|
||||
$cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_MANUAL);
|
||||
$current = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null);
|
||||
$c->expects($this->at(0))
|
||||
->method('is_enabled')
|
||||
->with($cm)
|
||||
->will($this->returnValue(true));
|
||||
$c->expects($this->at(1)) // Pretend the user has the required capability for overriding completion statuses.
|
||||
->method('user_can_override_completion')
|
||||
->will($this->returnValue(true));
|
||||
$c->expects($this->at(2))
|
||||
->method('get_data')
|
||||
->with($cm, false, 100)
|
||||
->will($this->returnValue($current));
|
||||
$changed = clone($current);
|
||||
$changed->timemodified = time();
|
||||
$changed->completionstate = COMPLETION_COMPLETE;
|
||||
$changed->overrideby = 314159;
|
||||
$comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
|
||||
$comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
|
||||
$c->expects($this->at(3))
|
||||
->method('internal_set_data')
|
||||
->with($cm, $comparewith);
|
||||
$c->update_state($cm, COMPLETION_COMPLETE, 100, true);
|
||||
// And confirm that the status can be changed back to incomplete without an override.
|
||||
$c->update_state($cm, COMPLETION_INCOMPLETE, 100);
|
||||
$c->expects($this->at(0))
|
||||
->method('get_data')
|
||||
->with($cm, false, 100)
|
||||
->will($this->returnValue($current));
|
||||
$c->get_data($cm, false, 100);
|
||||
|
||||
// Auto, change state via override, incomplete to complete.
|
||||
$cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
|
||||
$current = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null);
|
||||
$c->expects($this->at(0))
|
||||
->method('is_enabled')
|
||||
->with($cm)
|
||||
->will($this->returnValue(true));
|
||||
$c->expects($this->at(1)) // Pretend the user has the required capability for overriding completion statuses.
|
||||
->method('user_can_override_completion')
|
||||
->will($this->returnValue(true));
|
||||
$c->expects($this->at(2))
|
||||
->method('get_data')
|
||||
->with($cm, false, 100)
|
||||
->will($this->returnValue($current));
|
||||
$changed = clone($current);
|
||||
$changed->timemodified = time();
|
||||
$changed->completionstate = COMPLETION_COMPLETE;
|
||||
$changed->overrideby = 314159;
|
||||
$comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
|
||||
$comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
|
||||
$c->expects($this->at(3))
|
||||
->method('internal_set_data')
|
||||
->with($cm, $comparewith);
|
||||
$c->update_state($cm, COMPLETION_COMPLETE, 100, true);
|
||||
$c->expects($this->at(0))
|
||||
->method('get_data')
|
||||
->with($cm, false, 100)
|
||||
->will($this->returnValue($changed));
|
||||
$c->get_data($cm, false, 100);
|
||||
|
||||
// Now confirm that the status cannot be changed back to incomplete without an override.
|
||||
// I.e. test that automatic completion won't trigger a change back to COMPLETION_INCOMPLETE when overridden.
|
||||
$c->update_state($cm, COMPLETION_INCOMPLETE, 100);
|
||||
$c->expects($this->at(0))
|
||||
->method('get_data')
|
||||
->with($cm, false, 100)
|
||||
->will($this->returnValue($changed));
|
||||
$c->get_data($cm, false, 100);
|
||||
|
||||
// Now confirm the status can be changed back from complete to incomplete using an override.
|
||||
$cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
|
||||
$current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => 2);
|
||||
$c->expects($this->at(0))
|
||||
->method('is_enabled')
|
||||
->with($cm)
|
||||
->will($this->returnValue(true));
|
||||
$c->expects($this->at(1)) // Pretend the user has the required capability for overriding completion statuses.
|
||||
->method('user_can_override_completion')
|
||||
->will($this->returnValue(true));
|
||||
$c->expects($this->at(2))
|
||||
->method('get_data')
|
||||
->with($cm, false, 100)
|
||||
->will($this->returnValue($current));
|
||||
$changed = clone($current);
|
||||
$changed->timemodified = time();
|
||||
$changed->completionstate = COMPLETION_INCOMPLETE;
|
||||
$changed->overrideby = 314159;
|
||||
$comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
|
||||
$comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
|
||||
$c->expects($this->at(3))
|
||||
->method('internal_set_data')
|
||||
->with($cm, $comparewith);
|
||||
$c->update_state($cm, COMPLETION_INCOMPLETE, 100, true);
|
||||
$c->expects($this->at(0))
|
||||
->method('get_data')
|
||||
->with($cm, false, 100)
|
||||
->will($this->returnValue($changed));
|
||||
$c->get_data($cm, false, 100);
|
||||
}
|
||||
|
||||
public function test_internal_get_state() {
|
||||
@ -416,8 +517,8 @@ class core_completionlib_testcase extends advanced_testcase {
|
||||
$modinfo->cms = array((object)array('id'=>13));
|
||||
$result=$c->get_data($cm, true, 123, $modinfo);
|
||||
$this->assertEquals((object)array(
|
||||
'id'=>'0', 'coursemoduleid'=>13, 'userid'=>123, 'completionstate'=>0,
|
||||
'viewed'=>0, 'timemodified'=>0), $result);
|
||||
'id' => '0', 'coursemoduleid' => 13, 'userid' => 123, 'completionstate' => 0,
|
||||
'viewed' => 0, 'timemodified' => 0, 'overrideby' => 0), $result);
|
||||
$this->assertEquals(false, $cache->get('123_42')); // Not current user is not cached.
|
||||
|
||||
// 3. Current user, single record, not from cache.
|
||||
@ -455,7 +556,7 @@ class core_completionlib_testcase extends advanced_testcase {
|
||||
$cachevalue = $cache->get('314159_42');
|
||||
$this->assertEquals($basicrecord, (object)$cachevalue[13]);
|
||||
$this->assertEquals(array('id' => '0', 'coursemoduleid' => 14,
|
||||
'userid'=>314159, 'completionstate'=>0, 'viewed'=>0, 'timemodified'=>0),
|
||||
'userid' => 314159, 'completionstate' => 0, 'viewed' => 0, 'overrideby' => 0, 'timemodified' => 0),
|
||||
$cachevalue[14]);
|
||||
}
|
||||
|
||||
@ -477,6 +578,7 @@ class core_completionlib_testcase extends advanced_testcase {
|
||||
$data->completionstate = COMPLETION_COMPLETE;
|
||||
$data->timemodified = time();
|
||||
$data->viewed = COMPLETION_NOT_VIEWED;
|
||||
$data->overrideby = null;
|
||||
|
||||
$c->internal_set_data($cm, $data);
|
||||
$d1 = $DB->get_field('course_modules_completion', 'id', array('coursemoduleid' => $cm->id));
|
||||
@ -498,6 +600,7 @@ class core_completionlib_testcase extends advanced_testcase {
|
||||
$d2->completionstate = COMPLETION_COMPLETE;
|
||||
$d2->timemodified = time();
|
||||
$d2->viewed = COMPLETION_NOT_VIEWED;
|
||||
$d2->overrideby = null;
|
||||
$c->internal_set_data($cm2, $d2);
|
||||
// Cache for current user returns the data.
|
||||
$cachevalue = $cache->get($data->userid . '_' . $cm->course);
|
||||
@ -518,6 +621,7 @@ class core_completionlib_testcase extends advanced_testcase {
|
||||
$d3->completionstate = COMPLETION_COMPLETE;
|
||||
$d3->timemodified = time();
|
||||
$d3->viewed = COMPLETION_NOT_VIEWED;
|
||||
$d3->overrideby = null;
|
||||
$DB->insert_record('course_modules_completion', $d3);
|
||||
$c->internal_set_data($cm, $data);
|
||||
}
|
||||
|
BIN
pix/i/completion-auto-n-override.png
Normal file
After Width: | Height: | Size: 228 B |
3
pix/i/completion-auto-n-override.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<!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>
|
After Width: | Height: | Size: 579 B |
BIN
pix/i/completion-auto-y-override.png
Normal file
After Width: | Height: | Size: 328 B |
3
pix/i/completion-auto-y-override.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<!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>
|
After Width: | Height: | Size: 779 B |
BIN
pix/i/completion-manual-n-override.png
Normal file
After Width: | Height: | Size: 206 B |
3
pix/i/completion-manual-n-override.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<!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>
|
After Width: | Height: | Size: 476 B |
BIN
pix/i/completion-manual-y-override.png
Normal file
After Width: | Height: | Size: 295 B |
3
pix/i/completion-manual-y-override.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<!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>
|
After Width: | Height: | Size: 682 B |
1
report/progress/amd/build/completion_override.min.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
define(["jquery","core/ajax","core/str","core/modal_factory","core/modal_events","core/notification","core/custom_interaction_events","core/templates"],function(a,b,c,d,e,f,g,h){var i,j,k=function(a,b){return a>0?"i/completion-"+b+"-y-override":"i/completion-"+b+"-n-override"},l=function(a){h.render("core/loading",{}).then(function(c){return j.append(c),b.call([{methodname:"core_completion_override_activity_completion_status",args:a}])[0]}).then(function(b){var d=b.state>0?1:0,e=d?"completion-y-override":"completion-n-override";c.get_string(e,"completion",i).then(function(a){var b={state:a,date:"",user:j.attr("data-userfullname"),activity:j.attr("data-activityname")};return c.get_string("progress-title","completion",b)}).then(function(a){var b=j.attr("data-completiontracking");return h.renderPix(k(d,b),"core",a)}).then(function(b){var c=d>0?0:1;j.find(".loading-icon").remove(),j.data("changecompl",a.userid+"-"+a.cmid+"-"+c),j.attr("data-changecompl",a.userid+"-"+a.cmid+"-"+c),j.children("img").replaceWith(b)})["catch"](f.exception)})["catch"](f.exception)},m=function(b,g){g.originalEvent.preventDefault(),g.originalEvent.stopPropagation(),b.preventDefault(),b.stopPropagation(),j=a(b.currentTarget);var h=j.data("changecompl").split("-"),i={userid:h[0],cmid:h[1],newstate:h[2]},k=1==i.newstate?"completion-y":"completion-n";c.get_strings([{key:k,component:"completion"}]).then(function(a){return c.get_strings([{key:"confirm",component:"moodle"},{key:"areyousureoverridecompletion",component:"completion",param:a[0]}])}).then(function(a){return d.create({type:d.types.CONFIRM,title:a[0],body:a[1]})}).then(function(a){a.getRoot().on(e.yes,function(){l(i)}),a.getRoot().on(e.hidden,function(){j.focus(),a.destroy()}),a.show()})["catch"](f.exception)},n=function(b){i=b,a("#completion-progress a.changecompl").each(function(a,b){g.define(b,[g.events.activate])}),a("#completion-progress").on(g.events.activate,"a.changecompl",function(a,b){m(a,b)})};return{init:n}});
|
179
report/progress/amd/src/completion_override.js
Normal file
@ -0,0 +1,179 @@
|
||||
// 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/modal_factory', 'core/modal_events', 'core/notification',
|
||||
'core/custom_interaction_events', 'core/templates'],
|
||||
function($, Ajax, Str, ModalFactory, ModalEvents, Notification, CustomEvents, Templates) {
|
||||
|
||||
/**
|
||||
* @type {String} the full name of the current user.
|
||||
* @private
|
||||
*/
|
||||
var userFullName;
|
||||
|
||||
/**
|
||||
* @type {JQuery} JQuery object containing the element (completion link) that was most recently activated.
|
||||
* @private
|
||||
*/
|
||||
var triggerElement;
|
||||
|
||||
/**
|
||||
* Helper function to get the pix icon key based on the completion state.
|
||||
* @method getIconDescriptorFromState
|
||||
* @param {number} state The current completion state.
|
||||
* @param {string} tracking The completion tracking type, either 'manual' or 'auto'.
|
||||
* @return {string} the key for the respective icon.
|
||||
* @private
|
||||
*/
|
||||
var getIconKeyFromState = function(state, tracking) {
|
||||
return state > 0 ? 'i/completion-' + tracking + '-y-override' : 'i/completion-' + tracking + '-n-override';
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the confirmation of an override change, calling the web service to update it.
|
||||
* @method setOverride
|
||||
* @param {Object} override the override data
|
||||
* @private
|
||||
*/
|
||||
var setOverride = function(override) {
|
||||
// Generate a loading spinner while we're working.
|
||||
Templates.render('core/loading', {}).then(function(html) {
|
||||
// Append the loading spinner to the trigger element.
|
||||
triggerElement.append(html);
|
||||
|
||||
// Update the completion status override.
|
||||
return Ajax.call([{
|
||||
methodname: 'core_completion_override_activity_completion_status',
|
||||
args: override
|
||||
}])[0];
|
||||
}).then(function(results) {
|
||||
var completionState = (results.state > 0) ? 1 : 0;
|
||||
|
||||
// Now, build the new title string, get the new icon, and update the DOM.
|
||||
var tooltipKey = completionState ? 'completion-y-override' : 'completion-n-override';
|
||||
Str.get_string(tooltipKey, 'completion', userFullName).then(function(stateString) {
|
||||
var params = {
|
||||
state: stateString,
|
||||
date: '',
|
||||
user: triggerElement.attr('data-userfullname'),
|
||||
activity: triggerElement.attr('data-activityname')
|
||||
};
|
||||
return Str.get_string('progress-title', 'completion', params);
|
||||
}).then(function(titleString) {
|
||||
var completionTracking = triggerElement.attr('data-completiontracking');
|
||||
return Templates.renderPix(getIconKeyFromState(completionState, completionTracking), 'core', titleString);
|
||||
}).then(function(html) {
|
||||
var oppositeState = completionState > 0 ? 0 : 1;
|
||||
triggerElement.find('.loading-icon').remove();
|
||||
triggerElement.data('changecompl', override.userid + '-' + override.cmid + '-' + oppositeState);
|
||||
triggerElement.attr('data-changecompl', override.userid + '-' + override.cmid + '-' + oppositeState);
|
||||
triggerElement.children("img").replaceWith(html);
|
||||
return;
|
||||
}).catch(Notification.exception);
|
||||
|
||||
return;
|
||||
}).catch(Notification.exception);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handler for activation of a completion status button element.
|
||||
* @method userConfirm
|
||||
* @param {Event} e the CustomEvents event (CustomEvents.events.activate in this case)
|
||||
* @param {Object} data an object containing the original event (click, keydown, etc.).
|
||||
* @private
|
||||
*/
|
||||
var userConfirm = function(e, data) {
|
||||
data.originalEvent.preventDefault();
|
||||
data.originalEvent.stopPropagation();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
triggerElement = $(e.currentTarget);
|
||||
var elemData = triggerElement.data('changecompl').split('-');
|
||||
var override = {
|
||||
userid: elemData[0],
|
||||
cmid: elemData[1],
|
||||
newstate: elemData[2]
|
||||
};
|
||||
var newStateStr = (override.newstate == 1) ? 'completion-y' : 'completion-n';
|
||||
|
||||
Str.get_strings([
|
||||
{key: newStateStr, component: 'completion'}
|
||||
]).then(function(strings) {
|
||||
return Str.get_strings([
|
||||
{key: 'confirm', component: 'moodle'},
|
||||
{key: 'areyousureoverridecompletion', component: 'completion', param: strings[0]}
|
||||
]);
|
||||
}).then(function(strings) {
|
||||
// Create a yes/no modal.
|
||||
return ModalFactory.create({
|
||||
type: ModalFactory.types.CONFIRM,
|
||||
title: strings[0],
|
||||
body: strings[1],
|
||||
});
|
||||
}).then(function(modal) {
|
||||
// Now set up the handlers for the confirmation or cancellation of the modal, and show it.
|
||||
|
||||
// Confirmation only.
|
||||
modal.getRoot().on(ModalEvents.yes, function() {
|
||||
setOverride(override);
|
||||
});
|
||||
|
||||
// Confirming, closing, or cancelling will destroy the modal and return focus to the trigger element.
|
||||
modal.getRoot().on(ModalEvents.hidden, function() {
|
||||
triggerElement.focus();
|
||||
modal.destroy();
|
||||
});
|
||||
|
||||
// Display.
|
||||
modal.show();
|
||||
return;
|
||||
}).catch(Notification.exception);
|
||||
};
|
||||
|
||||
/**
|
||||
* Init this module which allows activity completion state to be changed via ajax.
|
||||
* @method init
|
||||
* @param {string} fullName The current user's full name.
|
||||
* @private
|
||||
*/
|
||||
var init = function(fullName) {
|
||||
userFullName = fullName;
|
||||
|
||||
// Register the click, space and enter events as activators for the trigger element.
|
||||
$('#completion-progress a.changecompl').each(function(index, element) {
|
||||
CustomEvents.define(element, [CustomEvents.events.activate]);
|
||||
});
|
||||
|
||||
// Set the handler on the parent element (the table), but filter so the callback is only called for <a> type children
|
||||
// having the '.changecompl' class. The <a> element can then be accessed in the callback via e.currentTarget.
|
||||
$('#completion-progress').on(CustomEvents.events.activate, "a.changecompl", function(e, data) {
|
||||
userConfirm(e, data);
|
||||
});
|
||||
};
|
||||
|
||||
return /** @alias module:report_progress/completion_override */ {
|
||||
init: init
|
||||
};
|
||||
});
|
@ -74,6 +74,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');
|
||||
|
||||
@ -173,6 +179,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', 'init', [fullname($USER)]);
|
||||
|
||||
// Handle groups (if enabled)
|
||||
groups_print_course_menu($course,$CFG->wwwroot.'/report/progress/?course='.$course->id);
|
||||
@ -360,28 +367,41 @@ foreach($progress as $user) {
|
||||
foreach($activities as $activity) {
|
||||
|
||||
// Get progress information and state
|
||||
if (array_key_exists($activity->id,$user->progress)) {
|
||||
$thisprogress=$user->progress[$activity->id];
|
||||
$state=$thisprogress->completionstate;
|
||||
$date=userdate($thisprogress->timemodified);
|
||||
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;
|
||||
$date='';
|
||||
$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;
|
||||
}
|
||||
$completiontrackingstring = $activity->completion == COMPLETION_TRACKING_AUTOMATIC ? 'auto' : 'manual';
|
||||
$completionicon = 'completion-' . $completiontrackingstring. '-' . $completiontype;
|
||||
|
||||
$completionicon='completion-'.
|
||||
($activity->completion==COMPLETION_TRACKING_AUTOMATIC ? 'auto' : 'manual').
|
||||
'-'.$completiontype;
|
||||
|
||||
$describe = get_string('completion-' . $completiontype, 'completion');
|
||||
if ($overrideby) {
|
||||
$overridebyuser = \core_user::get_user($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 +412,20 @@ foreach($progress as $user) {
|
||||
if ($csv) {
|
||||
print $sep.csv_quote($describe).$sep.csv_quote($date);
|
||||
} else {
|
||||
$celltext = $OUTPUT->pix_icon('i/' . $completionicon, 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, ['sesskey' => sesskey()]);
|
||||
$celltext = html_writer::link($url, $celltext, array('class' => 'changecompl', 'data-changecompl' => $changecompl,
|
||||
'data-activityname' => $a->activity,
|
||||
'data-userfullname' => $a->user,
|
||||
'data-completiontracking' => $completiontrackingstring,
|
||||
'aria-role' => 'button'));
|
||||
}
|
||||
print '<td class="completion-progresscell '.$formattedactivities[$activity->id]->datepassedclass.'">'.
|
||||
$OUTPUT->pix_icon('i/' . $completionicon, $fulldescribe) . '</td>';
|
||||
$celltext . '</td>';
|
||||
}
|
||||
}
|
||||
|
||||
|
107
report/progress/tests/behat/activity_completion_report.feature
Normal file
@ -0,0 +1,107 @@
|
||||
@report @report_progress
|
||||
Feature: Teacher can view and override users' activity completion data via the progress report.
|
||||
In order to view and override a student's activity completion status
|
||||
As a teacher
|
||||
I need to view the course progress report and click the respective completion status icon
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | format | enablecompletion |
|
||||
| Course 1 | C1 | topics | 1 |
|
||||
And the following "activities" exist:
|
||||
| activity | name | intro | course | idnumber | section | completion | completionview | completionusegrade | assignsubmission_onlinetext_enabled | submissiondrafts |
|
||||
| assign | my assignment | A1 desc | C1 | assign1 | 0 | 1 | 0 | | 0 | 0 |
|
||||
| assign | my assignment 2 | A2 desc | C1 | assign2 | 0 | 2 | 1 | | 0 | 0 |
|
||||
| assign | my assignment 3 | A3 desc | C1 | assign3 | 0 | 2 | 1 | 1 | 1 | 0 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | One | teacher1@example.com |
|
||||
| student1 | Student | One | student1@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
|
||||
# Course comprising one activity with auto completion (student must view it) and one with manual completion.
|
||||
# This confirms that after being completed by the student and overridden by the teacher, that both activities can still be
|
||||
# completed again via normal mechanisms.
|
||||
@javascript
|
||||
Scenario: Given the status has been overridden, when a student tries to complete it again, completion can still occur.
|
||||
# Student completes the activities, manual and automatic completion.
|
||||
Given I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And "Not completed: my assignment. Select to mark as complete." "icon" should exist in the "my assignment" "list_item"
|
||||
And "Not completed: my assignment 2" "icon" should exist in the "my assignment 2" "list_item"
|
||||
And I click on "Not completed: my assignment. Select to mark as complete." "icon"
|
||||
And "Completed: my assignment. Select to mark as not complete." "icon" should exist in the "my assignment" "list_item"
|
||||
And I click on "my assignment 2" "link"
|
||||
And I am on "Course 1" course homepage
|
||||
And "Completed: my assignment 2" "icon" should exist in the "my assignment 2" "list_item"
|
||||
And I log out
|
||||
# Teacher overrides the activity completion statuses to incomplete.
|
||||
When I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Activity completion" node in "Course administration > Reports"
|
||||
And "Student One, my assignment: Completed" "icon" should exist in the "Student One" "table_row"
|
||||
And "Student One, my assignment 2: Completed" "icon" should exist in the "Student One" "table_row"
|
||||
And I click on "my assignment" "link" in the "Student One" "table_row"
|
||||
And I click on "Save changes" "button"
|
||||
And "Student One, my assignment: Not completed (set by Teacher One)" "icon" should exist in the "Student One" "table_row"
|
||||
And I click on "my assignment 2" "link" in the "Student One" "table_row"
|
||||
And I click on "Save changes" "button"
|
||||
And "Student One, my assignment 2: Not completed (set by Teacher One)" "icon" should exist in the "Student One" "table_row"
|
||||
And I log out
|
||||
# Student can now complete the activities again, via normal means.
|
||||
Then I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And "Not completed: my assignment (set by Teacher One). Select to mark as complete." "icon" should exist in the "my assignment" "list_item"
|
||||
And "Not completed: my assignment 2 (set by Teacher One)" "icon" should exist in the "my assignment 2" "list_item"
|
||||
And I click on "Not completed: my assignment (set by Teacher One). Select to mark as complete." "icon"
|
||||
And "Completed: my assignment. Select to mark as not complete." "icon" should exist in the "my assignment" "list_item"
|
||||
And I click on "my assignment 2" "link"
|
||||
And I am on "Course 1" course homepage
|
||||
And "Completed: my assignment 2" "icon" should exist in the "my assignment 2" "list_item"
|
||||
And I log out
|
||||
# And the activity completion report should show the same.
|
||||
When I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Activity completion" node in "Course administration > Reports"
|
||||
And "Student One, my assignment: Completed" "icon" should exist in the "Student One" "table_row"
|
||||
And "Student One, my assignment 2: Completed" "icon" should exist in the "Student One" "table_row"
|
||||
|
||||
# Course comprising one activity with auto completion (student must view it and receive a grade) and one with manual completion.
|
||||
# This confirms that after being overridden to complete by the teacher, that the completion status for activities with automatic
|
||||
# completion can no longer be affected by any normal completion mechanisms triggered by the student. Manual completion unaffected.
|
||||
@javascript
|
||||
Scenario: Given the status has been overridden to complete, when a student triggers completion updates, the status remains fixed.
|
||||
# When the teacher overrides the activity completion statuses to complete.
|
||||
When I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Activity completion" node in "Course administration > Reports"
|
||||
And "Student One, my assignment: Not completed" "icon" should exist in the "Student One" "table_row"
|
||||
And "Student One, my assignment 3: Not completed" "icon" should exist in the "Student One" "table_row"
|
||||
And I click on "my assignment" "link" in the "Student One" "table_row"
|
||||
And I click on "Save changes" "button"
|
||||
And "Student One, my assignment: Completed (set by Teacher One)" "icon" should exist in the "Student One" "table_row"
|
||||
And I click on "my assignment 3" "link" in the "Student One" "table_row"
|
||||
And I click on "Save changes" "button"
|
||||
And "Student One, my assignment 3: Completed (set by Teacher One)" "icon" should exist in the "Student One" "table_row"
|
||||
And I log out
|
||||
# Then as a student, confirm that automatic completion checks are no longer triggered (such as after an assign submission).
|
||||
Then I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And "Completed: my assignment 3 (set by Teacher One)" "icon" should exist in the "my assignment 3" "list_item"
|
||||
And I click on "my assignment 3" "link"
|
||||
And I press "Add submission"
|
||||
And I set the following fields to these values:
|
||||
| Online text | I'm the student first submission |
|
||||
And I press "Save changes"
|
||||
And I should see "Submitted for grading"
|
||||
And I am on "Course 1" course homepage
|
||||
And "Completed: my assignment 3 (set by Teacher One)" "icon" should exist in the "my assignment 3" "list_item"
|
||||
# And Confirm that manual completion changes are still allowed.
|
||||
And I am on "Course 1" course homepage
|
||||
And "Completed: my assignment (set by Teacher One). Select to mark as not complete." "icon" should exist in the "my assignment" "list_item"
|
||||
And I click on "Completed: my assignment (set by Teacher One). Select to mark as not complete." "icon"
|
||||
And "Not completed: my assignment. Select to mark as complete." "icon" should exist in the "my assignment" "list_item"
|
||||
And I log out
|
@ -29,7 +29,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$version = 2017101000.00; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
$version = 2017101000.01; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
// RR = release increments - 00 in DEV branches.
|
||||
// .XX = incremental changes.
|
||||
|
||||
|