MDL-59758 core_user: Replace old bulk actions
authorDamyon Wiese <damyon@moodle.com>
Tue, 7 Nov 2017 02:32:51 +0000 (10:32 +0800)
committerDamyon Wiese <damyon@moodle.com>
Tue, 7 Nov 2017 06:18:49 +0000 (14:18 +0800)
The participants page has some clunky multi page forms for bulk actions. Replaces it with an ajax alternative.

22 files changed:
lang/en/deprecated.txt
lang/en/message.php
lang/en/moodle.php
lang/en/notes.php
lib/amd/build/auto_rows.min.js
lib/amd/src/auto_rows.js
lib/db/services.php
notes/lib.php
notes/tests/behat/participants_notes.feature
report/participation/index.php
report/participation/module.js [deleted file]
report/participation/tests/behat/message_participants.feature
user/action_redir.php
user/addnote.php [deleted file]
user/amd/build/participants.min.js [new file with mode: 0644]
user/amd/src/participants.js [new file with mode: 0644]
user/groupaddnote.php [deleted file]
user/index.php
user/module.js [deleted file]
user/templates/add_bulk_note.mustache [new file with mode: 0644]
user/templates/send_bulk_message.mustache [new file with mode: 0644]
user/tests/behat/behat_user.php [new file with mode: 0644]

index b912982..c7b2559 100644 (file)
@@ -159,3 +159,7 @@ privacy,core_hub
 privacy_help,core_hub
 configloginhttps,core_admin
 loginhttps,core_admin
+groupaddnewnote,core_notes
+selectnotestate,core_notes
+extendenrol,core
+groupextendenrol,core
index 64a7588..ab3e90d 100644 (file)
@@ -111,6 +111,8 @@ $string['send'] = 'Send';
 $string['sendingvia'] = 'Sending "{$a->provider}" via "{$a->processor}"';
 $string['sendingviawhen'] = 'Sending "{$a->provider}" via "{$a->processor}" when {$a->state}';
 $string['sendmessage'] = 'Send message';
+$string['sendbulkmessage'] = 'Send message to {$a} people';
+$string['sendbulkmessagesent'] = 'Message sent to {$a} people.';
 $string['sendmessageto'] = 'Send message to {$a}';
 $string['sendmessagetopopup'] = 'Send message to {$a} - new window';
 $string['settings'] = 'Settings';
index 71e29cd..f1eb252 100644 (file)
@@ -793,7 +793,6 @@ $string['expand'] = 'Expand';
 $string['expandall'] = 'Expand all';
 $string['expandcategory'] = 'Expand {$a}';
 $string['explanation'] = 'Explanation';
-$string['extendenrol'] = 'Extend enrolment (individual)';
 $string['extendperiod'] = 'Extended period';
 $string['failedloginattempts'] = '{$a->attempts} failed logins since your last login';
 $string['feedback'] = 'Feedback';
@@ -904,7 +903,6 @@ $string['gravatarenabled'] = '<a href="http://www.gravatar.com/">Gravatar</a> ha
 $string['group'] = 'Group';
 $string['groupadd'] = 'Add new group';
 $string['groupaddusers'] = 'Add selected to group';
-$string['groupextendenrol'] = 'Extend enrolment (common)';
 $string['groupfor'] = 'for group';
 $string['groupinfo'] = 'Info about selected group';
 $string['groupinfoedit'] = 'Edit group settings';
@@ -2165,3 +2163,5 @@ $string['sectionusedefaultname'] = 'Use default section name';
 
 // Deprecated since Moodle 3.4.
 $string['publish'] = 'Publish';
+$string['extendenrol'] = 'Extend enrolment (individual)';
+$string['groupextendenrol'] = 'Extend enrolment (common)';
index 1146eac..efc196c 100644 (file)
@@ -24,6 +24,8 @@
  */
 
 $string['addnewnote'] = 'Add a new note';
+$string['addbulknote'] = 'Add a new note to {$a} people';
+$string['addbulknotedone'] = 'Note added to {$a} people';
 $string['addnewnoteselect'] = 'Select users to write notes about';
 $string['bynameondate'] = 'by {$a->name} - {$a->date}';
 $string['configenablenotes'] = 'Enable storing of notes about individual users.';
@@ -39,7 +41,6 @@ $string['eventnotecreated'] = 'Note created';
 $string['eventnoteupdated'] = 'Note updated';
 $string['eventnotedeleted'] = 'Note deleted';
 $string['eventnotesviewed'] = 'Notes viewed';
-$string['groupaddnewnote'] = 'Add a common note';
 $string['invalidid'] = 'Invalid note ID specified';
 $string['invaliduserid'] = 'Invalid user id: {$a}';
 $string['myprofileownnotes'] = 'My notes';
@@ -61,8 +62,10 @@ $string['publishstate_help'] = 'A note\'s context determines who can see the not
 * Personal - The note will be visible only to you
 * Course - The note will be visible to teachers in this course
 * Site - The note will be visible to teachers in all courses';
-$string['selectnotestate'] = "Select note state";
 $string['site'] = 'site';
 $string['sitenotes'] = 'Site notes';
 $string['unknown'] = 'unknown';
 
+// Deprecated since Moodle 3.4
+$string['groupaddnewnote'] = 'Add a common note';
+$string['selectnotestate'] = "Select note state";
index c95209b..5666dc3 100644 (file)
Binary files a/lib/amd/build/auto_rows.min.js and b/lib/amd/build/auto_rows.min.js differ
index 2c04a47..ab7a876 100644 (file)
@@ -75,7 +75,7 @@ define(['jquery'], function($) {
      * @param {Event} e The triggered event.
      * @private
      */
-    var changeListener = function(root, e) {
+    var changeListener = function(e) {
         var element = $(e.target);
         var minRows = element.data('min-rows');
         var currentRows = element.attr('rows');
@@ -100,9 +100,9 @@ define(['jquery'], function($) {
      */
     var init = function(root) {
         if ($(root).data('auto-rows')) {
-            $(root).on('input propertychange', changeListener.bind(this, root));
+            $(root).on('input propertychange', changeListener.bind(this));
         } else {
-            $(root).on('input propertychange', SELECTORS.ELEMENT, changeListener.bind(this, root));
+            $(root).on('input propertychange', SELECTORS.ELEMENT, changeListener.bind(this));
         }
     };
 
index 34425f6..3bef679 100644 (file)
@@ -1034,6 +1034,7 @@ $functions = array(
         'classpath' => 'notes/externallib.php',
         'description' => 'Create notes',
         'type' => 'write',
+        'ajax' => true,
         'capabilities' => 'moodle/notes:manage',
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
index b21f1f6..041e613 100644 (file)
@@ -113,6 +113,11 @@ function note_save(&$note) {
     if (empty($note->publishstate)) {
         $note->publishstate = NOTES_STATE_PUBLIC;
     }
+
+    if (empty(trim($note->content))) {
+        // Don't save empty notes.
+        return false;
+    }
     // Save data.
     if (empty($note->id)) {
         // Insert new note.
index aae0dc5..a2df671 100644 (file)
@@ -1,4 +1,4 @@
-@core @core_notes
+@core @core_notes @javascript
 Feature: Add notes to course participants
   In order to share information with other staff
   As a teacher
@@ -34,15 +34,19 @@ Feature: Add notes to course participants
     And I am on "Course 1" course homepage
     And I follow "Participants"
     And I set the field with xpath "//tr[contains(normalize-space(.), 'Student 1')]//input[@type='checkbox']" to "1"
+    And I choose "Add a new note" from the participants page bulk action menu
+    And I set the field "bulk-note" to "Student 1 needs to pick up his game"
+    And I press "Add a new note to 1 people"
+    And I set the field with xpath "//tr[contains(normalize-space(.), 'Student 1')]//input[@type='checkbox']" to "0"
     And I set the field with xpath "//tr[contains(normalize-space(.), 'Student 2')]//input[@type='checkbox']" to "1"
+    And I choose "Add a new note" from the participants page bulk action menu
+    And I set the field "bulk-note" to ""
+    And I press "Add a new note to 1 people"
+    And I set the field with xpath "//tr[contains(normalize-space(.), 'Student 2')]//input[@type='checkbox']" to "0"
     And I set the field with xpath "//tr[contains(normalize-space(.), 'Student 3')]//input[@type='checkbox']" to "1"
-    And I set the field "With selected users..." to "Add a new note"
-    And I press "OK"
-    # Add a note to student 1, but leave student 2 empty and student 3 with space.
-    When I set the field with xpath "//tr[contains(normalize-space(.), 'Student 1')]//textarea" to "Student 1 needs to pick up his game"
-    And I set the field with xpath "//tr[contains(normalize-space(.), 'Student 2')]//textarea" to ""
-    And I set the field with xpath "//tr[contains(normalize-space(.), 'Student 3')]//textarea" to "  "
-    And I press "Save changes"
+    And I choose "Add a new note" from the participants page bulk action menu
+    And I set the field "bulk-note" to "  "
+    And I press "Add a new note to 1 people"
     And I follow "Student 1"
     And I follow "Notes"
     # Student 1 has note from Teacher
index 0400e6e..ef66eef 100644 (file)
@@ -25,6 +25,7 @@
 
 require('../../config.php');
 require_once($CFG->dirroot.'/lib/tablelib.php');
+require_once($CFG->dirroot.'/notes/lib.php');
 require_once($CFG->dirroot.'/report/participation/locallib.php');
 
 define('DEFAULT_PAGE_SIZE', 20);
@@ -336,7 +337,7 @@ if (!empty($instanceid) && !empty($roleid)) {
     echo '<h2>'.get_string('counteditems', '', $a).'</h2>'."\n";
 
     if (!empty($CFG->messaging)) {
-        echo '<form action="'.$CFG->wwwroot.'/user/action_redir.php" method="post" id="studentsform">'."\n";
+        echo '<form action="'.$CFG->wwwroot.'/user/action_redir.php" method="post" id="participantsform">'."\n";
         echo '<div>'."\n";
         echo '<input type="hidden" name="id" value="'.$id.'" />'."\n";
         echo '<input type="hidden" name="returnto" value="'. s($PAGE->url) .'" />'."\n";
@@ -372,23 +373,25 @@ if (!empty($instanceid) && !empty($roleid)) {
     if (!empty($CFG->messaging)) {
         $buttonclasses = 'btn btn-secondary';
         echo '<div class="selectbuttons btn-group">';
-        echo '<input type="button" id="checkall" value="'.get_string('selectall').'" class="'. $buttonclasses .'"> '."\n";
+        echo '<input type="button" id="checkallonpage" value="'.get_string('selectall').'" class="'. $buttonclasses .'"> '."\n";
         echo '<input type="button" id="checknone" value="'.get_string('deselectall').'" class="'. $buttonclasses .'"> '."\n";
         if ($perpage >= $matchcount) {
-            echo '<input type="button" id="checknos" value="'.get_string('selectnos').'" class="'. $buttonclasses .'">'."\n";
+            echo '<input type="button" id="checkallnos" value="'.get_string('selectnos').'" class="'. $buttonclasses .'">'."\n";
         }
         echo '</div>';
         echo '<div class="p-y-1">';
         echo html_writer::label(get_string('withselectedusers'), 'formactionselect');
-        $displaylist['messageselect.php'] = get_string('messageselectadd');
-        echo html_writer::select($displaylist, 'formaction', '', array('' => 'choosedots'), array('id' => 'formactionselect'));
-        echo $OUTPUT->help_icon('withselectedusers');
-        echo '<input type="submit" value="' . get_string('ok') . '" class="'. $buttonclasses .'"/>'."\n";
+        $displaylist['#messageselect'] = get_string('messageselectadd');
+        echo html_writer::select($displaylist, 'formaction', '', array('' => 'choosedots'), array('id' => 'formactionid'));
         echo '</div>';
         echo '</div>'."\n";
         echo '</form>'."\n";
 
-        $PAGE->requires->js_init_call('M.report_participation.init');
+        $options = new stdClass();
+        $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]);
     }
     echo '</div>'."\n";
 }
diff --git a/report/participation/module.js b/report/participation/module.js
deleted file mode 100644 (file)
index d398439..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-
-M.report_participation = {};
-
-M.report_participation.init = function(Y) {
-
-    Y.on('submit', function(e) {
-            Y.one('#formactionselect').get('options').each(function() {
-                if (this.get('selected') && this.get('value') == '') {
-                    // no action selected
-                    e.preventDefault();
-                }
-            });
-            var ok = false;
-            Y.all('input.usercheckbox').each(function() {
-                if (this.get('checked')) {
-                    ok = true;
-                }
-            });
-            if (!ok) {
-                // no checkbox selected
-                e.preventDefault();
-            }
-        }, '#studentsform');
-
-    Y.on('click', function(e) {
-        Y.all('input.usercheckbox').each(function() {
-            this.set('checked', 'checked');
-        });
-    }, '#checkall');
-
-    Y.on('click', function(e) {
-        Y.all('input.usercheckbox').each(function() {
-            this.set('checked', '');
-        });
-    }, '#checknone');
-
-    Y.on('click', function(e) {
-        Y.all('input.usercheckbox').each(function() {
-            if (this.get('value') == 0) {
-                this.set('checked', 'checked');
-            }
-        });
-    }, '#checknos');
-};
\ No newline at end of file
index 88aae4e..a9a9d7e 100644 (file)
@@ -46,12 +46,8 @@ Feature: Use the particiaption report to message groups of students
     And I should see "No" in the "Student 2" "table_row"
     And I should see "No" in the "Student 3" "table_row"
     When I press "Select all 'No'"
-    And I set the field "With selected users..." to "Send a message"
-    And I press "OK"
-    Then I should see "Added 2 new recipients"
-    And I should see "Student 2" in the "Currently selected users" "table"
-    And I should see "Student 3" in the "Currently selected users" "table"
-    And I should not see "Student 1" in the "Currently selected users" "table"
+    And I choose "Send a message" from the participants page bulk action menu
+    Then I should see "Send message to 2 people"
 
   Scenario: Ensure no message options when messaging is disabled
     Given I log in as "admin"
index 2401cec..53f6618 100644 (file)
@@ -30,13 +30,8 @@ $id = required_param('id', PARAM_INT);
 $PAGE->set_url('/user/action_redir.php', array('formaction' => $formaction, 'id' => $id));
 list($formaction) = explode('?', $formaction, 2);
 
-// Add every page will be redirected by this script.
-$actions = array(
-        'messageselect.php',
-        'addnote.php',
-        'groupaddnote.php',
-        'bulkchange.php'
-        );
+// This page now only handles the bulk enrolment change actions, other actions are done with ajax.
+$actions = array('bulkchange.php');
 
 if (array_search($formaction, $actions) === false) {
     print_error('unknownuseraction');
@@ -175,5 +170,5 @@ if ($formaction == 'bulkchange.php') {
     exit();
 
 } else {
-    require_once($formaction);
+    throw new coding_exception('invalidaction');
 }
diff --git a/user/addnote.php b/user/addnote.php
deleted file mode 100644 (file)
index d4f764b..0000000
+++ /dev/null
@@ -1,118 +0,0 @@
-<?php
-// 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/>.
-
-/**
- * This file allows you to add a note for a user
- *
- * @copyright 1999 Martin Dougiamas  http://dougiamas.com
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @package core_user
- */
-
-require_once("../config.php");
-require_once($CFG->dirroot .'/notes/lib.php');
-
-$id    = required_param('id', PARAM_INT);              // Course id.
-$users = optional_param_array('userid', array(), PARAM_INT); // Array of user id.
-$contents = optional_param_array('contents', array(), PARAM_RAW); // Array of user notes.
-$states = optional_param_array('states', array(), PARAM_ALPHA); // Array of notes states.
-$PAGE->set_url('/user/addnote.php', array('id' => $id));
-
-if (! $course = $DB->get_record('course', array('id' => $id))) {
-    print_error('invalidcourseid');
-}
-
-$context = context_course::instance($id);
-require_login($course);
-
-// To create notes the current user needs a capability.
-require_capability('moodle/notes:manage', $context);
-
-if (empty($CFG->enablenotes)) {
-    print_error('notesdisabled', 'notes');
-}
-
-if (!empty($users) && confirm_sesskey()) {
-    if (count($users) != count($contents) || count($users) != count($states)) {
-        print_error('invalidformdata', '', $CFG->wwwroot.'/user/index.php?id='.$id);
-    }
-
-    $note = new stdClass();
-    $note->courseid = $id;
-    $note->format = FORMAT_PLAIN;
-    foreach ($users as $k => $v) {
-        $user = $DB->get_record('user', array('id' => $v));
-        $content = trim($contents[$k]);
-        if (!$user || empty($content)) {
-            continue;
-        }
-        $note->id = 0;
-        $note->content = $content;
-        $note->publishstate = $states[$k];
-        $note->userid = $v;
-        note_save($note);
-    }
-    redirect("$CFG->wwwroot/user/index.php?id=$id");
-}
-
-// Print headers.
-$straddnote = get_string('addnewnote', 'notes');
-
-$PAGE->navbar->add($straddnote);
-$PAGE->set_title("$course->shortname: ".get_string('extendenrol'));
-$PAGE->set_heading($course->fullname);
-
-echo $OUTPUT->header();
-// This will contain all available the based On select options, but we'll disable some on them on a per user basis.
-echo $OUTPUT->heading($straddnote);
-echo '<form method="post" action="addnote.php">';
-echo '<fieldset class="invisiblefieldset">';
-echo '<input type="hidden" name="id" value="'.$course->id.'" />';
-echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
-echo '</fieldset>';
-$table = new html_table();
-$table->head  = array (get_string('fullnameuser'),
-    get_string('content', 'notes'),
-    get_string('publishstate', 'notes') . $OUTPUT->help_icon('publishstate', 'notes'),
-    );
-$table->align = array ('left', 'center', 'center');
-$statenames = note_get_state_names();
-
-// The first time list hack.
-if (empty($users) and $post = data_submitted()) {
-    foreach ($post as $k => $v) {
-        if (preg_match('/^user(\d+)$/', $k, $m)) {
-            $users[] = $m[1];
-        }
-    }
-}
-foreach ($users as $k => $v) {
-    if (!$user = $DB->get_record('user', array('id' => $v))) {
-        continue;
-    }
-    $checkbox = html_writer::label(get_string('selectnotestate', 'notes'), 'menustates_'.$v, false, array('class' => 'accesshide'));
-    $checkbox .= html_writer::select($statenames, 'states[' . $k . ']',
-        empty($states[$k]) ? NOTES_STATE_PUBLIC : $states[$k], false, array('id' => 'menustates_'.$v));
-    $table->data[] = array(
-        '<input type="hidden" name="userid['.$k.']" value="'.$v.'" />'. fullname($user, true),
-        '<textarea name="contents['. $k . ']" rows="2" cols="40" spellcheck="true">' . strip_tags(@$contents[$k]) . '</textarea>',
-        $checkbox
-    );
-}
-echo html_writer::table($table);
-echo '<div style="width:100%;text-align:center;"><input type="submit" value="' . get_string('savechanges'). '" /></div></form>';
-echo $OUTPUT->footer();
-
diff --git a/user/amd/build/participants.min.js b/user/amd/build/participants.min.js
new file mode 100644 (file)
index 0000000..267d51d
Binary files /dev/null and b/user/amd/build/participants.min.js differ
diff --git a/user/amd/src/participants.js b/user/amd/src/participants.js
new file mode 100644 (file)
index 0000000..9fc7a59
--- /dev/null
@@ -0,0 +1,309 @@
+// 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
+ */
+define(['jquery', 'core/str', 'core/modal_factory', 'core/modal_events', 'core/templates', 'core/notification', 'core/ajax'],
+        function($, Str, ModalFactory, ModalEvents, Templates, Notification, Ajax) {
+
+    var SELECTORS = {
+        BULKACTIONSELECT: "#formactionid",
+        BULKUSERCHECKBOXES: "input.usercheckbox",
+        BULKUSERNOSCHECKBOXES: "input.usercheckbox[value='0']",
+        BULKUSERSELECTEDCHECKBOXES: "input.usercheckbox:checked",
+        BULKACTIONFORM: "#participantsform",
+        CHECKALLBUTTON: "#checkall",
+        CHECKALLNOSBUTTON: "#checkallnos",
+        CHECKALLONPAGEBUTTON: "#checkallonpage",
+        CHECKNONEBUTTON: "#checknone"
+    };
+
+    /**
+     * 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 = "";
+
+    /**
+     * Private method
+     *
+     * @method attachEventListeners
+     * @private
+     */
+    Participants.prototype.attachEventListeners = function() {
+        $(SELECTORS.BULKACTIONSELECT).on('change', function(e) {
+            var action = $(e.target).val();
+            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);
+                });
+
+                if (action == '#messageselect') {
+                    this.showSendMessage(ids).fail(Notification.exception);
+                } else if (action == '#addgroupnote') {
+                    this.showAddNote(ids).fail(Notification.exception);
+                }
+                $(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');
+                }
+            }
+        }.bind(this));
+
+        $(SELECTORS.CHECKALLBUTTON).on('click', function() {
+            var showallink = $(this).data('showallink');
+            if (showallink) {
+                window.location = showallink;
+            }
+        });
+
+        $(SELECTORS.CHECKALLNOSBUTTON).on('click', function() {
+            $(SELECTORS.BULKUSERNOSCHECKBOXES).prop('checked', true);
+        });
+        $(SELECTORS.CHECKALLONPAGEBUTTON).on('click', function() {
+            $(SELECTORS.BULKUSERCHECKBOXES).prop('checked', true);
+        });
+
+        $(SELECTORS.CHECKNONEBUTTON).on('click', function() {
+            $(SELECTORS.BULKUSERCHECKBOXES).prop('checked', false);
+        });
+    };
+
+    /**
+     * 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) {
+            states.push({value: key, label: this.noteStateNames[key]});
+        }
+
+        var context = {stateNames: states, stateHelpIcon: this.stateHelpIcon};
+
+        return $.when(
+            ModalFactory.create({
+                type: ModalFactory.types.SAVE_CANCEL,
+                body: Templates.render('core_user/add_bulk_note', context)
+            }),
+            Str.get_string('addbulknote', 'core_notes', users.length)
+        ).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));
+
+            this.modal.getRoot().on(ModalEvents.save, this.submitAddNote.bind(this, users));
+
+            this.modal.show();
+
+            return this.modal;
+        }.bind(this));
+    };
+
+    /**
+     * 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) {
+            return Str.get_string('addbulknotedone', 'core_notes', noteIds.length);
+        }).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 bodyPromise = Templates.render('core_user/send_bulk_message', {});
+
+        return $.when(
+            ModalFactory.create({
+                type: ModalFactory.types.SAVE_CANCEL,
+                body: bodyPromise
+            }),
+            Str.get_string('sendbulkmessage', 'core_message', users.length)
+        ).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));
+    };
+
+    /**
+     * 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) {
+
+        var messageText = this.modal.getRoot().find('form textarea').val();
+
+        var messages = [],
+            i = 0;
+
+        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) {
+            return Str.get_string('sendbulkmessagesent', 'core_message', messageIds.length);
+        }).then(function(msg) {
+            Notification.addNotification({
+                message: msg,
+                type: "success"
+            });
+            return true;
+        }).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);
+        }
+    };
+});
diff --git a/user/groupaddnote.php b/user/groupaddnote.php
deleted file mode 100644 (file)
index a859234..0000000
+++ /dev/null
@@ -1,123 +0,0 @@
-<?php
-// 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/>.
-
-/**
- * This file is part of the User section Moodle
- *
- * @copyright 1999 Martin Dougiamas  http://dougiamas.com
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @package core_user
- */
-
-require_once("../config.php");
-require_once($CFG->dirroot .'/notes/lib.php');
-
-$id    = required_param('id', PARAM_INT);              // Course id.
-$users = optional_param_array('userid', array(), PARAM_INT); // Array of user id.
-$content = optional_param('content', '', PARAM_RAW); // Note content.
-$state = optional_param('state', '', PARAM_ALPHA); // Note publish state.
-
-$url = new moodle_url('/user/groupaddnote.php', array('id' => $id));
-if ($content !== '') {
-    $url->param('content', $content);
-}
-if ($state !== '') {
-    $url->param('state', $state);
-}
-$PAGE->set_url($url);
-
-if (! $course = $DB->get_record('course', array('id' => $id))) {
-    print_error('invalidcourseid');
-}
-
-$context = context_course::instance($id);
-require_login($course);
-
-// To create notes the current user needs a capability.
-require_capability('moodle/notes:manage', $context);
-
-if (empty($CFG->enablenotes)) {
-    print_error('notesdisabled', 'notes');
-}
-
-if (!empty($users) && !empty($content) && confirm_sesskey()) {
-    $note = new stdClass();
-    $note->courseid = $id;
-    $note->format = FORMAT_PLAIN;
-    $note->content = $content;
-    $note->publishstate = $state;
-    foreach ($users as $k => $v) {
-        if (!$user = $DB->get_record('user', array('id' => $v))) {
-            continue;
-        }
-        $note->id = 0;
-        $note->userid = $v;
-        note_save($note);
-    }
-
-    redirect("$CFG->wwwroot/user/index.php?id=$id");
-}
-
-$straddnote = get_string('groupaddnewnote', 'notes');
-
-$PAGE->navbar->add($straddnote);
-$PAGE->set_title("$course->shortname: ".get_string('extendenrol'));
-$PAGE->set_heading($course->fullname);
-
-// Print headers.
-echo $OUTPUT->header();
-
-// This will contain all available the based On select options, but we'll disable some on them on a per user basis.
-
-echo $OUTPUT->heading($straddnote);
-echo '<form method="post" action="groupaddnote.php" >';
-echo '<div style="width:100%;text-align:center;">';
-echo '<input type="hidden" name="id" value="'.$course->id.'" />';
-echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
-$statenames = note_get_state_names();
-
-// The first time list hack.
-if (empty($users) and $post = data_submitted()) {
-    foreach ($post as $k => $v) {
-        if (preg_match('/^user(\d+)$/', $k, $m)) {
-            $users[] = $m[1];
-        }
-    }
-}
-
-$userlist = array();
-foreach ($users as $k => $v) {
-    if (!$user = $DB->get_record('user', array('id' => $v))) {
-        continue;
-    }
-    echo '<input type="hidden" name="userid['.$k.']" value="'.$v.'" />';
-    $userlist[] = fullname($user, true);
-}
-echo '<p>';
-echo get_string('users'). ': ' . implode(', ', $userlist) . '.';
-echo '</p>';
-
-echo '<p>' . get_string('content', 'notes');
-echo '<br /><textarea name="content" rows="5" cols="50" spellcheck="true">' . strip_tags(@$content) . '</textarea></p>';
-
-echo '<p>';
-echo html_writer::label(get_string('publishstate', 'notes'), 'menustate');
-echo $OUTPUT->help_icon('publishstate', 'notes');
-echo html_writer::select($statenames, 'state', empty($state) ? NOTES_STATE_PUBLIC : $state, false);
-echo '</p>';
-
-echo '<input type="submit" value="' . get_string('savechanges'). '" /></div></form>';
-echo $OUTPUT->footer();
index e89aab7..69f8f94 100644 (file)
@@ -25,6 +25,7 @@
 require_once('../config.php');
 require_once($CFG->dirroot.'/user/lib.php');
 require_once($CFG->dirroot.'/course/lib.php');
+require_once($CFG->dirroot.'/notes/lib.php');
 require_once($CFG->libdir.'/tablelib.php');
 require_once($CFG->libdir.'/filelib.php');
 require_once($CFG->dirroot.'/enrol/locallib.php');
@@ -292,10 +293,9 @@ if ($bulkoperations) {
         'value' => get_string('deselectall')));
     echo html_writer::end_tag('div');
     $displaylist = array();
-    $displaylist['messageselect.php'] = get_string('messageselectadd');
+    $displaylist['#messageselect'] = get_string('messageselectadd');
     if (!empty($CFG->enablenotes) && has_capability('moodle/notes:manage', $context) && $context->id != $frontpagectx->id) {
-        $displaylist['addnote.php'] = get_string('addnewnote', 'notes');
-        $displaylist['groupaddnote.php'] = get_string('groupaddnewnote', 'notes');
+        $displaylist['#addgroupnote'] = get_string('addnewnote', 'notes');
     }
 
     if ($context->id != $frontpagectx->id) {
@@ -333,8 +333,11 @@ if ($bulkoperations) {
     echo '</div></div>';
     echo '</form>';
 
-    $module = array('name' => 'core_user', 'fullpath' => '/user/module.js');
-    $PAGE->requires->js_init_call('M.core_user.init_participation', null, false, $module);
+    $options = new stdClass();
+    $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]);
 }
 
 echo '</div>';  // Userlist.
diff --git a/user/module.js b/user/module.js
deleted file mode 100644 (file)
index 98c9040..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-
-M.core_user = {};
-
-M.core_user.init_participation = function(Y) {
-       Y.on('change', function() {
-               var action = Y.one('#formactionid');
-               if (action.get('value') == '') {
-                       return;
-               }
-        var ok = false;
-        Y.all('input.usercheckbox').each(function() {
-            if (this.get('checked')) {
-                ok = true;
-            }
-        });
-        if (!ok) {
-            // no checkbox selected
-            return;
-        }
-        Y.one('#participantsform').submit();
-       }, '#formactionid');
-
-    Y.on('click', function(e) {
-        // Presence of a show all link indicates we should redirect to
-        // a page with all users listed and checked, otherwise just check
-        // those already shown.
-        var showallink = this.getAttribute('data-showallink');
-        if (showallink) {
-            window.location = showallink;
-        }
-        Y.all('input.usercheckbox').each(function() {
-            this.set('checked', 'checked');
-        });
-    }, '#checkall, #checkallonpage');
-
-    Y.on('click', function(e) {
-        Y.all('input.usercheckbox').each(function() {
-            this.set('checked', '');
-        });
-    }, '#checknone');
-};
-
-M.core_user.init_tree = function(Y, expand_all, htmlid) {
-    Y.use('yui2-treeview', function(Y) {
-        var tree = new Y.YUI2.widget.TreeView(htmlid);
-
-        tree.subscribe("clickEvent", function(node, event) {
-            // we want normal clicking which redirects to url
-            return false;
-        });
-
-        if (expand_all) {
-            tree.expandAll();
-        }
-
-        tree.render();
-    });
-};
diff --git a/user/templates/add_bulk_note.mustache b/user/templates/add_bulk_note.mustache
new file mode 100644 (file)
index 0000000..30ba370
--- /dev/null
@@ -0,0 +1,55 @@
+{{!
+    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/>.
+}}
+{{!
+    @template core_user/add_bulk_note
+
+    Template for the add bulk note modal.
+
+    Context variables required for this template:
+    * stateNames array - List of value / label pairs of valid publish states for notes.
+    * stateHelpIcon string - Rendered help icon for the publish state.
+
+    Example context (json):
+    {
+        "stateNames": [ { "value": 0, "label": "State 1"}, { "value": 1, "label": "State 2"} ],
+        "stateHelpIcon": "(help me)"
+    }
+}}
+<form>
+<p>
+<label for="bulk-state" class="m-r-2">
+{{#str}}publishstate, core_notes{{/str}}
+</label>
+<select name="state" id="bulk-state" class="custom-select">
+{{#stateNames}}
+    <option value="{{value}}">{{label}}</option>
+{{/stateNames}}
+</select>
+{{{stateHelpIcon}}}
+</p>
+<p>
+<label for="bulk-note">
+<span class="sr-only">{{#str}}note, core_note{{/str}}</span>
+</label>
+<textarea id="bulk-note" rows="3" data-max-rows="10" data-auto-rows="true" cols="30" class="form-control"></textarea>
+</p>
+</form>
+{{#js}}
+require(['core/auto_rows'], function(AutoRows) {
+    AutoRows.init(document.getElementById('bulk-note'));
+});
+{{/js}}
diff --git a/user/templates/send_bulk_message.mustache b/user/templates/send_bulk_message.mustache
new file mode 100644 (file)
index 0000000..34f9c91
--- /dev/null
@@ -0,0 +1,41 @@
+{{!
+    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/>.
+}}
+{{!
+    @template core_user/send_bulk_message
+
+    Template for the send bulk message modal.
+
+    Context variables required for this template:
+      None
+
+    Example context (json):
+    {
+    }
+}}
+<form>
+<p>
+<label for="bulk-message">
+<span class="sr-only">{{#str}}message, core_message{{/str}}</span>
+</label>
+<textarea id="bulk-message" rows="3" data-max-rows="10" data-auto-rows="true" cols="30" class="form-control"></textarea>
+</p>
+</form>
+{{#js}}
+require(['core/auto_rows'], function(AutoRows) {
+    AutoRows.init(document.getElementById('bulk-message'));
+});
+{{/js}}
diff --git a/user/tests/behat/behat_user.php b/user/tests/behat/behat_user.php
new file mode 100644 (file)
index 0000000..36339e6
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+// 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/>.
+
+/**
+ * User steps definition.
+ *
+ * @package    core_user
+ * @category   test
+ * @copyright  2017 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
+
+require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
+
+/**
+ * Steps definitions for users.
+ *
+ * @package    core_user
+ * @category   test
+ * @copyright  2017 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_user extends behat_base {
+
+    /**
+     * Choose from the bulk action menu.
+     *
+     * @Given /^I choose "(?P<nodetext_string>(?:[^"]|\\")*)" from the participants page bulk action menu$/
+     * @param string $nodetext The menu item to select.
+     */
+    public function i_choose_from_the_participants_page_bulk_action_menu($nodetext) {
+        $nodetext = behat_context_helper::escape($nodetext);
+
+        // Open the select.
+        $this->execute("behat_general::i_click_on", array("//select[@id='formactionid']", "xpath_element"));
+
+        // Click on the option.
+        $this->execute("behat_general::i_click_on", array("//select[@id='formactionid']" .
+                                                          "/option[contains(., " . $nodetext . ")]", "xpath_element"));
+    }
+}