Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Multiple choice question renderer classes.
19
 *
20
 * @package    qtype
21
 * @subpackage multichoice
22
 * @copyright  2009 The Open University
23
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24
 */
25
 
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
 
30
/**
31
 * Base class for generating the bits of output common to multiple choice
32
 * single and multiple questions.
33
 *
34
 * @copyright  2009 The Open University
35
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36
 */
37
abstract class qtype_multichoice_renderer_base extends qtype_with_combined_feedback_renderer {
38
 
39
    /**
40
     * Method to generating the bits of output after question choices.
41
     *
42
     * @param question_attempt $qa The question attempt object.
43
     * @param question_display_options $options controls what should and should not be displayed.
44
     *
45
     * @return string HTML output.
46
     */
47
    abstract protected function after_choices(question_attempt $qa, question_display_options $options);
48
 
49
    abstract protected function get_input_type();
50
 
51
    abstract protected function get_input_name(question_attempt $qa, $value);
52
 
53
    abstract protected function get_input_value($value);
54
 
55
    abstract protected function get_input_id(question_attempt $qa, $value);
56
 
57
    /**
58
     * Whether a choice should be considered right, wrong or partially right.
59
     * @param question_answer $ans representing one of the choices.
60
     * @return float 1.0, 0.0 or something in between, respectively.
61
     */
62
    abstract protected function is_right(question_answer $ans);
63
 
64
    abstract protected function prompt();
65
 
66
    public function formulation_and_controls(question_attempt $qa,
67
            question_display_options $options) {
68
 
69
        $question = $qa->get_question();
70
        $response = $question->get_response($qa);
71
 
72
        $inputname = $qa->get_qt_field_name('answer');
73
        $inputattributes = array(
74
            'type' => $this->get_input_type(),
75
            'name' => $inputname,
76
        );
77
 
78
        if ($options->readonly) {
79
            $inputattributes['disabled'] = 'disabled';
80
        }
81
 
82
        $radiobuttons = array();
83
        $feedbackimg = array();
84
        $feedback = array();
85
        $classes = array();
86
        foreach ($question->get_order($qa) as $value => $ansid) {
87
            $ans = $question->answers[$ansid];
88
            $inputattributes['name'] = $this->get_input_name($qa, $value);
89
            $inputattributes['value'] = $this->get_input_value($value);
90
            $inputattributes['id'] = $this->get_input_id($qa, $value);
91
            $inputattributes['aria-labelledby'] = $inputattributes['id'] . '_label';
92
            $isselected = $question->is_choice_selected($response, $value);
93
            if ($isselected) {
94
                $inputattributes['checked'] = 'checked';
95
            } else {
96
                unset($inputattributes['checked']);
97
            }
98
            $hidden = '';
99
            if (!$options->readonly && $this->get_input_type() == 'checkbox') {
100
                $hidden = html_writer::empty_tag('input', array(
101
                    'type' => 'hidden',
102
                    'name' => $inputattributes['name'],
103
                    'value' => 0,
104
                ));
105
            }
106
 
107
            $choicenumber = '';
108
            if ($question->answernumbering !== 'none') {
109
                $choicenumber = html_writer::span(
110
                        $this->number_in_style($value, $question->answernumbering), 'answernumber');
111
            }
112
            $choicetext = $question->format_text($ans->answer, $ans->answerformat, $qa, 'question', 'answer', $ansid);
113
            $choice = html_writer::div($choicetext, 'flex-fill ml-1');
114
 
115
            $radiobuttons[] = $hidden . html_writer::empty_tag('input', $inputattributes) .
116
                    html_writer::div($choicenumber . $choice, 'd-flex w-auto', [
117
                        'id' => $inputattributes['id'] . '_label',
118
                        'data-region' => 'answer-label',
119
                    ]);
120
 
121
            qtype_multichoice::support_legacy_review_options_hack($options);
122
            if ($options->feedback && $options->feedback !== qtype_multichoice::COMBINED_BUT_NOT_CHOICE_FEEDBACK &&
123
                    $isselected && trim($ans->feedback)) {
124
                $feedback[] = html_writer::tag('div',
125
                        $question->make_html_inline($question->format_text(
126
                                $ans->feedback, $ans->feedbackformat,
127
                                $qa, 'question', 'answerfeedback', $ansid)),
128
                        array('class' => 'specificfeedback'));
129
            } else {
130
                $feedback[] = '';
131
            }
132
            $class = 'r' . ($value % 2);
133
            if ($options->correctness && $isselected) {
134
                // Feedback images will be rendered using Font awesome.
135
                // Font awesome icons are actually characters(text) with special glyphs,
136
                // so the icons cannot be aligned correctly even if the parent div wrapper is using align-items: flex-start.
137
                // To make the Font awesome icons follow align-items: flex-start, we need to wrap them inside a span tag.
138
                $feedbackimg[] = html_writer::span($this->feedback_image($this->is_right($ans)), 'ml-1');
139
                $class .= ' ' . $this->feedback_class($this->is_right($ans));
140
            } else {
141
                $feedbackimg[] = '';
142
            }
143
            $classes[] = $class;
144
        }
145
 
146
        $result = '';
147
        $result .= html_writer::tag('div', $question->format_questiontext($qa),
148
                array('class' => 'qtext'));
149
 
150
        $result .= html_writer::start_tag('fieldset', array('class' => 'ablock no-overflow visual-scroll-x'));
151
        if ($question->showstandardinstruction == 1) {
152
            $legendclass = '';
153
            $questionnumber = $options->add_question_identifier_to_label($this->prompt(), true, true);
154
        } else {
155
            $questionnumber = $options->add_question_identifier_to_label(get_string('answer'), true, true);
156
            $legendclass = 'sr-only';
157
        }
158
        $legendattrs = [
159
            'class' => 'prompt h6 font-weight-normal ' . $legendclass,
160
        ];
161
        $result .= html_writer::tag('legend', $questionnumber, $legendattrs);
162
 
163
        $result .= html_writer::start_tag('div', array('class' => 'answer'));
164
        foreach ($radiobuttons as $key => $radio) {
165
            $result .= html_writer::tag('div', $radio . ' ' . $feedbackimg[$key] . $feedback[$key],
166
                    array('class' => $classes[$key])) . "\n";
167
        }
168
        $result .= html_writer::end_tag('div'); // Answer.
169
 
170
        // Load JS module for the question answers.
171
        $this->page->requires->js_call_amd('qtype_multichoice/answers', 'init',
172
            [$qa->get_outer_question_div_unique_id()]);
173
        $result .= $this->after_choices($qa, $options);
174
 
175
        $result .= html_writer::end_tag('fieldset'); // Ablock.
176
 
177
        if ($qa->get_state() == question_state::$invalid) {
178
            $result .= html_writer::nonempty_tag('div',
179
                    $question->get_validation_error($qa->get_last_qt_data()),
180
                    array('class' => 'validationerror'));
181
        }
182
 
183
        return $result;
184
    }
185
 
186
    protected function number_html($qnum) {
187
        return $qnum . '. ';
188
    }
189
 
190
    /**
191
     * @param int $num The number, starting at 0.
192
     * @param string $style The style to render the number in. One of the
193
     * options returned by {@link qtype_multichoice:;get_numbering_styles()}.
194
     * @return string the number $num in the requested style.
195
     */
196
    protected function number_in_style($num, $style) {
197
        switch($style) {
198
            case 'abc':
199
                $number = chr(ord('a') + $num);
200
                break;
201
            case 'ABCD':
202
                $number = chr(ord('A') + $num);
203
                break;
204
            case '123':
205
                $number = $num + 1;
206
                break;
207
            case 'iii':
208
                $number = question_utils::int_to_roman($num + 1);
209
                break;
210
            case 'IIII':
211
                $number = strtoupper(question_utils::int_to_roman($num + 1));
212
                break;
213
            case 'none':
214
                return '';
215
            default:
216
                return 'ERR';
217
        }
218
        return $this->number_html($number);
219
    }
220
 
221
    public function specific_feedback(question_attempt $qa) {
222
        return $this->combined_feedback($qa);
223
    }
224
 
225
    /**
226
     * Function returns string based on number of correct answers
227
     * @param array $right An Array of correct responses to the current question
228
     * @return string based on number of correct responses
229
     */
230
    protected function correct_choices(array $right) {
231
        // Return appropriate string for single/multiple correct answer(s).
232
        if (count($right) == 1) {
233
                return get_string('correctansweris', 'qtype_multichoice',
234
                        implode(', ', $right));
235
        } else if (count($right) > 1) {
236
                return get_string('correctanswersare', 'qtype_multichoice',
237
                        implode(', ', $right));
238
        } else {
239
                return "";
240
        }
241
    }
242
}
243
 
244
 
245
/**
246
 * Subclass for generating the bits of output specific to multiple choice
247
 * single questions.
248
 *
249
 * @copyright  2009 The Open University
250
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
251
 */
252
class qtype_multichoice_single_renderer extends qtype_multichoice_renderer_base {
253
    protected function get_input_type() {
254
        return 'radio';
255
    }
256
 
257
    protected function get_input_name(question_attempt $qa, $value) {
258
        return $qa->get_qt_field_name('answer');
259
    }
260
 
261
    protected function get_input_value($value) {
262
        return $value;
263
    }
264
 
265
    protected function get_input_id(question_attempt $qa, $value) {
266
        return $qa->get_qt_field_name('answer' . $value);
267
    }
268
 
269
    protected function is_right(question_answer $ans) {
270
        return $ans->fraction;
271
    }
272
 
273
    protected function prompt() {
274
        return get_string('selectone', 'qtype_multichoice');
275
    }
276
 
277
    public function correct_response(question_attempt $qa) {
278
        $question = $qa->get_question();
279
 
280
        // Put all correct answers (100% grade) into $right.
281
        $right = array();
282
        foreach ($question->answers as $ansid => $ans) {
283
            if (question_state::graded_state_for_fraction($ans->fraction) ==
284
                    question_state::$gradedright) {
285
                $right[] = $question->make_html_inline($question->format_text($ans->answer, $ans->answerformat,
286
                        $qa, 'question', 'answer', $ansid));
287
            }
288
        }
289
        return $this->correct_choices($right);
290
    }
291
 
292
    public function after_choices(question_attempt $qa, question_display_options $options) {
293
        // Only load the clear choice feature if it's not read only.
294
        if ($options->readonly) {
295
            return '';
296
        }
297
 
298
        $question = $qa->get_question();
299
        $response = $question->get_response($qa);
300
        $hascheckedchoice = false;
301
        foreach ($question->get_order($qa) as $value => $ansid) {
302
            if ($question->is_choice_selected($response, $value)) {
303
                $hascheckedchoice = true;
304
                break;
305
            }
306
        }
307
 
308
        $clearchoiceid = $this->get_input_id($qa, -1);
309
        $clearchoicefieldname = $qa->get_qt_field_name('clearchoice');
310
        $clearchoiceradioattrs = [
311
            'type' => $this->get_input_type(),
312
            'name' => $qa->get_qt_field_name('answer'),
313
            'id' => $clearchoiceid,
314
            'value' => -1,
315
            'class' => 'sr-only',
316
            'aria-hidden' => 'true'
317
        ];
318
        $clearchoicewrapperattrs = [
319
            'id' => $clearchoicefieldname,
320
            'class' => 'qtype_multichoice_clearchoice',
321
        ];
322
 
323
        // When no choice selected during rendering, then hide the clear choice option.
324
        // We are using .sr-only and aria-hidden together so while the element is hidden
325
        // from both the monitor and the screen-reader, it is still tabbable.
326
        $linktabindex = 0;
327
        if (!$hascheckedchoice && $response == -1) {
328
            $clearchoicewrapperattrs['class'] .= ' sr-only';
329
            $clearchoicewrapperattrs['aria-hidden'] = 'true';
330
            $clearchoiceradioattrs['checked'] = 'checked';
331
            $linktabindex = -1;
332
        }
333
        // Adds an hidden radio that will be checked to give the impression the choice has been cleared.
334
        $clearchoiceradio = html_writer::empty_tag('input', $clearchoiceradioattrs);
335
        $clearchoice = html_writer::link('#', get_string('clearchoice', 'qtype_multichoice'),
336
            ['tabindex' => $linktabindex, 'role' => 'button', 'class' => 'btn btn-link ml-3 mt-n1']);
337
        $clearchoiceradio .= html_writer::label($clearchoice, $clearchoiceid);
338
 
339
        // Now wrap the radio and label inside a div.
340
        $result = html_writer::tag('div', $clearchoiceradio, $clearchoicewrapperattrs);
341
 
342
        // Load required clearchoice AMD module.
343
        $this->page->requires->js_call_amd('qtype_multichoice/clearchoice', 'init',
344
            [$qa->get_outer_question_div_unique_id(), $clearchoicefieldname]);
345
 
346
        return $result;
347
    }
348
 
349
}
350
 
351
/**
352
 * Subclass for generating the bits of output specific to multiple choice
353
 * multi=select questions.
354
 *
355
 * @copyright  2009 The Open University
356
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
357
 */
358
class qtype_multichoice_multi_renderer extends qtype_multichoice_renderer_base {
359
    protected function after_choices(question_attempt $qa, question_display_options $options) {
360
        return '';
361
    }
362
 
363
    protected function get_input_type() {
364
        return 'checkbox';
365
    }
366
 
367
    protected function get_input_name(question_attempt $qa, $value) {
368
        return $qa->get_qt_field_name('choice' . $value);
369
    }
370
 
371
    protected function get_input_value($value) {
372
        return 1;
373
    }
374
 
375
    protected function get_input_id(question_attempt $qa, $value) {
376
        return $this->get_input_name($qa, $value);
377
    }
378
 
379
    protected function is_right(question_answer $ans) {
380
        if ($ans->fraction > 0) {
381
            return 1;
382
        } else {
383
            return 0;
384
        }
385
    }
386
 
387
    protected function prompt() {
388
        return get_string('selectmulti', 'qtype_multichoice');
389
    }
390
 
391
    public function correct_response(question_attempt $qa) {
392
        $question = $qa->get_question();
393
 
394
        $right = array();
395
        foreach ($question->answers as $ansid => $ans) {
396
            if ($ans->fraction > 0) {
397
                $right[] = $question->make_html_inline($question->format_text($ans->answer, $ans->answerformat,
398
                        $qa, 'question', 'answer', $ansid));
399
            }
400
        }
401
        return $this->correct_choices($right);
402
    }
403
 
404
    protected function num_parts_correct(question_attempt $qa) {
405
        if ($qa->get_question()->get_num_selected_choices($qa->get_last_qt_data()) >
406
                $qa->get_question()->get_num_correct_choices()) {
407
            return get_string('toomanyselected', 'qtype_multichoice');
408
        }
409
 
410
        return parent::num_parts_correct($qa);
411
    }
412
}