Merge branch 'MDL-65399-master' of git://github.com/rezaies/moodle

This commit is contained in:
Jun Pataleta 2019-05-06 16:26:42 +08:00 committed by Adrian Greeve
commit b6082744ed
11 changed files with 281 additions and 35 deletions

View File

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

View File

@ -47,9 +47,6 @@ class structure {
*/
protected $questions = array();
/** @var \stdClass[] quiz_slots.id => the quiz_slots rows for this quiz, agumented by sectionid. */
protected $slots = array();
/** @var \stdClass[] quiz_slots.slot => the quiz_slots rows for this quiz, agumented by sectionid. */
protected $slotsinorder = array();
@ -314,7 +311,7 @@ class structure {
* @return \stdClass[] the slots in this quiz.
*/
public function get_slots() {
return $this->slots;
return array_column($this->slotsinorder, null, 'id');
}
/**
@ -409,12 +406,16 @@ class structure {
* Get a slot by it's id. Throws an exception if it is missing.
* @param int $slotid the slot id.
* @return \stdClass the requested quiz_slots row.
* @throws \coding_exception
*/
public function get_slot_by_id($slotid) {
if (!array_key_exists($slotid, $this->slots)) {
throw new \coding_exception('The \'slotid\' could not be found.');
foreach ($this->slotsinorder as $slot) {
if ($slot->id == $slotid) {
return $slot;
}
}
return $this->slots[$slotid];
throw new \coding_exception('The \'slotid\' could not be found.');
}
/**
@ -425,13 +426,10 @@ class structure {
* @throws \coding_exception
*/
public function get_slot_by_number($slotnumber) {
foreach ($this->slots as $slot) {
if ($slot->slot == $slotnumber) {
return $slot;
}
if (!array_key_exists($slotnumber, $this->slotsinorder)) {
throw new \coding_exception('The \'slotnumber\' could not be found.');
}
throw new \coding_exception('The \'slotnumber\' could not be found.');
return $this->slotsinorder[$slotnumber];
}
/**
@ -617,7 +615,6 @@ class structure {
$slots = $this->populate_missing_questions($slots);
$this->questions = array();
$this->slots = array();
$this->slotsinorder = array();
foreach ($slots as $slotdata) {
$this->questions[$slotdata->questionid] = $slotdata;
@ -631,7 +628,6 @@ class structure {
$slot->maxmark = $slotdata->maxmark;
$slot->requireprevious = $slotdata->requireprevious;
$this->slots[$slot->id] = $slot;
$this->slotsinorder[$slot->slot] = $slot;
}
@ -692,7 +688,7 @@ class structure {
*/
protected function populate_question_numbers() {
$number = 1;
foreach ($this->slots as $slot) {
foreach ($this->slotsinorder as $slot) {
if ($this->questions[$slot->questionid]->length == 0) {
$slot->displayednumber = get_string('infoshort', 'quiz');
} else {
@ -720,7 +716,7 @@ class structure {
$this->check_can_be_edited();
$movingslot = $this->slots[$idmove];
$movingslot = $this->get_slot_by_id($idmove);
if (empty($movingslot)) {
throw new \moodle_exception('Bad slot ID ' . $idmove);
}
@ -730,7 +726,7 @@ class structure {
if (empty($idmoveafter)) {
$moveafterslotnumber = 0;
} else {
$moveafterslotnumber = (int) $this->slots[$idmoveafter]->slot;
$moveafterslotnumber = (int) $this->get_slot_by_id($idmoveafter)->slot;
}
// If the action came in as moving a slot to itself, normalise this to
@ -904,7 +900,9 @@ class structure {
/**
* Remove a slot from a quiz
*
* @param int $slotnumber The number of the slot to be deleted.
* @throws \coding_exception
*/
public function remove_slot($slotnumber) {
global $DB;
@ -927,6 +925,9 @@ class structure {
for ($i = $slot->slot + 1; $i <= $maxslot; $i++) {
$DB->set_field('quiz_slots', 'slot', $i - 1,
array('quizid' => $this->get_quizid(), 'slot' => $i));
$this->slotsinorder[$i]->slot = $i - 1;
$this->slotsinorder[$i - 1] = $this->slotsinorder[$i];
unset($this->slotsinorder[$i]);
}
$qtype = $DB->get_field('question', 'qtype', array('id' => $slot->questionid));
@ -936,6 +937,13 @@ class structure {
}
quiz_update_section_firstslots($this->get_quizid(), -1, $slotnumber);
foreach ($this->sections as $key => $section) {
if ($section->firstslot > $slotnumber) {
$this->sections[$key]->firstslot--;
}
}
$this->populate_slots_with_sections();
$this->populate_question_numbers();
unset($this->questions[$slot->questionid]);
$this->refresh_page_numbers_and_update_db();
@ -1066,7 +1074,7 @@ class structure {
* Set up this class with the slot tags for each of the slots.
*/
protected function populate_slot_tags() {
$slotids = array_keys($this->slots);
$slotids = array_column($this->slotsinorder, 'id');
$this->slottags = quiz_retrieve_tags_for_slot_ids($slotids);
}

View File

@ -137,6 +137,8 @@ $string['cannotinsertrandomquestion'] = 'Could not insert new random question!';
$string['cannotloadquestion'] = 'Could not load question options';
$string['cannotloadtypeinfo'] = 'Unable to load questiontype specific question information';
$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['cannotreviewopen'] = 'You cannot review this attempt, it is still open.';
$string['cannotsavelayout'] = 'Could not save layout';

View File

@ -1190,11 +1190,9 @@ table#categoryquestions {
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 .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 .instancesectioncontainer h3 {
#page-mod-quiz-edit.select-multiple .mod-quiz-edit-content .section-heading .instanceshufflequestions {
display: none;
}

View File

@ -167,3 +167,74 @@ Feature: Edit quiz page - remove multiple questions
When I click on "Deselect all" "link"
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

@ -720,6 +720,24 @@ class mod_quiz_structure_testcase extends advanced_testcase {
$this->assertFalse($DB->record_exists('question', array('id' => $randomq->id)));
}
/**
* Unit test to make sue it is not possible to remove all slots in a section at once.
*
* @expectedException coding_exception
*/
public function test_cannot_remove_all_slots_in_a_section() {
$quizobj = $this->create_test_quiz(array(
array('TF1', 1, 'truefalse'),
array('TF2', 1, 'truefalse'),
'Heading 2',
array('TF3', 2, 'truefalse'),
));
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
$structure->remove_slot(1);
$structure->remove_slot(2);
}
/**
* @expectedException coding_exception
*/

View File

@ -64,6 +64,7 @@ var CSS = {
NUMQUESTIONS: '.numberofquestions',
PAGECONTENT: 'div#page-content',
PAGELI: 'li.page',
SECTIONLI: 'li.section',
SECTIONUL: 'ul.section',
SECTIONFORM: '.instancesectioncontainer form',
SECTIONINPUT: 'input[name=section]',
@ -337,7 +338,7 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
Y.one(SELECTOR.SELECTMULTIPLEDELETEBUTTON).setAttribute('disabled', 'disabled');
// 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.
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 {Node} button The button that triggered this action.
* @param {Node} activity The activity node that this action will be performed on.
* @chainable
*/
delete_with_confirmation: function(ev, button, activity) {
// Prevent the default button action.
@ -481,13 +481,62 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
}, 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 (typeof problemsection !== 'undefined') {
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.
*
* @protected
* @method delete_multiple_with_confirmation
* @param {EventFacade} ev The event that was fired.
* @chainable
*/
delete_multiple_with_confirmation: function(ev) {
ev.preventDefault();

File diff suppressed because one or more lines are too long

View File

@ -64,6 +64,7 @@ var CSS = {
NUMQUESTIONS: '.numberofquestions',
PAGECONTENT: 'div#page-content',
PAGELI: 'li.page',
SECTIONLI: 'li.section',
SECTIONUL: 'ul.section',
SECTIONFORM: '.instancesectioncontainer form',
SECTIONINPUT: 'input[name=section]',
@ -337,7 +338,7 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
Y.one(SELECTOR.SELECTMULTIPLEDELETEBUTTON).setAttribute('disabled', 'disabled');
// 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.
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 {Node} button The button that triggered this action.
* @param {Node} activity The activity node that this action will be performed on.
* @chainable
*/
delete_with_confirmation: function(ev, button, activity) {
// Prevent the default button action.
@ -481,13 +481,62 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
}, 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 (typeof problemsection !== 'undefined') {
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.
*
* @protected
* @method delete_multiple_with_confirmation
* @param {EventFacade} ev The event that was fired.
* @chainable
*/
delete_multiple_with_confirmation: function(ev) {
ev.preventDefault();

View File

@ -104,7 +104,7 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
Y.one(SELECTOR.SELECTMULTIPLEDELETEBUTTON).setAttribute('disabled', 'disabled');
// 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.
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 {Node} button The button that triggered this action.
* @param {Node} activity The activity node that this action will be performed on.
* @chainable
*/
delete_with_confirmation: function(ev, button, activity) {
// Prevent the default button action.
@ -248,13 +247,62 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
}, 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 (typeof problemsection !== 'undefined') {
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.
*
* @protected
* @method delete_multiple_with_confirmation
* @param {EventFacade} ev The event that was fired.
* @chainable
*/
delete_multiple_with_confirmation: function(ev) {
ev.preventDefault();

View File

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