Proyectos de Subversion Moodle

Rev

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