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
 * Matching question definition class.
19
 *
20
 * @package   qtype_match
21
 * @copyright 2009 The Open University
22
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
 
26
defined('MOODLE_INTERNAL') || die();
27
 
28
require_once($CFG->dirroot . '/question/type/questionbase.php');
29
 
30
/**
31
 * Represents a matching question.
32
 *
33
 * @copyright 2009 The Open University
34
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35
 */
36
class qtype_match_question extends question_graded_automatically_with_countback {
37
    /** @var boolean Whether the question stems should be shuffled. */
38
    public $shufflestems;
39
 
40
    public $correctfeedback;
41
    public $correctfeedbackformat;
42
    public $partiallycorrectfeedback;
43
    public $partiallycorrectfeedbackformat;
44
    public $incorrectfeedback;
45
    public $incorrectfeedbackformat;
46
 
47
    /** @var array of question stems. */
48
    public $stems;
49
    /** @var int[] FORMAT_... type for each stem. */
50
    public $stemformat;
51
    /** @var array of choices that can be matched to each stem. */
52
    public $choices;
53
    /** @var array index of the right choice for each stem. */
54
    public $right;
55
 
56
    /** @var array shuffled stem indexes. */
57
    protected $stemorder;
58
    /** @var array shuffled choice indexes. */
59
    protected $choiceorder;
60
 
61
    public function start_attempt(question_attempt_step $step, $variant) {
62
        $this->stemorder = array_keys($this->stems);
63
        if ($this->shufflestems) {
64
            shuffle($this->stemorder);
65
        }
66
        $step->set_qt_var('_stemorder', implode(',', $this->stemorder));
67
 
68
        $choiceorder = array_keys($this->choices);
69
        shuffle($choiceorder);
70
        $step->set_qt_var('_choiceorder', implode(',', $choiceorder));
71
        $this->set_choiceorder($choiceorder);
72
    }
73
 
74
    public function apply_attempt_state(question_attempt_step $step) {
75
        $this->stemorder = explode(',', $step->get_qt_var('_stemorder'));
76
        $this->set_choiceorder(explode(',', $step->get_qt_var('_choiceorder')));
77
 
78
        // Add any missing subquestions. Sometimes people edit questions after they
79
        // have been attempted which breaks things.
80
        foreach ($this->stemorder as $stemid) {
81
            if (!isset($this->stems[$stemid])) {
82
                $this->stems[$stemid] = html_writer::span(
83
                        get_string('deletedsubquestion', 'qtype_match'), 'notifyproblem');
84
                $this->stemformat[$stemid] = FORMAT_HTML;
85
                $this->right[$stemid] = 0;
86
            }
87
        }
88
 
89
        // Add any missing choices. Sometimes people edit questions after they
90
        // have been attempted which breaks things.
91
        foreach ($this->choiceorder as $choiceid) {
92
            if (!isset($this->choices[$choiceid])) {
93
                $this->choices[$choiceid] = get_string('deletedchoice', 'qtype_match');
94
            }
95
        }
96
    }
97
 
98
    /**
99
     * Helper method used by both {@link start_attempt()} and
100
     * {@link apply_attempt_state()}.
101
     * @param array $choiceorder the choices, in order.
102
     */
103
    protected function set_choiceorder($choiceorder) {
104
        $this->choiceorder = array();
105
        foreach ($choiceorder as $key => $choiceid) {
106
            $this->choiceorder[$key + 1] = $choiceid;
107
        }
108
    }
109
 
110
    public function validate_can_regrade_with_other_version(question_definition $otherversion): ?string {
111
        $basemessage = parent::validate_can_regrade_with_other_version($otherversion);
112
        if ($basemessage) {
113
            return $basemessage;
114
        }
115
 
116
        if (count($this->stems) != count($otherversion->stems)) {
117
            return get_string('regradeissuenumstemschanged', 'qtype_match');
118
        }
119
 
120
        if (count($this->choices) != count($otherversion->choices)) {
121
            return get_string('regradeissuenumchoiceschanged', 'qtype_match');
122
        }
123
 
124
        return null;
125
    }
126
 
127
    public function update_attempt_state_data_for_new_version(
128
            question_attempt_step $oldstep, question_definition $otherversion) {
129
        $startdata = parent::update_attempt_state_data_for_new_version($oldstep, $otherversion);
130
 
131
        // Process stems.
132
        $mapping = array_combine(array_keys($otherversion->stems), array_keys($this->stems));
133
        $oldstemorder = explode(',', $oldstep->get_qt_var('_stemorder'));
134
        $newstemorder = [];
135
        foreach ($oldstemorder as $oldid) {
136
            $newstemorder[] = $mapping[$oldid] ?? $oldid;
137
        }
138
        $startdata['_stemorder'] = implode(',', $newstemorder);
139
 
140
        // Process choices.
141
        $mapping = array_combine(array_keys($otherversion->choices), array_keys($this->choices));
142
        $oldchoiceorder = explode(',', $oldstep->get_qt_var('_choiceorder'));
143
        $newchoiceorder = [];
144
        foreach ($oldchoiceorder as $oldid) {
145
            $newchoiceorder[] = $mapping[$oldid] ?? $oldid;
146
        }
147
        $startdata['_choiceorder'] = implode(',', $newchoiceorder);
148
 
149
        return $startdata;
150
    }
151
 
152
    public function get_question_summary() {
153
        $question = $this->html_to_text($this->questiontext, $this->questiontextformat);
154
        $stems = array();
155
        foreach ($this->stemorder as $stemid) {
156
            $stems[] = $this->html_to_text($this->stems[$stemid], $this->stemformat[$stemid]);
157
        }
158
        $choices = array();
159
        foreach ($this->choiceorder as $choiceid) {
160
            $choices[] = $this->choices[$choiceid];
161
        }
162
        return $question . ' {' . implode('; ', $stems) . '} -> {' .
163
                implode('; ', $choices) . '}';
164
    }
165
 
166
    public function summarise_response(array $response) {
167
        $matches = array();
168
        foreach ($this->stemorder as $key => $stemid) {
169
            if (array_key_exists($this->field($key), $response) && $response[$this->field($key)]) {
170
                $matches[] = $this->html_to_text($this->stems[$stemid],
171
                        $this->stemformat[$stemid]) . ' -> ' .
172
                        $this->choices[$this->choiceorder[$response[$this->field($key)]]];
173
            }
174
        }
175
        if (empty($matches)) {
176
            return null;
177
        }
178
        return implode('; ', $matches);
179
    }
180
 
181
    public function classify_response(array $response) {
182
        $selectedchoicekeys = array();
183
        foreach ($this->stemorder as $key => $stemid) {
184
            if (array_key_exists($this->field($key), $response) && $response[$this->field($key)]) {
185
                $selectedchoicekeys[$stemid] = $this->choiceorder[$response[$this->field($key)]];
186
            } else {
187
                $selectedchoicekeys[$stemid] = 0;
188
            }
189
        }
190
 
191
        $parts = array();
192
        foreach ($this->stems as $stemid => $stem) {
193
            if ($this->right[$stemid] == 0 || !isset($selectedchoicekeys[$stemid])) {
194
                // Choice for a deleted subquestion, ignore. (See apply_attempt_state.)
195
                continue;
196
            }
197
            $selectedchoicekey = $selectedchoicekeys[$stemid];
198
            if (empty($selectedchoicekey)) {
199
                $parts[$stemid] = question_classified_response::no_response();
200
                continue;
201
            }
202
            $choice = $this->choices[$selectedchoicekey];
203
            if ($choice == get_string('deletedchoice', 'qtype_match')) {
204
                // Deleted choice, ignore. (See apply_attempt_state.)
205
                continue;
206
            }
207
            $parts[$stemid] = new question_classified_response(
208
                    $selectedchoicekey, $choice,
209
                    ($selectedchoicekey == $this->right[$stemid]) / count($this->stems));
210
        }
211
        return $parts;
212
    }
213
 
214
    public function clear_wrong_from_response(array $response) {
215
        foreach ($this->stemorder as $key => $stemid) {
216
            if (!array_key_exists($this->field($key), $response) ||
217
                    $response[$this->field($key)] != $this->get_right_choice_for($stemid)) {
218
                $response[$this->field($key)] = 0;
219
            }
220
        }
221
        return $response;
222
    }
223
 
224
    public function get_num_parts_right(array $response) {
225
        $numright = 0;
226
        foreach ($this->stemorder as $key => $stemid) {
227
            $fieldname = $this->field($key);
228
            if (!array_key_exists($fieldname, $response)) {
229
                continue;
230
            }
231
 
232
            $choice = $response[$fieldname];
233
            if ($choice && $this->choiceorder[$choice] == $this->right[$stemid]) {
234
                $numright += 1;
235
            }
236
        }
237
        return array($numright, count($this->stemorder));
238
    }
239
 
240
    /**
241
     * @param int $key stem number
242
     * @return string the question-type variable name.
243
     */
244
    protected function field($key) {
245
        return 'sub' . $key;
246
    }
247
 
248
    public function get_expected_data() {
249
        $vars = array();
250
        foreach ($this->stemorder as $key => $notused) {
251
            $vars[$this->field($key)] = PARAM_INT;
252
        }
253
        return $vars;
254
    }
255
 
256
    public function get_correct_response() {
257
        $response = array();
258
        foreach ($this->stemorder as $key => $stemid) {
259
            $response[$this->field($key)] = $this->get_right_choice_for($stemid);
260
        }
261
        return $response;
262
    }
263
 
264
    public function prepare_simulated_post_data($simulatedresponse) {
265
        $postdata = array();
266
        $stemtostemids = array_flip(clean_param_array($this->stems, PARAM_NOTAGS));
267
        $choicetochoiceno = array_flip($this->choices);
268
        $choicenotochoiceselectvalue = array_flip($this->choiceorder);
269
        foreach ($simulatedresponse as $stem => $choice) {
270
            $choice = clean_param($choice, PARAM_NOTAGS);
271
            $stemid = $stemtostemids[$stem];
272
            $shuffledstemno = array_search($stemid, $this->stemorder);
273
            if (empty($choice)) {
274
                $choiceselectvalue = 0;
275
            } else if ($choicetochoiceno[$choice]) {
276
                $choiceselectvalue = $choicenotochoiceselectvalue[$choicetochoiceno[$choice]];
277
            } else {
278
                throw new coding_exception("Unknown choice {$choice} in matching question - {$this->name}.");
279
            }
280
            $postdata[$this->field($shuffledstemno)] = $choiceselectvalue;
281
        }
282
        return $postdata;
283
    }
284
 
285
    public function get_student_response_values_for_simulation($postdata) {
286
        $simulatedresponse = array();
287
        foreach ($this->stemorder as $shuffledstemno => $stemid) {
288
            if (!empty($postdata[$this->field($shuffledstemno)])) {
289
                $choiceselectvalue = $postdata[$this->field($shuffledstemno)];
290
                $choiceno = $this->choiceorder[$choiceselectvalue];
291
                $choice = clean_param($this->choices[$choiceno], PARAM_NOTAGS);
292
                $stem = clean_param($this->stems[$stemid], PARAM_NOTAGS);
293
                $simulatedresponse[$stem] = $choice;
294
            }
295
        }
296
        ksort($simulatedresponse);
297
        return $simulatedresponse;
298
    }
299
 
300
    public function get_right_choice_for($stemid) {
301
        foreach ($this->choiceorder as $choicekey => $choiceid) {
302
            if ($this->right[$stemid] == $choiceid) {
303
                return $choicekey;
304
            }
305
        }
306
    }
307
 
308
    public function is_complete_response(array $response) {
309
        $complete = true;
310
        foreach ($this->stemorder as $key => $stemid) {
311
            $complete = $complete && !empty($response[$this->field($key)]);
312
        }
313
        return $complete;
314
    }
315
 
316
    public function is_gradable_response(array $response) {
317
        foreach ($this->stemorder as $key => $stemid) {
318
            if (!empty($response[$this->field($key)])) {
319
                return true;
320
            }
321
        }
322
        return false;
323
    }
324
 
325
    public function get_validation_error(array $response) {
326
        if ($this->is_complete_response($response)) {
327
            return '';
328
        }
329
        return get_string('pleaseananswerallparts', 'qtype_match');
330
    }
331
 
332
    public function is_same_response(array $prevresponse, array $newresponse) {
333
        foreach ($this->stemorder as $key => $notused) {
334
            $fieldname = $this->field($key);
335
            if (!question_utils::arrays_same_at_key_integer(
336
                    $prevresponse, $newresponse, $fieldname)) {
337
                return false;
338
            }
339
        }
340
        return true;
341
    }
342
 
343
    public function grade_response(array $response) {
344
        list($right, $total) = $this->get_num_parts_right($response);
345
        $fraction = $right / $total;
346
        return array($fraction, question_state::graded_state_for_fraction($fraction));
347
    }
348
 
349
    public function compute_final_grade($responses, $totaltries) {
350
        $totalstemscore = 0;
351
        foreach ($this->stemorder as $key => $stemid) {
352
            $fieldname = $this->field($key);
353
 
354
            $lastwrongindex = -1;
355
            $finallyright = false;
356
            foreach ($responses as $i => $response) {
357
                if (!array_key_exists($fieldname, $response) || !$response[$fieldname] ||
358
                        $this->choiceorder[$response[$fieldname]] != $this->right[$stemid]) {
359
                    $lastwrongindex = $i;
360
                    $finallyright = false;
361
                } else {
362
                    $finallyright = true;
363
                }
364
            }
365
 
366
            if ($finallyright) {
367
                $totalstemscore += max(0, 1 - ($lastwrongindex + 1) * $this->penalty);
368
            }
369
        }
370
 
371
        return $totalstemscore / count($this->stemorder);
372
    }
373
 
374
    public function get_stem_order() {
375
        return $this->stemorder;
376
    }
377
 
378
    public function get_choice_order() {
379
        return $this->choiceorder;
380
    }
381
 
382
    public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) {
383
        if ($component == 'qtype_match' && $filearea == 'subquestion') {
384
            $subqid = reset($args); // Itemid is sub question id.
385
            return array_key_exists($subqid, $this->stems);
386
 
387
        } else if ($component == 'question' && in_array($filearea,
388
                array('correctfeedback', 'partiallycorrectfeedback', 'incorrectfeedback'))) {
389
            return $this->check_combined_feedback_file_access($qa, $options, $filearea, $args);
390
 
391
        } else if ($component == 'question' && $filearea == 'hint') {
392
            return $this->check_hint_file_access($qa, $options, $args);
393
 
394
        } else {
395
            return parent::check_file_access($qa, $options, $component, $filearea,
396
                    $args, $forcedownload);
397
        }
398
    }
399
 
400
    /**
401
     * Return the question settings that define this question as structured data.
402
     *
403
     * @param question_attempt $qa the current attempt for which we are exporting the settings.
404
     * @param question_display_options $options the question display options which say which aspects of the question
405
     * should be visible.
406
     * @return mixed structure representing the question settings. In web services, this will be JSON-encoded.
407
     */
408
    public function get_question_definition_for_external_rendering(question_attempt $qa, question_display_options $options) {
409
        // This is a partial implementation, returning only the most relevant question settings for now,
410
        // ideally, we should return as much as settings as possible (depending on the state and display options).
411
 
412
        return [
413
            'shufflestems' => $this->shufflestems,
414
        ];
415
    }
416
}