MDL-65399 mod_quiz: UI should prevent deleting the last slot of section

This commit is contained in:
Shamim Rezaie 2019-04-26 02:20:05 +10:00
parent a834294228
commit aa6a8b98c6
9 changed files with 236 additions and 16 deletions

View File

@ -1149,6 +1149,8 @@ class edit_renderer extends \plugin_renderer_base {
$this->page->requires->strings_for_js(array( $this->page->requires->strings_for_js(array(
'addpagebreak', 'addpagebreak',
'cannotremoveallsectionslots',
'cannotremoveslots',
'confirmremovesectionheading', 'confirmremovesectionheading',
'confirmremovequestion', 'confirmremovequestion',
'dragtoafter', 'dragtoafter',

View File

@ -137,6 +137,8 @@ $string['cannotinsertrandomquestion'] = 'Could not insert new random question!';
$string['cannotloadquestion'] = 'Could not load question options'; $string['cannotloadquestion'] = 'Could not load question options';
$string['cannotloadtypeinfo'] = 'Unable to load questiontype specific question information'; $string['cannotloadtypeinfo'] = 'Unable to load questiontype specific question information';
$string['cannotopen'] = 'Cannot open export file ({$a})'; $string['cannotopen'] = 'Cannot open export file ({$a})';
$string['cannotremoveallsectionslots'] = 'You have selected all questions of the \'{$a}\' section heading. It is not allowed to remove all questions under a section heading.';
$string['cannotremoveslots'] = 'Can not remove questions';
$string['cannotrestore'] = 'Could not restore question sessions'; $string['cannotrestore'] = 'Could not restore question sessions';
$string['cannotreviewopen'] = 'You cannot review this attempt, it is still open.'; $string['cannotreviewopen'] = 'You cannot review this attempt, it is still open.';
$string['cannotsavelayout'] = 'Could not save layout'; $string['cannotsavelayout'] = 'Could not save layout';

View File

@ -1190,11 +1190,9 @@ table#categoryquestions {
display: none; display: none;
} }
#page-mod-quiz-edit.select-multiple .mod-quiz-edit-content .section-heading, #page-mod-quiz-edit.select-multiple .mod-quiz-edit-content .section-heading a,
#page-mod-quiz-edit.select-multiple .mod-quiz-edit-content .section-heading form, #page-mod-quiz-edit.select-multiple .mod-quiz-edit-content .section-heading form,
#page-mod-quiz-edit.select-multiple .mod-quiz-edit-content .section-heading .instancesectioncontainer, #page-mod-quiz-edit.select-multiple .mod-quiz-edit-content .section-heading .instanceshufflequestions {
#page-mod-quiz-edit.select-multiple .mod-quiz-edit-content .section-heading .instanceshufflequestions,
#page-mod-quiz-edit.select-multiple .mod-quiz-edit-content .section-heading .instancesectioncontainer h3 {
display: none; display: none;
} }

View File

@ -167,3 +167,74 @@ Feature: Edit quiz page - remove multiple questions
When I click on "Deselect all" "link" When I click on "Deselect all" "link"
Then the field "selectquestion-3" matches value "0" Then the field "selectquestion-3" matches value "0"
@javascript
Scenario: Delete multiple questions from sections
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | Question A | First question |
| Test questions | truefalse | Question B | Second question |
| Test questions | truefalse | Question C | Third question |
| Test questions | truefalse | Question D | Fourth question |
| Test questions | truefalse | Question E | Fifth question |
| Test questions | truefalse | Question F | Sixth question |
And quiz "Quiz 1" contains the following questions:
| question | page |
| Question A | 1 |
| Question B | 2 |
| Question C | 3 |
| Question D | 4 |
| Question E | 5 |
| Question F | 6 |
And quiz "Quiz 1" contains the following sections:
| heading | firstslot | shuffle |
| Section 1 | 1 | 0 |
| Section 2 | 2 | 0 |
| Section 3 | 4 | 0 |
And I navigate to "Edit quiz" in current page administration
When I click on "Select multiple items" "button"
And I click on "selectquestion-3" "checkbox"
And I click on "selectquestion-5" "checkbox"
And I click on "selectquestion-6" "checkbox"
And I click on "Delete selected" "button"
And I click on "Yes" "button" in the "Confirm" "dialogue"
Then I should see "Question A" on quiz page "1"
And I should see "Question B" on quiz page "2"
And I should see "Question D" on quiz page "3"
And I should not see "Question C"
And I should not see "Question E"
And I should not see "Question F"
@javascript
Scenario: Attempting to delete all questions of a sections
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | Question A | First question |
| Test questions | truefalse | Question B | Second question |
| Test questions | truefalse | Question C | Third question |
| Test questions | truefalse | Question D | Fourth question |
| Test questions | truefalse | Question E | Fifth question |
| Test questions | truefalse | Question F | Sixth question |
And quiz "Quiz 1" contains the following questions:
| question | page |
| Question A | 1 |
| Question B | 2 |
| Question C | 3 |
| Question D | 4 |
| Question E | 5 |
| Question F | 6 |
And quiz "Quiz 1" contains the following sections:
| heading | firstslot | shuffle |
| Section 1 | 1 | 0 |
| Section 2 | 2 | 0 |
| Section 3 | 4 | 0 |
And I navigate to "Edit quiz" in current page administration
When I click on "Select multiple items" "button"
And I click on "selectquestion-2" "checkbox"
And I click on "selectquestion-3" "checkbox"
And I click on "Delete selected" "button"
Then I should see "Can not remove questions"

View File

@ -64,6 +64,7 @@ var CSS = {
NUMQUESTIONS: '.numberofquestions', NUMQUESTIONS: '.numberofquestions',
PAGECONTENT: 'div#page-content', PAGECONTENT: 'div#page-content',
PAGELI: 'li.page', PAGELI: 'li.page',
SECTIONLI: 'li.section',
SECTIONUL: 'ul.section', SECTIONUL: 'ul.section',
SECTIONFORM: '.instancesectioncontainer form', SECTIONFORM: '.instancesectioncontainer form',
SECTIONINPUT: 'input[name=section]', SECTIONINPUT: 'input[name=section]',
@ -337,7 +338,7 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
Y.one(SELECTOR.SELECTMULTIPLEDELETEBUTTON).setAttribute('disabled', 'disabled'); Y.one(SELECTOR.SELECTMULTIPLEDELETEBUTTON).setAttribute('disabled', 'disabled');
// Assign the delete method to the delete multiple button. // Assign the delete method to the delete multiple button.
Y.delegate('click', this.delete_multiple_with_confirmation, BODY, SELECTOR.SELECTMULTIPLEDELETEBUTTON, this); Y.delegate('click', this.delete_multiple_action, BODY, SELECTOR.SELECTMULTIPLEDELETEBUTTON, this);
// Enable the delete all button only when at least one slot is selected. // Enable the delete all button only when at least one slot is selected.
Y.delegate('click', this.toggle_select_all_buttons_enabled, BODY, SELECTOR.SELECTMULTIPLECHECKBOX, this); Y.delegate('click', this.toggle_select_all_buttons_enabled, BODY, SELECTOR.SELECTMULTIPLECHECKBOX, this);
@ -439,7 +440,6 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
* @param {EventFacade} ev The event that was fired. * @param {EventFacade} ev The event that was fired.
* @param {Node} button The button that triggered this action. * @param {Node} button The button that triggered this action.
* @param {Node} activity The activity node that this action will be performed on. * @param {Node} activity The activity node that this action will be performed on.
* @chainable
*/ */
delete_with_confirmation: function(ev, button, activity) { delete_with_confirmation: function(ev, button, activity) {
// Prevent the default button action. // Prevent the default button action.
@ -481,13 +481,62 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
}, this); }, this);
}, },
/**
* Finds the section that would become empty if we remove the selected slots.
*
* @protected
* @method find_sections_that_would_become_empty
* @returns {String} The name of the first section found
*/
find_sections_that_would_become_empty: function() {
var section;
var sectionnodes = Y.all(SELECTOR.SECTIONLI);
if (sectionnodes.size() > 1) {
sectionnodes.some(function(node) {
var sectionname = node.one(SELECTOR.INSTANCESECTION).getContent();
var checked = node.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':checked');
var unchecked = node.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':not(:checked)');
if (!checked.isEmpty() && unchecked.isEmpty()) {
section = sectionname;
}
return section;
});
}
return section;
},
/**
* Takes care of what needs to happen when the user clicks on the delete multiple button.
*
* @protected
* @method delete_multiple_action
* @param {EventFacade} ev The event that was fired.
*/
delete_multiple_action: function(ev) {
var problemsection = this.find_sections_that_would_become_empty();
if (problemsection) {
var alert = new M.core.alert({
title: M.util.get_string('cannotremoveslots', 'quiz'),
message: M.util.get_string('cannotremoveallsectionslots', 'quiz', problemsection)
});
alert.show();
} else {
this.delete_multiple_with_confirmation(ev);
}
},
/** /**
* Deletes the given activities or resources after confirmation. * Deletes the given activities or resources after confirmation.
* *
* @protected * @protected
* @method delete_multiple_with_confirmation * @method delete_multiple_with_confirmation
* @param {EventFacade} ev The event that was fired. * @param {EventFacade} ev The event that was fired.
* @chainable
*/ */
delete_multiple_with_confirmation: function(ev) { delete_multiple_with_confirmation: function(ev) {
ev.preventDefault(); ev.preventDefault();

File diff suppressed because one or more lines are too long

View File

@ -64,6 +64,7 @@ var CSS = {
NUMQUESTIONS: '.numberofquestions', NUMQUESTIONS: '.numberofquestions',
PAGECONTENT: 'div#page-content', PAGECONTENT: 'div#page-content',
PAGELI: 'li.page', PAGELI: 'li.page',
SECTIONLI: 'li.section',
SECTIONUL: 'ul.section', SECTIONUL: 'ul.section',
SECTIONFORM: '.instancesectioncontainer form', SECTIONFORM: '.instancesectioncontainer form',
SECTIONINPUT: 'input[name=section]', SECTIONINPUT: 'input[name=section]',
@ -337,7 +338,7 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
Y.one(SELECTOR.SELECTMULTIPLEDELETEBUTTON).setAttribute('disabled', 'disabled'); Y.one(SELECTOR.SELECTMULTIPLEDELETEBUTTON).setAttribute('disabled', 'disabled');
// Assign the delete method to the delete multiple button. // Assign the delete method to the delete multiple button.
Y.delegate('click', this.delete_multiple_with_confirmation, BODY, SELECTOR.SELECTMULTIPLEDELETEBUTTON, this); Y.delegate('click', this.delete_multiple_action, BODY, SELECTOR.SELECTMULTIPLEDELETEBUTTON, this);
// Enable the delete all button only when at least one slot is selected. // Enable the delete all button only when at least one slot is selected.
Y.delegate('click', this.toggle_select_all_buttons_enabled, BODY, SELECTOR.SELECTMULTIPLECHECKBOX, this); Y.delegate('click', this.toggle_select_all_buttons_enabled, BODY, SELECTOR.SELECTMULTIPLECHECKBOX, this);
@ -439,7 +440,6 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
* @param {EventFacade} ev The event that was fired. * @param {EventFacade} ev The event that was fired.
* @param {Node} button The button that triggered this action. * @param {Node} button The button that triggered this action.
* @param {Node} activity The activity node that this action will be performed on. * @param {Node} activity The activity node that this action will be performed on.
* @chainable
*/ */
delete_with_confirmation: function(ev, button, activity) { delete_with_confirmation: function(ev, button, activity) {
// Prevent the default button action. // Prevent the default button action.
@ -481,13 +481,62 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
}, this); }, this);
}, },
/**
* Finds the section that would become empty if we remove the selected slots.
*
* @protected
* @method find_sections_that_would_become_empty
* @returns {String} The name of the first section found
*/
find_sections_that_would_become_empty: function() {
var section;
var sectionnodes = Y.all(SELECTOR.SECTIONLI);
if (sectionnodes.size() > 1) {
sectionnodes.some(function(node) {
var sectionname = node.one(SELECTOR.INSTANCESECTION).getContent();
var checked = node.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':checked');
var unchecked = node.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':not(:checked)');
if (!checked.isEmpty() && unchecked.isEmpty()) {
section = sectionname;
}
return section;
});
}
return section;
},
/**
* Takes care of what needs to happen when the user clicks on the delete multiple button.
*
* @protected
* @method delete_multiple_action
* @param {EventFacade} ev The event that was fired.
*/
delete_multiple_action: function(ev) {
var problemsection = this.find_sections_that_would_become_empty();
if (problemsection) {
var alert = new M.core.alert({
title: M.util.get_string('cannotremoveslots', 'quiz'),
message: M.util.get_string('cannotremoveallsectionslots', 'quiz', problemsection)
});
alert.show();
} else {
this.delete_multiple_with_confirmation(ev);
}
},
/** /**
* Deletes the given activities or resources after confirmation. * Deletes the given activities or resources after confirmation.
* *
* @protected * @protected
* @method delete_multiple_with_confirmation * @method delete_multiple_with_confirmation
* @param {EventFacade} ev The event that was fired. * @param {EventFacade} ev The event that was fired.
* @chainable
*/ */
delete_multiple_with_confirmation: function(ev) { delete_multiple_with_confirmation: function(ev) {
ev.preventDefault(); ev.preventDefault();

View File

@ -104,7 +104,7 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
Y.one(SELECTOR.SELECTMULTIPLEDELETEBUTTON).setAttribute('disabled', 'disabled'); Y.one(SELECTOR.SELECTMULTIPLEDELETEBUTTON).setAttribute('disabled', 'disabled');
// Assign the delete method to the delete multiple button. // Assign the delete method to the delete multiple button.
Y.delegate('click', this.delete_multiple_with_confirmation, BODY, SELECTOR.SELECTMULTIPLEDELETEBUTTON, this); Y.delegate('click', this.delete_multiple_action, BODY, SELECTOR.SELECTMULTIPLEDELETEBUTTON, this);
// Enable the delete all button only when at least one slot is selected. // Enable the delete all button only when at least one slot is selected.
Y.delegate('click', this.toggle_select_all_buttons_enabled, BODY, SELECTOR.SELECTMULTIPLECHECKBOX, this); Y.delegate('click', this.toggle_select_all_buttons_enabled, BODY, SELECTOR.SELECTMULTIPLECHECKBOX, this);
@ -206,7 +206,6 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
* @param {EventFacade} ev The event that was fired. * @param {EventFacade} ev The event that was fired.
* @param {Node} button The button that triggered this action. * @param {Node} button The button that triggered this action.
* @param {Node} activity The activity node that this action will be performed on. * @param {Node} activity The activity node that this action will be performed on.
* @chainable
*/ */
delete_with_confirmation: function(ev, button, activity) { delete_with_confirmation: function(ev, button, activity) {
// Prevent the default button action. // Prevent the default button action.
@ -248,13 +247,62 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
}, this); }, this);
}, },
/**
* Finds the section that would become empty if we remove the selected slots.
*
* @protected
* @method find_sections_that_would_become_empty
* @returns {String} The name of the first section found
*/
find_sections_that_would_become_empty: function() {
var section;
var sectionnodes = Y.all(SELECTOR.SECTIONLI);
if (sectionnodes.size() > 1) {
sectionnodes.some(function(node) {
var sectionname = node.one(SELECTOR.INSTANCESECTION).getContent();
var checked = node.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':checked');
var unchecked = node.all(SELECTOR.SELECTMULTIPLECHECKBOX + ':not(:checked)');
if (!checked.isEmpty() && unchecked.isEmpty()) {
section = sectionname;
}
return section;
});
}
return section;
},
/**
* Takes care of what needs to happen when the user clicks on the delete multiple button.
*
* @protected
* @method delete_multiple_action
* @param {EventFacade} ev The event that was fired.
*/
delete_multiple_action: function(ev) {
var problemsection = this.find_sections_that_would_become_empty();
if (problemsection) {
var alert = new M.core.alert({
title: M.util.get_string('cannotremoveslots', 'quiz'),
message: M.util.get_string('cannotremoveallsectionslots', 'quiz', problemsection)
});
alert.show();
} else {
this.delete_multiple_with_confirmation(ev);
}
},
/** /**
* Deletes the given activities or resources after confirmation. * Deletes the given activities or resources after confirmation.
* *
* @protected * @protected
* @method delete_multiple_with_confirmation * @method delete_multiple_with_confirmation
* @param {EventFacade} ev The event that was fired. * @param {EventFacade} ev The event that was fired.
* @chainable
*/ */
delete_multiple_with_confirmation: function(ev) { delete_multiple_with_confirmation: function(ev) {
ev.preventDefault(); ev.preventDefault();

View File

@ -62,6 +62,7 @@ var CSS = {
NUMQUESTIONS: '.numberofquestions', NUMQUESTIONS: '.numberofquestions',
PAGECONTENT: 'div#page-content', PAGECONTENT: 'div#page-content',
PAGELI: 'li.page', PAGELI: 'li.page',
SECTIONLI: 'li.section',
SECTIONUL: 'ul.section', SECTIONUL: 'ul.section',
SECTIONFORM: '.instancesectioncontainer form', SECTIONFORM: '.instancesectioncontainer form',
SECTIONINPUT: 'input[name=section]', SECTIONINPUT: 'input[name=section]',