Merge branch 'MDL-68463-master-3' of git://github.com/andrewnicols/moodle
authorJun Pataleta <jun@moodle.com>
Tue, 19 May 2020 04:24:14 +0000 (12:24 +0800)
committerJun Pataleta <jun@moodle.com>
Tue, 19 May 2020 04:24:14 +0000 (12:24 +0800)
22 files changed:
lang/en/moodle.php
lang/en/user.php
lib/amd/build/checkbox-toggleall.min.js
lib/amd/build/checkbox-toggleall.min.js.map
lib/amd/src/checkbox-toggleall.js
lib/tablelib.php
report/participation/amd/build/participants.min.js [new file with mode: 0644]
report/participation/amd/build/participants.min.js.map [new file with mode: 0644]
report/participation/amd/src/participants.js [new file with mode: 0644]
report/participation/index.php
user/amd/build/local/participants/bulkactions.min.js [new file with mode: 0644]
user/amd/build/local/participants/bulkactions.min.js.map [new file with mode: 0644]
user/amd/build/participants.min.js
user/amd/build/participants.min.js.map
user/amd/build/repository.min.js
user/amd/build/repository.min.js.map
user/amd/src/local/participants/bulkactions.js [new file with mode: 0644]
user/amd/src/participants.js
user/amd/src/repository.js
user/classes/table/participants.php
user/index.php
user/tests/behat/filter_participants_showall.feature

index 6ba4010..81fc120 100644 (file)
@@ -1524,7 +1524,6 @@ $string['parentcategory'] = 'Parent category';
 $string['parentcoursenotfound'] = 'Parent course not found!';
 $string['parentfolder'] = 'Parent folder';
 $string['participants'] = 'Participants';
-$string['participantscount'] = 'Number of participants: {$a}';
 $string['participantslist'] = 'Participants list';
 $string['participationratio'] = 'Participation ratio';
 $string['participationreport'] = 'Participation report';
@@ -2291,3 +2290,6 @@ $string['messagedselectedcountusersfailed'] = 'A problem occurred and {$a} messa
 $string['messagedselecteduserfailed'] = 'The message was not sent to user {$a->fullname}.';
 $string['previewhtml'] = 'HTML format preview';
 $string['sitemessage'] = 'Message users';
+
+// Deprecated since Moodle 3.9.
+$string['participantscount'] = 'Number of participants: {$a}';
index 6160014..e010e09 100644 (file)
@@ -22,6 +22,7 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['countparticipantsfound'] = '{$a} participants found';
 $string['privacy:courserequestpath'] = 'Requested courses';
 $string['privacy:descriptionpath'] = 'Profile description';
 $string['privacy:devicespath'] = 'User devices';
index be9c0ae..4fd1ed4 100644 (file)
Binary files a/lib/amd/build/checkbox-toggleall.min.js and b/lib/amd/build/checkbox-toggleall.min.js differ
index 8468421..e7a5c40 100644 (file)
Binary files a/lib/amd/build/checkbox-toggleall.min.js.map and b/lib/amd/build/checkbox-toggleall.min.js.map differ
index 345db8f..473a514 100644 (file)
@@ -113,6 +113,38 @@ define(['jquery', 'core/pubsub'], function($, PubSub) {
             targetState = target.data('checkall') === 1;
         }
 
+        toggleSlavesToState(root, toggleGroupName, targetState);
+    };
+
+    /**
+     * Toggles the slave checkboxes from the masters.
+     *
+     * @param {HTMLElement} root
+     * @param {String} toggleGroupName
+     */
+    var updateSlavesFromMasterState = function(root, toggleGroupName) {
+        // Normalise to jQuery Object.
+        root = $(root);
+
+        var target = getControlCheckboxes(root, toggleGroupName, false);
+        var targetState;
+        if (target.is(':checkbox')) {
+            targetState = target.is(':checked');
+        } else {
+            targetState = target.data('checkall') === 1;
+        }
+
+        toggleSlavesToState(root, toggleGroupName, targetState);
+    };
+
+    /**
+     * Toggles the slave checkboxes to a specific state.
+     *
+     * @param {HTMLElement} root
+     * @param {String} toggleGroupName
+     * @param {Bool} targetState
+     */
+    var toggleSlavesToState = function(root, toggleGroupName, targetState) {
         var slaves = getAllSlaveCheckboxes(root, toggleGroupName);
         var checkedSlaves = slaves.filter(':checked');
 
@@ -132,6 +164,22 @@ define(['jquery', 'core/pubsub'], function($, PubSub) {
         });
     };
 
+    /**
+     * Set the state for an entire group of checkboxes.
+     *
+     * @param {HTMLElement} root
+     * @param {String} toggleGroupName
+     * @param {Bool} targetState
+     */
+    var setGroupState = function(root, toggleGroupName, targetState) {
+        // Normalise to jQuery Object.
+        root = $(root);
+
+        // Set the master and slaves.
+        setMasterStates(root, toggleGroupName, targetState, true);
+        toggleSlavesToState(root, toggleGroupName, targetState);
+    };
+
     /**
      * Toggles the master checkboxes in a given toggle group when all or none of the slave checkboxes in the same toggle group
      * have been selected.
@@ -242,5 +290,7 @@ define(['jquery', 'core/pubsub'], function($, PubSub) {
             registerListeners();
         },
         events: events,
+        setGroupState: setGroupState,
+        updateSlavesFromMasterState: updateSlavesFromMasterState,
     };
 });
index 2c46b2f..997a965 100644 (file)
@@ -1658,6 +1658,7 @@ class flexible_table {
                 'data-table-page-number' => $this->currpage + 1,
                 'data-table-page-size' => $this->pagesize,
                 'data-table-hidden-columns' => json_encode(array_keys($this->prefs['collapse'])),
+                'data-table-total-rows' => $this->totalrows,
             ]);
         }
 
diff --git a/report/participation/amd/build/participants.min.js b/report/participation/amd/build/participants.min.js
new file mode 100644 (file)
index 0000000..fa9364b
Binary files /dev/null and b/report/participation/amd/build/participants.min.js differ
diff --git a/report/participation/amd/build/participants.min.js.map b/report/participation/amd/build/participants.min.js.map
new file mode 100644 (file)
index 0000000..23f6cb6
Binary files /dev/null and b/report/participation/amd/build/participants.min.js.map differ
diff --git a/report/participation/amd/src/participants.js b/report/participation/amd/src/participants.js
new file mode 100644 (file)
index 0000000..7af7b60
--- /dev/null
@@ -0,0 +1,88 @@
+// 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/>.
+
+/**
+ * Some UI stuff for participants page.
+ * This is also used by the report/participants/index.php because it has the same functionality.
+ *
+ * @module     core_user/participants
+ * @package    core_user
+ * @copyright  2017 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import jQuery from 'jquery';
+import CustomEvents from 'core/custom_interaction_events';
+import ModalEvents from 'core/modal_events';
+import Notification from 'core/notification';
+import {showSendMessage} from 'core_user/local/participants/bulkactions';
+
+const Selectors = {
+    bulkActionSelect: "#formactionid",
+    bulkUserSelectedCheckBoxes: "input[data-togglegroup^='participants-table']:checked",
+    participantsForm: '#participantsform',
+};
+
+export const init = () => {
+    const root = document.querySelector(Selectors.participantsForm);
+
+    /**
+     * Private method.
+     *
+     * @method registerEventListeners
+     * @private
+     */
+    const registerEventListeners = () => {
+        CustomEvents.define(Selectors.bulkActionSelect, [CustomEvents.events.accessibleChange]);
+        jQuery(Selectors.bulkActionSelect).on(CustomEvents.events.accessibleChange, e => {
+            const action = e.target.value;
+            const checkboxes = root.querySelectorAll(Selectors.bulkUserSelectedCheckBoxes);
+
+            if (action.indexOf('#') !== -1) {
+                e.preventDefault();
+
+                const ids = [];
+                checkboxes.forEach(checkbox => {
+                    ids.push(checkbox.getAttribute('name').replace('user', ''));
+                });
+
+                if (action === '#messageselect') {
+                    showSendMessage(ids)
+                    .then(modal => {
+                        modal.getRoot().on(ModalEvents.hidden, () => {
+                            // Focus on the action select when the dialog is closed.
+                            const bulkActionSelector = root.querySelector(Selectors.bulkActionSelect);
+                            resetBulkAction(bulkActionSelector);
+                            bulkActionSelector.focus();
+                        });
+
+                        return modal;
+                    })
+                    .catch(Notification.exception);
+                }
+            } else if (action !== '' && checkboxes.length) {
+                e.target.form().submit();
+            }
+
+            resetBulkAction(e.target);
+        });
+    };
+
+    const resetBulkAction = bulkActionSelect => {
+        bulkActionSelect.value = '';
+    };
+
+    registerEventListeners();
+};
index 4101947..ef7a617 100644 (file)
@@ -385,7 +385,7 @@ if (!empty($instanceid) && !empty($roleid)) {
         $options->courseid = $course->id;
         $options->noteStateNames = note_get_state_names();
         $options->stateHelpIcon = $OUTPUT->help_icon('publishstate', 'notes');
-        $PAGE->requires->js_call_amd('core_user/participants', 'init', [$options]);
+        $PAGE->requires->js_call_amd('report_participation/participants', 'init', [$options]);
     }
     echo '</div>'."\n";
 }
diff --git a/user/amd/build/local/participants/bulkactions.min.js b/user/amd/build/local/participants/bulkactions.min.js
new file mode 100644 (file)
index 0000000..90f87b3
Binary files /dev/null and b/user/amd/build/local/participants/bulkactions.min.js differ
diff --git a/user/amd/build/local/participants/bulkactions.min.js.map b/user/amd/build/local/participants/bulkactions.min.js.map
new file mode 100644 (file)
index 0000000..c09406f
Binary files /dev/null and b/user/amd/build/local/participants/bulkactions.min.js.map differ
index 56be84f..b963d28 100644 (file)
Binary files a/user/amd/build/participants.min.js and b/user/amd/build/participants.min.js differ
index d204dba..7a9ea4a 100644 (file)
Binary files a/user/amd/build/participants.min.js.map and b/user/amd/build/participants.min.js.map differ
index 5ff57f0..fec9ab0 100644 (file)
Binary files a/user/amd/build/repository.min.js and b/user/amd/build/repository.min.js differ
index 7d60e60..b117500 100644 (file)
Binary files a/user/amd/build/repository.min.js.map and b/user/amd/build/repository.min.js.map differ
diff --git a/user/amd/src/local/participants/bulkactions.js b/user/amd/src/local/participants/bulkactions.js
new file mode 100644 (file)
index 0000000..3872f71
--- /dev/null
@@ -0,0 +1,191 @@
+// 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/>.
+
+/**
+ * Bulk actions for lists of participants.
+ *
+ * @module     core_user/local/participants/bulkactions
+ * @package    core_user
+ * @copyright  2020 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import * as Repository from 'core_user/repository';
+import * as Str from 'core/str';
+import ModalEvents from 'core/modal_events';
+import ModalFactory from 'core/modal_factory';
+import Templates from 'core/templates';
+import {add as notifyUser} from 'core/toast';
+
+/**
+ * Show the add note popup
+ *
+ * @param {Number} courseid
+ * @param {Number[]} users
+ * @param {String[]} noteStateNames
+ * @param {HTMLElement} stateHelpIcon
+ * @return {Promise}
+ */
+export const showAddNote = (courseid, users, noteStateNames, stateHelpIcon) => {
+    if (!users.length) {
+        // No users were selected.
+        return Promise.resolve();
+    }
+
+    const states = [];
+    for (let key in noteStateNames) {
+        switch (key) {
+            case 'draft':
+                states.push({value: 'personal', label: noteStateNames[key]});
+                break;
+            case 'public':
+                states.push({value: 'course', label: noteStateNames[key], selected: 1});
+                break;
+            case 'site':
+                states.push({value: key, label: noteStateNames[key]});
+                break;
+        }
+    }
+
+    const context = {
+        stateNames: states,
+        stateHelpIcon: stateHelpIcon.innerHTML,
+    };
+
+    let titlePromise = null;
+    if (users.length === 1) {
+        titlePromise = Str.get_string('addbulknotesingle', 'core_notes');
+    } else {
+        titlePromise = Str.get_string('addbulknote', 'core_notes', users.length);
+    }
+
+    return ModalFactory.create({
+        type: ModalFactory.types.SAVE_CANCEL,
+        body: Templates.render('core_user/add_bulk_note', context),
+        title: titlePromise,
+        buttons: {
+            save: titlePromise,
+        },
+        removeOnClose: true,
+    })
+    .then(modal => {
+        modal.getRoot().on(ModalEvents.save, () => submitAddNote(courseid, users, modal));
+
+        modal.show();
+
+        return modal;
+    });
+};
+
+/**
+ * Add a note to this list of users.
+ *
+ * @param {Number} courseid
+ * @param {Number[]} users
+ * @param {Modal} modal
+ * @return {Promise}
+ */
+const submitAddNote = (courseid, users, modal) => {
+    const text = modal.getRoot().find('form textarea').val();
+    const publishstate = modal.getRoot().find('form select').val();
+
+    const notes = users.map(userid => {
+        return {
+            userid,
+            text,
+            courseid,
+            publishstate,
+        };
+    });
+
+    return Repository.createNotesForUsers(notes)
+    .then(noteIds => {
+        if (noteIds.length === 1) {
+            return Str.get_string('addbulknotedonesingle', 'core_notes');
+        } else {
+            return Str.get_string('addbulknotedone', 'core_notes', noteIds.length);
+        }
+    })
+    .then(msg => notifyUser(msg))
+    .catch(Notification.exception);
+};
+
+/**
+ * Show the send message popup.
+ *
+ * @param {Number[]} users
+ * @return {Promise}
+ */
+export const showSendMessage = users => {
+    if (!users.length) {
+        // Nothing to do.
+        return Promise.resolve();
+    }
+
+    let titlePromise;
+    if (users.length === 1) {
+        titlePromise = Str.get_string('sendbulkmessagesingle', 'core_message');
+    } else {
+        titlePromise = Str.get_string('sendbulkmessage', 'core_message', users.length);
+    }
+
+    return ModalFactory.create({
+        type: ModalFactory.types.SAVE_CANCEL,
+        body: Templates.render('core_user/send_bulk_message', {}),
+        title: titlePromise,
+        buttons: {
+            save: titlePromise,
+        },
+        removeOnClose: true,
+    })
+    .then(modal => {
+        modal.getRoot().on(ModalEvents.save, () => {
+            submitSendMessage(modal, users);
+        });
+
+        modal.show();
+
+        return modal;
+    });
+};
+
+/**
+ * Send a message to these users.
+ *
+ * @param {Modal} modal
+ * @param {Number[]} users
+ * @return {Promise}
+ */
+const submitSendMessage = (modal, users) => {
+    const text = modal.getRoot().find('form textarea').val();
+
+    const messages = users.map(touserid => {
+        return {
+            touserid,
+            text,
+        };
+    });
+
+    return Repository.sendMessagesToUsers(messages)
+    .then(messageIds => {
+        if (messageIds.length == 1) {
+            return Str.get_string('sendbulkmessagesentsingle', 'core_message');
+        } else {
+            return Str.get_string('sendbulkmessagesent', 'core_message', messageIds.length);
+        }
+    })
+    .then(msg => notifyUser(msg))
+    .catch(Notification.exception);
+};
index d2156d5..f9b62a6 100644 (file)
  * @copyright  2017 Damyon Wiese
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/templates', 'core/notification', 'core/ajax',
-        'core/custom_interaction_events'],
-        function($, Str, ModalFactory, ModalEvents, Templates, Notification, Ajax, CustomEvents) {
-
-    var SELECTORS = {
-        BULKACTIONSELECT: "#formactionid",
-        BULKUSERCHECKBOXES: "input.usercheckbox",
-        BULKUSERNOSCHECKBOXES: "input.usercheckbox[value='0']",
-        BULKUSERSELECTEDCHECKBOXES: "input.usercheckbox:checked",
-        BULKACTIONFORM: "#participantsform",
-        CHECKALLBUTTON: "#checkall",
-        CHECKALLNOSBUTTON: "#checkallnos"
-    };
-
-    /**
-     * Constructor
-     *
-     * @param {Object} options Object containing options. Contextid is required.
-     * Each call to templates.render gets it's own instance of this class.
-     */
-    var Participants = function(options) {
-
-        this.courseId = options.courseid;
-        this.noteStateNames = options.noteStateNames;
-        this.stateHelpIcon = options.stateHelpIcon;
-
-        this.attachEventListeners();
-    };
-    // Class variables and functions.
-
-    /**
-     * @var {Modal} modal
-     * @private
-     */
-    Participants.prototype.modal = null;
-
-    /**
-     * @var {int} courseId
-     * @private
-     */
-    Participants.prototype.courseId = -1;
-
-    /**
-     * @var {Object} noteStateNames
-     * @private
-     */
-    Participants.prototype.noteStateNames = {};
 
-    /**
-     * @var {String} stateHelpIcon
-     * @private
-     */
-    Participants.prototype.stateHelpIcon = "";
+import * as DynamicTable from 'core_table/dynamic';
+import * as Str from 'core/str';
+import CheckboxToggleAll from 'core/checkbox-toggleall';
+import CustomEvents from 'core/custom_interaction_events';
+import DynamicTableSelectors from 'core_table/local/dynamic/selectors';
+import ModalEvents from 'core/modal_events';
+import Notification from 'core/notification';
+import jQuery from 'jquery';
+import {showAddNote, showSendMessage} from 'core_user/local/participants/bulkactions';
+
+const Selectors = {
+    bulkActionSelect: "#formactionid",
+    bulkUserSelectedCheckBoxes: "input[data-togglegroup='participants-table']:checked",
+    checkCountButton: "#checkall",
+    showCountText: '[data-region="participant-count"]',
+    showCountToggle: '[data-action="showcount"]',
+    stateHelpIcon: '[data-region="state-help-icon"]',
+    tableForm: uniqueId => `form[data-table-unique-id="${uniqueId}"]`,
+};
+
+export const init = ({
+    uniqueid,
+    noteStateNames = {},
+}) => {
+    const root = document.querySelector(Selectors.tableForm(uniqueid));
+    const getTableFromUniqueId = uniqueId => root.querySelector(DynamicTableSelectors.main.fromRegionId(uniqueId));
 
     /**
-     * Private method
+     * Private method.
      *
-     * @method attachEventListeners
+     * @method registerEventListeners
      * @private
      */
-    Participants.prototype.attachEventListeners = function() {
-        CustomEvents.define(SELECTORS.BULKACTIONSELECT, [CustomEvents.events.accessibleChange]);
-        $(SELECTORS.BULKACTIONSELECT).on(CustomEvents.events.accessibleChange, function(e) {
-            var action = $(e.target).val();
+    const registerEventListeners = () => {
+        CustomEvents.define(Selectors.bulkActionSelect, [CustomEvents.events.accessibleChange]);
+        jQuery(Selectors.bulkActionSelect).on(CustomEvents.events.accessibleChange, e => {
+            const action = e.target.value;
+            const tableRoot = getTableFromUniqueId(uniqueid);
+            const checkboxes = tableRoot.querySelectorAll(Selectors.bulkUserSelectedCheckBoxes);
+
             if (action.indexOf('#') !== -1) {
                 e.preventDefault();
 
-                var ids = [];
-                $(SELECTORS.BULKUSERSELECTEDCHECKBOXES).each(function(index, ele) {
-                    var name = $(ele).attr('name');
-                    var id = name.replace('user', '');
-                    ids.push(id);
+                const ids = [];
+                checkboxes.forEach(checkbox => {
+                    ids.push(checkbox.getAttribute('name').replace('user', ''));
                 });
 
-                if (action == '#messageselect') {
-                    this.showSendMessage(ids).fail(Notification.exception);
-                } else if (action == '#addgroupnote') {
-                    this.showAddNote(ids).fail(Notification.exception);
+                let bulkAction;
+                if (action === '#messageselect') {
+                    bulkAction = showSendMessage(ids);
+                } else if (action === '#addgroupnote') {
+                    bulkAction = showAddNote(
+                        root.dataset.courseId,
+                        ids,
+                        noteStateNames,
+                        root.querySelector(Selectors.stateHelpIcon)
+                    );
                 }
-                $(SELECTORS.BULKACTIONSELECT + ' option[value=""]').prop('selected', 'selected');
-            } else if (action !== '') {
-                if ($(SELECTORS.BULKUSERSELECTEDCHECKBOXES).length > 0) {
-                    $(SELECTORS.BULKACTIONFORM).submit();
-                } else {
-                    $(SELECTORS.BULKACTIONSELECT + ' option[value=""]').prop('selected', 'selected');
+
+                if (bulkAction) {
+                    bulkAction
+                    .then(modal => {
+                        modal.getRoot().on(ModalEvents.hidden, () => {
+                            // Focus on the action select when the dialog is closed.
+                            const bulkActionSelector = root.querySelector(Selectors.bulkActionSelect);
+                            bulkActionSelector.focus();
+                        });
+
+                        return modal;
+                    })
+                    .catch(Notification.exception);
                 }
+            } else if (action !== '' && checkboxes.length) {
+                e.target.form.submit();
             }
-        }.bind(this));
 
-        $(SELECTORS.CHECKALLBUTTON).on('click', function() {
-            var showallink = $(this).data('showallink');
-            if (showallink) {
-                window.location = showallink;
-            }
+            resetBulkAction(e.target);
         });
 
-        $(SELECTORS.CHECKALLNOSBUTTON).on('click', function() {
-            $(SELECTORS.BULKUSERNOSCHECKBOXES).prop('checked', true);
-        });
-    };
+        root.addEventListener('click', e => {
+            // Handle clicking of the "Show [all|count]" and "Select all" actions.
+            const showCountLink = root.querySelector(Selectors.showCountToggle);
+            const checkCountButton = root.querySelector(Selectors.checkCountButton);
 
-    /**
-     * Show the add note popup
-     *
-     * @method showAddNote
-     * @private
-     * @param {int[]} users
-     * @return {Promise}
-     */
-    Participants.prototype.showAddNote = function(users) {
-
-        if (users.length == 0) {
-            // Nothing to do.
-            return $.Deferred().resolve().promise();
-        }
-
-        var states = [];
-        for (var key in this.noteStateNames) {
-            switch (key) {
-                case 'draft':
-                    states.push({value: 'personal', label: this.noteStateNames[key]});
-                    break;
-                case 'public':
-                    states.push({value: 'course', label: this.noteStateNames[key], selected: 1});
-                    break;
-                case 'site':
-                    states.push({value: key, label: this.noteStateNames[key]});
-                    break;
-            }
-        }
-
-        var context = {stateNames: states, stateHelpIcon: this.stateHelpIcon};
-        var titlePromise = null;
-        if (users.length == 1) {
-            titlePromise = Str.get_string('addbulknotesingle', 'core_notes');
-        } else {
-            titlePromise = Str.get_string('addbulknote', 'core_notes', users.length);
-        }
-
-        return $.when(
-            ModalFactory.create({
-                type: ModalFactory.types.SAVE_CANCEL,
-                body: Templates.render('core_user/add_bulk_note', context)
-            }),
-            titlePromise
-        ).then(function(modal, title) {
-            // Keep a reference to the modal.
-            this.modal = modal;
-            this.modal.setTitle(title);
-            this.modal.setSaveButtonText(title);
-
-            // We want to focus on the action select when the dialog is closed.
-            this.modal.getRoot().on(ModalEvents.hidden, function() {
-                var notification = $('#user-notifications [role=alert]');
-                if (notification.length) {
-                    notification.focus();
-                } else {
-                    $(SELECTORS.BULKACTIONSELECT).focus();
-                }
-                this.modal.getRoot().remove();
-            }.bind(this));
+            const showCountLinkClicked = showCountLink && showCountLink.contains(e.target);
+            const checkCountButtonClicked = checkCountButton && checkCountButton.contains(e.target);
 
-            this.modal.getRoot().on(ModalEvents.save, this.submitAddNote.bind(this, users));
+            if (showCountLinkClicked || checkCountButtonClicked) {
+                e.preventDefault();
 
-            this.modal.show();
+                const tableRoot = getTableFromUniqueId(uniqueid);
 
-            return this.modal;
-        }.bind(this));
-    };
+                DynamicTable.setPageSize(tableRoot, showCountLink.dataset.targetPageSize)
+                .then(tableRoot => {
+                    // Always update the toggle state.
+                    // This ensures that the bulk actions are disabled after changing the page size.
+                    CheckboxToggleAll.setGroupState(tableRoot, 'participants-table', checkCountButtonClicked);
 
-    /**
-     * Add a note to this list of users.
-     *
-     * @method submitAddNote
-     * @private
-     * @param {int[]} users
-     * @return {Promise}
-     */
-    Participants.prototype.submitAddNote = function(users) {
-        var noteText = this.modal.getRoot().find('form textarea').val();
-        var publishState = this.modal.getRoot().find('form select').val();
-        var notes = [],
-            i = 0;
-
-        for (i = 0; i < users.length; i++) {
-            notes.push({userid: users[i], text: noteText, courseid: this.courseId, publishstate: publishState});
-        }
-
-        return Ajax.call([{
-            methodname: 'core_notes_create_notes',
-            args: {notes: notes}
-        }])[0].then(function(noteIds) {
-            if (noteIds.length == 1) {
-                return Str.get_string('addbulknotedonesingle', 'core_notes');
-            } else {
-                return Str.get_string('addbulknotedone', 'core_notes', noteIds.length);
+                    return tableRoot;
+                })
+                .catch(Notification.exception);
             }
-        }).then(function(msg) {
-            Notification.addNotification({
-                message: msg,
-                type: "success"
-            });
-            return true;
-        }).catch(Notification.exception);
-    };
+        });
 
-    /**
-     * Show the send message popup.
-     *
-     * @method showSendMessage
-     * @private
-     * @param {int[]} users
-     * @return {Promise}
-     */
-    Participants.prototype.showSendMessage = function(users) {
-
-        if (users.length == 0) {
-            // Nothing to do.
-            return $.Deferred().resolve().promise();
-        }
-        var titlePromise = null;
-        if (users.length == 1) {
-            titlePromise = Str.get_string('sendbulkmessagesingle', 'core_message');
-        } else {
-            titlePromise = Str.get_string('sendbulkmessage', 'core_message', users.length);
-        }
-
-        return $.when(
-            ModalFactory.create({
-                type: ModalFactory.types.SAVE_CANCEL,
-                body: Templates.render('core_user/send_bulk_message', {})
-            }),
-            titlePromise
-        ).then(function(modal, title) {
-            // Keep a reference to the modal.
-            this.modal = modal;
-
-            this.modal.setTitle(title);
-            this.modal.setSaveButtonText(title);
-
-            // We want to focus on the action select when the dialog is closed.
-            this.modal.getRoot().on(ModalEvents.hidden, function() {
-                $(SELECTORS.BULKACTIONSELECT).focus();
-                this.modal.getRoot().remove();
-            }.bind(this));
-
-            this.modal.getRoot().on(ModalEvents.save, this.submitSendMessage.bind(this, users));
-
-            this.modal.show();
-
-            return this.modal;
-        }.bind(this));
-    };
+        // When the content is refreshed, update the row counts in various places.
+        root.addEventListener(DynamicTable.Events.tableContentRefreshed, e => {
+            const showCountLink = root.querySelector(Selectors.showCountToggle);
+            const checkCountButton = root.querySelector(Selectors.checkCountButton);
 
-    /**
-     * Send a message to these users.
-     *
-     * @method submitSendMessage
-     * @private
-     * @param {int[]} users
-     * @param {Event} e Form submission event.
-     * @return {Promise}
-     */
-    Participants.prototype.submitSendMessage = function(users) {
+            const tableRoot = e.target;
+
+            const defaultPageSize = parseInt(root.dataset.tableDefaultPerPage, 10);
+            const currentPageSize = parseInt(tableRoot.dataset.tablePageSize, 10);
+            const totalRowCount = parseInt(tableRoot.dataset.tableTotalRows, 10);
 
-        var messageText = this.modal.getRoot().find('form textarea').val();
+            CheckboxToggleAll.updateSlavesFromMasterState(tableRoot, 'participants-table');
 
-        var messages = [],
-            i = 0;
+            const pageCountStrings = [
+                {
+                    key: 'countparticipantsfound',
+                    component: 'core_user',
+                    param: totalRowCount,
+                },
+            ];
 
-        for (i = 0; i < users.length; i++) {
-            messages.push({touserid: users[i], text: messageText});
-        }
 
-        return Ajax.call([{
-            methodname: 'core_message_send_instant_messages',
-            args: {messages: messages}
-        }])[0].then(function(messageIds) {
-            if (messageIds.length == 1) {
-                return Str.get_string('sendbulkmessagesentsingle', 'core_message');
+            if (totalRowCount <= defaultPageSize) {
+                // There are fewer than the default page count numbers of rows.
+                showCountLink.classList.add('hidden');
+
+                if (checkCountButton) {
+                    checkCountButton.classList.add('hidden');
+                }
+            } else if (totalRowCount <= currentPageSize) {
+                // The are fewer than the current page size.
+                pageCountStrings.push({
+                    key: 'showperpage',
+                    component: 'core',
+                    param: defaultPageSize,
+                });
+
+                pageCountStrings.push({
+                    key: 'selectalluserswithcount',
+                    component: 'core',
+                    param: defaultPageSize,
+                });
+
+                // Show the 'Show [x]' link.
+                showCountLink.classList.remove('hidden');
+                showCountLink.dataset.targetPageSize = defaultPageSize;
+
+                if (checkCountButton) {
+                    // The 'Check all [x]' button is only visible when there are values to set.
+                    checkCountButton.classList.add('hidden');
+                }
             } else {
-                return Str.get_string('sendbulkmessagesent', 'core_message', messageIds.length);
+                pageCountStrings.push({
+                    key: 'showall',
+                    component: 'core',
+                    param: totalRowCount,
+                });
+
+                pageCountStrings.push({
+                    key: 'selectalluserswithcount',
+                    component: 'core',
+                    param: totalRowCount,
+                });
+
+                // Show both the 'Show [x]' link, and the 'Check all [x]' button.
+                showCountLink.classList.remove('hidden');
+                showCountLink.dataset.targetPageSize = totalRowCount;
+
+                if (checkCountButton) {
+                    checkCountButton.classList.remove('hidden');
+                }
             }
-        }).then(function(msg) {
-            Notification.addNotification({
-                message: msg,
-                type: "success"
-            });
-            return true;
-        }).catch(Notification.exception);
+
+            Str.get_strings(pageCountStrings)
+            .then(([showingParticipantCountString, showCountString, selectCountString]) => {
+                const showingParticipantCount = root.querySelector(Selectors.showCountText);
+                showingParticipantCount.innerHTML = showingParticipantCountString;
+
+                if (showCountString) {
+                    showCountLink.innerHTML = showCountString;
+                }
+
+                if (selectCountString && checkCountButton) {
+                    checkCountButton.value = selectCountString;
+                }
+
+                return;
+            })
+            .catch(Notification.exception);
+        });
     };
 
-    return /** @alias module:core_user/participants */ {
-        // Public variables and functions.
-
-        /**
-         * Initialise the unified user filter.
-         *
-         * @method init
-         * @param {Object} options - List of options.
-         * @return {Participants}
-         */
-        'init': function(options) {
-            return new Participants(options);
-        }
+    const resetBulkAction = bulkActionSelect => {
+        bulkActionSelect.value = '';
     };
-});
+
+    registerEventListeners();
+};
index 7be9378..c719bc1 100644 (file)
@@ -51,3 +51,19 @@ export const submitUserEnrolmentForm = formdata => {
         },
     }])[0];
 };
+
+export const createNotesForUsers = notes => {
+    return fetchMany([{
+        methodname: 'core_notes_create_notes',
+        args: {
+            notes
+        }
+    }])[0];
+};
+
+export const sendMessagesToUsers = messages => {
+    return fetchMany([{
+        methodname: 'core_message_send_instant_messages',
+        args: {messages}
+    }])[0];
+};
index c1058cd..04d47f5 100644 (file)
@@ -54,11 +54,6 @@ class participants extends \table_sql implements dynamic_table {
      */
     protected $courseid;
 
-    /**
-     * @var bool $selectall Has the user selected all users on the page?
-     */
-    protected $selectall;
-
     /**
      * @var string[] The list of countries.
      */
@@ -134,10 +129,10 @@ class participants extends \table_sql implements dynamic_table {
             $mastercheckbox = new \core\output\checkbox_toggleall('participants-table', true, [
                 'id' => 'select-all-participants',
                 'name' => 'select-all-participants',
-                'label' => $this->selectall ? get_string('deselectall') : get_string('selectall'),
+                'label' => get_string('selectall'),
                 'labelclasses' => 'sr-only',
                 'classes' => 'm-1',
-                'checked' => $this->selectall
+                'checked' => false,
             ]);
             $headers[] = $OUTPUT->render($mastercheckbox);
             $columns[] = 'select';
@@ -236,7 +231,7 @@ class participants extends \table_sql implements dynamic_table {
             'classes' => 'usercheckbox m-1',
             'id' => 'user' . $data->id,
             'name' => 'user' . $data->id,
-            'checked' => $this->selectall,
+            'checked' => false,
             'label' => get_string('selectitem', 'moodle', fullname($data)),
             'labelclasses' => 'accesshide',
         ]);
@@ -453,17 +448,6 @@ class participants extends \table_sql implements dynamic_table {
         return '';
     }
 
-    /**
-     * Set the value for selectall.
-     *
-     * Note: This will be removed later in the 3.9 development cycle.
-     *
-     * @param bool $selectall
-     */
-    public function set_selectall(bool $selectall): void {
-        $this->selectall = $selectall;
-    }
-
     /**
      * Set filters and build table structure.
      *
index 92f81e2..2e9290f 100644 (file)
@@ -42,7 +42,6 @@ $perpage      = optional_param('perpage', DEFAULT_PAGE_SIZE, PARAM_INT); // How
 $contextid    = optional_param('contextid', 0, PARAM_INT); // One of this or.
 $courseid     = optional_param('id', 0, PARAM_INT); // This are required.
 $newcourse    = optional_param('newcourse', false, PARAM_BOOL);
-$selectall    = optional_param('selectall', false, PARAM_BOOL); // When rendering checkboxes against users mark them all checked.
 $roleid       = optional_param('roleid', 0, PARAM_INT);
 $groupparam   = optional_param('group', 0, PARAM_INT);
 
@@ -276,7 +275,6 @@ if (count($keywordfilter)) {
 }
 
 $participanttable = new \core_user\table\participants("user-index-participants-{$course->id}");
-$participanttable->set_selectall($selectall);
 $participanttable->set_filterset($filterset);
 $participanttable->define_baseurl($baseurl);
 
@@ -286,48 +284,73 @@ $participanttable->out($perpage, true);
 $participanttablehtml = ob_get_contents();
 ob_end_clean();
 
-echo html_writer::tag('p', get_string('participantscount', 'moodle', $participanttable->totalrows));
-
 if ($bulkoperations) {
-    echo '<form action="action_redir.php" method="post" id="participantsform">';
+    echo html_writer::start_tag('form', [
+        'action' => 'action_redir.php',
+        'method' => 'post',
+        'id' => 'participantsform',
+        'data-course-id' => $course->id,
+        'data-table-unique-id' => $participanttable->uniqueid,
+        'data-table-default-per-page' => ($perpage < DEFAULT_PAGE_SIZE) ? $perpage : DEFAULT_PAGE_SIZE,
+    ]);
     echo '<div>';
     echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
     echo '<input type="hidden" name="returnto" value="'.s($PAGE->url->out(false)).'" />';
 }
 
+echo html_writer::tag(
+    'p',
+    get_string('countparticipantsfound', 'core_user', $participanttable->totalrows),
+    [
+        'data-region' => 'participant-count',
+    ]
+);
+
 echo $participanttablehtml;
 
 $perpageurl = clone($baseurl);
 $perpageurl->remove_params('perpage');
+$perpagesize = DEFAULT_PAGE_SIZE;
+$perpagevisible = false;
+
 if ($perpage == SHOW_ALL_PAGE_SIZE && $participanttable->totalrows > DEFAULT_PAGE_SIZE) {
     $perpageurl->param('perpage', DEFAULT_PAGE_SIZE);
-    echo $OUTPUT->container(html_writer::link($perpageurl, get_string('showperpage', '', DEFAULT_PAGE_SIZE)), array(), 'showall');
-
+    $perpagevisible = true;
 } else if ($participanttable->get_page_size() < $participanttable->totalrows) {
     $perpageurl->param('perpage', SHOW_ALL_PAGE_SIZE);
-    echo $OUTPUT->container(html_writer::link($perpageurl, get_string('showall', '', $participanttable->totalrows)),
-        array(), 'showall');
+    $pagesize = SHOW_ALL_PAGE_SIZE;
+    $perpagevisible = true;
+}
+
+$perpageclasses = '';
+if (!$perpagevisible) {
+    $perpageclasses = 'hidden';
 }
+echo $OUTPUT->container(html_writer::link(
+    $perpageurl,
+    get_string('showperpage', '', DEFAULT_PAGE_SIZE),
+    [
+        'data-action' => 'showcount',
+        'data-target-page-size' => $perpagesize,
+        'class' => $perpageclasses,
+    ]
+), [], 'showall');
 
 if ($bulkoperations) {
     echo '<br /><div class="buttons"><div class="form-inline">';
 
-    if ($participanttable->get_page_size() < $participanttable->totalrows) {
-        $perpageurl = clone($baseurl);
-        $perpageurl->remove_params('perpage');
-        $perpageurl->param('perpage', SHOW_ALL_PAGE_SIZE);
-        $perpageurl->param('selectall', true);
-        $showalllink = $perpageurl;
-    } else {
-        $showalllink = false;
-    }
-
     echo html_writer::start_tag('div', array('class' => 'btn-group'));
+
     if ($participanttable->get_page_size() < $participanttable->totalrows) {
-        // Select all users, refresh page showing all users and mark them all selected.
+        // Select all users, refresh table showing all users and mark them all selected.
         $label = get_string('selectalluserswithcount', 'moodle', $participanttable->totalrows);
-        echo html_writer::empty_tag('input', array('type' => 'button', 'id' => 'checkall', 'class' => 'btn btn-secondary',
-                'value' => $label, 'data-showallink' => $showalllink));
+        echo html_writer::empty_tag('input', [
+            'type' => 'button',
+            'id' => 'checkall',
+            'class' => 'btn btn-secondary',
+            'value' => $label,
+            'data-target-page-size' => $participanttable->totalrows,
+        ]);
     }
     echo html_writer::end_tag('div');
     $displaylist = array();
@@ -392,13 +415,14 @@ if ($bulkoperations) {
     echo html_writer::tag('div', $label . $select);
 
     echo '<input type="hidden" name="id" value="' . $course->id . '" />';
+    echo '<div class="d-none" data-region="state-help-icon">' . $OUTPUT->help_icon('publishstate', 'notes') . '</div>';
     echo '</div></div></div>';
     echo '</form>';
 
-    $options = new stdClass();
-    $options->courseid = $course->id;
-    $options->noteStateNames = note_get_state_names();
-    $options->stateHelpIcon = $OUTPUT->help_icon('publishstate', 'notes');
+    $options = (object) [
+        'uniqueid' => $participanttable->uniqueid,
+        'noteStateNames' => note_get_state_names(),
+    ];
     $PAGE->requires->js_call_amd('core_user/participants', 'init', [$options]);
 }
 
index 6cec23e..d50851f 100644 (file)
@@ -86,7 +86,7 @@ Feature: Course participants can be filtered to display all the users
     And I click on "Role: Student" item in the autocomplete list
     And I click on "Show all 24" "link"
     Then I should see "Role: Student"
-    And I should see "Number of participants: 24" in the "//div[@class='userlist']" "xpath_element"
+    And I should see "24 participants found"
     And I should see "Show 20 per page"
 
   @javascript
@@ -101,7 +101,7 @@ Feature: Course participants can be filtered to display all the users
     And I click on "Show all 23" "link"
     Then I should see "Role: Student"
     And I should see "Status: Active"
-    And I should see "Number of participants: 23" in the "//div[@class='userlist']" "xpath_element"
+    And I should see "23 participants found"
     And I should see "Student 1"
     And I should not see "Student 24"
     And I should see "Show 20 per page"