mirror of
https://github.com/moodle/moodle.git
synced 2025-01-18 22:08:20 +01:00
Merge branch 'MDL-5311-master' of git://github.com/lameze/moodle
This commit is contained in:
commit
fa1d3dff72
1
question/type/multichoice/amd/build/clearchoice.min.js
vendored
Normal file
1
question/type/multichoice/amd/build/clearchoice.min.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
define(["jquery","core/custom_interaction_events"],function(a,b){var c={CHOICE_ELEMENT:".answer input",CLEAR_CHOICE_ELEMENT:'div[class="qtype_multichoice_clearchoice"]'},d=function(a){a.find('input[type="radio"]').prop("checked",!0)},e=function(a,b){return a.find('div[id="'+b+'"]')},f=function(a){a.addClass("sr-only")},g=function(a){a.removeClass("sr-only")},h=function(a,h){var i=e(a,h);a.on(b.events.activate,c.CLEAR_CHOICE_ELEMENT,function(){d(i),f(i)}),a.on(b.events.activate,c.CHOICE_ELEMENT,function(){g(i)})},i=function(b,c){b=a("#"+b),h(b,c)};return{init:i}});
|
105
question/type/multichoice/amd/src/clearchoice.js
Normal file
105
question/type/multichoice/amd/src/clearchoice.js
Normal file
@ -0,0 +1,105 @@
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* Manages 'Clear my choice' functionality actions.
|
||||
*
|
||||
* @module qtype_multichoice/clearchoice
|
||||
* @copyright 2019 Simey Lameze <simey@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @since 3.7
|
||||
*/
|
||||
define(['jquery', 'core/custom_interaction_events'], function($, CustomEvents) {
|
||||
|
||||
var SELECTORS = {
|
||||
CHOICE_ELEMENT: '.answer input',
|
||||
CLEAR_CHOICE_ELEMENT: 'div[class="qtype_multichoice_clearchoice"]'
|
||||
};
|
||||
|
||||
/**
|
||||
* Mark clear choice radio as checked.
|
||||
*
|
||||
* @param {Object} clearChoiceContainer The clear choice option container.
|
||||
*/
|
||||
var checkClearChoiceRadio = function(clearChoiceContainer) {
|
||||
clearChoiceContainer.find('input[type="radio"]').prop('checked', true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the clear choice div container.
|
||||
*
|
||||
* @param {Object} root The question root element.
|
||||
* @param {string} fieldPrefix The question outer div prefix.
|
||||
* @returns {Object} The clear choice div container.
|
||||
*/
|
||||
var getClearChoiceElement = function(root, fieldPrefix) {
|
||||
return root.find('div[id="' + fieldPrefix + '"]');
|
||||
};
|
||||
|
||||
/**
|
||||
* Hide clear choice option.
|
||||
*
|
||||
* @param {Object} clearChoiceContainer The clear choice option container.
|
||||
*/
|
||||
var hideClearChoiceOption = function(clearChoiceContainer) {
|
||||
clearChoiceContainer.addClass('sr-only');
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows clear choice option.
|
||||
*
|
||||
* @param {Object} clearChoiceContainer The clear choice option container.
|
||||
*/
|
||||
var showClearChoiceOption = function(clearChoiceContainer) {
|
||||
clearChoiceContainer.removeClass('sr-only');
|
||||
};
|
||||
|
||||
/**
|
||||
* Register event listeners for the clear choice module.
|
||||
*
|
||||
* @param {Object} root The question outer div prefix.
|
||||
* @param {string} fieldPrefix The "Clear choice" div prefix.
|
||||
*/
|
||||
var registerEventListeners = function(root, fieldPrefix) {
|
||||
var clearChoiceContainer = getClearChoiceElement(root, fieldPrefix);
|
||||
|
||||
root.on(CustomEvents.events.activate, SELECTORS.CLEAR_CHOICE_ELEMENT, function() {
|
||||
// Mark the clear choice radio element as checked.
|
||||
checkClearChoiceRadio(clearChoiceContainer);
|
||||
// Now that the hidden radio has been checked, hide the clear choice option.
|
||||
hideClearChoiceOption(clearChoiceContainer);
|
||||
});
|
||||
|
||||
root.on(CustomEvents.events.activate, SELECTORS.CHOICE_ELEMENT, function() {
|
||||
// If the event has been triggered by any other choice, show the clear choice option.
|
||||
showClearChoiceOption(clearChoiceContainer);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialise clear choice module.
|
||||
|
||||
* @param {string} root The question outer div prefix.
|
||||
* @param {string} fieldPrefix The "Clear choice" div prefix.
|
||||
*/
|
||||
var init = function(root, fieldPrefix) {
|
||||
root = $('#' + root);
|
||||
registerEventListeners(root, fieldPrefix);
|
||||
};
|
||||
|
||||
return {
|
||||
init: init
|
||||
};
|
||||
});
|
@ -37,6 +37,7 @@ $string['answersingleno'] = 'Multiple answers allowed';
|
||||
$string['answersingleyes'] = 'One answer only';
|
||||
$string['choiceno'] = 'Choice {$a}';
|
||||
$string['choices'] = 'Available choices';
|
||||
$string['clearchoice'] = 'Clear my choice';
|
||||
$string['clozeaid'] = 'Enter missing word';
|
||||
$string['correctansweris'] = 'The correct answer is: {$a}';
|
||||
$string['correctanswersare'] = 'The correct answers are: {$a}';
|
||||
|
@ -176,8 +176,7 @@ class qtype_multichoice_single_question extends qtype_multichoice_base {
|
||||
}
|
||||
|
||||
public function summarise_response(array $response) {
|
||||
if (!array_key_exists('answer', $response) ||
|
||||
!array_key_exists($response['answer'], $this->order)) {
|
||||
if (!$this->is_complete_response($response)) {
|
||||
return null;
|
||||
}
|
||||
$ansid = $this->order[$response['answer']];
|
||||
@ -186,8 +185,7 @@ class qtype_multichoice_single_question extends qtype_multichoice_base {
|
||||
}
|
||||
|
||||
public function classify_response(array $response) {
|
||||
if (!array_key_exists('answer', $response) ||
|
||||
!array_key_exists($response['answer'], $this->order)) {
|
||||
if (!$this->is_complete_response($response)) {
|
||||
return array($this->id => question_classified_response::no_response());
|
||||
}
|
||||
$choiceid = $this->order[$response['answer']];
|
||||
@ -230,11 +228,18 @@ class qtype_multichoice_single_question extends qtype_multichoice_base {
|
||||
}
|
||||
|
||||
public function is_same_response(array $prevresponse, array $newresponse) {
|
||||
if (!$this->is_complete_response($prevresponse)) {
|
||||
$prevresponse = [];
|
||||
}
|
||||
if (!$this->is_complete_response($newresponse)) {
|
||||
$newresponse = [];
|
||||
}
|
||||
return question_utils::arrays_same_at_key($prevresponse, $newresponse, 'answer');
|
||||
}
|
||||
|
||||
public function is_complete_response(array $response) {
|
||||
return array_key_exists('answer', $response) && $response['answer'] !== '';
|
||||
return array_key_exists('answer', $response) && $response['answer'] !== ''
|
||||
&& (string) $response['answer'] !== '-1';
|
||||
}
|
||||
|
||||
public function is_gradable_response(array $response) {
|
||||
|
@ -35,6 +35,17 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
abstract class qtype_multichoice_renderer_base extends qtype_with_combined_feedback_renderer {
|
||||
|
||||
/**
|
||||
* Method to generating the bits of output after question choices.
|
||||
*
|
||||
* @param question_attempt $qa The question attempt object.
|
||||
* @param question_display_options $options controls what should and should not be displayed.
|
||||
*
|
||||
* @return string HTML output.
|
||||
*/
|
||||
protected abstract function after_choices(question_attempt $qa, question_display_options $options);
|
||||
|
||||
protected abstract function get_input_type();
|
||||
|
||||
protected abstract function get_input_name(question_attempt $qa, $value);
|
||||
@ -136,6 +147,8 @@ abstract class qtype_multichoice_renderer_base extends qtype_with_combined_feedb
|
||||
}
|
||||
$result .= html_writer::end_tag('div'); // Answer.
|
||||
|
||||
$result .= $this->after_choices($qa, $options);
|
||||
|
||||
$result .= html_writer::end_tag('div'); // Ablock.
|
||||
|
||||
if ($qa->get_state() == question_state::$invalid) {
|
||||
@ -252,6 +265,54 @@ class qtype_multichoice_single_renderer extends qtype_multichoice_renderer_base
|
||||
}
|
||||
return $this->correct_choices($right);
|
||||
}
|
||||
|
||||
public function after_choices(question_attempt $qa, question_display_options $options) {
|
||||
// Only load the clear choice feature if it's not read only.
|
||||
if ($options->readonly) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$question = $qa->get_question();
|
||||
$response = $question->get_response($qa);
|
||||
$hascheckedchoice = false;
|
||||
foreach ($question->get_order($qa) as $value => $ansid) {
|
||||
if ($question->is_choice_selected($response, $value)) {
|
||||
$hascheckedchoice = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$clearchoiceid = $this->get_input_id($qa, -1);
|
||||
$clearchoicefieldname = $qa->get_qt_field_name('clearchoice');
|
||||
$clearchoiceradioattrs = [
|
||||
'type' => $this->get_input_type(),
|
||||
'name' => $qa->get_qt_field_name('answer'),
|
||||
'id' => $clearchoiceid,
|
||||
'value' => -1,
|
||||
'class' => 'sr-only'
|
||||
];
|
||||
|
||||
$cssclass = 'qtype_multichoice_clearchoice';
|
||||
// When no choice selected during rendering, then hide the clear choice option.
|
||||
if (!$hascheckedchoice && $response == -1) {
|
||||
$cssclass .= ' sr-only';
|
||||
$clearchoiceradioattrs['checked'] = 'checked';
|
||||
}
|
||||
// Adds an hidden radio that will be checked to give the impression the choice has been cleared.
|
||||
$clearchoiceradio = html_writer::empty_tag('input', $clearchoiceradioattrs);
|
||||
$clearchoiceradio .= html_writer::tag('label', get_string('clearchoice', 'qtype_multichoice'),
|
||||
['for' => $clearchoiceid]);
|
||||
|
||||
// Now wrap the radio and label inside a div.
|
||||
$result = html_writer::tag('div', $clearchoiceradio, ['id' => $clearchoicefieldname, 'class' => $cssclass]);
|
||||
|
||||
// Load required clearchoice AMD module.
|
||||
$this->page->requires->js_call_amd('qtype_multichoice/clearchoice', 'init',
|
||||
[$qa->get_outer_question_div_unique_id(), $clearchoicefieldname]);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -262,6 +323,10 @@ class qtype_multichoice_single_renderer extends qtype_multichoice_renderer_base
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_multichoice_multi_renderer extends qtype_multichoice_renderer_base {
|
||||
protected function after_choices(question_attempt $qa, question_display_options $options) {
|
||||
return '';
|
||||
}
|
||||
|
||||
protected function get_input_type() {
|
||||
return 'checkbox';
|
||||
}
|
||||
|
@ -70,3 +70,15 @@ Feature: Preview a Multiple choice question
|
||||
And I should see "Well done!"
|
||||
And I should see "The correct answer is: One"
|
||||
And I switch to the main window
|
||||
|
||||
@javascript @_switch_window
|
||||
Scenario: Preview a multiple choice question (single response) and clear a previous selected option.
|
||||
When I click on "Preview" "link" in the "Multi-choice-002" "table_row"
|
||||
And I switch to "questionpreview" window
|
||||
And I set the field "How questions behave" to "Immediate feedback"
|
||||
And I press "Start again with these options"
|
||||
And I click on "One" "radio"
|
||||
Then I should see "Clear my choice"
|
||||
And I click on "Clear my choice" "text"
|
||||
And I should not see "Clear my choice"
|
||||
And I switch to the main window
|
||||
|
@ -133,6 +133,17 @@ class qtype_multichoice_multi_question_test extends advanced_testcase {
|
||||
$this->assertEquals('B; C', $summary);
|
||||
}
|
||||
|
||||
public function test_summarise_response_clearchoice() {
|
||||
$mc = test_question_maker::make_a_multichoice_multi_question();
|
||||
$mc->shuffleanswers = false;
|
||||
$mc->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$summary = $mc->summarise_response($mc->prepare_simulated_post_data(array('clearchoice' => -1)),
|
||||
test_question_maker::get_a_qa($mc));
|
||||
|
||||
$this->assertNull($summary);
|
||||
}
|
||||
|
||||
public function test_classify_response() {
|
||||
$mc = test_question_maker::make_a_multichoice_multi_question();
|
||||
$mc->start_attempt(new question_attempt_step(), 1);
|
||||
|
@ -47,6 +47,8 @@ class qtype_multichoice_single_question_test extends advanced_testcase {
|
||||
$this->assertFalse($question->is_complete_response(array()));
|
||||
$this->assertTrue($question->is_complete_response(array('answer' => '0')));
|
||||
$this->assertTrue($question->is_complete_response(array('answer' => '2')));
|
||||
$this->assertFalse($question->is_complete_response(array('answer' => '-1')));
|
||||
$this->assertFalse($question->is_complete_response(array('answer' => -1)));
|
||||
}
|
||||
|
||||
public function test_is_gradable_response() {
|
||||
@ -55,6 +57,7 @@ class qtype_multichoice_single_question_test extends advanced_testcase {
|
||||
$this->assertFalse($question->is_gradable_response(array()));
|
||||
$this->assertTrue($question->is_gradable_response(array('answer' => '0')));
|
||||
$this->assertTrue($question->is_gradable_response(array('answer' => '2')));
|
||||
$this->assertFalse($question->is_gradable_response(array('answer' => '-1')));
|
||||
}
|
||||
|
||||
public function test_is_same_response() {
|
||||
@ -80,6 +83,22 @@ class qtype_multichoice_single_question_test extends advanced_testcase {
|
||||
$this->assertTrue($question->is_same_response(
|
||||
array('answer' => '2'),
|
||||
array('answer' => '2')));
|
||||
|
||||
$this->assertFalse($question->is_same_response(
|
||||
array('answer' => '0'),
|
||||
array('answer' => '-1')));
|
||||
|
||||
$this->assertFalse($question->is_same_response(
|
||||
array('answer' => '-1'),
|
||||
array('answer' => '0')));
|
||||
|
||||
$this->assertTrue($question->is_same_response(
|
||||
array('answer' => '-1'),
|
||||
array('answer' => '-1')));
|
||||
|
||||
$this->assertTrue($question->is_same_response(
|
||||
array(),
|
||||
array('answer' => '-1')));
|
||||
}
|
||||
|
||||
public function test_grading() {
|
||||
@ -138,6 +157,9 @@ class qtype_multichoice_single_question_test extends advanced_testcase {
|
||||
test_question_maker::get_a_qa($mc));
|
||||
|
||||
$this->assertEquals('A', $summary);
|
||||
|
||||
$this->assertNull($mc->summarise_response(array(), test_question_maker::get_a_qa($mc)));
|
||||
$this->assertNull($mc->summarise_response(array('answer' => '-1'), test_question_maker::get_a_qa($mc)));
|
||||
}
|
||||
|
||||
public function test_classify_response() {
|
||||
@ -149,7 +171,11 @@ class qtype_multichoice_single_question_test extends advanced_testcase {
|
||||
|
||||
$this->assertEquals(array(
|
||||
$mc->id => question_classified_response::no_response(),
|
||||
), $mc->classify_response(array()));
|
||||
), $mc->classify_response(array()));
|
||||
|
||||
$this->assertEquals(array(
|
||||
$mc->id => question_classified_response::no_response(),
|
||||
), $mc->classify_response(array('answer' => '-1')));
|
||||
}
|
||||
|
||||
public function test_make_html_inline() {
|
||||
|
@ -97,4 +97,73 @@ class qtype_multichoice_walkthrough_test extends qbehaviour_walkthrough_test_bas
|
||||
new question_pattern_expectation('/class="r0 correct"/'),
|
||||
new question_pattern_expectation('/class="r1"/'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for clear choice option.
|
||||
*/
|
||||
public function test_deferredfeedback_feedback_multichoice_clearchoice() {
|
||||
|
||||
// Create a multichoice, single question.
|
||||
$mc = test_question_maker::make_a_multichoice_single_question();
|
||||
$mc->shuffleanswers = false;
|
||||
|
||||
$clearchoice = -1;
|
||||
$rightchoice = 0;
|
||||
$wrongchoice = 2;
|
||||
|
||||
$this->start_attempt_at_question($mc, 'deferredfeedback', 3);
|
||||
|
||||
// Let's first submit the wrong choice (2).
|
||||
$this->process_submission(array('answer' => $wrongchoice)); // Wrong choice (2).
|
||||
|
||||
$this->check_current_mark(null);
|
||||
// Clear choice radio should not be checked.
|
||||
$this->check_current_output(
|
||||
$this->get_contains_mc_radio_expectation($rightchoice, true, false), // Not checked.
|
||||
$this->get_contains_mc_radio_expectation($rightchoice + 1, true, false), // Not checked.
|
||||
$this->get_contains_mc_radio_expectation($rightchoice + 2, true, true), // Wrong choice (2) checked.
|
||||
$this->get_contains_mc_radio_expectation($clearchoice, true, false), // Not checked.
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_does_not_contain_feedback_expectation()
|
||||
);
|
||||
|
||||
// Now, let's clear our previous choice.
|
||||
$this->process_submission(array('answer' => $clearchoice)); // Clear choice (-1).
|
||||
$this->check_current_mark(null);
|
||||
|
||||
// This time, the clear choice radio should be the only one checked.
|
||||
$this->check_current_output(
|
||||
$this->get_contains_mc_radio_expectation($rightchoice, true, false), // Not checked.
|
||||
$this->get_contains_mc_radio_expectation($rightchoice + 1, true, false), // Not checked.
|
||||
$this->get_contains_mc_radio_expectation($rightchoice + 2, true, false), // Not checked.
|
||||
$this->get_contains_mc_radio_expectation($clearchoice, true, true), // Clear choice radio checked.
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_does_not_contain_feedback_expectation()
|
||||
);
|
||||
|
||||
// Finally, let's submit the right choice.
|
||||
$this->process_submission(array('answer' => $rightchoice)); // Right choice (0).
|
||||
$this->check_current_state(question_state::$complete);
|
||||
$this->check_current_mark(null);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_mc_radio_expectation($rightchoice, true, true),
|
||||
$this->get_contains_mc_radio_expectation($rightchoice + 1, true, false),
|
||||
$this->get_contains_mc_radio_expectation($rightchoice + 2, true, false),
|
||||
$this->get_contains_mc_radio_expectation($clearchoice, true, false),
|
||||
$this->get_does_not_contain_correctness_expectation(),
|
||||
$this->get_does_not_contain_feedback_expectation()
|
||||
);
|
||||
|
||||
// Finish the attempt.
|
||||
$this->finish();
|
||||
|
||||
// Verify.
|
||||
$this->check_current_state(question_state::$gradedright);
|
||||
$this->check_current_mark(3);
|
||||
$this->check_current_output(
|
||||
$this->get_contains_mc_radio_expectation($rightchoice, false, true),
|
||||
$this->get_contains_correct_expectation(),
|
||||
new question_pattern_expectation('/class="r0 correct"/'),
|
||||
new question_pattern_expectation('/class="r1"/'));
|
||||
}
|
||||
}
|
||||
|
@ -310,6 +310,14 @@ body.path-question-type {
|
||||
.que.multichoice .answer div.r1 .icon.fa-remove {
|
||||
text-indent: 0;
|
||||
}
|
||||
.qtype_multichoice_clearchoice {
|
||||
padding-top: 10px;
|
||||
label {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
padding-left: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.formulation input[type="text"],
|
||||
.formulation select {
|
||||
|
@ -14028,6 +14028,13 @@ body.path-question-type {
|
||||
.que.multichoice .answer div.r1 .icon.fa-remove {
|
||||
text-indent: 0; }
|
||||
|
||||
.qtype_multichoice_clearchoice {
|
||||
padding-top: 10px; }
|
||||
.qtype_multichoice_clearchoice label {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
padding-left: 30px; }
|
||||
|
||||
.formulation input[type="text"],
|
||||
.formulation select {
|
||||
width: auto;
|
||||
|
@ -14279,6 +14279,13 @@ body.path-question-type {
|
||||
.que.multichoice .answer div.r1 .icon.fa-remove {
|
||||
text-indent: 0; }
|
||||
|
||||
.qtype_multichoice_clearchoice {
|
||||
padding-top: 10px; }
|
||||
.qtype_multichoice_clearchoice label {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
padding-left: 30px; }
|
||||
|
||||
.formulation input[type="text"],
|
||||
.formulation select {
|
||||
width: auto;
|
||||
|
Loading…
x
Reference in New Issue
Block a user