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 definition 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
require_once($CFG->dirroot . '/question/type/questionbase.php');
30
 
31
/**
32
 * Base class for multiple choice questions. The parts that are common to
33
 * single select and multiple select.
34
 *
35
 * @copyright  2009 The Open University
36
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37
 */
38
abstract class qtype_multichoice_base extends question_graded_automatically {
39
    const LAYOUT_DROPDOWN = 0;
40
    const LAYOUT_VERTICAL = 1;
41
    const LAYOUT_HORIZONTAL = 2;
42
 
43
    public $answers;
44
 
45
    public $shuffleanswers;
46
    public $answernumbering;
47
    /**
48
     * @var int standard instruction to be displayed if enabled.
49
     */
50
    public $showstandardinstruction = 0;
51
    public $layout = self::LAYOUT_VERTICAL;
52
 
53
    public $correctfeedback;
54
    public $correctfeedbackformat;
55
    public $partiallycorrectfeedback;
56
    public $partiallycorrectfeedbackformat;
57
    public $incorrectfeedback;
58
    public $incorrectfeedbackformat;
59
 
60
    protected $order = null;
61
 
62
    public function start_attempt(question_attempt_step $step, $variant) {
63
        $this->order = array_keys($this->answers);
64
        if ($this->shuffleanswers) {
65
            shuffle($this->order);
66
        }
67
        $step->set_qt_var('_order', implode(',', $this->order));
68
    }
69
 
70
    public function apply_attempt_state(question_attempt_step $step) {
71
        $this->order = explode(',', $step->get_qt_var('_order'));
72
 
73
        // Add any missing answers. Sometimes people edit questions after they
74
        // have been attempted which breaks things.
75
        foreach ($this->order as $ansid) {
76
            if (isset($this->answers[$ansid])) {
77
                continue;
78
            }
79
            $a = new stdClass();
80
            $a->id = 0;
81
            $a->answer = html_writer::span(get_string('deletedchoice', 'qtype_multichoice'),
82
                    'notifyproblem');
83
            $a->answerformat = FORMAT_HTML;
84
            $a->fraction = 0;
85
            $a->feedback = '';
86
            $a->feedbackformat = FORMAT_HTML;
87
            $this->answers[$ansid] = $this->qtype->make_answer($a);
88
            $this->answers[$ansid]->answerformat = FORMAT_HTML;
89
        }
90
    }
91
 
92
    public function validate_can_regrade_with_other_version(question_definition $otherversion): ?string {
93
        $basemessage = parent::validate_can_regrade_with_other_version($otherversion);
94
        if ($basemessage) {
95
            return $basemessage;
96
        }
97
 
98
        if (count($this->answers) != count($otherversion->answers)) {
99
            return get_string('regradeissuenumchoiceschanged', 'qtype_multichoice');
100
        }
101
 
102
        return null;
103
    }
104
 
105
    public function update_attempt_state_data_for_new_version(
106
            question_attempt_step $oldstep, question_definition $otherversion) {
107
        $startdata = parent::update_attempt_state_data_for_new_version($oldstep, $otherversion);
108
 
109
        $mapping = array_combine(array_keys($otherversion->answers), array_keys($this->answers));
110
 
111
        $oldorder = explode(',', $oldstep->get_qt_var('_order'));
112
        $neworder = [];
113
        foreach ($oldorder as $oldid) {
114
            $neworder[] = $mapping[$oldid] ?? $oldid;
115
        }
116
        $startdata['_order'] = implode(',', $neworder);
117
 
118
        return $startdata;
119
    }
120
 
121
    public function get_question_summary() {
122
        $question = $this->html_to_text($this->questiontext, $this->questiontextformat);
123
        $choices = array();
124
        foreach ($this->order as $ansid) {
125
            $choices[] = $this->html_to_text($this->answers[$ansid]->answer,
126
                    $this->answers[$ansid]->answerformat);
127
        }
128
        return $question . ': ' . implode('; ', $choices);
129
    }
130
 
131
    public function get_order(question_attempt $qa) {
132
        $this->init_order($qa);
133
        return $this->order;
134
    }
135
 
136
    protected function init_order(question_attempt $qa) {
137
        if (is_null($this->order)) {
138
            $this->order = explode(',', $qa->get_step(0)->get_qt_var('_order'));
139
        }
140
    }
141
 
142
    abstract public function get_response(question_attempt $qa);
143
 
144
    abstract public function is_choice_selected($response, $value);
145
 
146
    public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) {
147
        if ($component == 'question' && in_array($filearea,
148
                array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'))) {
149
            return $this->check_combined_feedback_file_access($qa, $options, $filearea, $args);
150
 
151
        } else if ($component == 'question' && $filearea == 'answer') {
152
            $answerid = reset($args); // Itemid is answer id.
153
            return  in_array($answerid, $this->order);
154
 
155
        } else if ($component == 'question' && $filearea == 'answerfeedback') {
156
            $answerid = reset($args); // Itemid is answer id.
157
            $response = $this->get_response($qa);
158
            $isselected = false;
159
            foreach ($this->order as $value => $ansid) {
160
                if ($ansid == $answerid) {
161
                    $isselected = $this->is_choice_selected($response, $value);
162
                    break;
163
                }
164
            }
165
            qtype_multichoice::support_legacy_review_options_hack($options);
166
            return $options->feedback &&
167
                    $options->feedback !== qtype_multichoice::COMBINED_BUT_NOT_CHOICE_FEEDBACK &&
168
                    $isselected;
169
 
170
        } else if ($component == 'question' && $filearea == 'hint') {
171
            return $this->check_hint_file_access($qa, $options, $args);
172
 
173
        } else {
174
            return parent::check_file_access($qa, $options, $component, $filearea,
175
                    $args, $forcedownload);
176
        }
177
    }
178
 
179
    /**
180
     * Return the question settings that define this question as structured data.
181
     *
182
     * @param question_attempt $qa the current attempt for which we are exporting the settings.
183
     * @param question_display_options $options the question display options which say which aspects of the question
184
     * should be visible.
185
     * @return mixed structure representing the question settings. In web services, this will be JSON-encoded.
186
     */
187
    public function get_question_definition_for_external_rendering(question_attempt $qa, question_display_options $options) {
188
        // This is a partial implementation, returning only the most relevant question settings for now,
189
        // ideally, we should return as much as settings as possible (depending on the state and display options).
190
 
191
        return [
192
            'shuffleanswers' => $this->shuffleanswers,
193
            'answernumbering' => $this->answernumbering,
194
            'showstandardinstruction' => $this->showstandardinstruction,
195
            'layout' => $this->layout,
196
        ];
197
    }
198
}
199
 
200
 
201
/**
202
 * Represents a multiple choice question where only one choice should be selected.
203
 *
204
 * @copyright  2009 The Open University
205
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
206
 */
207
class qtype_multichoice_single_question extends qtype_multichoice_base {
208
    public function get_renderer(moodle_page $page) {
209
        return $page->get_renderer('qtype_multichoice', 'single');
210
    }
211
 
212
    public function get_min_fraction() {
213
        $minfraction = 0;
214
        foreach ($this->answers as $ans) {
215
            $minfraction = min($minfraction, $ans->fraction);
216
        }
217
        return $minfraction;
218
    }
219
 
220
    /**
221
     * Return an array of the question type variables that could be submitted
222
     * as part of a question of this type, with their types, so they can be
223
     * properly cleaned.
224
     * @return array variable name => PARAM_... constant.
225
     */
226
    public function get_expected_data() {
227
        return array('answer' => PARAM_INT);
228
    }
229
 
230
    public function summarise_response(array $response) {
231
        if (!$this->is_complete_response($response)) {
232
            return null;
233
        }
234
        $answerid = $this->order[$response['answer']];
235
        return $this->html_to_text($this->answers[$answerid]->answer,
236
                $this->answers[$answerid]->answerformat);
237
    }
238
 
239
    public function un_summarise_response(string $summary) {
240
        foreach ($this->order as $key => $answerid) {
241
            if ($summary === $this->html_to_text($this->answers[$answerid]->answer,
242
                    $this->answers[$answerid]->answerformat)) {
243
                return ['answer' => $key];
244
            }
245
        }
246
        return [];
247
    }
248
 
249
    public function classify_response(array $response) {
250
        if (!$this->is_complete_response($response)) {
251
            return array($this->id => question_classified_response::no_response());
252
        }
253
        $choiceid = $this->order[$response['answer']];
254
        $ans = $this->answers[$choiceid];
255
        return array($this->id => new question_classified_response($choiceid,
256
                $this->html_to_text($ans->answer, $ans->answerformat), $ans->fraction));
257
    }
258
 
259
    public function get_correct_response() {
260
        foreach ($this->order as $key => $answerid) {
261
            if (question_state::graded_state_for_fraction(
262
                    $this->answers[$answerid]->fraction)->is_correct()) {
263
                return array('answer' => $key);
264
            }
265
        }
266
        return array();
267
    }
268
 
269
    public function prepare_simulated_post_data($simulatedresponse) {
270
        $ansid = 0;
271
        foreach ($this->answers as $answer) {
272
            if (clean_param($answer->answer, PARAM_NOTAGS) == $simulatedresponse['answer']) {
273
                $ansid = $answer->id;
274
            }
275
        }
276
        if ($ansid) {
277
            return array('answer' => array_search($ansid, $this->order));
278
        } else {
279
            return array();
280
        }
281
    }
282
 
283
    public function get_student_response_values_for_simulation($postdata) {
284
        if (!isset($postdata['answer'])) {
285
            return array();
286
        } else {
287
            $answer = $this->answers[$this->order[$postdata['answer']]];
288
            return array('answer' => clean_param($answer->answer, PARAM_NOTAGS));
289
        }
290
    }
291
 
292
    public function is_same_response(array $prevresponse, array $newresponse) {
293
        if (!$this->is_complete_response($prevresponse)) {
294
            $prevresponse = [];
295
        }
296
        if (!$this->is_complete_response($newresponse)) {
297
            $newresponse = [];
298
        }
299
        return question_utils::arrays_same_at_key($prevresponse, $newresponse, 'answer');
300
    }
301
 
302
    public function is_complete_response(array $response) {
303
        return array_key_exists('answer', $response) && $response['answer'] !== ''
304
                && (string) $response['answer'] !== '-1';
305
    }
306
 
307
    public function is_gradable_response(array $response) {
308
        return $this->is_complete_response($response);
309
    }
310
 
311
    public function grade_response(array $response) {
312
        if (array_key_exists('answer', $response) &&
313
                array_key_exists($response['answer'], $this->order)) {
314
            $fraction = $this->answers[$this->order[$response['answer']]]->fraction;
315
        } else {
316
            $fraction = 0;
317
        }
318
        return array($fraction, question_state::graded_state_for_fraction($fraction));
319
    }
320
 
321
    public function get_validation_error(array $response) {
322
        if ($this->is_gradable_response($response)) {
323
            return '';
324
        }
325
        return get_string('pleaseselectananswer', 'qtype_multichoice');
326
    }
327
 
328
    public function get_response(question_attempt $qa) {
329
        return $qa->get_last_qt_var('answer', -1);
330
    }
331
 
332
    public function is_choice_selected($response, $value) {
333
        return (string) $response === (string) $value;
334
    }
335
}
336
 
337
 
338
/**
339
 * Represents a multiple choice question where multiple choices can be selected.
340
 *
341
 * @copyright  2009 The Open University
342
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
343
 */
344
class qtype_multichoice_multi_question extends qtype_multichoice_base {
345
    public function get_renderer(moodle_page $page) {
346
        return $page->get_renderer('qtype_multichoice', 'multi');
347
    }
348
 
349
    public function get_min_fraction() {
350
        return 0;
351
    }
352
 
353
    public function clear_wrong_from_response(array $response) {
354
        foreach ($this->order as $key => $ans) {
355
            if (array_key_exists($this->field($key), $response) &&
356
                    question_state::graded_state_for_fraction(
357
                    $this->answers[$ans]->fraction)->is_incorrect()) {
358
                $response[$this->field($key)] = 0;
359
            }
360
        }
361
        return $response;
362
    }
363
 
364
    public function get_num_parts_right(array $response) {
365
        $numright = 0;
366
        foreach ($this->order as $key => $ans) {
367
            $fieldname = $this->field($key);
368
            if (!array_key_exists($fieldname, $response) || !$response[$fieldname]) {
369
                continue;
370
            }
371
 
372
            if (!question_state::graded_state_for_fraction(
373
                    $this->answers[$ans]->fraction)->is_incorrect()) {
374
                $numright += 1;
375
            }
376
        }
377
        return array($numright, count($this->order));
378
    }
379
 
380
    /**
381
     * @param int $key choice number
382
     * @return string the question-type variable name.
383
     */
384
    protected function field($key) {
385
        return 'choice' . $key;
386
    }
387
 
388
    public function get_expected_data() {
389
        $expected = array();
390
        foreach ($this->order as $key => $notused) {
391
            $expected[$this->field($key)] = PARAM_BOOL;
392
        }
393
        return $expected;
394
    }
395
 
396
    public function summarise_response(array $response) {
397
        $selectedchoices = array();
398
        foreach ($this->order as $key => $ans) {
399
            $fieldname = $this->field($key);
400
            if (array_key_exists($fieldname, $response) && $response[$fieldname]) {
401
                $selectedchoices[] = $this->html_to_text($this->answers[$ans]->answer,
402
                        $this->answers[$ans]->answerformat);
403
            }
404
        }
405
        if (empty($selectedchoices)) {
406
            return null;
407
        }
408
        return implode('; ', $selectedchoices);
409
    }
410
 
411
    public function un_summarise_response(string $summary) {
412
        // This implementation is not perfect. It will fail if an answer contains '; ',
413
        // but this method is only for testing, so it is good enough.
414
        $selectedchoices = explode('; ', $summary);
415
        $response = [];
416
        foreach ($this->order as $key => $answerid) {
417
            if (in_array($this->html_to_text($this->answers[$answerid]->answer,
418
                    $this->answers[$answerid]->answerformat), $selectedchoices)) {
419
                $response[$this->field($key)] = '1';
420
            }
421
        }
422
        return $response;
423
    }
424
 
425
    public function classify_response(array $response) {
426
        $selectedchoices = array();
427
        foreach ($this->order as $key => $ansid) {
428
            $fieldname = $this->field($key);
429
            if (array_key_exists($fieldname, $response) && $response[$fieldname]) {
430
                $selectedchoices[$ansid] = 1;
431
            }
432
        }
433
        $choices = array();
434
        foreach ($this->answers as $ansid => $ans) {
435
            if (isset($selectedchoices[$ansid])) {
436
                $choices[$ansid] = new question_classified_response($ansid,
437
                        $this->html_to_text($ans->answer, $ans->answerformat), $ans->fraction);
438
            }
439
        }
440
        return $choices;
441
    }
442
 
443
    public function get_correct_response() {
444
        $response = array();
445
        foreach ($this->order as $key => $ans) {
446
            if (!question_state::graded_state_for_fraction(
447
                    $this->answers[$ans]->fraction)->is_incorrect()) {
448
                $response[$this->field($key)] = 1;
449
            }
450
        }
451
        return $response;
452
    }
453
 
454
    public function prepare_simulated_post_data($simulatedresponse) {
455
        $postdata = array();
456
        foreach ($simulatedresponse as $ans => $checked) {
457
            foreach ($this->answers as $ansid => $answer) {
458
                if (clean_param($answer->answer, PARAM_NOTAGS) == $ans) {
459
                    $fieldno = array_search($ansid, $this->order);
460
                    $postdata[$this->field($fieldno)] = $checked;
461
                    break;
462
                }
463
            }
464
        }
465
        return $postdata;
466
    }
467
 
468
    public function get_student_response_values_for_simulation($postdata) {
469
        $simulatedresponse = array();
470
        foreach ($this->order as $fieldno => $ansid) {
471
            if (isset($postdata[$this->field($fieldno)])) {
472
                $checked = $postdata[$this->field($fieldno)];
473
                $simulatedresponse[clean_param($this->answers[$ansid]->answer, PARAM_NOTAGS)] = $checked;
474
            }
475
        }
476
        ksort($simulatedresponse);
477
        return $simulatedresponse;
478
    }
479
 
480
    public function is_same_response(array $prevresponse, array $newresponse) {
481
        foreach ($this->order as $key => $notused) {
482
            $fieldname = $this->field($key);
483
            if (!question_utils::arrays_same_at_key_integer($prevresponse, $newresponse, $fieldname)) {
484
                return false;
485
            }
486
        }
487
        return true;
488
    }
489
 
490
    public function is_complete_response(array $response) {
491
        foreach ($this->order as $key => $notused) {
492
            if (!empty($response[$this->field($key)])) {
493
                return true;
494
            }
495
        }
496
        return false;
497
    }
498
 
499
    public function is_gradable_response(array $response) {
500
        return $this->is_complete_response($response);
501
    }
502
 
503
    /**
504
     * @param array $response responses, as returned by
505
     *      {@link question_attempt_step::get_qt_data()}.
506
     * @return int the number of choices that were selected. in this response.
507
     */
508
    public function get_num_selected_choices(array $response) {
509
        $numselected = 0;
510
        foreach ($response as $key => $value) {
511
            // Response keys starting with _ are internal values like _order, so ignore them.
512
            if (!empty($value) && $key[0] != '_') {
513
                $numselected += 1;
514
            }
515
        }
516
        return $numselected;
517
    }
518
 
519
    /**
520
     * @return int the number of choices that are correct.
521
     */
522
    public function get_num_correct_choices() {
523
        $numcorrect = 0;
524
        foreach ($this->answers as $ans) {
525
            if (!question_state::graded_state_for_fraction($ans->fraction)->is_incorrect()) {
526
                $numcorrect += 1;
527
            }
528
        }
529
        return $numcorrect;
530
    }
531
 
532
    public function grade_response(array $response) {
533
        $fraction = 0;
534
        foreach ($this->order as $key => $ansid) {
535
            if (!empty($response[$this->field($key)])) {
536
                $fraction += $this->answers[$ansid]->fraction;
537
            }
538
        }
539
        $fraction = min(max(0, $fraction), 1.0);
540
        return array($fraction, question_state::graded_state_for_fraction($fraction));
541
    }
542
 
543
    public function get_validation_error(array $response) {
544
        if ($this->is_gradable_response($response)) {
545
            return '';
546
        }
547
        return get_string('pleaseselectatleastoneanswer', 'qtype_multichoice');
548
    }
549
 
550
    /**
551
     * Disable those hint settings that we don't want when the student has selected
552
     * more choices than the number of right choices. This avoids giving the game away.
553
     * @param question_hint_with_parts $hint a hint.
554
     */
555
    protected function disable_hint_settings_when_too_many_selected(
556
            question_hint_with_parts $hint) {
557
        $hint->clearwrong = false;
558
    }
559
 
560
    public function get_hint($hintnumber, question_attempt $qa) {
561
        $hint = parent::get_hint($hintnumber, $qa);
562
        if (is_null($hint)) {
563
            return $hint;
564
        }
565
 
566
        if ($this->get_num_selected_choices($qa->get_last_qt_data()) >
567
                $this->get_num_correct_choices()) {
568
            $hint = clone($hint);
569
            $this->disable_hint_settings_when_too_many_selected($hint);
570
        }
571
        return $hint;
572
    }
573
 
574
    public function get_response(question_attempt $qa) {
575
        return $qa->get_last_qt_data();
576
    }
577
 
578
    public function is_choice_selected($response, $value) {
579
        return !empty($response['choice' . $value]);
580
    }
581
}