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
/**
19
 * Multianswer question definition class.
20
 *
21
 * @package    qtype
22
 * @subpackage multianswer
23
 * @copyright  2010 Pierre Pichet
24
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25
 */
26
 
27
require_once($CFG->dirroot . '/question/type/questionbase.php');
28
require_once($CFG->dirroot . '/question/type/shortanswer/question.php');
29
require_once($CFG->dirroot . '/question/type/numerical/question.php');
30
require_once($CFG->dirroot . '/question/type/multichoice/question.php');
31
 
32
 
33
/**
34
 * Represents a multianswer question.
35
 *
36
 * A multi-answer question is made of of several subquestions of various types.
37
 * You can think of it as an application of the composite pattern to qusetion
38
 * types.
39
 *
40
 * @copyright  2010 Pierre Pichet
41
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
42
 */
43
class qtype_multianswer_question extends question_graded_automatically_with_countback {
44
    /** @var array of question_graded_automatically. */
45
    public $subquestions = array();
46
 
47
    /**
48
     * @var array place number => insex in the $subquestions array. Places are
49
     * numbered from 1.
50
     */
51
    public $places;
52
 
53
    /**
54
     * @var array of strings, one longer than $places, which is achieved by
55
     * indexing from 0. The bits of question text that go between the subquestions.
56
     */
57
    public $textfragments;
58
 
59
    /**
60
     * Get a question_attempt_step_subquestion_adapter
61
     * @param question_attempt_step $step the step to adapt.
62
     * @param int $i the subquestion index.
63
     * @return question_attempt_step_subquestion_adapter.
64
     */
65
    protected function get_substep($step, $i) {
66
        return new question_attempt_step_subquestion_adapter($step, 'sub' . $i . '_');
67
    }
68
 
69
    public function start_attempt(question_attempt_step $step, $variant) {
70
        foreach ($this->subquestions as $i => $subq) {
71
            $subq->start_attempt($this->get_substep($step, $i), $variant);
72
        }
73
    }
74
 
75
    public function apply_attempt_state(question_attempt_step $step) {
76
        foreach ($this->subquestions as $i => $subq) {
77
            $subq->apply_attempt_state($this->get_substep($step, $i));
78
        }
79
    }
80
 
81
    public function validate_can_regrade_with_other_version(question_definition $otherversion): ?string {
82
        $basemessage = parent::validate_can_regrade_with_other_version($otherversion);
83
        if ($basemessage) {
84
            return $basemessage;
85
        }
86
 
87
        if (count($this->subquestions) != count($otherversion->subquestions)) {
88
            return get_string('regradeissuenumsubquestionschanged', 'qtype_multianswer');
89
        }
90
 
91
        foreach ($this->subquestions as $i => $subq) {
92
            $subqmessage = $subq->validate_can_regrade_with_other_version($otherversion->subquestions[$i]);
93
            if ($subqmessage) {
94
                return $subqmessage;
95
            }
96
        }
97
 
98
        return null;
99
    }
100
 
101
    public function update_attempt_state_data_for_new_version(
102
            question_attempt_step $oldstep, question_definition $oldquestion) {
103
        parent::update_attempt_state_data_for_new_version($oldstep, $oldquestion);
104
 
105
        $result = [];
106
        foreach ($this->subquestions as $i => $subq) {
107
            $substep = $this->get_substep($oldstep, $i);
108
            $statedata = $subq->update_attempt_state_data_for_new_version(
109
                    $substep, $oldquestion->subquestions[$i]);
110
            foreach ($statedata as $name => $value) {
111
                $result[$substep->add_prefix($name)] = $value;
112
            }
113
        }
114
 
115
        return $result;
116
    }
117
 
118
    public function get_question_summary() {
119
        $summary = $this->html_to_text($this->questiontext, $this->questiontextformat);
120
        foreach ($this->subquestions as $i => $subq) {
121
            switch ($subq->qtype->name()) {
122
                case 'multichoice':
123
                    $choices = array();
124
                    $dummyqa = new question_attempt($subq, $this->contextid);
125
                    foreach ($subq->get_order($dummyqa) as $ansid) {
126
                        $choices[] = $this->html_to_text($subq->answers[$ansid]->answer,
127
                                $subq->answers[$ansid]->answerformat);
128
                    }
129
                    $answerbit = '{' . implode('; ', $choices) . '}';
130
                    break;
131
                case 'numerical':
132
                case 'shortanswer':
133
                    $answerbit = '_____';
134
                    break;
135
                default:
136
                    $answerbit = '{ERR unknown sub-question type}';
137
            }
138
            $summary = str_replace('{#' . $i . '}', $answerbit, $summary);
139
        }
140
        return $summary;
141
    }
142
 
143
    public function get_min_fraction() {
144
        $fractionsum = 0;
145
        $fractionmax = 0;
146
        foreach ($this->subquestions as $i => $subq) {
147
            $fractionmax += $subq->defaultmark;
148
            $fractionsum += $subq->defaultmark * $subq->get_min_fraction();
149
        }
150
        if (empty($fractionsum)) {
151
            return 0;
152
        }
153
        return $fractionsum / (!empty($this->subquestions) ? $fractionmax : 1);
154
    }
155
 
156
    public function get_max_fraction() {
157
        $fractionsum = 0;
158
        $fractionmax = 0;
159
        foreach ($this->subquestions as $i => $subq) {
160
            $fractionmax += $subq->defaultmark;
161
            $fractionsum += $subq->defaultmark * $subq->get_max_fraction();
162
        }
163
        if (empty($fractionsum)) {
164
            return 1;
165
        }
166
        return $fractionsum / (!empty($this->subquestions) ? $fractionmax : 1);
167
    }
168
 
169
    public function get_expected_data() {
170
        $expected = array();
171
        foreach ($this->subquestions as $i => $subq) {
172
            $substep = $this->get_substep(null, $i);
173
            foreach ($subq->get_expected_data() as $name => $type) {
174
                if ($subq->qtype->name() == 'multichoice' &&
175
                        $subq->layout == qtype_multichoice_base::LAYOUT_DROPDOWN) {
176
                    // Hack or MC inline does not work.
177
                    $expected[$substep->add_prefix($name)] = PARAM_RAW;
178
                } else {
179
                    $expected[$substep->add_prefix($name)] = $type;
180
                }
181
            }
182
        }
183
        return $expected;
184
    }
185
 
186
    public function get_correct_response() {
187
        $right = array();
188
        foreach ($this->subquestions as $i => $subq) {
189
            $substep = $this->get_substep(null, $i);
190
            foreach ($subq->get_correct_response() as $name => $type) {
191
                $right[$substep->add_prefix($name)] = $type;
192
            }
193
        }
194
        return $right;
195
    }
196
 
197
    public function prepare_simulated_post_data($simulatedresponse) {
198
        $postdata = array();
199
        foreach ($this->subquestions as $i => $subq) {
200
            $substep = $this->get_substep(null, $i);
201
            foreach ($subq->prepare_simulated_post_data($simulatedresponse[$i]) as $name => $value) {
202
                $postdata[$substep->add_prefix($name)] = $value;
203
            }
204
        }
205
        return $postdata;
206
    }
207
 
208
    public function get_student_response_values_for_simulation($postdata) {
209
        $simulatedresponse = array();
210
        foreach ($this->subquestions as $i => $subq) {
211
            $substep = $this->get_substep(null, $i);
212
            $subqpostdata = $substep->filter_array($postdata);
213
            $subqsimulatedresponse = $subq->get_student_response_values_for_simulation($subqpostdata);
214
            foreach ($subqsimulatedresponse as $subresponsekey => $responsevalue) {
215
                $simulatedresponse[$i.'.'.$subresponsekey] = $responsevalue;
216
            }
217
        }
218
        ksort($simulatedresponse);
219
        return $simulatedresponse;
220
    }
221
 
222
    public function is_complete_response(array $response) {
223
        foreach ($this->subquestions as $i => $subq) {
224
            $substep = $this->get_substep(null, $i);
225
            if (!$subq->is_complete_response($substep->filter_array($response))) {
226
                return false;
227
            }
228
        }
229
        return true;
230
    }
231
 
232
    public function is_gradable_response(array $response) {
233
        foreach ($this->subquestions as $i => $subq) {
234
            $substep = $this->get_substep(null, $i);
235
            if ($subq->is_gradable_response($substep->filter_array($response))) {
236
                return true;
237
            }
238
        }
239
        return false;
240
    }
241
 
242
    public function is_same_response(array $prevresponse, array $newresponse) {
243
        foreach ($this->subquestions as $i => $subq) {
244
            $substep = $this->get_substep(null, $i);
245
            if (!$subq->is_same_response($substep->filter_array($prevresponse),
246
                    $substep->filter_array($newresponse))) {
247
                return false;
248
            }
249
        }
250
        return true;
251
    }
252
 
253
    public function get_validation_error(array $response) {
254
        if ($this->is_complete_response($response)) {
255
            return '';
256
        }
257
        return get_string('pleaseananswerallparts', 'qtype_multianswer');
258
    }
259
 
260
    /**
261
     * Used by grade_response to combine the states of the subquestions.
262
     * The combined state is accumulates in $overallstate. That will be right
263
     * if all the separate states are right; and wrong if all the separate states
264
     * are wrong, otherwise, it will be partially right.
265
     * @param question_state $overallstate the result so far.
266
     * @param question_state $newstate the new state to add to the combination.
267
     * @return question_state the new combined state.
268
     */
269
    protected function combine_states($overallstate, $newstate) {
270
        if (is_null($overallstate)) {
271
            return $newstate;
272
        } else if ($overallstate == question_state::$gaveup &&
273
                $newstate == question_state::$gaveup) {
274
            return question_state::$gaveup;
275
        } else if ($overallstate == question_state::$gaveup &&
276
                $newstate == question_state::$gradedwrong) {
277
            return question_state::$gradedwrong;
278
        } else if ($overallstate == question_state::$gradedwrong &&
279
                $newstate == question_state::$gaveup) {
280
            return question_state::$gradedwrong;
281
        } else if ($overallstate == question_state::$gradedwrong &&
282
                $newstate == question_state::$gradedwrong) {
283
            return question_state::$gradedwrong;
284
        } else if ($overallstate == question_state::$gradedright &&
285
                $newstate == question_state::$gradedright) {
286
            return question_state::$gradedright;
287
        } else {
288
            return question_state::$gradedpartial;
289
        }
290
    }
291
 
292
    public function grade_response(array $response) {
293
        $overallstate = null;
294
        $fractionsum = 0;
295
        $fractionmax = 0;
296
        foreach ($this->subquestions as $i => $subq) {
297
            $fractionmax += $subq->defaultmark;
298
            $substep = $this->get_substep(null, $i);
299
            $subresp = $substep->filter_array($response);
300
            if (!$subq->is_gradable_response($subresp)) {
301
                $overallstate = $this->combine_states($overallstate, question_state::$gaveup);
302
            } else {
303
                list($subfraction, $newstate) = $subq->grade_response($subresp);
304
                $fractionsum += $subfraction * $subq->defaultmark;
305
                $overallstate = $this->combine_states($overallstate, $newstate);
306
            }
307
        }
308
        if (empty($fractionmax)) {
309
            return array(null, $overallstate ?? question_state::$finished);
310
        }
311
        return array($fractionsum / $fractionmax, $overallstate);
312
    }
313
 
314
    public function clear_wrong_from_response(array $response) {
315
        foreach ($this->subquestions as $i => $subq) {
316
            $substep = $this->get_substep(null, $i);
317
            $subresp = $substep->filter_array($response);
318
            list($subfraction, $newstate) = $subq->grade_response($subresp);
319
            if ($newstate != question_state::$gradedright) {
320
                foreach ($subresp as $ind => $resp) {
321
                    if ($subq->qtype == 'multichoice' && ($subq->layout == qtype_multichoice_base::LAYOUT_VERTICAL
322
                            || $subq->layout == qtype_multichoice_base::LAYOUT_HORIZONTAL)) {
323
                        $response[$substep->add_prefix($ind)] = '-1';
324
                    } else {
325
                        $response[$substep->add_prefix($ind)] = '';
326
                    }
327
                }
328
            }
329
        }
330
        return $response;
331
    }
332
 
333
    public function get_num_parts_right(array $response) {
334
        $numright = 0;
335
        foreach ($this->subquestions as $i => $subq) {
336
            $substep = $this->get_substep(null, $i);
337
            $subresp = $substep->filter_array($response);
338
            list($subfraction, $newstate) = $subq->grade_response($subresp);
339
            if ($newstate == question_state::$gradedright) {
340
                $numright += 1;
341
            }
342
        }
343
        return array($numright, count($this->subquestions));
344
    }
345
 
346
    public function compute_final_grade($responses, $totaltries) {
347
        $fractionsum = 0;
348
        $fractionmax = 0;
349
        foreach ($this->subquestions as $i => $subq) {
350
            $fractionmax += $subq->defaultmark;
351
 
352
            $lastresponse = array();
353
            $lastchange = 0;
354
            $subfraction = 0;
355
            foreach ($responses as $responseindex => $response) {
356
                $substep = $this->get_substep(null, $i);
357
                $subresp = $substep->filter_array($response);
358
                if ($subq->is_same_response($lastresponse, $subresp)) {
359
                    continue;
360
                }
361
                $lastresponse = $subresp;
362
                $lastchange = $responseindex;
363
                list($subfraction, $newstate) = $subq->grade_response($subresp);
364
            }
365
 
366
            $fractionsum += $subq->defaultmark * max(0, $subfraction - $lastchange * $this->penalty);
367
        }
368
 
369
        return $fractionsum / $fractionmax;
370
    }
371
 
372
    public function summarise_response(array $response) {
373
        $summary = array();
374
        foreach ($this->subquestions as $i => $subq) {
375
            $substep = $this->get_substep(null, $i);
376
            $a = new stdClass();
377
            $a->i = $i;
378
            $a->response = $subq->summarise_response($substep->filter_array($response));
379
            $summary[] = get_string('subqresponse', 'qtype_multianswer', $a);
380
        }
381
 
382
        return implode('; ', $summary);
383
    }
384
 
385
    public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) {
386
        if ($component == 'question' && $filearea == 'answer') {
387
            return true;
388
 
389
        } else if ($component == 'question' && $filearea == 'answerfeedback') {
390
            // Full logic to control which feedbacks a student can see is too complex.
391
            // Just allow access to all images. There is a theoretical chance the
392
            // students could see files they are not meant to see by guessing URLs,
393
            // but it is remote.
394
            return $options->feedback;
395
 
396
        } else if ($component == 'question' && $filearea == 'hint') {
397
            return $this->check_hint_file_access($qa, $options, $args);
398
 
399
        } else {
400
            return parent::check_file_access($qa, $options, $component, $filearea,
401
                    $args, $forcedownload);
402
        }
403
    }
404
 
405
    /**
406
     * Return the question settings that define this question as structured data.
407
     *
408
     * @param question_attempt $qa the current attempt for which we are exporting the settings.
409
     * @param question_display_options $options the question display options which say which aspects of the question
410
     * should be visible.
411
     * @return mixed structure representing the question settings. In web services, this will be JSON-encoded.
412
     */
413
    public function get_question_definition_for_external_rendering(question_attempt $qa, question_display_options $options) {
414
        // Empty implementation for now in order to avoid debugging in core questions (generated in the parent class),
415
        // ideally, we should return as much as settings as possible (depending on the state and display options).
416
 
417
        return null;
418
    }
419
}