MDL-37361 completion: Enabled overriding activity completion status
authorEiz Eddin Al Katrib <eiz@barasoft.co.uk>
Fri, 17 Feb 2017 14:48:02 +0000 (14:48 +0000)
committerJake Dallimore <jake@moodle.com>
Tue, 10 Oct 2017 09:10:15 +0000 (17:10 +0800)
22 files changed:
completion/classes/external.php
lang/en/completion.php
lang/en/role.php
lib/classes/event/course_module_completion_updated.php
lib/completionlib.php
lib/db/access.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
pix/i/completion-auto-n-override.png [new file with mode: 0644]
pix/i/completion-auto-n-override.svg [new file with mode: 0644]
pix/i/completion-auto-y-override.png [new file with mode: 0644]
pix/i/completion-auto-y-override.svg [new file with mode: 0644]
pix/i/completion-manual-n-override.png [new file with mode: 0644]
pix/i/completion-manual-n-override.svg [new file with mode: 0644]
pix/i/completion-manual-y-override.png [new file with mode: 0644]
pix/i/completion-manual-y-override.svg [new file with mode: 0644]
report/progress/amd/build/completion_override.min.js [new file with mode: 0644]
report/progress/amd/src/completion_override.js [new file with mode: 0644]
report/progress/index.php
report/progress/styles.css
version.php

index 7979e61..685c3b9 100644 (file)
@@ -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
      *
index 4bd4742..db94880 100644 (file)
@@ -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.
 
index 7af4a20..3ac7994 100644 (file)
@@ -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';
index 5142621..7ac1ff9 100644 (file)
@@ -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}'.";
+        }
     }
 
     /**
index c8bf817..d5d93c2 100644 (file)
@@ -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);
index 7b3a37a..0d72b98 100644 (file)
@@ -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,
index bd36ac7..7b537c5 100644 (file)
         <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>
index 2cf3f73..2f2814e 100644 (file)
@@ -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',
index d0fb99f..b695f35 100644 (file)
@@ -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;
 }
diff --git a/pix/i/completion-auto-n-override.png b/pix/i/completion-auto-n-override.png
new file mode 100644 (file)
index 0000000..bdb98ce
Binary files /dev/null and b/pix/i/completion-auto-n-override.png differ
diff --git a/pix/i/completion-auto-n-override.svg b/pix/i/completion-auto-n-override.svg
new file mode 100644 (file)
index 0000000..6100638
--- /dev/null
@@ -0,0 +1,3 @@
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
+       <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
+]><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
diff --git a/pix/i/completion-auto-y-override.png b/pix/i/completion-auto-y-override.png
new file mode 100644 (file)
index 0000000..aeca4ed
Binary files /dev/null and b/pix/i/completion-auto-y-override.png differ
diff --git a/pix/i/completion-auto-y-override.svg b/pix/i/completion-auto-y-override.svg
new file mode 100644 (file)
index 0000000..13cf5d7
--- /dev/null
@@ -0,0 +1,3 @@
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
+       <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
+]><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
diff --git a/pix/i/completion-manual-n-override.png b/pix/i/completion-manual-n-override.png
new file mode 100644 (file)
index 0000000..6f95e98
Binary files /dev/null and b/pix/i/completion-manual-n-override.png differ
diff --git a/pix/i/completion-manual-n-override.svg b/pix/i/completion-manual-n-override.svg
new file mode 100644 (file)
index 0000000..cccfb99
--- /dev/null
@@ -0,0 +1,3 @@
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
+       <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
+]><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
diff --git a/pix/i/completion-manual-y-override.png b/pix/i/completion-manual-y-override.png
new file mode 100644 (file)
index 0000000..bdbc46b
Binary files /dev/null and b/pix/i/completion-manual-y-override.png differ
diff --git a/pix/i/completion-manual-y-override.svg b/pix/i/completion-manual-y-override.svg
new file mode 100644 (file)
index 0000000..69270ba
--- /dev/null
@@ -0,0 +1,3 @@
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
+       <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
+]><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
diff --git a/report/progress/amd/build/completion_override.min.js b/report/progress/amd/build/completion_override.min.js
new file mode 100644 (file)
index 0000000..d521363
Binary files /dev/null and b/report/progress/amd/build/completion_override.min.js differ
diff --git a/report/progress/amd/src/completion_override.js b/report/progress/amd/src/completion_override.js
new file mode 100644 (file)
index 0000000..410f8d1
--- /dev/null
@@ -0,0 +1,85 @@
+// 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);
+
+            });
+        }
+    };
+});
index 2895e13..064461c 100644 (file)
@@ -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>';
         }
     }
 
index ede47d0..425d067 100644 (file)
@@ -59,4 +59,9 @@
 #page-report-progress-index .modicon {
     padding-top: 5px;
 }
+
+#page-report-progress-index #completion-progress td a .ajaxworking {
+    height: 16px;
+    background: url([[pix:i/ajaxloader]]) no-repeat;
+}
 /*rtl:end:ignore*/
index a8c58dd..0a6f2d6 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2017100600.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2017100600.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.