+ * @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 type children
+ // having the '.changecompl' class. The 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
+ };
+ });
diff --git a/report/progress/index.php b/report/progress/index.php
index 2895e1343f1..a3105fe1857 100644
--- a/report/progress/index.php
+++ b/report/progress/index.php
@@ -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 '| '.
- $OUTPUT->pix_icon('i/' . $completionicon, $fulldescribe) . ' | ';
+ $celltext . '';
}
}
diff --git a/report/progress/tests/behat/activity_completion_report.feature b/report/progress/tests/behat/activity_completion_report.feature
new file mode 100644
index 00000000000..dca3cf35111
--- /dev/null
+++ b/report/progress/tests/behat/activity_completion_report.feature
@@ -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
diff --git a/version.php b/version.php
index 15238f65bec..b9152fc754c 100644
--- a/version.php
+++ b/version.php
@@ -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.