Proyectos de Subversion Moodle

Rev

Rev 1 | Rev 704 | Ir a la última revisión | | 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
namespace mod_quiz\output;
18
 
19
use cm_info;
20
use coding_exception;
21
use context;
22
use context_module;
23
use html_table;
24
use html_table_cell;
25
use html_writer;
26
use mod_quiz\access_manager;
27
use mod_quiz\form\preflight_check_form;
28
use mod_quiz\output\grades\grade_out_of;
29
use mod_quiz\question\display_options;
30
use mod_quiz\quiz_attempt;
31
use moodle_url;
32
use plugin_renderer_base;
33
use popup_action;
34
use question_display_options;
35
use mod_quiz\quiz_settings;
36
use renderable;
37
use single_button;
38
use stdClass;
39
 
40
/**
41
 * The main renderer for the quiz module.
42
 *
43
 * @package   mod_quiz
44
 * @category  output
45
 * @copyright 2011 The Open University
46
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
47
 */
48
class renderer extends plugin_renderer_base {
49
    /**
50
     * Builds the review page
51
     *
52
     * @param quiz_attempt $attemptobj an instance of quiz_attempt.
53
     * @param array $slots of slots to be displayed.
54
     * @param int $page the current page number
55
     * @param bool $showall whether to show entire attempt on one page.
56
     * @param bool $lastpage if true the current page is the last page.
57
     * @param display_options $displayoptions instance of display_options.
58
     * @param attempt_summary_information|array $summarydata summary information about the attempt.
59
     *      Passing an array is deprecated.
60
     * @return string HTML to display.
61
     */
62
    public function review_page(quiz_attempt $attemptobj, $slots, $page, $showall,
63
            $lastpage, display_options $displayoptions, $summarydata) {
64
        if (is_array($summarydata)) {
65
            debugging('Since Moodle 4.4, $summarydata passed to review_page should be a attempt_summary_information.',
66
                DEBUG_DEVELOPER);
67
            $summarydata = $this->filter_review_summary_table($summarydata, $page);
68
            $summarydata = attempt_summary_information::create_from_legacy_array($summarydata);
69
        }
70
 
71
        $output = '';
72
        $output .= $this->header();
73
        $output .= $this->review_attempt_summary($summarydata, $page);
74
        $output .= $this->review_form($page, $showall, $displayoptions,
75
                $this->questions($attemptobj, true, $slots, $page, $showall, $displayoptions),
76
                $attemptobj);
77
 
78
        $output .= $this->review_next_navigation($attemptobj, $page, $lastpage, $showall);
79
        $output .= $this->footer();
80
        return $output;
81
    }
82
 
83
    /**
84
     * Renders the review question pop-up.
85
     *
86
     * @param quiz_attempt $attemptobj an instance of quiz_attempt.
87
     * @param int $slot which question to display.
88
     * @param int $seq which step of the question attempt to show. null = latest.
89
     * @param display_options $displayoptions instance of display_options.
90
     * @param attempt_summary_information|array $summarydata summary information about the attempt.
91
     *      Passing an array is deprecated.
92
     * @return string HTML to display.
93
     */
94
    public function review_question_page(quiz_attempt $attemptobj, $slot, $seq,
95
            display_options $displayoptions, $summarydata) {
96
        if (is_array($summarydata)) {
97
            debugging('Since Moodle 4.4, $summarydata passed to review_question_page should be a attempt_summary_information.',
98
                DEBUG_DEVELOPER);
99
            $summarydata = attempt_summary_information::create_from_legacy_array($summarydata);
100
        }
101
 
102
        $output = '';
103
        $output .= $this->header();
104
        $output .= html_writer::div($this->render($summarydata), 'mb-3');
105
 
106
        if (!is_null($seq)) {
107
            $output .= $attemptobj->render_question_at_step($slot, $seq, true, $this);
108
        } else {
109
            $output .= $attemptobj->render_question($slot, true, $this);
110
        }
111
 
112
        $output .= $this->close_window_button();
113
        $output .= $this->footer();
114
        return $output;
115
    }
116
 
117
    /**
118
     * Renders the review question pop-up.
119
     *
120
     * @param quiz_attempt $attemptobj an instance of quiz_attempt.
121
     * @param string $message Why the review is not allowed.
122
     * @return string html to output.
123
     */
124
    public function review_question_not_allowed(quiz_attempt $attemptobj, $message) {
125
        $output = '';
126
        $output .= $this->header();
127
        $output .= $this->heading(format_string($attemptobj->get_quiz_name(), true,
128
                ["context" => $attemptobj->get_quizobj()->get_context()]));
129
        $output .= $this->notification($message);
130
        $output .= $this->close_window_button();
131
        $output .= $this->footer();
132
        return $output;
133
    }
134
 
135
    /**
136
     * A chance to filter the information before display.
137
     *
138
     * Moodle core uses this to display less infomrmation on pages after the first.
139
     * This is a separate method, becaus it is a useful hook where themes can overrid things.
140
     *
141
     * @param attempt_summary_information $summarydata the data that will be displayed. Modify as desired.
142
     * @param int $page contains the current page number
143
     */
144
    public function filter_review_attempt_summary(
145
        attempt_summary_information $summarydata,
146
        int $page
147
    ): void {
148
        if ($page > 0) {
149
            $summarydata->filter_keeping_only(['user', 'attemptlist']);
150
        }
151
    }
152
 
153
    /**
154
     * Outputs the overall summary of the attempt at the top of the review page.
155
     *
156
     * @param attempt_summary_information $summarydata contains row data for table.
157
     * @param int $page contains the current page number
158
     * @return string HTML to display.
159
     */
160
    public function review_attempt_summary(
161
        attempt_summary_information $summarydata,
162
        int $page
163
    ): string {
164
        $this->filter_review_attempt_summary($summarydata, $page);
165
        return html_writer::div($this->render($summarydata), 'mb-3');
166
    }
167
 
168
    /**
169
     * Filters the summarydata array.
170
     *
171
     * @param array $summarydata contains row data for table
172
     * @param int $page the current page number
173
     * @return array updated version of the $summarydata array.
174
     * @deprecated since Moodle 4.4. Replaced by filter_review_attempt_summary.
175
     */
176
    protected function filter_review_summary_table($summarydata, $page) {
177
        debugging('filter_review_summary_table() is deprecated. Replaced by filter_review_attempt_summary().', DEBUG_DEVELOPER);
178
        if ($page == 0) {
179
            return $summarydata;
180
        }
181
 
182
        // Only show some of summary table on subsequent pages.
183
        foreach ($summarydata as $key => $rowdata) {
184
            if (!in_array($key, ['user', 'attemptlist'])) {
185
                unset($summarydata[$key]);
186
            }
187
        }
188
 
189
        return $summarydata;
190
    }
191
 
192
    /**
193
     * Outputs the table containing data from summary data array
194
     *
195
     * @param array $summarydata contains row data for table
196
     * @param int $page contains the current page number
197
     * @return string HTML to display.
198
     * @deprecated since Moodle 4.4. Replaced by review_attempt_summary.
199
     */
200
    public function review_summary_table($summarydata, $page) {
201
        debugging('review_summary_table() is deprecated. Please use review_attempt_summary() instead.', DEBUG_DEVELOPER);
202
        $summarydata = $this->filter_review_summary_table($summarydata, $page);
203
        $this->render(attempt_summary_information::create_from_legacy_array($summarydata));
204
    }
205
 
206
    /**
207
     * Renders each question
208
     *
209
     * @param quiz_attempt $attemptobj instance of quiz_attempt
210
     * @param bool $reviewing
211
     * @param array $slots array of integers relating to questions
212
     * @param int $page current page number
213
     * @param bool $showall if true shows attempt on single page
214
     * @param display_options $displayoptions instance of display_options
215
     */
216
    public function questions(quiz_attempt $attemptobj, $reviewing, $slots, $page, $showall,
217
            display_options $displayoptions) {
218
        $output = '';
219
        foreach ($slots as $slot) {
220
            $output .= $attemptobj->render_question($slot, $reviewing, $this,
221
                    $attemptobj->review_url($slot, $page, $showall));
222
        }
223
        return $output;
224
    }
225
 
226
    /**
227
     * Renders the main bit of the review page.
228
     *
229
     * @param int $page current page number
230
     * @param bool $showall if true display attempt on one page
231
     * @param display_options $displayoptions instance of display_options
232
     * @param string $content the rendered display of each question
233
     * @param quiz_attempt $attemptobj instance of quiz_attempt
234
     * @return string HTML to display.
235
     */
236
    public function review_form($page, $showall, $displayoptions, $content, $attemptobj) {
237
        if ($displayoptions->flags != question_display_options::EDITABLE) {
238
            return $content;
239
        }
240
 
241
        $this->page->requires->js_init_call('M.mod_quiz.init_review_form', null, false,
242
                quiz_get_js_module());
243
 
244
        $output = '';
245
        $output .= html_writer::start_tag('form', ['action' => $attemptobj->review_url(null,
246
                $page, $showall), 'method' => 'post', 'class' => 'questionflagsaveform']);
247
        $output .= html_writer::start_tag('div');
248
        $output .= $content;
249
        $output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'sesskey',
250
                'value' => sesskey()]);
251
        $output .= html_writer::start_tag('div', ['class' => 'submitbtns']);
252
        $output .= html_writer::empty_tag('input', ['type' => 'submit',
253
                'class' => 'questionflagsavebutton btn btn-secondary', 'name' => 'savingflags',
254
                'value' => get_string('saveflags', 'question')]);
255
        $output .= html_writer::end_tag('div');
256
        $output .= html_writer::end_tag('div');
257
        $output .= html_writer::end_tag('form');
258
 
259
        return $output;
260
    }
261
 
262
    /**
263
     * Returns either a link or button.
264
     *
265
     * @param quiz_attempt $attemptobj instance of quiz_attempt
266
     */
267
    public function finish_review_link(quiz_attempt $attemptobj) {
268
        $url = $attemptobj->view_url();
269
 
270
        if ($attemptobj->get_access_manager(time())->attempt_must_be_in_popup()) {
271
            $this->page->requires->js_init_call('M.mod_quiz.secure_window.init_close_button',
272
                    [$url->out(false)], false, quiz_get_js_module());
273
            return html_writer::empty_tag('input', ['type' => 'button',
274
                    'value' => get_string('finishreview', 'quiz'),
275
                    'id' => 'secureclosebutton',
276
                    'class' => 'mod_quiz-next-nav btn btn-primary']);
277
 
278
        } else {
279
            return html_writer::link($url, get_string('finishreview', 'quiz'),
280
                    ['class' => 'mod_quiz-next-nav']);
281
        }
282
    }
283
 
284
    /**
285
     * Creates the navigation links/buttons at the bottom of the review attempt page.
286
     *
287
     * Note, the name of this function is no longer accurate, but when the design
288
     * changed, it was decided to keep the old name for backwards compatibility.
289
     *
290
     * @param quiz_attempt $attemptobj instance of quiz_attempt
291
     * @param int $page the current page
292
     * @param bool $lastpage if true current page is the last page
293
     * @param bool|null $showall if true, the URL will be to review the entire attempt on one page,
294
     *      and $page will be ignored. If null, a sensible default will be chosen.
295
     *
296
     * @return string HTML fragment.
297
     */
298
    public function review_next_navigation(quiz_attempt $attemptobj, $page, $lastpage, $showall = null) {
299
        $nav = '';
300
        if ($page > 0) {
301
            $nav .= link_arrow_left(get_string('navigateprevious', 'quiz'),
302
                    $attemptobj->review_url(null, $page - 1, $showall), false, 'mod_quiz-prev-nav');
303
        }
304
        if ($lastpage) {
305
            $nav .= $this->finish_review_link($attemptobj);
306
        } else {
307
            $nav .= link_arrow_right(get_string('navigatenext', 'quiz'),
308
                    $attemptobj->review_url(null, $page + 1, $showall), false, 'mod_quiz-next-nav');
309
        }
310
        return html_writer::tag('div', $nav, ['class' => 'submitbtns']);
311
    }
312
 
313
    /**
314
     * Return the HTML of the quiz timer.
315
     *
316
     * @param quiz_attempt $attemptobj instance of quiz_attempt
317
     * @param int $timenow timestamp to use as 'now'.
318
     * @return string HTML content.
319
     */
320
    public function countdown_timer(quiz_attempt $attemptobj, $timenow) {
321
 
322
        $timeleft = $attemptobj->get_time_left_display($timenow);
323
        if ($timeleft !== false) {
324
            $ispreview = $attemptobj->is_preview();
325
            $timerstartvalue = $timeleft;
326
            if (!$ispreview) {
327
                // Make sure the timer starts just above zero. If $timeleft was <= 0, then
328
                // this will just have the effect of causing the quiz to be submitted immediately.
329
                $timerstartvalue = max($timerstartvalue, 1);
330
            }
331
            $this->initialise_timer($timerstartvalue, $ispreview);
332
        }
333
 
334
        return $this->output->render_from_template('mod_quiz/timer', (object) []);
335
    }
336
 
337
    /**
338
     * Create a preview link
339
     *
340
     * @param moodle_url $url URL to restart the attempt.
341
     */
342
    public function restart_preview_button($url) {
343
        return $this->single_button($url, get_string('startnewpreview', 'quiz'));
344
    }
345
 
346
    /**
347
     * Outputs the navigation block panel
348
     *
349
     * @param navigation_panel_base $panel
350
     */
351
    public function navigation_panel(navigation_panel_base $panel) {
352
 
353
        $output = '';
354
        $userpicture = $panel->user_picture();
355
        if ($userpicture) {
356
            $fullname = fullname($userpicture->user);
357
            if ($userpicture->size) {
358
                $fullname = html_writer::div($fullname);
359
            }
360
            $output .= html_writer::tag('div', $this->render($userpicture) . $fullname,
361
                    ['id' => 'user-picture', 'class' => 'clearfix']);
362
        }
363
        $output .= $panel->render_before_button_bits($this);
364
 
365
        $bcc = $panel->get_button_container_class();
366
        $output .= html_writer::start_tag('div', ['class' => "qn_buttons clearfix $bcc"]);
367
        foreach ($panel->get_question_buttons() as $button) {
368
            $output .= $this->render($button);
369
        }
370
        $output .= html_writer::end_tag('div');
371
 
372
        $output .= html_writer::tag('div', $panel->render_end_bits($this),
373
                ['class' => 'othernav']);
374
 
375
        $this->page->requires->js_init_call('M.mod_quiz.nav.init', null, false,
376
                quiz_get_js_module());
377
 
378
        return $output;
379
    }
380
 
381
    /**
382
     * Display a quiz navigation button.
383
     *
384
     * @param navigation_question_button $button
385
     * @return string HTML fragment.
386
     */
387
    protected function render_navigation_question_button(navigation_question_button $button) {
388
        $classes = ['qnbutton', $button->stateclass, $button->navmethod, 'btn'];
389
        $extrainfo = [];
390
 
391
        if ($button->currentpage) {
392
            $classes[] = 'thispage';
393
            $extrainfo[] = get_string('onthispage', 'quiz');
394
        }
395
 
396
        // Flagged?
397
        if ($button->flagged) {
398
            $classes[] = 'flagged';
399
            $flaglabel = get_string('flagged', 'question');
400
        } else {
401
            $flaglabel = '';
402
        }
403
        $extrainfo[] = html_writer::tag('span', $flaglabel, ['class' => 'flagstate']);
404
 
405
        if ($button->isrealquestion) {
406
            $qnostring = 'questionnonav';
407
        } else {
408
            $qnostring = 'questionnonavinfo';
409
        }
410
 
411
        $tooltip = get_string('questionx', 'question', s($button->number)) . ' - ' . $button->statestring;
412
 
413
        $a = new stdClass();
414
        $a->number = s($button->number);
415
        $a->attributes = implode(' ', $extrainfo);
416
        $tagcontents = html_writer::tag('span', '', ['class' => 'thispageholder']) .
417
                html_writer::tag('span', '', ['class' => 'trafficlight']) .
418
                get_string($qnostring, 'quiz', $a);
419
        $tagattributes = ['class' => implode(' ', $classes), 'id' => $button->id,
420
                'title' => $tooltip, 'data-quiz-page' => $button->page];
421
 
422
        if ($button->url) {
423
            return html_writer::link($button->url, $tagcontents, $tagattributes);
424
        } else {
425
            return html_writer::tag('span', $tagcontents, $tagattributes);
426
        }
427
    }
428
 
429
    /**
430
     * Display a quiz navigation heading.
431
     *
432
     * @param navigation_section_heading $heading the heading.
433
     * @return string HTML fragment.
434
     */
435
    protected function render_navigation_section_heading(navigation_section_heading $heading) {
436
        if (empty($heading->heading)) {
437
            $headingtext = get_string('sectionnoname', 'quiz');
438
            $class = ' dimmed_text';
439
        } else {
440
            $headingtext = $heading->heading;
441
            $class = '';
442
        }
443
        return $this->heading($headingtext, 3, 'mod_quiz-section-heading' . $class);
444
    }
445
 
446
    /**
447
     * Renders a list of links the other attempts.
448
     *
449
     * @param links_to_other_attempts $links
450
     * @return string HTML fragment.
451
     */
452
    protected function render_links_to_other_attempts(
453
            links_to_other_attempts $links) {
454
        $attemptlinks = [];
455
        foreach ($links->links as $attempt => $url) {
456
            if (!$url) {
457
                $attemptlinks[] = html_writer::tag('strong', $attempt);
458
            } else {
459
                if ($url instanceof renderable) {
460
                    $attemptlinks[] = $this->render($url);
461
                } else {
462
                    $attemptlinks[] = html_writer::link($url, $attempt);
463
                }
464
            }
465
        }
466
        return implode(', ', $attemptlinks);
467
    }
468
 
469
    /**
470
     * Render a {@see grade_out_of}.
471
     *
472
     * Most of the logic is in methods of the grade_out_of class. However,
473
     * having this renderer method allows themes to override the default rendering.
474
     *
475
     * @param grade_out_of $grade
476
     * @return string HTML to output.
477
     */
478
    protected function render_grade_out_of(grade_out_of $grade): string {
479
        return get_string($grade->get_string_key(), 'quiz',
480
                $grade->style_formatted_values($grade->get_formatted_values()));
481
    }
482
 
483
    /**
484
     * Render the 'start attempt' page.
485
     *
486
     * The student gets here if their interaction with the preflight check
487
     * from fails in some way (e.g. they typed the wrong password).
488
     *
489
     * @param \mod_quiz\quiz_settings $quizobj
490
     * @param preflight_check_form $mform
491
     * @return string
492
     */
493
    public function start_attempt_page(quiz_settings $quizobj, preflight_check_form $mform) {
494
        $output = '';
495
        $output .= $this->header();
496
        $output .= $this->during_attempt_tertiary_nav($quizobj->view_url());
497
        $output .= $this->heading(format_string($quizobj->get_quiz_name(), true,
498
                ["context" => $quizobj->get_context()]));
499
        $output .= $this->quiz_intro($quizobj->get_quiz(), $quizobj->get_cm());
500
        $output .= $mform->render();
501
        $output .= $this->footer();
502
        return $output;
503
    }
504
 
505
    /**
506
     * Attempt Page
507
     *
508
     * @param quiz_attempt $attemptobj Instance of quiz_attempt
509
     * @param int $page Current page number
510
     * @param access_manager $accessmanager Instance of access_manager
511
     * @param array $messages An array of messages
512
     * @param array $slots Contains an array of integers that relate to questions
513
     * @param int $id The ID of an attempt
514
     * @param int $nextpage The number of the next page
515
     * @return string HTML to output.
516
     */
517
    public function attempt_page($attemptobj, $page, $accessmanager, $messages, $slots, $id,
518
            $nextpage) {
519
        $output = '';
520
        $output .= $this->header();
521
        $output .= $this->during_attempt_tertiary_nav($attemptobj->view_url());
522
        $output .= $this->quiz_notices($messages);
523
        $output .= $this->countdown_timer($attemptobj, time());
524
        $output .= $this->attempt_form($attemptobj, $page, $slots, $id, $nextpage);
525
        $output .= $this->footer();
526
        return $output;
527
    }
528
 
529
    /**
530
     * Render the tertiary navigation for pages during the attempt.
531
     *
532
     * @param string|moodle_url $quizviewurl url of the view.php page for this quiz.
533
     * @return string HTML to output.
534
     */
535
    public function during_attempt_tertiary_nav($quizviewurl): string {
536
        $output = '';
537
        $output .= html_writer::start_div('container-fluid tertiary-navigation');
538
        $output .= html_writer::start_div('row');
539
        $output .= html_writer::start_div('navitem');
540
        $output .= html_writer::link($quizviewurl, get_string('back'),
541
                ['class' => 'btn btn-secondary']);
542
        $output .= html_writer::end_div();
543
        $output .= html_writer::end_div();
544
        $output .= html_writer::end_div();
545
        return $output;
546
    }
547
 
548
    /**
549
     * Returns any notices.
550
     *
551
     * @param array $messages
552
     */
553
    public function quiz_notices($messages) {
554
        if (!$messages) {
555
            return '';
556
        }
557
        return $this->notification(
558
                html_writer::tag('p', get_string('accessnoticesheader', 'quiz')) . $this->access_messages($messages),
559
                'warning',
560
                false
561
        );
562
    }
563
 
564
    /**
565
     * Outputs the form for making an attempt
566
     *
567
     * @param quiz_attempt $attemptobj
568
     * @param int $page Current page number
569
     * @param array $slots Array of integers relating to questions
570
     * @param int $id ID of the attempt
571
     * @param int $nextpage Next page number
572
     */
573
    public function attempt_form($attemptobj, $page, $slots, $id, $nextpage) {
574
        $output = '';
575
 
576
        // Start the form.
577
        $output .= html_writer::start_tag('form',
578
                ['action' => new moodle_url($attemptobj->processattempt_url(),
579
                        ['cmid' => $attemptobj->get_cmid()]), 'method' => 'post',
580
                        'enctype' => 'multipart/form-data', 'accept-charset' => 'utf-8',
581
                        'id' => 'responseform']);
582
        $output .= html_writer::start_tag('div');
583
 
584
        // Print all the questions.
585
        foreach ($slots as $slot) {
586
            $output .= $attemptobj->render_question($slot, false, $this,
587
                    $attemptobj->attempt_url($slot, $page));
588
        }
589
 
590
        $navmethod = $attemptobj->get_quiz()->navmethod;
591
        $output .= $this->attempt_navigation_buttons($page, $attemptobj->is_last_page($page), $navmethod);
592
 
593
        // Some hidden fields to track what is going on.
594
        $output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'attempt',
595
                'value' => $attemptobj->get_attemptid()]);
596
        $output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'thispage',
597
                'value' => $page, 'id' => 'followingpage']);
598
        $output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'nextpage',
599
                'value' => $nextpage]);
600
        $output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'timeup',
601
                'value' => '0', 'id' => 'timeup']);
602
        $output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'sesskey',
603
                'value' => sesskey()]);
604
        $output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'mdlscrollto',
605
                'value' => '', 'id' => 'mdlscrollto']);
606
 
607
        // Add a hidden field with questionids. Do this at the end of the form, so
608
        // if you navigate before the form has finished loading, it does not wipe all
609
        // the student's answers.
610
        $output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => 'slots',
611
                'value' => implode(',', $attemptobj->get_active_slots($page))]);
612
 
613
        // Finish the form.
614
        $output .= html_writer::end_tag('div');
615
        $output .= html_writer::end_tag('form');
616
 
617
        $output .= $this->connection_warning();
618
 
619
        return $output;
620
    }
621
 
622
    /**
623
     * Display the prev/next buttons that go at the bottom of each page of the attempt.
624
     *
625
     * @param int $page the page number. Starts at 0 for the first page.
626
     * @param bool $lastpage is this the last page in the quiz?
627
     * @param string $navmethod Optional quiz attribute, 'free' (default) or 'sequential'
628
     * @return string HTML fragment.
629
     */
630
    protected function attempt_navigation_buttons($page, $lastpage, $navmethod = 'free') {
631
        $output = '';
632
 
633
        $output .= html_writer::start_tag('div', ['class' => 'submitbtns']);
634
        if ($page > 0 && $navmethod == 'free') {
635
            $output .= html_writer::empty_tag('input', ['type' => 'submit', 'name' => 'previous',
636
                    'value' => get_string('navigateprevious', 'quiz'), 'class' => 'mod_quiz-prev-nav btn btn-secondary',
637
                    'id' => 'mod_quiz-prev-nav']);
638
            $this->page->requires->js_call_amd('core_form/submit', 'init', ['mod_quiz-prev-nav']);
639
        }
640
        if ($lastpage) {
641
            $nextlabel = get_string('endtest', 'quiz');
642
        } else {
643
            $nextlabel = get_string('navigatenext', 'quiz');
644
        }
645
        $output .= html_writer::empty_tag('input', ['type' => 'submit', 'name' => 'next',
646
                'value' => $nextlabel, 'class' => 'mod_quiz-next-nav btn btn-primary', 'id' => 'mod_quiz-next-nav']);
647
        $output .= html_writer::end_tag('div');
648
        $this->page->requires->js_call_amd('core_form/submit', 'init', ['mod_quiz-next-nav']);
649
 
650
        return $output;
651
    }
652
 
653
    /**
654
     * Render a button which allows students to redo a question in the attempt.
655
     *
656
     * @param int $slot the number of the slot to generate the button for.
657
     * @param bool $disabled if true, output the button disabled.
658
     * @return string HTML fragment.
659
     */
660
    public function redo_question_button($slot, $disabled) {
661
        $attributes = ['type' => 'submit', 'name' => 'redoslot' . $slot,
662
                'value' => get_string('redoquestion', 'quiz'),
663
                'class' => 'mod_quiz-redo_question_button btn btn-secondary',
664
                'id' => 'redoslot' . $slot . '-submit',
665
                'data-savescrollposition' => 'true',
666
            ];
667
        if ($disabled) {
668
            $attributes['disabled'] = 'disabled';
669
        } else {
670
            $this->page->requires->js_call_amd('core_question/question_engine', 'initSubmitButton', [$attributes['id']]);
671
        }
672
        return html_writer::div(html_writer::empty_tag('input', $attributes));
673
    }
674
 
675
    /**
676
     * Initialise the JavaScript required to initialise the countdown timer.
677
     *
678
     * @param int $timerstartvalue time remaining, in seconds.
679
     * @param bool $ispreview true if this is a preview attempt.
680
     */
681
    public function initialise_timer($timerstartvalue, $ispreview) {
682
        $options = [$timerstartvalue, (bool) $ispreview];
683
        $this->page->requires->js_init_call('M.mod_quiz.timer.init', $options, false, quiz_get_js_module());
684
    }
685
 
686
    /**
687
     * Output a page with an optional message, and JavaScript code to close the
688
     * current window and redirect the parent window to a new URL.
689
     *
690
     * @param moodle_url $url the URL to redirect the parent window to.
691
     * @param string $message message to display before closing the window. (optional)
692
     * @return string HTML to output.
693
     */
694
    public function close_attempt_popup($url, $message = '') {
695
        $output = '';
696
        $output .= $this->header();
697
        $output .= $this->box_start();
698
 
699
        if ($message) {
700
            $output .= html_writer::tag('p', $message);
701
            $output .= html_writer::tag('p', get_string('windowclosing', 'quiz'));
702
            $delay = 5;
703
        } else {
704
            $output .= html_writer::tag('p', get_string('pleaseclose', 'quiz'));
705
            $delay = 0;
706
        }
707
        $this->page->requires->js_init_call('M.mod_quiz.secure_window.close',
11 efrain 708
            [$url->out(false), $delay], false, quiz_get_js_module());
1 efrain 709
 
710
        $output .= $this->box_end();
711
        $output .= $this->footer();
712
        return $output;
713
    }
714
 
715
    /**
716
     * Print each message in an array, surrounded by &lt;p>, &lt;/p> tags.
717
     *
718
     * @param array $messages the array of message strings.
719
     * @return string HTML to output.
720
     */
721
    public function access_messages($messages) {
722
        $output = '';
723
        foreach ($messages as $message) {
724
            $output .= html_writer::tag('p', $message, ['class' => 'text-left']);
725
        }
726
        return $output;
727
    }
728
 
729
    /*
730
     * Summary Page
731
     */
732
    /**
733
     * Create the summary page
734
     *
735
     * @param quiz_attempt $attemptobj
736
     * @param display_options $displayoptions
737
     */
738
    public function summary_page($attemptobj, $displayoptions) {
739
        $output = '';
740
        $output .= $this->header();
741
        $output .= $this->during_attempt_tertiary_nav($attemptobj->view_url());
742
        $output .= $this->heading(format_string($attemptobj->get_quiz_name()));
743
        $output .= $this->heading(get_string('summaryofattempt', 'quiz'), 3);
744
        $output .= $this->summary_table($attemptobj, $displayoptions);
745
        $output .= $this->summary_page_controls($attemptobj);
746
        $output .= $this->footer();
747
        return $output;
748
    }
749
 
750
    /**
751
     * Generates the table of summarydata
752
     *
753
     * @param quiz_attempt $attemptobj
754
     * @param display_options $displayoptions
755
     */
756
    public function summary_table($attemptobj, $displayoptions) {
757
        // Prepare the summary table header.
758
        $table = new html_table();
759
        $table->attributes['class'] = 'generaltable quizsummaryofattempt boxaligncenter';
760
        $table->head = [get_string('question', 'quiz'), get_string('status', 'quiz')];
761
        $table->align = ['left', 'left'];
762
        $table->size = ['', ''];
763
        $markscolumn = $displayoptions->marks >= question_display_options::MARK_AND_MAX;
764
        if ($markscolumn) {
765
            $table->head[] = get_string('marks', 'quiz');
766
            $table->align[] = 'left';
767
            $table->size[] = '';
768
        }
769
        $tablewidth = count($table->align);
770
        $table->data = [];
771
 
772
        // Get the summary info for each question.
773
        $slots = $attemptobj->get_slots();
774
        foreach ($slots as $slot) {
775
            // Add a section headings if we need one here.
776
            $heading = $attemptobj->get_heading_before_slot($slot);
777
            if ($heading !== null) {
778
                // There is a heading here.
779
                $rowclasses = 'quizsummaryheading';
780
                if ($heading) {
781
                    $heading = format_string($heading);
782
                } else {
783
                    if (count($attemptobj->get_quizobj()->get_sections()) > 1) {
784
                        // If this is the start of an unnamed section, and the quiz has more
785
                        // than one section, then add a default heading.
786
                        $heading = get_string('sectionnoname', 'quiz');
787
                        $rowclasses .= ' dimmed_text';
788
                    }
789
                }
790
                $cell = new html_table_cell(format_string($heading));
791
                $cell->header = true;
792
                $cell->colspan = $tablewidth;
793
                $table->data[] = [$cell];
794
                $table->rowclasses[] = $rowclasses;
795
            }
796
 
797
            // Don't display information items.
798
            if (!$attemptobj->is_real_question($slot)) {
799
                continue;
800
            }
801
 
802
            // Real question, show it.
803
            $flag = '';
804
            if ($attemptobj->is_question_flagged($slot)) {
805
                // Quiz has custom JS manipulating these image tags - so we can't use the pix_icon method here.
806
                $flag = html_writer::empty_tag('img', ['src' => $this->image_url('i/flagged'),
807
                        'alt' => get_string('flagged', 'question'), 'class' => 'questionflag icon-post']);
808
            }
809
            if ($attemptobj->can_navigate_to($slot)) {
810
                $row = [html_writer::link($attemptobj->attempt_url($slot),
811
                        $attemptobj->get_question_number($slot) . $flag),
812
                        $attemptobj->get_question_status($slot, $displayoptions->correctness)];
813
            } else {
814
                $row = [$attemptobj->get_question_number($slot) . $flag,
815
                        $attemptobj->get_question_status($slot, $displayoptions->correctness)];
816
            }
817
            if ($markscolumn) {
818
                $row[] = $attemptobj->get_question_mark($slot);
819
            }
820
            $table->data[] = $row;
821
            $table->rowclasses[] = 'quizsummary' . $slot . ' ' . $attemptobj->get_question_state_class(
822
                            $slot, $displayoptions->correctness);
823
        }
824
 
825
        // Print the summary table.
826
        return html_writer::table($table);
827
    }
828
 
829
    /**
830
     * Creates any controls the page should have.
831
     *
832
     * @param quiz_attempt $attemptobj
833
     */
834
    public function summary_page_controls($attemptobj) {
835
        $output = '';
836
 
837
        // Return to place button.
838
        if ($attemptobj->get_state() == quiz_attempt::IN_PROGRESS) {
839
            $button = new single_button(
840
                    new moodle_url($attemptobj->attempt_url(null, $attemptobj->get_currentpage())),
841
                    get_string('returnattempt', 'quiz'));
842
            $output .= $this->container($this->container($this->render($button),
843
                    'controls'), 'submitbtns mdl-align');
844
        }
845
 
846
        // Finish attempt button.
847
        $options = [
848
                'attempt' => $attemptobj->get_attemptid(),
849
                'finishattempt' => 1,
850
                'timeup' => 0,
851
                'slots' => '',
852
                'cmid' => $attemptobj->get_cmid(),
853
                'sesskey' => sesskey(),
854
        ];
855
 
856
        $button = new single_button(
857
                new moodle_url($attemptobj->processattempt_url(), $options),
858
                get_string('submitallandfinish', 'quiz'));
859
        $button->class = 'btn-finishattempt';
860
        $button->formid = 'frm-finishattempt';
861
        if ($attemptobj->get_state() == quiz_attempt::IN_PROGRESS) {
862
            $totalunanswered = 0;
863
            if ($attemptobj->get_quiz()->navmethod == 'free') {
864
                // Only count the unanswered question if the navigation method is set to free.
865
                $totalunanswered = $attemptobj->get_number_of_unanswered_questions();
866
            }
867
            $this->page->requires->js_call_amd('mod_quiz/submission_confirmation', 'init', [$totalunanswered]);
868
        }
869
        $button->type = \single_button::BUTTON_PRIMARY;
870
 
871
        $duedate = $attemptobj->get_due_date();
872
        $message = '';
873
        if ($attemptobj->get_state() == quiz_attempt::OVERDUE) {
874
            $message = get_string('overduemustbesubmittedby', 'quiz', userdate($duedate));
875
 
876
        } else {
877
            if ($duedate) {
878
                $message = get_string('mustbesubmittedby', 'quiz', userdate($duedate));
879
            }
880
        }
881
 
882
        $output .= $this->countdown_timer($attemptobj, time());
883
        $output .= $this->container($message . $this->container(
884
                        $this->render($button), 'controls'), 'submitbtns mdl-align');
885
 
886
        return $output;
887
    }
888
 
889
    /*
890
     * View Page
891
     */
892
    /**
893
     * Generates the view page
894
     *
895
     * @param stdClass $course the course settings row from the database.
896
     * @param stdClass $quiz the quiz settings row from the database.
897
     * @param stdClass $cm the course_module settings row from the database.
898
     * @param context_module $context the quiz context.
899
     * @param view_page $viewobj
900
     * @return string HTML to display
901
     */
902
    public function view_page($course, $quiz, $cm, $context, $viewobj) {
903
        $output = '';
904
 
905
        $output .= $this->view_page_tertiary_nav($viewobj);
906
        $output .= $this->view_information($quiz, $cm, $context, $viewobj->infomessages);
907
        $output .= $this->view_result_info($quiz, $context, $cm, $viewobj);
908
        $output .= $this->render($viewobj->attemptslist);
909
        $output .= $this->box($this->view_page_buttons($viewobj), 'quizattempt');
910
        return $output;
911
    }
912
 
913
    /**
914
     * Render the tertiary navigation for the view page.
915
     *
916
     * @param view_page $viewobj the information required to display the view page.
917
     * @return string HTML to output.
918
     */
919
    public function view_page_tertiary_nav(view_page $viewobj): string {
920
        $content = '';
921
 
922
        if ($viewobj->buttontext) {
923
            $attemptbtn = $this->start_attempt_button($viewobj->buttontext,
924
                    $viewobj->startattempturl, $viewobj->preflightcheckform,
925
                    $viewobj->popuprequired, $viewobj->popupoptions);
926
            $content .= $attemptbtn;
927
        }
928
 
929
        if ($viewobj->canedit && !$viewobj->quizhasquestions) {
930
            $content .= html_writer::link($viewobj->editurl, get_string('addquestion', 'quiz'),
931
                    ['class' => 'btn btn-secondary']);
932
        }
933
 
934
        if ($content) {
935
            return html_writer::div(html_writer::div($content, 'row'), 'container-fluid tertiary-navigation');
936
        } else {
937
            return '';
938
        }
939
    }
940
 
941
    /**
942
     * Work out, and render, whatever buttons, and surrounding info, should appear
943
     * at the end of the review page.
944
     *
945
     * @param view_page $viewobj the information required to display the view page.
946
     * @return string HTML to output.
947
     */
948
    public function view_page_buttons(view_page $viewobj) {
949
        $output = '';
950
 
951
        if (!$viewobj->quizhasquestions) {
952
            $output .= html_writer::div(
953
                    $this->notification(get_string('noquestions', 'quiz'), 'warning', false),
954
                    'text-left mb-3');
955
        }
956
        $output .= $this->access_messages($viewobj->preventmessages);
957
 
958
        if ($viewobj->showbacktocourse) {
959
            $output .= $this->single_button($viewobj->backtocourseurl,
960
                    get_string('backtocourse', 'quiz'), 'get',
961
                    ['class' => 'continuebutton']);
962
        }
963
 
964
        return $output;
965
    }
966
 
967
    /**
968
     * Generates the view attempt button
969
     *
970
     * @param string $buttontext the label to display on the button.
971
     * @param moodle_url $url The URL to POST to in order to start the attempt.
972
     * @param preflight_check_form|null $preflightcheckform deprecated.
973
     * @param bool $popuprequired whether the attempt needs to be opened in a pop-up.
974
     * @param array $popupoptions the options to use if we are opening a popup.
975
     * @return string HTML fragment.
976
     */
977
    public function start_attempt_button($buttontext, moodle_url $url,
978
            preflight_check_form $preflightcheckform = null,
979
            $popuprequired = false, $popupoptions = null) {
980
 
981
        $button = new single_button($url, $buttontext, 'post', single_button::BUTTON_PRIMARY);
982
        $button->class .= ' quizstartbuttondiv';
983
        if ($popuprequired) {
984
            $button->class .= ' quizsecuremoderequired';
985
        }
986
 
987
        $popupjsoptions = null;
988
        if ($popuprequired && $popupoptions) {
989
            $action = new popup_action('click', $url, 'popup', $popupoptions);
990
            $popupjsoptions = $action->get_js_options();
991
        }
992
 
993
        $this->page->requires->js_call_amd('mod_quiz/preflightcheck', 'init',
994
                ['.quizstartbuttondiv [type=submit]', get_string('startattempt', 'quiz'),
995
                        '#mod_quiz_preflight_form', $popupjsoptions]);
996
 
997
        return $this->render($button) . ($preflightcheckform ? $preflightcheckform->render() : '');
998
    }
999
 
1000
    /**
1001
     * Generate a message saying that this quiz has no questions, with a button to
1002
     * go to the edit page, if the user has the right capability.
1003
     *
1004
     * @param bool $canedit can the current user edit the quiz?
1005
     * @param moodle_url $editurl URL of the edit quiz page.
1006
     * @return string HTML to output.
1007
     *
1008
     * @deprecated since Moodle 4.0 MDL-71915 - please do not use this function any more.
1009
     */
1010
    public function no_questions_message($canedit, $editurl) {
1011
        debugging('no_questions_message() is deprecated, please use generate_no_questions_message() instead.', DEBUG_DEVELOPER);
1012
 
1013
        $output = html_writer::start_tag('div', ['class' => 'card text-center mb-3']);
1014
        $output .= html_writer::start_tag('div', ['class' => 'card-body']);
1015
 
1016
        $output .= $this->notification(get_string('noquestions', 'quiz'), 'warning', false);
1017
        if ($canedit) {
1018
            $output .= $this->single_button($editurl, get_string('editquiz', 'quiz'), 'get');
1019
        }
1020
        $output .= html_writer::end_tag('div');
1021
        $output .= html_writer::end_tag('div');
1022
 
1023
        return $output;
1024
    }
1025
 
1026
    /**
1027
     * Outputs an error message for any guests accessing the quiz
1028
     *
1029
     * @param stdClass $course the course settings row from the database.
1030
     * @param stdClass $quiz the quiz settings row from the database.
1031
     * @param stdClass $cm the course_module settings row from the database.
1032
     * @param context_module $context the quiz context.
1033
     * @param array $messages Array containing any messages
1034
     * @param view_page $viewobj
1035
     */
1036
    public function view_page_guest($course, $quiz, $cm, $context, $messages, $viewobj) {
1037
        $output = '';
1038
        $output .= $this->view_page_tertiary_nav($viewobj);
1039
        $output .= $this->view_information($quiz, $cm, $context, $messages);
1040
        $guestno = html_writer::tag('p', get_string('guestsno', 'quiz'));
1041
        $liketologin = html_writer::tag('p', get_string('liketologin'));
1042
        $referer = get_local_referer(false);
1043
        $output .= $this->confirm($guestno . "\n\n" . $liketologin . "\n", get_login_url(), $referer);
1044
        return $output;
1045
    }
1046
 
1047
    /**
1048
     * Outputs and error message for anyone who is not enrolled on the course.
1049
     *
1050
     * @param stdClass $course the course settings row from the database.
1051
     * @param stdClass $quiz the quiz settings row from the database.
1052
     * @param stdClass $cm the course_module settings row from the database.
1053
     * @param context_module $context the quiz context.
1054
     * @param array $messages Array containing any messages
1055
     * @param view_page $viewobj
1056
     */
1057
    public function view_page_notenrolled($course, $quiz, $cm, $context, $messages, $viewobj) {
1058
        global $CFG;
1059
        $output = '';
1060
        $output .= $this->view_page_tertiary_nav($viewobj);
1061
        $output .= $this->view_information($quiz, $cm, $context, $messages);
1062
        $youneedtoenrol = html_writer::tag('p', get_string('youneedtoenrol', 'quiz'));
1063
        $button = html_writer::tag('p',
1064
                $this->continue_button($CFG->wwwroot . '/course/view.php?id=' . $course->id));
1065
        $output .= $this->box($youneedtoenrol . "\n\n" . $button . "\n", 'generalbox', 'notice');
1066
        return $output;
1067
    }
1068
 
1069
    /**
1070
     * Output the page information
1071
     *
1072
     * @param stdClass $quiz the quiz settings.
1073
     * @param cm_info|stdClass $cm the course_module object.
1074
     * @param context $context the quiz context.
1075
     * @param array $messages any access messages that should be described.
1076
     * @param bool $quizhasquestions does quiz has questions added.
1077
     * @return string HTML to output.
1078
     */
1079
    public function view_information($quiz, $cm, $context, $messages, bool $quizhasquestions = false) {
1080
        $output = '';
1081
 
1082
        // Output any access messages.
1083
        if ($messages) {
1084
            $output .= $this->box($this->access_messages($messages), 'quizinfo');
1085
        }
1086
 
1087
        // Show number of attempts summary to those who can view reports.
1088
        if (has_capability('mod/quiz:viewreports', $context)) {
1089
            if ($strattemptnum = $this->quiz_attempt_summary_link_to_reports($quiz, $cm,
1090
                    $context)) {
1091
                $output .= html_writer::tag('div', $strattemptnum,
1092
                        ['class' => 'quizattemptcounts']);
1093
            }
1094
        }
1095
 
1096
        if (has_any_capability(['mod/quiz:manageoverrides', 'mod/quiz:viewoverrides'], $context)) {
1097
            if ($overrideinfo = $this->quiz_override_summary_links($quiz, $cm)) {
1098
                $output .= html_writer::tag('div', $overrideinfo, ['class' => 'quizattemptcounts']);
1099
            }
1100
        }
1101
 
1102
        return $output;
1103
    }
1104
 
1105
    /**
1106
     * Output the quiz intro.
1107
     *
1108
     * @param stdClass $quiz the quiz settings.
1109
     * @param stdClass $cm the course_module object.
1110
     * @return string HTML to output.
1111
     */
1112
    public function quiz_intro($quiz, $cm) {
1113
        if (html_is_blank($quiz->intro)) {
1114
            return '';
1115
        }
1116
 
1117
        return $this->box(format_module_intro('quiz', $quiz, $cm->id), 'generalbox', 'intro');
1118
    }
1119
 
1120
    /**
1121
     * Generates the table heading.
1122
     */
1123
    public function view_table_heading() {
1124
        return $this->heading(get_string('summaryofattempts', 'quiz'), 3);
1125
    }
1126
 
1127
    /**
1128
     * Generates the table of data
1129
     *
1130
     * @param stdClass $quiz the quiz settings.
1131
     * @param context_module $context the quiz context.
1132
     * @param view_page $viewobj
1133
     * @deprecated Since 4.4 please use the {@see list_of_attempts} renderable instead.
1134
     */
1135
    public function view_table($quiz, $context, $viewobj) {
1136
        debugging('view_table has been deprecated since 4.4 please use the list_of_attempts renderable instead.');
1137
        if (!$viewobj->attempts) {
1138
            return '';
1139
        }
1140
 
1141
        // Prepare table header.
1142
        $table = new html_table();
1143
        $table->attributes['class'] = 'generaltable quizattemptsummary';
1144
        $table->caption = get_string('summaryofattempts', 'quiz');
1145
        $table->captionhide = true;
1146
        $table->head = [];
1147
        $table->align = [];
1148
        $table->size = [];
1149
        if ($viewobj->attemptcolumn) {
1150
            $table->head[] = get_string('attemptnumber', 'quiz');
1151
            $table->align[] = 'center';
1152
            $table->size[] = '';
1153
        }
1154
        $table->head[] = get_string('attemptstate', 'quiz');
1155
        $table->align[] = 'left';
1156
        $table->size[] = '';
1157
        if ($viewobj->markcolumn) {
1158
            $table->head[] = get_string('marks', 'quiz') . ' / ' .
1159
                    quiz_format_grade($quiz, $quiz->sumgrades);
1160
            $table->align[] = 'center';
1161
            $table->size[] = '';
1162
        }
1163
        if ($viewobj->gradecolumn) {
1164
            $table->head[] = get_string('gradenoun') . ' / ' .
1165
                    quiz_format_grade($quiz, $quiz->grade);
1166
            $table->align[] = 'center';
1167
            $table->size[] = '';
1168
        }
1169
        if ($viewobj->canreviewmine) {
1170
            $table->head[] = get_string('review', 'quiz');
1171
            $table->align[] = 'center';
1172
            $table->size[] = '';
1173
        }
1174
        if ($viewobj->feedbackcolumn) {
1175
            $table->head[] = get_string('feedback', 'quiz');
1176
            $table->align[] = 'left';
1177
            $table->size[] = '';
1178
        }
1179
 
1180
        // One row for each attempt.
1181
        foreach ($viewobj->attemptobjs as $attemptobj) {
1182
            $attemptoptions = $attemptobj->get_display_options(true);
1183
            $row = [];
1184
 
1185
            // Add the attempt number.
1186
            if ($viewobj->attemptcolumn) {
1187
                if ($attemptobj->is_preview()) {
1188
                    $row[] = get_string('preview', 'quiz');
1189
                } else {
1190
                    $row[] = $attemptobj->get_attempt_number();
1191
                }
1192
            }
1193
 
1194
            $row[] = $this->attempt_state($attemptobj);
1195
 
1196
            if ($viewobj->markcolumn) {
1197
                if ($attemptoptions->marks >= question_display_options::MARK_AND_MAX &&
1198
                        $attemptobj->is_finished()) {
1199
                    $row[] = quiz_format_grade($quiz, $attemptobj->get_sum_marks());
1200
                } else {
1201
                    $row[] = '';
1202
                }
1203
            }
1204
 
1205
            // Outside the if because we may be showing feedback but not grades.
1206
            $attemptgrade = quiz_rescale_grade($attemptobj->get_sum_marks(), $quiz, false);
1207
 
1208
            if ($viewobj->gradecolumn) {
1209
                if ($attemptoptions->marks >= question_display_options::MARK_AND_MAX &&
1210
                        $attemptobj->is_finished()) {
1211
 
1212
                    // Highlight the highest grade if appropriate.
1213
                    if ($viewobj->overallstats && !$attemptobj->is_preview()
1214
                            && $viewobj->numattempts > 1 && !is_null($viewobj->mygrade)
1215
                            && $attemptobj->get_state() == quiz_attempt::FINISHED
1216
                            && $attemptgrade == $viewobj->mygrade
1217
                            && $quiz->grademethod == QUIZ_GRADEHIGHEST) {
1218
                        $table->rowclasses[$attemptobj->get_attempt_number()] = 'bestrow';
1219
                    }
1220
 
1221
                    $row[] = quiz_format_grade($quiz, $attemptgrade);
1222
                } else {
1223
                    $row[] = '';
1224
                }
1225
            }
1226
 
1227
            if ($viewobj->canreviewmine) {
1228
                $row[] = $viewobj->accessmanager->make_review_link($attemptobj->get_attempt(),
1229
                        $attemptoptions, $this);
1230
            }
1231
 
1232
            if ($viewobj->feedbackcolumn && $attemptobj->is_finished()) {
1233
                if ($attemptoptions->overallfeedback) {
1234
                    $row[] = quiz_feedback_for_grade($attemptgrade, $quiz, $context);
1235
                } else {
1236
                    $row[] = '';
1237
                }
1238
            }
1239
 
1240
            if ($attemptobj->is_preview()) {
1241
                $table->data['preview'] = $row;
1242
            } else {
1243
                $table->data[$attemptobj->get_attempt_number()] = $row;
1244
            }
1245
        } // End of loop over attempts.
1246
 
1247
        $output = '';
1248
        $output .= $this->view_table_heading();
1249
        $output .= html_writer::table($table);
1250
        return $output;
1251
    }
1252
 
1253
    /**
1254
     * Generate a brief textual description of the current state of an attempt.
1255
     *
1256
     * @param quiz_attempt $attemptobj the attempt
1257
     * @return string the appropriate lang string to describe the state.
1258
     */
1259
    public function attempt_state($attemptobj) {
1260
        switch ($attemptobj->get_state()) {
1261
            case quiz_attempt::IN_PROGRESS:
1262
                return get_string('stateinprogress', 'quiz');
1263
 
1264
            case quiz_attempt::OVERDUE:
1265
                return get_string('stateoverdue', 'quiz') . html_writer::tag('span',
1266
                                get_string('stateoverduedetails', 'quiz',
1267
                                        userdate($attemptobj->get_due_date())),
1268
                                ['class' => 'statedetails']);
1269
 
1270
            case quiz_attempt::FINISHED:
1271
                return get_string('statefinished', 'quiz') . html_writer::tag('span',
1272
                                get_string('statefinisheddetails', 'quiz',
1273
                                        userdate($attemptobj->get_submitted_date())),
1274
                                ['class' => 'statedetails']);
1275
 
1276
            case quiz_attempt::ABANDONED:
1277
                return get_string('stateabandoned', 'quiz');
1278
 
1279
            default:
1280
                throw new coding_exception('Unexpected attempt state');
1281
        }
1282
    }
1283
 
1284
    /**
1285
     * Generates data pertaining to quiz results
1286
     *
1287
     * @param stdClass $quiz Array containing quiz data
1288
     * @param context_module $context The quiz context.
1289
     * @param stdClass|cm_info $cm The course module information.
1290
     * @param view_page $viewobj
1291
     * @return string HTML to display.
1292
     */
1293
    public function view_result_info($quiz, $context, $cm, $viewobj) {
1294
        $output = '';
1295
        if (!$viewobj->numattempts && !$viewobj->gradecolumn && is_null($viewobj->mygrade)) {
1296
            return $output;
1297
        }
1298
        $resultinfo = '';
1299
 
1300
        if ($viewobj->overallstats) {
1301
            if ($viewobj->moreattempts) {
1302
                $a = new stdClass();
1303
                $a->method = quiz_get_grading_option_name($quiz->grademethod);
1304
                $a->mygrade = quiz_format_grade($quiz, $viewobj->mygrade);
1305
                $a->quizgrade = quiz_format_grade($quiz, $quiz->grade);
1306
                $resultinfo .= $this->heading(get_string('gradesofar', 'quiz', $a), 3);
1307
            } else {
1308
                $a = new stdClass();
1309
                $a->grade = quiz_format_grade($quiz, $viewobj->mygrade);
1310
                $a->maxgrade = quiz_format_grade($quiz, $quiz->grade);
1311
                $a = get_string('outofshort', 'quiz', $a);
1312
                $resultinfo .= $this->heading(get_string('yourfinalgradeis', 'quiz', $a), 3);
1313
            }
1314
        }
1315
 
1316
        if ($viewobj->mygradeoverridden) {
1317
 
1318
            $resultinfo .= html_writer::tag('p', get_string('overriddennotice', 'grades'),
1319
                            ['class' => 'overriddennotice']) . "\n";
1320
        }
1321
        if ($viewobj->gradebookfeedback) {
1322
            $resultinfo .= $this->heading(get_string('comment', 'quiz'), 3);
1323
            $resultinfo .= html_writer::div($viewobj->gradebookfeedback, 'quizteacherfeedback') . "\n";
1324
        }
1325
        if ($viewobj->feedbackcolumn) {
1326
            $resultinfo .= $this->heading(get_string('overallfeedback', 'quiz'), 3);
1327
            $resultinfo .= html_writer::div(
1328
                            quiz_feedback_for_grade($viewobj->mygrade, $quiz, $context),
1329
                            'quizgradefeedback') . "\n";
1330
        }
1331
 
1332
        if ($resultinfo) {
1333
            $output .= $this->box($resultinfo, 'generalbox', 'feedback');
1334
        }
1335
        return $output;
1336
    }
1337
 
1338
    /**
1339
     * Output either a link to the review page for an attempt, or a button to
1340
     * open the review in a popup window.
1341
     *
1342
     * @param moodle_url $url of the target page.
1343
     * @param bool $reviewinpopup whether a pop-up is required.
1344
     * @param array $popupoptions options to pass to the popup_action constructor.
1345
     * @return string HTML to output.
1346
     */
1347
    public function review_link($url, $reviewinpopup, $popupoptions) {
1348
        if ($reviewinpopup) {
1349
            $button = new single_button($url, get_string('review', 'quiz'));
1350
            $button->add_action(new popup_action('click', $url, 'quizpopup', $popupoptions));
1351
            return $this->render($button);
1352
 
1353
        } else {
1354
            return html_writer::link($url, get_string('review', 'quiz'),
1355
                    ['title' => get_string('reviewthisattempt', 'quiz')]);
1356
        }
1357
    }
1358
 
1359
    /**
1360
     * Displayed where there might normally be a review link, to explain why the
1361
     * review is not available at this time.
1362
     *
1363
     * @param string $message optional message explaining why the review is not possible.
1364
     * @return string HTML to output.
1365
     */
1366
    public function no_review_message($message) {
1367
        return html_writer::nonempty_tag('span', $message,
1368
                ['class' => 'noreviewmessage']);
1369
    }
1370
 
1371
    /**
1372
     * Returns the same as {@see quiz_num_attempt_summary()} but wrapped in a link to the quiz reports.
1373
     *
1374
     * @param stdClass $quiz the quiz object. Only $quiz->id is used at the moment.
1375
     * @param stdClass $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid
1376
     * fields are used at the moment.
1377
     * @param context $context the quiz context.
1378
     * @param bool $returnzero if false (default), when no attempts have been made '' is returned
1379
     *      instead of 'Attempts: 0'.
1380
     * @param int $currentgroup if there is a concept of current group where this method is being
1381
     *      called (e.g. a report) pass it in here. Default 0 which means no current group.
1382
     * @return string HTML fragment for the link.
1383
     */
1384
    public function quiz_attempt_summary_link_to_reports($quiz, $cm, $context,
1385
            $returnzero = false, $currentgroup = 0) {
1386
        global $CFG;
1387
        $summary = quiz_num_attempt_summary($quiz, $cm, $returnzero, $currentgroup);
1388
        if (!$summary) {
1389
            return '';
1390
        }
1391
 
1392
        require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
1393
        $url = new moodle_url('/mod/quiz/report.php', [
1394
                'id' => $cm->id, 'mode' => quiz_report_default_report($context)]);
1395
        return html_writer::link($url, $summary);
1396
    }
1397
 
1398
    /**
1399
     * Render a summary of the number of group and user overrides, with corresponding links.
1400
     *
1401
     * @param stdClass $quiz the quiz settings.
1402
     * @param cm_info|stdClass $cm the cm object.
1403
     * @param int $currentgroup currently selected group, if there is one.
1404
     * @return string HTML fragment for the link.
1405
     */
1406
    public function quiz_override_summary_links(stdClass $quiz, cm_info|stdClass $cm, $currentgroup = 0): string {
1407
 
1408
        $baseurl = new moodle_url('/mod/quiz/overrides.php', ['cmid' => $cm->id]);
1409
        $counts = quiz_override_summary($quiz, $cm, $currentgroup);
1410
 
1411
        $links = [];
1412
        if ($counts['group']) {
1413
            $links[] = html_writer::link(new moodle_url($baseurl, ['mode' => 'group']),
1414
                    get_string('overridessummarygroup', 'quiz', $counts['group']));
1415
        }
1416
        if ($counts['user']) {
1417
            $links[] = html_writer::link(new moodle_url($baseurl, ['mode' => 'user']),
1418
                    get_string('overridessummaryuser', 'quiz', $counts['user']));
1419
        }
1420
 
1421
        if (!$links) {
1422
            return '';
1423
        }
1424
 
1425
        $links = implode(', ', $links);
1426
        switch ($counts['mode']) {
1427
            case 'onegroup':
1428
                return get_string('overridessummarythisgroup', 'quiz', $links);
1429
 
1430
            case 'somegroups':
1431
                return get_string('overridessummaryyourgroups', 'quiz', $links);
1432
 
1433
            case 'allgroups':
1434
                return get_string('overridessummary', 'quiz', $links);
1435
 
1436
            default:
1437
                throw new coding_exception('Unexpected mode ' . $counts['mode']);
1438
        }
1439
    }
1440
 
1441
    /**
1442
     * Outputs a chart.
1443
     *
1444
     * @param \core\chart_base $chart The chart.
1445
     * @param string $title The title to display above the graph.
1446
     * @param array $attrs extra container html attributes.
1447
     * @return string HTML of the graph.
1448
     */
1449
    public function chart(\core\chart_base $chart, $title, $attrs = []) {
1450
        return $this->heading($title, 3) . html_writer::tag('div',
1451
                        $this->render($chart), array_merge(['class' => 'graph'], $attrs));
1452
    }
1453
 
1454
    /**
1455
     * Output a graph, or a message saying that GD is required.
1456
     *
1457
     * @param moodle_url $url the URL of the graph.
1458
     * @param string $title the title to display above the graph.
1459
     * @return string HTML of the graph.
1460
     */
1461
    public function graph(moodle_url $url, $title) {
1462
        $graph = html_writer::empty_tag('img', ['src' => $url, 'alt' => $title]);
1463
 
1464
        return $this->heading($title, 3) . html_writer::tag('div', $graph, ['class' => 'graph']);
1465
    }
1466
 
1467
    /**
1468
     * Output the connection warning messages, which are initially hidden, and
1469
     * only revealed by JavaScript if necessary.
1470
     */
1471
    public function connection_warning() {
1472
        $options = ['filter' => false, 'newlines' => false];
1473
        $warning = format_text(get_string('connectionerror', 'quiz'), FORMAT_MARKDOWN, $options);
1474
        $ok = format_text(get_string('connectionok', 'quiz'), FORMAT_MARKDOWN, $options);
1475
        return html_writer::tag('div', $warning,
1476
                        ['id' => 'connection-error', 'style' => 'display: none;', 'role' => 'alert']) .
1477
                html_writer::tag('div', $ok, ['id' => 'connection-ok', 'style' => 'display: none;', 'role' => 'alert']);
1478
    }
1479
 
1480
    /**
1481
     * Deprecated version of render_links_to_other_attempts.
1482
     *
1483
     * @param links_to_other_attempts $links
1484
     * @return string HTML fragment.
1485
     * @deprecated since Moodle 4.2. Please use render_links_to_other_attempts instead.
1486
     * @todo MDL-76612 Final deprecation in Moodle 4.6
1487
     */
1488
    protected function render_mod_quiz_links_to_other_attempts(links_to_other_attempts $links) {
1489
        return $this->render_links_to_other_attempts($links);
1490
    }
1491
 
1492
    /**
1493
     * Deprecated version of render_navigation_question_button.
1494
     *
1495
     * @param navigation_question_button $button
1496
     * @return string HTML fragment.
1497
     * @deprecated since Moodle 4.2. Please use render_links_to_other_attempts instead.
1498
     * @todo MDL-76612 Final deprecation in Moodle 4.6
1499
     */
1500
    protected function render_quiz_nav_question_button(navigation_question_button $button) {
1501
        return $this->render_navigation_question_button($button);
1502
    }
1503
 
1504
    /**
1505
     * Deprecated version of render_navigation_section_heading.
1506
     *
1507
     * @param navigation_section_heading $heading the heading.
1508
     * @return string HTML fragment.
1509
     * @deprecated since Moodle 4.2. Please use render_links_to_other_attempts instead.
1510
     * @todo MDL-76612 Final deprecation in Moodle 4.6
1511
     */
1512
    protected function render_quiz_nav_section_heading(navigation_section_heading $heading) {
1513
        return $this->render_navigation_section_heading($heading);
1514
    }
1515
}