Commit b5b81de3 authored by Damyon Wiese's avatar Damyon Wiese
Browse files

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.
parent b9ae53f8
......@@ -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
......@@ -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';
......
......@@ -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)';
......@@ -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";
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
......@@ -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));
}
};
......
......@@ -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),
),
......
......@@ -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.
......
@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
......
......@@ -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";
}
......
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
......@@ -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"
......
......@@ -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');
}
<?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();
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
// 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});