From b5b81de3d63e941dd90cbb970ab50c343af750ab Mon Sep 17 00:00:00 2001 From: Damyon Wiese Date: Tue, 7 Nov 2017 10:32:51 +0800 Subject: [PATCH] MDL-59758 core_user: Replace old bulk actions The participants page has some clunky multi page forms for bulk actions. Replaces it with an ajax alternative. --- lang/en/deprecated.txt | 4 + lang/en/message.php | 2 + lang/en/moodle.php | 4 +- lang/en/notes.php | 7 +- lib/amd/build/auto_rows.min.js | 2 +- lib/amd/src/auto_rows.js | 6 +- lib/db/services.php | 1 + notes/lib.php | 5 + notes/tests/behat/participants_notes.feature | 20 +- report/participation/index.php | 19 +- report/participation/module.js | 44 --- .../tests/behat/message_participants.feature | 8 +- user/action_redir.php | 11 +- user/addnote.php | 118 ------- user/amd/build/participants.min.js | 1 + user/amd/src/participants.js | 309 ++++++++++++++++++ user/groupaddnote.php | 123 ------- user/index.php | 13 +- user/module.js | 58 ---- user/templates/add_bulk_note.mustache | 55 ++++ user/templates/send_bulk_message.mustache | 41 +++ user/tests/behat/behat_user.php | 56 ++++ 22 files changed, 521 insertions(+), 386 deletions(-) delete mode 100644 report/participation/module.js delete mode 100644 user/addnote.php create mode 100644 user/amd/build/participants.min.js create mode 100644 user/amd/src/participants.js delete mode 100644 user/groupaddnote.php delete mode 100644 user/module.js create mode 100644 user/templates/add_bulk_note.mustache create mode 100644 user/templates/send_bulk_message.mustache create mode 100644 user/tests/behat/behat_user.php diff --git a/lang/en/deprecated.txt b/lang/en/deprecated.txt index b912982899b..c7b255937e6 100644 --- a/lang/en/deprecated.txt +++ b/lang/en/deprecated.txt @@ -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 diff --git a/lang/en/message.php b/lang/en/message.php index 64a7588c064..ab3e90dbeb5 100644 --- a/lang/en/message.php +++ b/lang/en/message.php @@ -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'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 71e29cd774b..f1eb2524416 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -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'] = 'Gravatar 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)'; diff --git a/lang/en/notes.php b/lang/en/notes.php index 1146eac9a9b..efc196c1532 100644 --- a/lang/en/notes.php +++ b/lang/en/notes.php @@ -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"; diff --git a/lib/amd/build/auto_rows.min.js b/lib/amd/build/auto_rows.min.js index c95209bc9f0..5666dc3d1a1 100644 --- a/lib/amd/build/auto_rows.min.js +++ b/lib/amd/build/auto_rows.min.js @@ -1 +1 @@ -define(["jquery"],function(a){var b={ELEMENT:"[data-auto-rows]"},c={ROW_CHANGE:"autorows:rowchange"},d=function(a){var b=a.attr("rows"),c=a.data("min-rows"),d=a.attr("data-max-rows"),e=a.height(),f=a.innerHeight(),g=f-e;a.height("1px");var h=a[0].scrollHeight,i=(h-g)/(e/b);return a.css("height",""),i=d?d:i},e=function(b,e){var f=a(e.target),g=f.data("min-rows"),h=f.attr("rows");"undefined"==typeof g&&f.data("min-rows",h);var i=d(f);i!=h&&(f.attr("rows",i),f.trigger(c.ROW_CHANGE))},f=function(c){a(c).data("auto-rows")?a(c).on("input propertychange",e.bind(this,c)):a(c).on("input propertychange",b.ELEMENT,e.bind(this,c))};return{init:f,events:c}}); \ No newline at end of file +define(["jquery"],function(a){var b={ELEMENT:"[data-auto-rows]"},c={ROW_CHANGE:"autorows:rowchange"},d=function(a){var b=a.attr("rows"),c=a.data("min-rows"),d=a.attr("data-max-rows"),e=a.height(),f=a.innerHeight(),g=f-e;a.height("1px");var h=a[0].scrollHeight,i=(h-g)/(e/b);return a.css("height",""),i=d?d:i},e=function(b){var e=a(b.target),f=e.data("min-rows"),g=e.attr("rows");"undefined"==typeof f&&e.data("min-rows",g);var h=d(e);h!=g&&(e.attr("rows",h),e.trigger(c.ROW_CHANGE))},f=function(c){a(c).data("auto-rows")?a(c).on("input propertychange",e.bind(this)):a(c).on("input propertychange",b.ELEMENT,e.bind(this))};return{init:f,events:c}}); \ No newline at end of file diff --git a/lib/amd/src/auto_rows.js b/lib/amd/src/auto_rows.js index 2c04a47fed9..ab7a876469a 100644 --- a/lib/amd/src/auto_rows.js +++ b/lib/amd/src/auto_rows.js @@ -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)); } }; diff --git a/lib/db/services.php b/lib/db/services.php index 34425f6262a..3bef679ab53 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -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), ), diff --git a/notes/lib.php b/notes/lib.php index b21f1f6514e..041e61392ba 100644 --- a/notes/lib.php +++ b/notes/lib.php @@ -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. diff --git a/notes/tests/behat/participants_notes.feature b/notes/tests/behat/participants_notes.feature index aae0dc5cf6c..a2df67154be 100644 --- a/notes/tests/behat/participants_notes.feature +++ b/notes/tests/behat/participants_notes.feature @@ -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 diff --git a/report/participation/index.php b/report/participation/index.php index 0400e6e5037..ef66eef7fd7 100644 --- a/report/participation/index.php +++ b/report/participation/index.php @@ -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 '

'.get_string('counteditems', '', $a).'

'."\n"; if (!empty($CFG->messaging)) { - echo '
'."\n"; + echo ''."\n"; echo '
'."\n"; echo ''."\n"; echo ''."\n"; @@ -372,23 +373,25 @@ if (!empty($instanceid) && !empty($roleid)) { if (!empty($CFG->messaging)) { $buttonclasses = 'btn btn-secondary'; echo '
'; - echo ' '."\n"; + echo ' '."\n"; echo ' '."\n"; if ($perpage >= $matchcount) { - echo ''."\n"; + echo ''."\n"; } echo '
'; echo '
'; 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 ''."\n"; + $displaylist['#messageselect'] = get_string('messageselectadd'); + echo html_writer::select($displaylist, 'formaction', '', array('' => 'choosedots'), array('id' => 'formactionid')); echo '
'; echo '
'."\n"; echo '
'."\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 ''."\n"; } diff --git a/report/participation/module.js b/report/participation/module.js deleted file mode 100644 index d3984392c17..00000000000 --- a/report/participation/module.js +++ /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 diff --git a/report/participation/tests/behat/message_participants.feature b/report/participation/tests/behat/message_participants.feature index 88aae4ed420..a9a9d7e56d8 100644 --- a/report/participation/tests/behat/message_participants.feature +++ b/report/participation/tests/behat/message_participants.feature @@ -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" diff --git a/user/action_redir.php b/user/action_redir.php index 2401cec5139..53f6618fe28 100644 --- a/user/action_redir.php +++ b/user/action_redir.php @@ -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 index d4f764be86e..00000000000 --- a/user/addnote.php +++ /dev/null @@ -1,118 +0,0 @@ -. - -/** - * 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 '
'; -echo '
'; -echo ''; -echo ''; -echo '
'; -$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( - ''. fullname($user, true), - '', - $checkbox - ); -} -echo html_writer::table($table); -echo '
'; -echo $OUTPUT->footer(); - diff --git a/user/amd/build/participants.min.js b/user/amd/build/participants.min.js new file mode 100644 index 00000000000..267d51d542b --- /dev/null +++ b/user/amd/build/participants.min.js @@ -0,0 +1 @@ +define(["jquery","core/str","core/modal_factory","core/modal_events","core/templates","core/notification","core/ajax"],function(a,b,c,d,e,f,g){var h={BULKACTIONSELECT:"#formactionid",BULKUSERCHECKBOXES:"input.usercheckbox",BULKUSERNOSCHECKBOXES:"input.usercheckbox[value='0']",BULKUSERSELECTEDCHECKBOXES:"input.usercheckbox:checked",BULKACTIONFORM:"#participantsform",CHECKALLBUTTON:"#checkall",CHECKALLNOSBUTTON:"#checkallnos",CHECKALLONPAGEBUTTON:"#checkallonpage",CHECKNONEBUTTON:"#checknone"},i=function(a){this.courseId=a.courseid,this.noteStateNames=a.noteStateNames,this.stateHelpIcon=a.stateHelpIcon,this.attachEventListeners()};return i.prototype.modal=null,i.prototype.courseId=-1,i.prototype.noteStateNames={},i.prototype.stateHelpIcon="",i.prototype.attachEventListeners=function(){a(h.BULKACTIONSELECT).on("change",function(b){var c=a(b.target).val();if(c.indexOf("#")!==-1){b.preventDefault();var d=[];a(h.BULKUSERSELECTEDCHECKBOXES).each(function(b,c){var e=a(c).attr("name"),f=e.replace("user","");d.push(f)}),"#messageselect"==c?this.showSendMessage(d).fail(f.exception):"#addgroupnote"==c&&this.showAddNote(d).fail(f.exception),a(h.BULKACTIONSELECT+' option[value=""]').prop("selected","selected")}else""!==c&&(a(h.BULKUSERSELECTEDCHECKBOXES).length>0?a(h.BULKACTIONFORM).submit():a(h.BULKACTIONSELECT+' option[value=""]').prop("selected","selected"))}.bind(this)),a(h.CHECKALLBUTTON).on("click",function(){var b=a(this).data("showallink");b&&(window.location=b)}),a(h.CHECKALLNOSBUTTON).on("click",function(){a(h.BULKUSERNOSCHECKBOXES).prop("checked",!0)}),a(h.CHECKALLONPAGEBUTTON).on("click",function(){a(h.BULKUSERCHECKBOXES).prop("checked",!0)}),a(h.CHECKNONEBUTTON).on("click",function(){a(h.BULKUSERCHECKBOXES).prop("checked",!1)})},i.prototype.showAddNote=function(f){if(0==f.length)return a.Deferred().resolve().promise();var g=[];for(var i in this.noteStateNames)g.push({value:i,label:this.noteStateNames[i]});var j={stateNames:g,stateHelpIcon:this.stateHelpIcon};return a.when(c.create({type:c.types.SAVE_CANCEL,body:e.render("core_user/add_bulk_note",j)}),b.get_string("addbulknote","core_notes",f.length)).then(function(b,c){return this.modal=b,this.modal.setTitle(c),this.modal.setSaveButtonText(c),this.modal.getRoot().on(d.hidden,function(){var b=a("#user-notifications [role=alert]");b.length?b.focus():a(h.BULKACTIONSELECT).focus(),this.modal.getRoot().remove()}.bind(this)),this.modal.getRoot().on(d.save,this.submitAddNote.bind(this,f)),this.modal.show(),this.modal}.bind(this))},i.prototype.submitAddNote=function(a){var c=this.modal.getRoot().find("form textarea").val(),d=this.modal.getRoot().find("form select").val(),e=[],h=0;for(h=0;h. + +/** + * 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 index a8592349a69..00000000000 --- a/user/groupaddnote.php +++ /dev/null @@ -1,123 +0,0 @@ -. - -/** - * 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 '
'; -echo '
'; -echo ''; -echo ''; -$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 ''; - $userlist[] = fullname($user, true); -} -echo '

'; -echo get_string('users'). ': ' . implode(', ', $userlist) . '.'; -echo '

'; - -echo '

' . get_string('content', 'notes'); -echo '

'; - -echo '

'; -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 '

'; - -echo '
'; -echo $OUTPUT->footer(); diff --git a/user/index.php b/user/index.php index e89aab71007..69f8f946c5a 100644 --- a/user/index.php +++ b/user/index.php @@ -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 ''; echo ''; - $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 ''; // Userlist. diff --git a/user/module.js b/user/module.js deleted file mode 100644 index 98c9040280f..00000000000 --- a/user/module.js +++ /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 index 00000000000..30ba3703fbd --- /dev/null +++ b/user/templates/add_bulk_note.mustache @@ -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 . +}} +{{! + @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)" + } +}} +
+

+ + +{{{stateHelpIcon}}} +

+

+ + +

+
+{{#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 index 00000000000..34f9c91427f --- /dev/null +++ b/user/templates/send_bulk_message.mustache @@ -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 . +}} +{{! + @template core_user/send_bulk_message + + Template for the send bulk message modal. + + Context variables required for this template: + None + + Example context (json): + { + } +}} +
+

+ + +

+
+{{#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 index 00000000000..36339e64276 --- /dev/null +++ b/user/tests/behat/behat_user.php @@ -0,0 +1,56 @@ +. + +/** + * 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(?:[^"]|\\")*)" 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")); + } +}