MDL-42181 quiz nav: highight all buttons when all Qs on one page
[moodle.git] / mod / quiz / renderer.php
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/>.
17 /**
18  * Defines the renderer for the quiz module.
19  *
20  * @package    mod
21  * @subpackage quiz
22  * @copyright  2011 The Open University
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
27 defined('MOODLE_INTERNAL') || die();
30 /**
31  * The renderer for the quiz module.
32  *
33  * @copyright  2011 The Open University
34  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35  */
36 class mod_quiz_renderer extends plugin_renderer_base {
37     /**
38      * Builds the review page
39      *
40      * @param quiz_attempt $attemptobj an instance of quiz_attempt.
41      * @param array $slots an array of intgers relating to questions.
42      * @param int $page the current page number
43      * @param bool $showall whether to show entire attempt on one page.
44      * @param bool $lastpage if true the current page is the last page.
45      * @param mod_quiz_display_options $displayoptions instance of mod_quiz_display_options.
46      * @param array $summarydata contains all table data
47      * @return $output containing html data.
48      */
49     public function review_page(quiz_attempt $attemptobj, $slots, $page, $showall,
50                                 $lastpage, mod_quiz_display_options $displayoptions,
51                                 $summarydata) {
53         $output = '';
54         $output .= $this->header();
55         $output .= $this->review_summary_table($summarydata, $page);
56         $output .= $this->review_form($page, $showall, $displayoptions,
57                 $this->questions($attemptobj, true, $slots, $page, $showall, $displayoptions),
58                 $attemptobj);
60         $output .= $this->review_next_navigation($attemptobj, $page, $lastpage);
61         $output .= $this->footer();
62         return $output;
63     }
65     /**
66      * Renders the review question pop-up.
67      *
68      * @param quiz_attempt $attemptobj an instance of quiz_attempt.
69      * @param int $slot which question to display.
70      * @param int $seq which step of the question attempt to show. null = latest.
71      * @param mod_quiz_display_options $displayoptions instance of mod_quiz_display_options.
72      * @param array $summarydata contains all table data
73      * @return $output containing html data.
74      */
75     public function review_question_page(quiz_attempt $attemptobj, $slot, $seq,
76             mod_quiz_display_options $displayoptions, $summarydata) {
78         $output = '';
79         $output .= $this->header();
80         $output .= $this->review_summary_table($summarydata, 0);
82         if (!is_null($seq)) {
83             $output .= $attemptobj->render_question_at_step($slot, $seq, true);
84         } else {
85             $output .= $attemptobj->render_question($slot, true);
86         }
88         $output .= $this->close_window_button();
89         $output .= $this->footer();
90         return $output;
91     }
93     /**
94      * Renders the review question pop-up.
95      *
96      * @param string $message Why the review is not allowed.
97      * @return string html to output.
98      */
99     public function review_question_not_allowed($message) {
100         $output = '';
101         $output .= $this->header();
102         $output .= $this->heading(format_string($attemptobj->get_quiz_name(), true,
103                                   array("context" => $attemptobj->get_quizobj()->get_context())));
104         $output .= $this->notification($message);
105         $output .= $this->close_window_button();
106         $output .= $this->footer();
107         return $output;
108     }
110     /**
111      * Filters the summarydata array.
112      *
113      * @param array $summarydata contains row data for table
114      * @param int $page the current page number
115      * @return $summarydata containing filtered row data
116      */
117     protected function filter_review_summary_table($summarydata, $page) {
118         if ($page == 0) {
119             return $summarydata;
120         }
122         // Only show some of summary table on subsequent pages.
123         foreach ($summarydata as $key => $rowdata) {
124             if (!in_array($key, array('user', 'attemptlist'))) {
125                 unset($summarydata[$key]);
126             }
127         }
129         return $summarydata;
130     }
132     /**
133      * Outputs the table containing data from summary data array
134      *
135      * @param array $summarydata contains row data for table
136      * @param int $page contains the current page number
137      */
138     public function review_summary_table($summarydata, $page) {
139         $summarydata = $this->filter_review_summary_table($summarydata, $page);
140         if (empty($summarydata)) {
141             return '';
142         }
144         $output = '';
145         $output .= html_writer::start_tag('table', array(
146                 'class' => 'generaltable generalbox quizreviewsummary'));
147         $output .= html_writer::start_tag('tbody');
148         foreach ($summarydata as $rowdata) {
149             if ($rowdata['title'] instanceof renderable) {
150                 $title = $this->render($rowdata['title']);
151             } else {
152                 $title = $rowdata['title'];
153             }
155             if ($rowdata['content'] instanceof renderable) {
156                 $content = $this->render($rowdata['content']);
157             } else {
158                 $content = $rowdata['content'];
159             }
161             $output .= html_writer::tag('tr',
162                 html_writer::tag('th', $title, array('class' => 'cell', 'scope' => 'row')) .
163                         html_writer::tag('td', $content, array('class' => 'cell'))
164             );
165         }
167         $output .= html_writer::end_tag('tbody');
168         $output .= html_writer::end_tag('table');
169         return $output;
170     }
172     /**
173      * Renders each question
174      *
175      * @param quiz_attempt $attemptobj instance of quiz_attempt
176      * @param bool $reviewing
177      * @param array $slots array of intgers relating to questions
178      * @param int $page current page number
179      * @param bool $showall if true shows attempt on single page
180      * @param mod_quiz_display_options $displayoptions instance of mod_quiz_display_options
181      */
182     public function questions(quiz_attempt $attemptobj, $reviewing, $slots, $page, $showall,
183                               mod_quiz_display_options $displayoptions) {
184         $output = '';
185         foreach ($slots as $slot) {
186             $output .= $attemptobj->render_question($slot, $reviewing,
187                     $attemptobj->review_url($slot, $page, $showall));
188         }
189         return $output;
190     }
192     /**
193      * Renders the main bit of the review page.
194      *
195      * @param array $summarydata contain row data for table
196      * @param int $page current page number
197      * @param mod_quiz_display_options $displayoptions instance of mod_quiz_display_options
198      * @param $content contains each question
199      * @param quiz_attempt $attemptobj instance of quiz_attempt
200      * @param bool $showall if true display attempt on one page
201      */
202     public function review_form($page, $showall, $displayoptions, $content, $attemptobj) {
203         if ($displayoptions->flags != question_display_options::EDITABLE) {
204             return $content;
205         }
207         $this->page->requires->js_init_call('M.mod_quiz.init_review_form', null, false,
208                 quiz_get_js_module());
210         $output = '';
211         $output .= html_writer::start_tag('form', array('action' => $attemptobj->review_url(null,
212                 $page, $showall), 'method' => 'post', 'class' => 'questionflagsaveform'));
213         $output .= html_writer::start_tag('div');
214         $output .= $content;
215         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey',
216                 'value' => sesskey()));
217         $output .= html_writer::start_tag('div', array('class' => 'submitbtns'));
218         $output .= html_writer::empty_tag('input', array('type' => 'submit',
219                 'class' => 'questionflagsavebutton', 'name' => 'savingflags',
220                 'value' => get_string('saveflags', 'question')));
221         $output .= html_writer::end_tag('div');
222         $output .= html_writer::end_tag('div');
223         $output .= html_writer::end_tag('form');
225         return $output;
226     }
228     /**
229      * Returns either a liink or button
230      *
231      * @param quiz_attempt $attemptobj instance of quiz_attempt
232      */
233     public function finish_review_link(quiz_attempt $attemptobj) {
234         $url = $attemptobj->view_url();
236         if ($attemptobj->get_access_manager(time())->attempt_must_be_in_popup()) {
237             $this->page->requires->js_init_call('M.mod_quiz.secure_window.init_close_button',
238                     array($url), quiz_get_js_module());
239             return html_writer::empty_tag('input', array('type' => 'button',
240                     'value' => get_string('finishreview', 'quiz'),
241                     'id' => 'secureclosebutton'));
243         } else {
244             return html_writer::link($url, get_string('finishreview', 'quiz'));
245         }
246     }
248     /**
249      * Creates a next page arrow or the finishing link
250      *
251      * @param quiz_attempt $attemptobj instance of quiz_attempt
252      * @param int $page the current page
253      * @param bool $lastpage if true current page is the last page
254      */
255     public function review_next_navigation(quiz_attempt $attemptobj, $page, $lastpage) {
256         if ($lastpage) {
257             $nav = $this->finish_review_link($attemptobj);
258         } else {
259             $nav = link_arrow_right(get_string('next'), $attemptobj->review_url(null, $page + 1));
260         }
261         return html_writer::tag('div', $nav, array('class' => 'submitbtns'));
262     }
264     /**
265      * Return the HTML of the quiz timer.
266      * @return string HTML content.
267      */
268     public function countdown_timer(quiz_attempt $attemptobj, $timenow) {
270         $timeleft = $attemptobj->get_time_left_display($timenow);
271         if ($timeleft !== false) {
272             $ispreview = $attemptobj->is_preview();
273             $timerstartvalue = $timeleft;
274             if (!$ispreview) {
275                 // Make sure the timer starts just above zero. If $timeleft was <= 0, then
276                 // this will just have the effect of causing the quiz to be submitted immediately.
277                 $timerstartvalue = max($timerstartvalue, 1);
278             }
279             $this->initialise_timer($timerstartvalue, $ispreview);
280         }
282         return html_writer::tag('div', get_string('timeleft', 'quiz') . ' ' .
283                 html_writer::tag('span', '', array('id' => 'quiz-time-left')),
284                 array('id' => 'quiz-timer', 'role' => 'timer',
285                     'aria-atomic' => 'true', 'aria-relevant' => 'text'));
286     }
288     /**
289      * Create a preview link
290      *
291      * @param $url contains a url to the given page
292      */
293     public function restart_preview_button($url) {
294         return $this->single_button($url, get_string('startnewpreview', 'quiz'));
295     }
297     /**
298      * Outputs the navigation block panel
299      *
300      * @param quiz_nav_panel_base $panel instance of quiz_nav_panel_base
301      */
302     public function navigation_panel(quiz_nav_panel_base $panel) {
304         $output = '';
305         $userpicture = $panel->user_picture();
306         if ($userpicture) {
307             $fullname = fullname($userpicture->user);
308             if ($userpicture->size === true) {
309                 $fullname = html_writer::div($fullname);
310             }
311             $output .= html_writer::tag('div', $this->render($userpicture) . $fullname,
312                     array('id' => 'user-picture', 'class' => 'clearfix'));
313         }
314         $output .= $panel->render_before_button_bits($this);
316         $bcc = $panel->get_button_container_class();
317         $output .= html_writer::start_tag('div', array('class' => "qn_buttons $bcc"));
318         foreach ($panel->get_question_buttons() as $button) {
319             $output .= $this->render($button);
320         }
321         $output .= html_writer::end_tag('div');
323         $output .= html_writer::tag('div', $panel->render_end_bits($this),
324                 array('class' => 'othernav'));
326         $this->page->requires->js_init_call('M.mod_quiz.nav.init', null, false,
327                 quiz_get_js_module());
329         return $output;
330     }
332     /**
333      * Returns the quizzes navigation button
334      *
335      * @param quiz_nav_question_button $button
336      */
337     protected function render_quiz_nav_question_button(quiz_nav_question_button $button) {
338         $classes = array('qnbutton', $button->stateclass, $button->navmethod);
339         $attributes = array();
341         if ($button->currentpage) {
342             $classes[] = 'thispage';
343             $attributes[] = get_string('onthispage', 'quiz');
344         }
346         // Flagged?
347         if ($button->flagged) {
348             $classes[] = 'flagged';
349             $flaglabel = get_string('flagged', 'question');
350         } else {
351             $flaglabel = '';
352         }
353         $attributes[] = html_writer::tag('span', $flaglabel, array('class' => 'flagstate'));
355         if (is_numeric($button->number)) {
356             $qnostring = 'questionnonav';
357         } else {
358             $qnostring = 'questionnonavinfo';
359         }
361         $a = new stdClass();
362         $a->number = $button->number;
363         $a->attributes = implode(' ', $attributes);
364         $tagcontents = html_writer::tag('span', '', array('class' => 'thispageholder')) .
365                         html_writer::tag('span', '', array('class' => 'trafficlight')) .
366                         get_string($qnostring, 'quiz', $a);
367         $tagattributes = array('class' => implode(' ', $classes), 'id' => $button->id,
368                                   'title' => $button->statestring);
370         if ($button->url) {
371             return html_writer::link($button->url, $tagcontents, $tagattributes);
372         } else {
373             return html_writer::tag('span', $tagcontents, $tagattributes);
374         }
375     }
377     /**
378      * outputs the link the other attempts.
379      *
380      * @param mod_quiz_links_to_other_attempts $links
381      */
382     protected function render_mod_quiz_links_to_other_attempts(
383             mod_quiz_links_to_other_attempts $links) {
384         $attemptlinks = array();
385         foreach ($links->links as $attempt => $url) {
386             if ($url) {
387                 $attemptlinks[] = html_writer::link($url, $attempt);
388             } else {
389                 $attemptlinks[] = html_writer::tag('strong', $attempt);
390             }
391         }
392         return implode(', ', $attemptlinks);
393     }
395     public function start_attempt_page(quiz $quizobj, mod_quiz_preflight_check_form $mform) {
396         $output = '';
397         $output .= $this->header();
398         $output .= $this->heading(format_string($quizobj->get_quiz_name(), true,
399                                   array("context" => $quizobj->get_context())));
400         $output .= $this->quiz_intro($quizobj->get_quiz(), $quizobj->get_cm());
401         ob_start();
402         $mform->display();
403         $output .= ob_get_clean();
404         $output .= $this->footer();
405         return $output;
406     }
408     /**
409      * Attempt Page
410      *
411      * @param quiz_attempt $attemptobj Instance of quiz_attempt
412      * @param int $page Current page number
413      * @param quiz_access_manager $accessmanager Instance of quiz_access_manager
414      * @param array $messages An array of messages
415      * @param array $slots Contains an array of integers that relate to questions
416      * @param int $id The ID of an attempt
417      * @param int $nextpage The number of the next page
418      */
419     public function attempt_page($attemptobj, $page, $accessmanager, $messages, $slots, $id,
420             $nextpage) {
421         $output = '';
422         $output .= $this->header();
423         $output .= $this->quiz_notices($messages);
424         $output .= $this->attempt_form($attemptobj, $page, $slots, $id, $nextpage);
425         $output .= $this->footer();
426         return $output;
427     }
429     /**
430      * Returns any notices.
431      *
432      * @param array $messages
433      */
434     public function quiz_notices($messages) {
435         if (!$messages) {
436             return '';
437         }
438         return $this->box($this->heading(get_string('accessnoticesheader', 'quiz'), 3) .
439                 $this->access_messages($messages), 'quizaccessnotices');
440     }
442     /**
443      * Ouputs the form for making an attempt
444      *
445      * @param quiz_attempt $attemptobj
446      * @param int $page Current page number
447      * @param array $slots Array of integers relating to questions
448      * @param int $id ID of the attempt
449      * @param int $nextpage Next page number
450      */
451     public function attempt_form($attemptobj, $page, $slots, $id, $nextpage) {
452         $output = '';
454         // Start the form.
455         $output .= html_writer::start_tag('form',
456                 array('action' => $attemptobj->processattempt_url(), 'method' => 'post',
457                 'enctype' => 'multipart/form-data', 'accept-charset' => 'utf-8',
458                 'id' => 'responseform'));
459         $output .= html_writer::start_tag('div');
461         // Print all the questions.
462         foreach ($slots as $slot) {
463             $output .= $attemptobj->render_question($slot, false,
464                     $attemptobj->attempt_url($slot, $page));
465         }
467         $output .= html_writer::start_tag('div', array('class' => 'submitbtns'));
468         $output .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'next',
469                 'value' => get_string('next')));
470         $output .= html_writer::end_tag('div');
472         // Some hidden fields to trach what is going on.
473         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'attempt',
474                 'value' => $attemptobj->get_attemptid()));
475         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'thispage',
476                 'value' => $page, 'id' => 'followingpage'));
477         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'nextpage',
478                 'value' => $nextpage));
479         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'timeup',
480                 'value' => '0', 'id' => 'timeup'));
481         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey',
482                 'value' => sesskey()));
483         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'scrollpos',
484                 'value' => '', 'id' => 'scrollpos'));
486         // Add a hidden field with questionids. Do this at the end of the form, so
487         // if you navigate before the form has finished loading, it does not wipe all
488         // the student's answers.
489         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'slots',
490                 'value' => implode(',', $slots)));
492         // Finish the form.
493         $output .= html_writer::end_tag('div');
494         $output .= html_writer::end_tag('form');
496         return $output;
497     }
499     /**
500      * Output the JavaScript required to initialise the countdown timer.
501      * @param int $timerstartvalue time remaining, in seconds.
502      */
503     public function initialise_timer($timerstartvalue, $ispreview) {
504         $options = array($timerstartvalue, (bool)$ispreview);
505         $this->page->requires->js_init_call('M.mod_quiz.timer.init', $options, false, quiz_get_js_module());
506     }
508     /**
509      * Output a page with an optional message, and JavaScript code to close the
510      * current window and redirect the parent window to a new URL.
511      * @param moodle_url $url the URL to redirect the parent window to.
512      * @param string $message message to display before closing the window. (optional)
513      * @return string HTML to output.
514      */
515     public function close_attempt_popup($url, $message = '') {
516         $output = '';
517         $output .= $this->header();
518         $output .= $this->box_start();
520         if ($message) {
521             $output .= html_writer::tag('p', $message);
522             $output .= html_writer::tag('p', get_string('windowclosing', 'quiz'));
523             $delay = 5;
524         } else {
525             $output .= html_writer::tag('p', get_string('pleaseclose', 'quiz'));
526             $delay = 0;
527         }
528         $this->page->requires->js_init_call('M.mod_quiz.secure_window.close',
529                 array($url, $delay), false, quiz_get_js_module());
531         $output .= $this->box_end();
532         $output .= $this->footer();
533         return $output;
534     }
536     /**
537      * Print each message in an array, surrounded by &lt;p>, &lt;/p> tags.
538      *
539      * @param array $messages the array of message strings.
540      * @param bool $return if true, return a string, instead of outputting.
541      *
542      * @return string HTML to output.
543      */
544     public function access_messages($messages) {
545         $output = '';
546         foreach ($messages as $message) {
547             $output .= html_writer::tag('p', $message) . "\n";
548         }
549         return $output;
550     }
552     /*
553      * Summary Page
554      */
555     /**
556      * Create the summary page
557      *
558      * @param quiz_attempt $attemptobj
559      * @param mod_quiz_display_options $displayoptions
560      */
561     public function summary_page($attemptobj, $displayoptions) {
562         $output = '';
563         $output .= $this->header();
564         $output .= $this->heading(format_string($attemptobj->get_quiz_name()));
565         $output .= $this->heading(get_string('summaryofattempt', 'quiz'), 3);
566         $output .= $this->summary_table($attemptobj, $displayoptions);
567         $output .= $this->summary_page_controls($attemptobj);
568         $output .= $this->footer();
569         return $output;
570     }
572     /**
573      * Generates the table of summarydata
574      *
575      * @param quiz_attempt $attemptobj
576      * @param mod_quiz_display_options $displayoptions
577      */
578     public function summary_table($attemptobj, $displayoptions) {
579         // Prepare the summary table header.
580         $table = new html_table();
581         $table->attributes['class'] = 'generaltable quizsummaryofattempt boxaligncenter';
582         $table->head = array(get_string('question', 'quiz'), get_string('status', 'quiz'));
583         $table->align = array('left', 'left');
584         $table->size = array('', '');
585         $markscolumn = $displayoptions->marks >= question_display_options::MARK_AND_MAX;
586         if ($markscolumn) {
587             $table->head[] = get_string('marks', 'quiz');
588             $table->align[] = 'left';
589             $table->size[] = '';
590         }
591         $table->data = array();
593         // Get the summary info for each question.
594         $slots = $attemptobj->get_slots();
595         foreach ($slots as $slot) {
596             if (!$attemptobj->is_real_question($slot)) {
597                 continue;
598             }
599             $flag = '';
600             if ($attemptobj->is_question_flagged($slot)) {
601                 $flag = html_writer::empty_tag('img', array('src' => $this->pix_url('i/flagged'),
602                         'alt' => get_string('flagged', 'question'), 'class' => 'questionflag icon-post'));
603             }
604             if ($attemptobj->can_navigate_to($slot)) {
605                 $row = array(html_writer::link($attemptobj->attempt_url($slot),
606                         $attemptobj->get_question_number($slot) . $flag),
607                         $attemptobj->get_question_status($slot, $displayoptions->correctness));
608             } else {
609                 $row = array($attemptobj->get_question_number($slot) . $flag,
610                                 $attemptobj->get_question_status($slot, $displayoptions->correctness));
611             }
612             if ($markscolumn) {
613                 $row[] = $attemptobj->get_question_mark($slot);
614             }
615             $table->data[] = $row;
616             $table->rowclasses[] = $attemptobj->get_question_state_class(
617                     $slot, $displayoptions->correctness);
618         }
620         // Print the summary table.
621         $output = html_writer::table($table);
623         return $output;
624     }
626     /**
627      * Creates any controls a the page should have.
628      *
629      * @param quiz_attempt $attemptobj
630      */
631     public function summary_page_controls($attemptobj) {
632         $output = '';
634         // Return to place button.
635         if ($attemptobj->get_state() == quiz_attempt::IN_PROGRESS) {
636             $button = new single_button(
637                     new moodle_url($attemptobj->attempt_url(null, $attemptobj->get_currentpage())),
638                     get_string('returnattempt', 'quiz'));
639             $output .= $this->container($this->container($this->render($button),
640                     'controls'), 'submitbtns mdl-align');
641         }
643         // Finish attempt button.
644         $options = array(
645             'attempt' => $attemptobj->get_attemptid(),
646             'finishattempt' => 1,
647             'timeup' => 0,
648             'slots' => '',
649             'sesskey' => sesskey(),
650         );
652         $button = new single_button(
653                 new moodle_url($attemptobj->processattempt_url(), $options),
654                 get_string('submitallandfinish', 'quiz'));
655         $button->id = 'responseform';
656         if ($attemptobj->get_state() == quiz_attempt::IN_PROGRESS) {
657             $button->add_action(new confirm_action(get_string('confirmclose', 'quiz'), null,
658                     get_string('submitallandfinish', 'quiz')));
659         }
661         $duedate = $attemptobj->get_due_date();
662         $message = '';
663         if ($attemptobj->get_state() == quiz_attempt::OVERDUE) {
664             $message = get_string('overduemustbesubmittedby', 'quiz', userdate($duedate));
666         } else if ($duedate) {
667             $message = get_string('mustbesubmittedby', 'quiz', userdate($duedate));
668         }
670         $output .= $this->countdown_timer($attemptobj, time());
671         $output .= $this->container($message . $this->container(
672                 $this->render($button), 'controls'), 'submitbtns mdl-align');
674         return $output;
675     }
677     /*
678      * View Page
679      */
680     /**
681      * Generates the view page
682      *
683      * @param int $course The id of the course
684      * @param array $quiz Array conting quiz data
685      * @param int $cm Course Module ID
686      * @param int $context The page context ID
687      * @param array $infomessages information about this quiz
688      * @param mod_quiz_view_object $viewobj
689      * @param string $buttontext text for the start/continue attempt button, if
690      *      it should be shown.
691      * @param array $infomessages further information about why the student cannot
692      *      attempt this quiz now, if appicable this quiz
693      */
694     public function view_page($course, $quiz, $cm, $context, $viewobj) {
695         $output = '';
696         $output .= $this->view_information($quiz, $cm, $context, $viewobj->infomessages);
697         $output .= $this->view_table($quiz, $context, $viewobj);
698         $output .= $this->view_result_info($quiz, $context, $cm, $viewobj);
699         $output .= $this->box($this->view_page_buttons($viewobj), 'quizattempt');
700         return $output;
701     }
703     /**
704      * Work out, and render, whatever buttons, and surrounding info, should appear
705      * at the end of the review page.
706      * @param mod_quiz_view_object $viewobj the information required to display
707      * the view page.
708      * @return string HTML to output.
709      */
710     public function view_page_buttons(mod_quiz_view_object $viewobj) {
711         global $CFG;
712         $output = '';
714         if (!$viewobj->quizhasquestions) {
715             $output .= $this->no_questions_message($viewobj->canedit, $viewobj->editurl);
716         }
718         $output .= $this->access_messages($viewobj->preventmessages);
720         if ($viewobj->buttontext) {
721             $output .= $this->start_attempt_button($viewobj->buttontext,
722                     $viewobj->startattempturl, $viewobj->startattemptwarning,
723                     $viewobj->popuprequired, $viewobj->popupoptions);
725         }
727         if ($viewobj->showbacktocourse) {
728             $output .= $this->single_button($viewobj->backtocourseurl,
729                     get_string('backtocourse', 'quiz'), 'get',
730                     array('class' => 'continuebutton'));
731         }
733         return $output;
734     }
736     /**
737      * Generates the view attempt button
738      *
739      * @param int $course The course ID
740      * @param array $quiz Array containging quiz date
741      * @param int $cm The Course Module ID
742      * @param int $context The page Context ID
743      * @param mod_quiz_view_object $viewobj
744      * @param string $buttontext
745      */
746     public function start_attempt_button($buttontext, moodle_url $url,
747             $startattemptwarning, $popuprequired, $popupoptions) {
749         $button = new single_button($url, $buttontext);
750         $button->class .= ' quizstartbuttondiv';
752         $warning = '';
753         if ($popuprequired) {
754             $this->page->requires->js_module(quiz_get_js_module());
755             $this->page->requires->js('/mod/quiz/module.js');
756             $popupaction = new popup_action('click', $url, 'quizpopup', $popupoptions);
758             $button->class .= ' quizsecuremoderequired';
759             $button->add_action(new component_action('click',
760                     'M.mod_quiz.secure_window.start_attempt_action', array(
761                         'url' => $url->out(false),
762                         'windowname' => 'quizpopup',
763                         'options' => $popupaction->get_js_options(),
764                         'fullscreen' => true,
765                         'startattemptwarning' => $startattemptwarning,
766                     )));
768             $warning = html_writer::tag('noscript', $this->heading(get_string('noscript', 'quiz')));
770         } else if ($startattemptwarning) {
771             $button->add_action(new confirm_action($startattemptwarning, null,
772                     get_string('startattempt', 'quiz')));
773         }
775         return $this->render($button) . $warning;
776     }
778     /**
779      * Generate a message saying that this quiz has no questions, with a button to
780      * go to the edit page, if the user has the right capability.
781      * @param object $quiz the quiz settings.
782      * @param object $cm the course_module object.
783      * @param object $context the quiz context.
784      * @return string HTML to output.
785      */
786     public function no_questions_message($canedit, $editurl) {
787         $output = '';
788         $output .= $this->notification(get_string('noquestions', 'quiz'));
789         if ($canedit) {
790             $output .= $this->single_button($editurl, get_string('editquiz', 'quiz'), 'get');
791         }
793         return $output;
794     }
796     /**
797      * Outputs an error message for any guests accessing the quiz
798      *
799      * @param int $course The course ID
800      * @param array $quiz Array contingin quiz data
801      * @param int $cm Course Module ID
802      * @param int $context The page contect ID
803      * @param array $messages Array containing any messages
804      */
805     public function view_page_guest($course, $quiz, $cm, $context, $messages) {
806         $output = '';
807         $output .= $this->view_information($quiz, $cm, $context, $messages);
808         $guestno = html_writer::tag('p', get_string('guestsno', 'quiz'));
809         $liketologin = html_writer::tag('p', get_string('liketologin'));
810         $output .= $this->confirm($guestno."\n\n".$liketologin."\n", get_login_url(),
811                 get_referer(false));
812         return $output;
813     }
815     /**
816      * Outputs and error message for anyone who is not enrolle don the course
817      *
818      * @param int $course The course ID
819      * @param array $quiz Array contingin quiz data
820      * @param int $cm Course Module ID
821      * @param int $context The page contect ID
822      * @param array $messages Array containing any messages
823      */
824     public function view_page_notenrolled($course, $quiz, $cm, $context, $messages) {
825         global $CFG;
826         $output = '';
827         $output .= $this->view_information($quiz, $cm, $context, $messages);
828         $youneedtoenrol = html_writer::tag('p', get_string('youneedtoenrol', 'quiz'));
829         $button = html_writer::tag('p',
830                 $this->continue_button($CFG->wwwroot . '/course/view.php?id=' . $course->id));
831         $output .= $this->box($youneedtoenrol."\n\n".$button."\n", 'generalbox', 'notice');
832         return $output;
833     }
835     /**
836      * Output the page information
837      *
838      * @param object $quiz the quiz settings.
839      * @param object $cm the course_module object.
840      * @param object $context the quiz context.
841      * @param array $messages any access messages that should be described.
842      * @return string HTML to output.
843      */
844     public function view_information($quiz, $cm, $context, $messages) {
845         global $CFG;
847         $output = '';
849         // Print quiz name and description.
850         $output .= $this->heading(format_string($quiz->name));
851         $output .= $this->quiz_intro($quiz, $cm);
853         // Output any access messages.
854         if ($messages) {
855             $output .= $this->box($this->access_messages($messages), 'quizinfo');
856         }
858         // Show number of attempts summary to those who can view reports.
859         if (has_capability('mod/quiz:viewreports', $context)) {
860             if ($strattemptnum = $this->quiz_attempt_summary_link_to_reports($quiz, $cm,
861                     $context)) {
862                 $output .= html_writer::tag('div', $strattemptnum,
863                         array('class' => 'quizattemptcounts'));
864             }
865         }
866         return $output;
867     }
869     /**
870      * Output the quiz intro.
871      * @param object $quiz the quiz settings.
872      * @param object $cm the course_module object.
873      * @return string HTML to output.
874      */
875     public function quiz_intro($quiz, $cm) {
876         if (html_is_blank($quiz->intro)) {
877             return '';
878         }
880         return $this->box(format_module_intro('quiz', $quiz, $cm->id), 'generalbox', 'intro');
881     }
883     /**
884      * Generates the table heading.
885      */
886     public function view_table_heading() {
887         return $this->heading(get_string('summaryofattempts', 'quiz'), 3);
888     }
890     /**
891      * Generates the table of data
892      *
893      * @param array $quiz Array contining quiz data
894      * @param int $context The page context ID
895      * @param mod_quiz_view_object $viewobj
896      */
897     public function view_table($quiz, $context, $viewobj) {
898         if (!$viewobj->attempts) {
899             return '';
900         }
902         // Prepare table header.
903         $table = new html_table();
904         $table->attributes['class'] = 'generaltable quizattemptsummary';
905         $table->head = array();
906         $table->align = array();
907         $table->size = array();
908         if ($viewobj->attemptcolumn) {
909             $table->head[] = get_string('attemptnumber', 'quiz');
910             $table->align[] = 'center';
911             $table->size[] = '';
912         }
913         $table->head[] = get_string('attemptstate', 'quiz');
914         $table->align[] = 'left';
915         $table->size[] = '';
916         if ($viewobj->markcolumn) {
917             $table->head[] = get_string('marks', 'quiz') . ' / ' .
918                     quiz_format_grade($quiz, $quiz->sumgrades);
919             $table->align[] = 'center';
920             $table->size[] = '';
921         }
922         if ($viewobj->gradecolumn) {
923             $table->head[] = get_string('grade') . ' / ' .
924                     quiz_format_grade($quiz, $quiz->grade);
925             $table->align[] = 'center';
926             $table->size[] = '';
927         }
928         if ($viewobj->canreviewmine) {
929             $table->head[] = get_string('review', 'quiz');
930             $table->align[] = 'center';
931             $table->size[] = '';
932         }
933         if ($viewobj->feedbackcolumn) {
934             $table->head[] = get_string('feedback', 'quiz');
935             $table->align[] = 'left';
936             $table->size[] = '';
937         }
939         // One row for each attempt.
940         foreach ($viewobj->attemptobjs as $attemptobj) {
941             $attemptoptions = $attemptobj->get_display_options(true);
942             $row = array();
944             // Add the attempt number.
945             if ($viewobj->attemptcolumn) {
946                 if ($attemptobj->is_preview()) {
947                     $row[] = get_string('preview', 'quiz');
948                 } else {
949                     $row[] = $attemptobj->get_attempt_number();
950                 }
951             }
953             $row[] = $this->attempt_state($attemptobj);
955             if ($viewobj->markcolumn) {
956                 if ($attemptoptions->marks >= question_display_options::MARK_AND_MAX &&
957                         $attemptobj->is_finished()) {
958                     $row[] = quiz_format_grade($quiz, $attemptobj->get_sum_marks());
959                 } else {
960                     $row[] = '';
961                 }
962             }
964             // Ouside the if because we may be showing feedback but not grades.
965             $attemptgrade = quiz_rescale_grade($attemptobj->get_sum_marks(), $quiz, false);
967             if ($viewobj->gradecolumn) {
968                 if ($attemptoptions->marks >= question_display_options::MARK_AND_MAX &&
969                         $attemptobj->is_finished()) {
971                     // Highlight the highest grade if appropriate.
972                     if ($viewobj->overallstats && !$attemptobj->is_preview()
973                             && $viewobj->numattempts > 1 && !is_null($viewobj->mygrade)
974                             && $attemptgrade == $viewobj->mygrade
975                             && $quiz->grademethod == QUIZ_GRADEHIGHEST) {
976                         $table->rowclasses[$attemptobj->get_attempt_number()] = 'bestrow';
977                     }
979                     $row[] = quiz_format_grade($quiz, $attemptgrade);
980                 } else {
981                     $row[] = '';
982                 }
983             }
985             if ($viewobj->canreviewmine) {
986                 $row[] = $viewobj->accessmanager->make_review_link($attemptobj->get_attempt(),
987                         $attemptoptions, $this);
988             }
990             if ($viewobj->feedbackcolumn && $attemptobj->is_finished()) {
991                 if ($attemptoptions->overallfeedback) {
992                     $row[] = quiz_feedback_for_grade($attemptgrade, $quiz, $context);
993                 } else {
994                     $row[] = '';
995                 }
996             }
998             if ($attemptobj->is_preview()) {
999                 $table->data['preview'] = $row;
1000             } else {
1001                 $table->data[$attemptobj->get_attempt_number()] = $row;
1002             }
1003         } // End of loop over attempts.
1005         $output = '';
1006         $output .= $this->view_table_heading();
1007         $output .= html_writer::table($table);
1008         return $output;
1009     }
1011     /**
1012      * Generate a brief textual desciption of the current state of an attempt.
1013      * @param quiz_attempt $attemptobj the attempt
1014      * @param int $timenow the time to use as 'now'.
1015      * @return string the appropriate lang string to describe the state.
1016      */
1017     public function attempt_state($attemptobj) {
1018         switch ($attemptobj->get_state()) {
1019             case quiz_attempt::IN_PROGRESS:
1020                 return get_string('stateinprogress', 'quiz');
1022             case quiz_attempt::OVERDUE:
1023                 return get_string('stateoverdue', 'quiz') . html_writer::tag('span',
1024                         get_string('stateoverduedetails', 'quiz',
1025                                 userdate($attemptobj->get_due_date())),
1026                         array('class' => 'statedetails'));
1028             case quiz_attempt::FINISHED:
1029                 return get_string('statefinished', 'quiz') . html_writer::tag('span',
1030                         get_string('statefinisheddetails', 'quiz',
1031                                 userdate($attemptobj->get_submitted_date())),
1032                         array('class' => 'statedetails'));
1034             case quiz_attempt::ABANDONED:
1035                 return get_string('stateabandoned', 'quiz');
1036         }
1037     }
1039     /**
1040      * Generates data pertaining to quiz results
1041      *
1042      * @param array $quiz Array containing quiz data
1043      * @param int $context The page context ID
1044      * @param int $cm The Course Module Id
1045      * @param mod_quiz_view_object $viewobj
1046      */
1047     public function view_result_info($quiz, $context, $cm, $viewobj) {
1048         $output = '';
1049         if (!$viewobj->numattempts && !$viewobj->gradecolumn && is_null($viewobj->mygrade)) {
1050             return $output;
1051         }
1052         $resultinfo = '';
1054         if ($viewobj->overallstats) {
1055             if ($viewobj->moreattempts) {
1056                 $a = new stdClass();
1057                 $a->method = quiz_get_grading_option_name($quiz->grademethod);
1058                 $a->mygrade = quiz_format_grade($quiz, $viewobj->mygrade);
1059                 $a->quizgrade = quiz_format_grade($quiz, $quiz->grade);
1060                 $resultinfo .= $this->heading(get_string('gradesofar', 'quiz', $a), 3);
1061             } else {
1062                 $a = new stdClass();
1063                 $a->grade = quiz_format_grade($quiz, $viewobj->mygrade);
1064                 $a->maxgrade = quiz_format_grade($quiz, $quiz->grade);
1065                 $a = get_string('outofshort', 'quiz', $a);
1066                 $resultinfo .= $this->heading(get_string('yourfinalgradeis', 'quiz', $a), 3);
1067             }
1068         }
1070         if ($viewobj->mygradeoverridden) {
1072             $resultinfo .= html_writer::tag('p', get_string('overriddennotice', 'grades'),
1073                     array('class' => 'overriddennotice'))."\n";
1074         }
1075         if ($viewobj->gradebookfeedback) {
1076             $resultinfo .= $this->heading(get_string('comment', 'quiz'), 3);
1077             $resultinfo .= html_writer::div($viewobj->gradebookfeedback, 'quizteacherfeedback') . "\n";
1078         }
1079         if ($viewobj->feedbackcolumn) {
1080             $resultinfo .= $this->heading(get_string('overallfeedback', 'quiz'), 3);
1081             $resultinfo .= html_writer::div(
1082                     quiz_feedback_for_grade($viewobj->mygrade, $quiz, $context),
1083                     'quizgradefeedback') . "\n";
1084         }
1086         if ($resultinfo) {
1087             $output .= $this->box($resultinfo, 'generalbox', 'feedback');
1088         }
1089         return $output;
1090     }
1092     /**
1093      * Output either a link to the review page for an attempt, or a button to
1094      * open the review in a popup window.
1095      *
1096      * @param moodle_url $url of the target page.
1097      * @param bool $reviewinpopup whether a pop-up is required.
1098      * @param array $popupoptions options to pass to the popup_action constructor.
1099      * @return string HTML to output.
1100      */
1101     public function review_link($url, $reviewinpopup, $popupoptions) {
1102         if ($reviewinpopup) {
1103             $button = new single_button($url, get_string('review', 'quiz'));
1104             $button->add_action(new popup_action('click', $url, 'quizpopup', $popupoptions));
1105             return $this->render($button);
1107         } else {
1108             return html_writer::link($url, get_string('review', 'quiz'),
1109                     array('title' => get_string('reviewthisattempt', 'quiz')));
1110         }
1111     }
1113     /**
1114      * Displayed where there might normally be a review link, to explain why the
1115      * review is not available at this time.
1116      * @param string $message optional message explaining why the review is not possible.
1117      * @return string HTML to output.
1118      */
1119     public function no_review_message($message) {
1120         return html_writer::nonempty_tag('span', $message,
1121                 array('class' => 'noreviewmessage'));
1122     }
1124     /**
1125      * Returns the same as {@link quiz_num_attempt_summary()} but wrapped in a link
1126      * to the quiz reports.
1127      *
1128      * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
1129      * @param object $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid
1130      * fields are used at the moment.
1131      * @param object $context the quiz context.
1132      * @param bool $returnzero if false (default), when no attempts have been made '' is returned
1133      * instead of 'Attempts: 0'.
1134      * @param int $currentgroup if there is a concept of current group where this method is being
1135      * called
1136      *         (e.g. a report) pass it in here. Default 0 which means no current group.
1137      * @return string HTML fragment for the link.
1138      */
1139     public function quiz_attempt_summary_link_to_reports($quiz, $cm, $context,
1140                                                           $returnzero = false, $currentgroup = 0) {
1141         global $CFG;
1142         $summary = quiz_num_attempt_summary($quiz, $cm, $returnzero, $currentgroup);
1143         if (!$summary) {
1144             return '';
1145         }
1147         require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
1148         $url = new moodle_url('/mod/quiz/report.php', array(
1149                 'id' => $cm->id, 'mode' => quiz_report_default_report($context)));
1150         return html_writer::link($url, $summary);
1151     }
1153     /**
1154      * Output a graph, or a message saying that GD is required.
1155      * @param moodle_url $url the URL of the graph.
1156      * @param string $title the title to display above the graph.
1157      * @return string HTML fragment for the graph.
1158      */
1159     public function graph(moodle_url $url, $title) {
1160         global $CFG;
1162         $graph = html_writer::empty_tag('img', array('src' => $url, 'alt' => $title));
1164         return $this->heading($title, 3) . html_writer::tag('div', $graph, array('class' => 'graph'));
1165     }
1168 class mod_quiz_links_to_other_attempts implements renderable {
1169     /**
1170      * @var array string attempt number => url, or null for the current attempt.
1171      */
1172     public $links = array();
1175 class mod_quiz_view_object {
1176     /** @var array $infomessages of messages with information to display about the quiz. */
1177     public $infomessages;
1178     /** @var array $attempts contains all the user's attempts at this quiz. */
1179     public $attempts;
1180     /** @var array $attemptobjs quiz_attempt objects corresponding to $attempts. */
1181     public $attemptobjs;
1182     /** @var quiz_access_manager $accessmanager contains various access rules. */
1183     public $accessmanager;
1184     /** @var bool $canreviewmine whether the current user has the capability to
1185      *       review their own attempts. */
1186     public $canreviewmine;
1187     /** @var bool $canedit whether the current user has the capability to edit the quiz. */
1188     public $canedit;
1189     /** @var moodle_url $editurl the URL for editing this quiz. */
1190     public $editurl;
1191     /** @var int $attemptcolumn contains the number of attempts done. */
1192     public $attemptcolumn;
1193     /** @var int $gradecolumn contains the grades of any attempts. */
1194     public $gradecolumn;
1195     /** @var int $markcolumn contains the marks of any attempt. */
1196     public $markcolumn;
1197     /** @var int $overallstats contains all marks for any attempt. */
1198     public $overallstats;
1199     /** @var string $feedbackcolumn contains any feedback for and attempt. */
1200     public $feedbackcolumn;
1201     /** @var string $timenow contains a timestamp in string format. */
1202     public $timenow;
1203     /** @var int $numattempts contains the total number of attempts. */
1204     public $numattempts;
1205     /** @var float $mygrade contains the user's final grade for a quiz. */
1206     public $mygrade;
1207     /** @var bool $moreattempts whether this user is allowed more attempts. */
1208     public $moreattempts;
1209     /** @var int $mygradeoverridden contains an overriden grade. */
1210     public $mygradeoverridden;
1211     /** @var string $gradebookfeedback contains any feedback for a gradebook. */
1212     public $gradebookfeedback;
1213     /** @var bool $unfinished contains 1 if an attempt is unfinished. */
1214     public $unfinished;
1215     /** @var object $lastfinishedattempt the last attempt from the attempts array. */
1216     public $lastfinishedattempt;
1217     /** @var array $preventmessages of messages telling the user why they can't
1218      *       attempt the quiz now. */
1219     public $preventmessages;
1220     /** @var string $buttontext caption for the start attempt button. If this is null, show no
1221      *      button, or if it is '' show a back to the course button. */
1222     public $buttontext;
1223     /** @var string $startattemptwarning alert to show the user before starting an attempt. */
1224     public $startattemptwarning;
1225     /** @var moodle_url $startattempturl URL to start an attempt. */
1226     public $startattempturl;
1227     /** @var moodle_url $startattempturl URL for any Back to the course button. */
1228     public $backtocourseurl;
1229     /** @var bool $showbacktocourse should we show a back to the course button? */
1230     public $showbacktocourse;
1231     /** @var bool whether the attempt must take place in a popup window. */
1232     public $popuprequired;
1233     /** @var array options to use for the popup window, if required. */
1234     public $popupoptions;
1235     /** @var bool $quizhasquestions whether the quiz has any questions. */
1236     public $quizhasquestions;