Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | 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
 * Renderers for outputting parts of the question engine.
19
 *
20
 * @package    moodlecore
21
 * @subpackage questionengine
22
 * @copyright  2009 The Open University
23
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24
 */
25
 
26
use core_question\output\question_version_info;
27
 
28
defined('MOODLE_INTERNAL') || die();
29
 
30
 
31
/**
32
 * This renderer controls the overall output of questions. It works with a
33
 * {@link qbehaviour_renderer} and a {@link qtype_renderer} to output the
34
 * type-specific bits. The main entry point is the {@link question()} method.
35
 *
36
 * @copyright  2009 The Open University
37
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38
 */
39
class core_question_renderer extends plugin_renderer_base {
40
 
41
    /**
42
     * Generate the display of a question in a particular state, and with certain
43
     * display options. Normally you do not call this method directly. Intsead
44
     * you call {@link question_usage_by_activity::render_question()} which will
45
     * call this method with appropriate arguments.
46
     *
47
     * @param question_attempt $qa the question attempt to display.
48
     * @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour
49
     *      specific parts.
50
     * @param qtype_renderer $qtoutput the renderer to output the question type
51
     *      specific parts.
52
     * @param question_display_options $options controls what should and should not be displayed.
53
     * @param string|null $number The question number to display. 'i' is a special
54
     *      value that gets displayed as Information. Null means no number is displayed.
55
     * @return string HTML representation of the question.
56
     */
57
    public function question(question_attempt $qa, qbehaviour_renderer $behaviouroutput,
58
            qtype_renderer $qtoutput, question_display_options $options, $number) {
59
 
60
        // If not already set, record the questionidentifier.
61
        $options = clone($options);
62
        if (!$options->has_question_identifier()) {
63
            $options->questionidentifier = $this->question_number_text($number);
64
        }
65
 
66
        $output = '';
67
        $output .= html_writer::start_tag('div', array(
68
            'id' => $qa->get_outer_question_div_unique_id(),
69
            'class' => implode(' ', array(
70
                'que',
71
                $qa->get_question(false)->get_type_name(),
72
                $qa->get_behaviour_name(),
73
                $qa->get_state_class($options->correctness && $qa->has_marks()),
74
            ))
75
        ));
76
 
77
        $output .= html_writer::tag('div',
78
                $this->info($qa, $behaviouroutput, $qtoutput, $options, $number),
79
                array('class' => 'info'));
80
 
81
        $output .= html_writer::start_tag('div', array('class' => 'content'));
82
 
83
        $output .= html_writer::tag('div',
84
                $this->add_part_heading($qtoutput->formulation_heading(),
85
                    $this->formulation($qa, $behaviouroutput, $qtoutput, $options)),
86
                array('class' => 'formulation clearfix'));
87
        $output .= html_writer::nonempty_tag('div',
88
                $this->add_part_heading(get_string('feedback', 'question'),
89
                    $this->outcome($qa, $behaviouroutput, $qtoutput, $options)),
90
                array('class' => 'outcome clearfix'));
91
        $output .= html_writer::nonempty_tag('div',
92
                $this->add_part_heading(get_string('comments', 'question'),
93
                    $this->manual_comment($qa, $behaviouroutput, $qtoutput, $options)),
94
                array('class' => 'comment clearfix'));
95
        $output .= html_writer::nonempty_tag('div',
96
                $this->response_history($qa, $behaviouroutput, $qtoutput, $options),
97
                array('class' => 'history clearfix border p-2'));
98
 
99
        $output .= html_writer::end_tag('div');
100
        $output .= html_writer::end_tag('div');
101
        return $output;
102
    }
103
 
104
    /**
105
     * Generate the information bit of the question display that contains the
106
     * metadata like the question number, current state, and mark.
107
     * @param question_attempt $qa the question attempt to display.
108
     * @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour
109
     *      specific parts.
110
     * @param qtype_renderer $qtoutput the renderer to output the question type
111
     *      specific parts.
112
     * @param question_display_options $options controls what should and should not be displayed.
113
     * @param string|null $number The question number to display. 'i' is a special
114
     *      value that gets displayed as Information. Null means no number is displayed.
115
     * @return HTML fragment.
116
     */
117
    protected function info(question_attempt $qa, qbehaviour_renderer $behaviouroutput,
118
            qtype_renderer $qtoutput, question_display_options $options, $number) {
119
        $output = '';
120
        $output .= $this->number($number);
121
        $output .= $this->status($qa, $behaviouroutput, $options);
122
        $output .= $this->mark_summary($qa, $behaviouroutput, $options);
123
        $output .= $this->question_flag($qa, $options->flags);
124
        $output .= $this->edit_question_link($qa, $options);
125
        if ($options->versioninfo) {
126
            $output .= $this->render(new question_version_info($qa->get_question(), true));
127
        }
128
        return $output;
129
    }
130
 
131
    /**
132
     * Generate the display of the question number.
133
     * @param string|null $number The question number to display. 'i' is a special
134
     *      value that gets displayed as Information. Null means no number is displayed.
135
     * @return HTML fragment.
136
     */
137
    protected function number($number) {
138
        if (trim($number ?? '') === '') {
139
            return '';
140
        }
141
        if (trim($number) === 'i') {
142
            $numbertext = get_string('information', 'question');
143
        } else {
144
            $numbertext = get_string('questionx', 'question',
145
                    html_writer::tag('span', s($number), array('class' => 'qno')));
146
        }
147
        return html_writer::tag('h3', $numbertext, array('class' => 'no'));
148
    }
149
 
150
    /**
151
     * Get the question number as a string.
152
     *
153
     * @param string|null $number e.g. '123' or 'i'. null or '' means do not display anything number-related.
154
     * @return string e.g. 'Question 123' or 'Information' or ''.
155
     */
156
    protected function question_number_text(?string $number): string {
157
        $number = $number ?? '';
158
        // Trim the question number of whitespace, including &nbsp;.
159
        $trimmed = trim(html_entity_decode($number), " \n\r\t\v\x00\xC2\xA0");
160
        if ($trimmed === '') {
161
            return '';
162
        }
163
        if (trim($number) === 'i') {
164
            return get_string('information', 'question');
165
        } else {
166
            return get_string('questionx', 'question', s($number));
167
        }
168
    }
169
 
170
    /**
171
     * Add an invisible heading like 'question text', 'feebdack' at the top of
172
     * a section's contents, but only if the section has some content.
173
     * @param string $heading the heading to add.
174
     * @param string $content the content of the section.
175
     * @return string HTML fragment with the heading added.
176
     */
177
    protected function add_part_heading($heading, $content) {
178
        if ($content) {
179
            $content = html_writer::tag('h4', $heading, array('class' => 'accesshide')) . $content;
180
        }
181
        return $content;
182
    }
183
 
184
    /**
185
     * Generate the display of the status line that gives the current state of
186
     * the question.
187
     * @param question_attempt $qa the question attempt to display.
188
     * @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour
189
     *      specific parts.
190
     * @param question_display_options $options controls what should and should not be displayed.
191
     * @return HTML fragment.
192
     */
193
    protected function status(question_attempt $qa, qbehaviour_renderer $behaviouroutput,
194
            question_display_options $options) {
195
        return html_writer::tag('div', $qa->get_state_string($options->correctness),
196
                array('class' => 'state'));
197
    }
198
 
199
    /**
200
     * Generate the display of the marks for this question.
201
     * @param question_attempt $qa the question attempt to display.
202
     * @param qbehaviour_renderer $behaviouroutput the behaviour renderer, which can generate a custom display.
203
     * @param question_display_options $options controls what should and should not be displayed.
204
     * @return HTML fragment.
205
     */
206
    protected function mark_summary(question_attempt $qa, qbehaviour_renderer $behaviouroutput, question_display_options $options) {
207
        return html_writer::nonempty_tag('div',
208
                $behaviouroutput->mark_summary($qa, $this, $options),
209
                array('class' => 'grade'));
210
    }
211
 
212
    /**
213
     * Generate the display of the marks for this question.
214
     * @param question_attempt $qa the question attempt to display.
215
     * @param question_display_options $options controls what should and should not be displayed.
216
     * @return HTML fragment.
217
     */
218
    public function standard_mark_summary(question_attempt $qa, qbehaviour_renderer $behaviouroutput, question_display_options $options) {
219
        if (!$options->marks) {
220
            return '';
221
 
222
        } else if ($qa->get_max_mark() == 0) {
223
            return get_string('notgraded', 'question');
224
 
225
        } else if ($options->marks == question_display_options::MAX_ONLY ||
226
                is_null($qa->get_fraction())) {
227
            return $behaviouroutput->marked_out_of_max($qa, $this, $options);
228
 
229
        } else {
230
            return $behaviouroutput->mark_out_of_max($qa, $this, $options);
231
        }
232
    }
233
 
234
    /**
235
     * Generate the display of the available marks for this question.
236
     * @param question_attempt $qa the question attempt to display.
237
     * @param question_display_options $options controls what should and should not be displayed.
238
     * @return HTML fragment.
239
     */
240
    public function standard_marked_out_of_max(question_attempt $qa, question_display_options $options) {
241
        return get_string('markedoutofmax', 'question', $qa->format_max_mark($options->markdp));
242
    }
243
 
244
    /**
245
     * Generate the display of the marks for this question out of the available marks.
246
     * @param question_attempt $qa the question attempt to display.
247
     * @param question_display_options $options controls what should and should not be displayed.
248
     * @return HTML fragment.
249
     */
250
    public function standard_mark_out_of_max(question_attempt $qa, question_display_options $options) {
251
        $a = new stdClass();
252
        $a->mark = $qa->format_mark($options->markdp);
253
        $a->max = $qa->format_max_mark($options->markdp);
254
        return get_string('markoutofmax', 'question', $a);
255
    }
256
 
257
    /**
258
     * Render the question flag, assuming $flagsoption allows it.
259
     *
260
     * @param question_attempt $qa the question attempt to display.
261
     * @param int $flagsoption the option that says whether flags should be displayed.
262
     */
263
    protected function question_flag(question_attempt $qa, $flagsoption) {
264
        $divattributes = array('class' => 'questionflag');
265
 
266
        switch ($flagsoption) {
267
            case question_display_options::VISIBLE:
268
                $flagcontent = $this->get_flag_html($qa->is_flagged());
269
                break;
270
 
271
            case question_display_options::EDITABLE:
272
                $id = $qa->get_flag_field_name();
273
                // The checkbox id must be different from any element name, because
274
                // of a stupid IE bug:
275
                // http://www.456bereastreet.com/archive/200802/beware_of_id_and_name_attribute_mixups_when_using_getelementbyid_in_internet_explorer/
276
                $checkboxattributes = array(
277
                    'type' => 'checkbox',
278
                    'id' => $id . 'checkbox',
279
                    'name' => $id,
280
                    'value' => 1,
281
                );
282
                if ($qa->is_flagged()) {
283
                    $checkboxattributes['checked'] = 'checked';
284
                }
285
                $postdata = question_flags::get_postdata($qa);
286
 
287
                $flagcontent = html_writer::empty_tag('input',
288
                                array('type' => 'hidden', 'name' => $id, 'value' => 0)) .
289
                        html_writer::empty_tag('input',
290
                                array('type' => 'hidden', 'value' => $postdata, 'class' => 'questionflagpostdata')) .
291
                        html_writer::empty_tag('input', $checkboxattributes) .
292
                        html_writer::tag('label', $this->get_flag_html($qa->is_flagged(), $id . 'img'),
293
                                array('id' => $id . 'label', 'for' => $id . 'checkbox')) . "\n";
294
 
295
                $divattributes = array(
296
                    'class' => 'questionflag editable',
297
                );
298
 
299
                break;
300
 
301
            default:
302
                $flagcontent = '';
303
        }
304
 
305
        return html_writer::nonempty_tag('div', $flagcontent, $divattributes);
306
    }
307
 
308
    /**
309
     * Work out the actual img tag needed for the flag
310
     *
311
     * @param bool $flagged whether the question is currently flagged.
312
     * @param string $id an id to be added as an attribute to the img (optional).
313
     * @return string the img tag.
314
     */
315
    protected function get_flag_html($flagged, $id = '') {
316
        if ($flagged) {
317
            $icon = 'i/flagged';
318
            $label = get_string('clickunflag', 'question');
319
        } else {
320
            $icon = 'i/unflagged';
321
            $label = get_string('clickflag', 'question');
322
        }
323
        $attributes = [
324
            'src' => $this->image_url($icon),
325
            'alt' => '',
326
            'class' => 'questionflagimage',
327
        ];
328
        if ($id) {
329
            $attributes['id'] = $id;
330
        }
331
        $img = html_writer::empty_tag('img', $attributes);
332
        $img .= html_writer::span($label);
333
 
334
        return $img;
335
    }
336
 
337
    /**
338
     * Generate the display of the edit question link.
339
     *
340
     * @param question_attempt $qa The question attempt to display.
341
     * @param question_display_options $options controls what should and should not be displayed.
342
     * @return string
343
     */
344
    protected function edit_question_link(question_attempt $qa, question_display_options $options) {
345
        if (empty($options->editquestionparams)) {
346
            return '';
347
        }
348
 
349
        $params = $options->editquestionparams;
350
        if ($params['returnurl'] instanceof moodle_url) {
351
            $params['returnurl'] = $params['returnurl']->out_as_local_url(false);
352
        }
353
        $params['id'] = $qa->get_question_id();
354
        $editurl = new moodle_url('/question/bank/editquestion/question.php', $params);
355
 
356
        return html_writer::tag('div', html_writer::link(
357
                $editurl, $this->pix_icon('t/edit', get_string('edit'), '', array('class' => 'iconsmall')) .
358
                get_string('editquestion', 'question')),
359
                array('class' => 'editquestion'));
360
    }
361
 
362
    /**
363
     * Generate the display of the formulation part of the question. This is the
364
     * area that contains the quetsion text, and the controls for students to
365
     * input their answers. Some question types also embed feedback, for
366
     * example ticks and crosses, in this area.
367
     *
368
     * @param question_attempt $qa the question attempt to display.
369
     * @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour
370
     *      specific parts.
371
     * @param qtype_renderer $qtoutput the renderer to output the question type
372
     *      specific parts.
373
     * @param question_display_options $options controls what should and should not be displayed.
374
     * @return HTML fragment.
375
     */
376
    protected function formulation(question_attempt $qa, qbehaviour_renderer $behaviouroutput,
377
            qtype_renderer $qtoutput, question_display_options $options) {
378
        $output = '';
379
        $output .= html_writer::empty_tag('input', array(
380
                'type' => 'hidden',
381
                'name' => $qa->get_control_field_name('sequencecheck'),
382
                'value' => $qa->get_sequence_check_count()));
383
        $output .= $qtoutput->formulation_and_controls($qa, $options);
384
        if ($options->clearwrong) {
385
            $output .= $qtoutput->clear_wrong($qa);
386
        }
387
        $output .= html_writer::nonempty_tag('div',
388
                $behaviouroutput->controls($qa, $options), array('class' => 'im-controls'));
389
        return $output;
390
    }
391
 
392
    /**
393
     * Generate the display of the outcome part of the question. This is the
394
     * area that contains the various forms of feedback.
395
     *
396
     * @param question_attempt $qa the question attempt to display.
397
     * @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour
398
     *      specific parts.
399
     * @param qtype_renderer $qtoutput the renderer to output the question type
400
     *      specific parts.
401
     * @param question_display_options $options controls what should and should not be displayed.
402
     * @return HTML fragment.
403
     */
404
    protected function outcome(question_attempt $qa, qbehaviour_renderer $behaviouroutput,
405
            qtype_renderer $qtoutput, question_display_options $options) {
406
        $output = '';
407
        $output .= html_writer::nonempty_tag('div',
408
                $qtoutput->feedback($qa, $options), array('class' => 'feedback'));
409
        $output .= html_writer::nonempty_tag('div',
410
                $behaviouroutput->feedback($qa, $options), array('class' => 'im-feedback'));
411
        $output .= html_writer::nonempty_tag('div',
412
                $options->extrainfocontent, array('class' => 'extra-feedback'));
413
        return $output;
414
    }
415
 
416
    protected function manual_comment(question_attempt $qa, qbehaviour_renderer $behaviouroutput,
417
            qtype_renderer $qtoutput, question_display_options $options) {
418
        return $qtoutput->manual_comment($qa, $options) .
419
                $behaviouroutput->manual_comment($qa, $options);
420
    }
421
 
422
    /**
423
     * Generate the display of the response history part of the question. This
424
     * is the table showing all the steps the question has been through.
425
     *
426
     * @param question_attempt $qa the question attempt to display.
427
     * @param qbehaviour_renderer $behaviouroutput the renderer to output the behaviour
428
     *      specific parts.
429
     * @param qtype_renderer $qtoutput the renderer to output the question type
430
     *      specific parts.
431
     * @param question_display_options $options controls what should and should not be displayed.
432
     * @return HTML fragment.
433
     */
434
    protected function response_history(question_attempt $qa, qbehaviour_renderer $behaviouroutput,
435
            qtype_renderer $qtoutput, question_display_options $options) {
436
 
437
        if (!$options->history) {
438
            return '';
439
        }
440
 
441
        $table = new html_table();
442
        $table->head  = array (
443
            get_string('step', 'question'),
444
            get_string('time'),
445
            get_string('action', 'question'),
446
            get_string('state', 'question'),
447
        );
448
        if ($options->marks >= question_display_options::MARK_AND_MAX) {
449
            $table->head[] = get_string('marks', 'question');
450
        }
451
 
452
        foreach ($qa->get_full_step_iterator() as $i => $step) {
453
            $stepno = $i + 1;
454
 
455
            $rowclass = '';
456
            if ($stepno == $qa->get_num_steps()) {
457
                $rowclass = 'current';
458
            } else if (!empty($options->questionreviewlink)) {
459
                $url = new moodle_url($options->questionreviewlink,
460
                        array('slot' => $qa->get_slot(), 'step' => $i));
461
                $stepno = $this->output->action_link($url, $stepno,
462
                        new popup_action('click', $url, 'reviewquestion',
463
                                array('width' => 450, 'height' => 650)),
464
                        array('title' => get_string('reviewresponse', 'question')));
465
            }
466
 
467
            $restrictedqa = new question_attempt_with_restricted_history($qa, $i, null);
468
 
469
            $row = [$stepno,
470
                    userdate($step->get_timecreated(), get_string('strftimedatetimeshortaccurate', 'core_langconfig')),
471
                    s($qa->summarise_action($step)) . $this->action_author($step, $options),
472
                    $restrictedqa->get_state_string($options->correctness)];
473
 
474
            if ($options->marks >= question_display_options::MARK_AND_MAX) {
475
                $row[] = $qa->format_fraction_as_mark($step->get_fraction(), $options->markdp);
476
            }
477
 
478
            $table->rowclasses[] = $rowclass;
479
            $table->data[] = $row;
480
        }
481
 
482
        return html_writer::tag('h4', get_string('responsehistory', 'question'),
483
                        array('class' => 'responsehistoryheader')) .
484
                $options->extrahistorycontent .
485
                html_writer::tag('div', html_writer::table($table, true),
486
                        array('class' => 'responsehistoryheader'));
487
    }
488
 
489
    /**
490
     * Action author's profile link.
491
     *
492
     * @param question_attempt_step $step The step.
493
     * @param question_display_options $options The display options.
494
     * @return string The link to user's profile.
495
     */
496
    protected function action_author(question_attempt_step $step, question_display_options $options): string {
497
        if ($options->userinfoinhistory && $step->get_user_id() != $options->userinfoinhistory) {
498
            return html_writer::link(
499
                    new moodle_url('/user/view.php', ['id' => $step->get_user_id(), 'course' => $this->page->course->id]),
500
                    $step->get_user_fullname(), ['class' => 'd-table-cell']);
501
        } else {
502
            return '';
503
        }
504
    }
505
}