6900a3b007c078b4ea851b566eca24596687a11e
[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         $output .= html_writer::start_tag('div', array('class' => 'qn_buttons'));
317         foreach ($panel->get_question_buttons() as $button) {
318             $output .= $this->render($button);
319         }
320         $output .= html_writer::end_tag('div');
322         $output .= html_writer::tag('div', $panel->render_end_bits($this),
323                 array('class' => 'othernav'));
325         $this->page->requires->js_init_call('M.mod_quiz.nav.init', null, false,
326                 quiz_get_js_module());
328         return $output;
329     }
331     /**
332      * Returns the quizzes navigation button
333      *
334      * @param quiz_nav_question_button $button
335      */
336     protected function render_quiz_nav_question_button(quiz_nav_question_button $button) {
337         $classes = array('qnbutton', $button->stateclass, $button->navmethod);
338         $attributes = array();
340         if ($button->currentpage) {
341             $classes[] = 'thispage';
342             $attributes[] = get_string('onthispage', 'quiz');
343         }
345         // Flagged?
346         if ($button->flagged) {
347             $classes[] = 'flagged';
348             $flaglabel = get_string('flagged', 'question');
349         } else {
350             $flaglabel = '';
351         }
352         $attributes[] = html_writer::tag('span', $flaglabel, array('class' => 'flagstate'));
354         if (is_numeric($button->number)) {
355             $qnostring = 'questionnonav';
356         } else {
357             $qnostring = 'questionnonavinfo';
358         }
360         $a = new stdClass();
361         $a->number = $button->number;
362         $a->attributes = implode(' ', $attributes);
363         $tagcontents = html_writer::tag('span', '', array('class' => 'thispageholder')) .
364                         html_writer::tag('span', '', array('class' => 'trafficlight')) .
365                         get_string($qnostring, 'quiz', $a);
366         $tagattributes = array('class' => implode(' ', $classes), 'id' => $button->id,
367                                   'title' => $button->statestring);
369         if ($button->url) {
370             return html_writer::link($button->url, $tagcontents, $tagattributes);
371         } else {
372             return html_writer::tag('span', $tagcontents, $tagattributes);
373         }
374     }
376     /**
377      * outputs the link the other attempts.
378      *
379      * @param mod_quiz_links_to_other_attempts $links
380      */
381     protected function render_mod_quiz_links_to_other_attempts(
382             mod_quiz_links_to_other_attempts $links) {
383         $attemptlinks = array();
384         foreach ($links->links as $attempt => $url) {
385             if ($url) {
386                 $attemptlinks[] = html_writer::link($url, $attempt);
387             } else {
388                 $attemptlinks[] = html_writer::tag('strong', $attempt);
389             }
390         }
391         return implode(', ', $attemptlinks);
392     }
394     public function start_attempt_page(quiz $quizobj, mod_quiz_preflight_check_form $mform) {
395         $output = '';
396         $output .= $this->header();
397         $output .= $this->heading(format_string($quizobj->get_quiz_name(), true,
398                                   array("context" => $quizobj->get_context())));
399         $output .= $this->quiz_intro($quizobj->get_quiz(), $quizobj->get_cm());
400         ob_start();
401         $mform->display();
402         $output .= ob_get_clean();
403         $output .= $this->footer();
404         return $output;
405     }
407     /**
408      * Attempt Page
409      *
410      * @param quiz_attempt $attemptobj Instance of quiz_attempt
411      * @param int $page Current page number
412      * @param quiz_access_manager $accessmanager Instance of quiz_access_manager
413      * @param array $messages An array of messages
414      * @param array $slots Contains an array of integers that relate to questions
415      * @param int $id The ID of an attempt
416      * @param int $nextpage The number of the next page
417      */
418     public function attempt_page($attemptobj, $page, $accessmanager, $messages, $slots, $id,
419             $nextpage) {
420         $output = '';
421         $output .= $this->header();
422         $output .= $this->quiz_notices($messages);
423         $output .= $this->attempt_form($attemptobj, $page, $slots, $id, $nextpage);
424         $output .= $this->footer();
425         return $output;
426     }
428     /**
429      * Returns any notices.
430      *
431      * @param array $messages
432      */
433     public function quiz_notices($messages) {
434         if (!$messages) {
435             return '';
436         }
437         return $this->box($this->heading(get_string('accessnoticesheader', 'quiz'), 3) .
438                 $this->access_messages($messages), 'quizaccessnotices');
439     }
441     /**
442      * Ouputs the form for making an attempt
443      *
444      * @param quiz_attempt $attemptobj
445      * @param int $page Current page number
446      * @param array $slots Array of integers relating to questions
447      * @param int $id ID of the attempt
448      * @param int $nextpage Next page number
449      */
450     public function attempt_form($attemptobj, $page, $slots, $id, $nextpage) {
451         $output = '';
453         // Start the form.
454         $output .= html_writer::start_tag('form',
455                 array('action' => $attemptobj->processattempt_url(), 'method' => 'post',
456                 'enctype' => 'multipart/form-data', 'accept-charset' => 'utf-8',
457                 'id' => 'responseform'));
458         $output .= html_writer::start_tag('div');
460         // Print all the questions.
461         foreach ($slots as $slot) {
462             $output .= $attemptobj->render_question($slot, false,
463                     $attemptobj->attempt_url($slot, $page));
464         }
466         $output .= html_writer::start_tag('div', array('class' => 'submitbtns'));
467         $output .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'next',
468                 'value' => get_string('next')));
469         $output .= html_writer::end_tag('div');
471         // Some hidden fields to trach what is going on.
472         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'attempt',
473                 'value' => $attemptobj->get_attemptid()));
474         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'thispage',
475                 'value' => $page, 'id' => 'followingpage'));
476         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'nextpage',
477                 'value' => $nextpage));
478         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'timeup',
479                 'value' => '0', 'id' => 'timeup'));
480         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey',
481                 'value' => sesskey()));
482         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'scrollpos',
483                 'value' => '', 'id' => 'scrollpos'));
485         // Add a hidden field with questionids. Do this at the end of the form, so
486         // if you navigate before the form has finished loading, it does not wipe all
487         // the student's answers.
488         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'slots',
489                 'value' => implode(',', $slots)));
491         // Finish the form.
492         $output .= html_writer::end_tag('div');
493         $output .= html_writer::end_tag('form');
495         return $output;
496     }
498     /**
499      * Output the JavaScript required to initialise the countdown timer.
500      * @param int $timerstartvalue time remaining, in seconds.
501      */
502     public function initialise_timer($timerstartvalue, $ispreview) {
503         $options = array($timerstartvalue, (bool)$ispreview);
504         $this->page->requires->js_init_call('M.mod_quiz.timer.init', $options, false, quiz_get_js_module());
505     }
507     /**
508      * Output a page with an optional message, and JavaScript code to close the
509      * current window and redirect the parent window to a new URL.
510      * @param moodle_url $url the URL to redirect the parent window to.
511      * @param string $message message to display before closing the window. (optional)
512      * @return string HTML to output.
513      */
514     public function close_attempt_popup($url, $message = '') {
515         $output = '';
516         $output .= $this->header();
517         $output .= $this->box_start();
519         if ($message) {
520             $output .= html_writer::tag('p', $message);
521             $output .= html_writer::tag('p', get_string('windowclosing', 'quiz'));
522             $delay = 5;
523         } else {
524             $output .= html_writer::tag('p', get_string('pleaseclose', 'quiz'));
525             $delay = 0;
526         }
527         $this->page->requires->js_init_call('M.mod_quiz.secure_window.close',
528                 array($url, $delay), false, quiz_get_js_module());
530         $output .= $this->box_end();
531         $output .= $this->footer();
532         return $output;
533     }
535     /**
536      * Print each message in an array, surrounded by &lt;p>, &lt;/p> tags.
537      *
538      * @param array $messages the array of message strings.
539      * @param bool $return if true, return a string, instead of outputting.
540      *
541      * @return string HTML to output.
542      */
543     public function access_messages($messages) {
544         $output = '';
545         foreach ($messages as $message) {
546             $output .= html_writer::tag('p', $message) . "\n";
547         }
548         return $output;
549     }
551     /*
552      * Summary Page
553      */
554     /**
555      * Create the summary page
556      *
557      * @param quiz_attempt $attemptobj
558      * @param mod_quiz_display_options $displayoptions
559      */
560     public function summary_page($attemptobj, $displayoptions) {
561         $output = '';
562         $output .= $this->header();
563         $output .= $this->heading(format_string($attemptobj->get_quiz_name()));
564         $output .= $this->heading(get_string('summaryofattempt', 'quiz'), 3);
565         $output .= $this->summary_table($attemptobj, $displayoptions);
566         $output .= $this->summary_page_controls($attemptobj);
567         $output .= $this->footer();
568         return $output;
569     }
571     /**
572      * Generates the table of summarydata
573      *
574      * @param quiz_attempt $attemptobj
575      * @param mod_quiz_display_options $displayoptions
576      */
577     public function summary_table($attemptobj, $displayoptions) {
578         // Prepare the summary table header.
579         $table = new html_table();
580         $table->attributes['class'] = 'generaltable quizsummaryofattempt boxaligncenter';
581         $table->head = array(get_string('question', 'quiz'), get_string('status', 'quiz'));
582         $table->align = array('left', 'left');
583         $table->size = array('', '');
584         $markscolumn = $displayoptions->marks >= question_display_options::MARK_AND_MAX;
585         if ($markscolumn) {
586             $table->head[] = get_string('marks', 'quiz');
587             $table->align[] = 'left';
588             $table->size[] = '';
589         }
590         $table->data = array();
592         // Get the summary info for each question.
593         $slots = $attemptobj->get_slots();
594         foreach ($slots as $slot) {
595             if (!$attemptobj->is_real_question($slot)) {
596                 continue;
597             }
598             $flag = '';
599             if ($attemptobj->is_question_flagged($slot)) {
600                 $flag = html_writer::empty_tag('img', array('src' => $this->pix_url('i/flagged'),
601                         'alt' => get_string('flagged', 'question'), 'class' => 'questionflag icon-post'));
602             }
603             if ($attemptobj->can_navigate_to($slot)) {
604                 $row = array(html_writer::link($attemptobj->attempt_url($slot),
605                         $attemptobj->get_question_number($slot) . $flag),
606                         $attemptobj->get_question_status($slot, $displayoptions->correctness));
607             } else {
608                 $row = array($attemptobj->get_question_number($slot) . $flag,
609                                 $attemptobj->get_question_status($slot, $displayoptions->correctness));
610             }
611             if ($markscolumn) {
612                 $row[] = $attemptobj->get_question_mark($slot);
613             }
614             $table->data[] = $row;
615             $table->rowclasses[] = $attemptobj->get_question_state_class(
616                     $slot, $displayoptions->correctness);
617         }
619         // Print the summary table.
620         $output = html_writer::table($table);
622         return $output;
623     }
625     /**
626      * Creates any controls a the page should have.
627      *
628      * @param quiz_attempt $attemptobj
629      */
630     public function summary_page_controls($attemptobj) {
631         $output = '';
633         // Return to place button.
634         if ($attemptobj->get_state() == quiz_attempt::IN_PROGRESS) {
635             $button = new single_button(
636                     new moodle_url($attemptobj->attempt_url(null, $attemptobj->get_currentpage())),
637                     get_string('returnattempt', 'quiz'));
638             $output .= $this->container($this->container($this->render($button),
639                     'controls'), 'submitbtns mdl-align');
640         }
642         // Finish attempt button.
643         $options = array(
644             'attempt' => $attemptobj->get_attemptid(),
645             'finishattempt' => 1,
646             'timeup' => 0,
647             'slots' => '',
648             'sesskey' => sesskey(),
649         );
651         $button = new single_button(
652                 new moodle_url($attemptobj->processattempt_url(), $options),
653                 get_string('submitallandfinish', 'quiz'));
654         $button->id = 'responseform';
655         if ($attemptobj->get_state() == quiz_attempt::IN_PROGRESS) {
656             $button->add_action(new confirm_action(get_string('confirmclose', 'quiz'), null,
657                     get_string('submitallandfinish', 'quiz')));
658         }
660         $duedate = $attemptobj->get_due_date();
661         $message = '';
662         if ($attemptobj->get_state() == quiz_attempt::OVERDUE) {
663             $message = get_string('overduemustbesubmittedby', 'quiz', userdate($duedate));
665         } else if ($duedate) {
666             $message = get_string('mustbesubmittedby', 'quiz', userdate($duedate));
667         }
669         $output .= $this->countdown_timer($attemptobj, time());
670         $output .= $this->container($message . $this->container(
671                 $this->render($button), 'controls'), 'submitbtns mdl-align');
673         return $output;
674     }
676     /*
677      * View Page
678      */
679     /**
680      * Generates the view page
681      *
682      * @param int $course The id of the course
683      * @param array $quiz Array conting quiz data
684      * @param int $cm Course Module ID
685      * @param int $context The page context ID
686      * @param array $infomessages information about this quiz
687      * @param mod_quiz_view_object $viewobj
688      * @param string $buttontext text for the start/continue attempt button, if
689      *      it should be shown.
690      * @param array $infomessages further information about why the student cannot
691      *      attempt this quiz now, if appicable this quiz
692      */
693     public function view_page($course, $quiz, $cm, $context, $viewobj) {
694         $output = '';
695         $output .= $this->view_information($quiz, $cm, $context, $viewobj->infomessages);
696         $output .= $this->view_table($quiz, $context, $viewobj);
697         $output .= $this->view_result_info($quiz, $context, $cm, $viewobj);
698         $output .= $this->box($this->view_page_buttons($viewobj), 'quizattempt');
699         return $output;
700     }
702     /**
703      * Work out, and render, whatever buttons, and surrounding info, should appear
704      * at the end of the review page.
705      * @param mod_quiz_view_object $viewobj the information required to display
706      * the view page.
707      * @return string HTML to output.
708      */
709     public function view_page_buttons(mod_quiz_view_object $viewobj) {
710         global $CFG;
711         $output = '';
713         if (!$viewobj->quizhasquestions) {
714             $output .= $this->no_questions_message($viewobj->canedit, $viewobj->editurl);
715         }
717         $output .= $this->access_messages($viewobj->preventmessages);
719         if ($viewobj->buttontext) {
720             $output .= $this->start_attempt_button($viewobj->buttontext,
721                     $viewobj->startattempturl, $viewobj->startattemptwarning,
722                     $viewobj->popuprequired, $viewobj->popupoptions);
724         }
726         if ($viewobj->showbacktocourse) {
727             $output .= $this->single_button($viewobj->backtocourseurl,
728                     get_string('backtocourse', 'quiz'), 'get',
729                     array('class' => 'continuebutton'));
730         }
732         return $output;
733     }
735     /**
736      * Generates the view attempt button
737      *
738      * @param int $course The course ID
739      * @param array $quiz Array containging quiz date
740      * @param int $cm The Course Module ID
741      * @param int $context The page Context ID
742      * @param mod_quiz_view_object $viewobj
743      * @param string $buttontext
744      */
745     public function start_attempt_button($buttontext, moodle_url $url,
746             $startattemptwarning, $popuprequired, $popupoptions) {
748         $button = new single_button($url, $buttontext);
749         $button->class .= ' quizstartbuttondiv';
751         $warning = '';
752         if ($popuprequired) {
753             $this->page->requires->js_module(quiz_get_js_module());
754             $this->page->requires->js('/mod/quiz/module.js');
755             $popupaction = new popup_action('click', $url, 'quizpopup', $popupoptions);
757             $button->class .= ' quizsecuremoderequired';
758             $button->add_action(new component_action('click',
759                     'M.mod_quiz.secure_window.start_attempt_action', array(
760                         'url' => $url->out(false),
761                         'windowname' => 'quizpopup',
762                         'options' => $popupaction->get_js_options(),
763                         'fullscreen' => true,
764                         'startattemptwarning' => $startattemptwarning,
765                     )));
767             $warning = html_writer::tag('noscript', $this->heading(get_string('noscript', 'quiz')));
769         } else if ($startattemptwarning) {
770             $button->add_action(new confirm_action($startattemptwarning, null,
771                     get_string('startattempt', 'quiz')));
772         }
774         return $this->render($button) . $warning;
775     }
777     /**
778      * Generate a message saying that this quiz has no questions, with a button to
779      * go to the edit page, if the user has the right capability.
780      * @param object $quiz the quiz settings.
781      * @param object $cm the course_module object.
782      * @param object $context the quiz context.
783      * @return string HTML to output.
784      */
785     public function no_questions_message($canedit, $editurl) {
786         $output = '';
787         $output .= $this->notification(get_string('noquestions', 'quiz'));
788         if ($canedit) {
789             $output .= $this->single_button($editurl, get_string('editquiz', 'quiz'), 'get');
790         }
792         return $output;
793     }
795     /**
796      * Outputs an error message for any guests accessing the quiz
797      *
798      * @param int $course The course ID
799      * @param array $quiz Array contingin quiz data
800      * @param int $cm Course Module ID
801      * @param int $context The page contect ID
802      * @param array $messages Array containing any messages
803      */
804     public function view_page_guest($course, $quiz, $cm, $context, $messages) {
805         $output = '';
806         $output .= $this->view_information($quiz, $cm, $context, $messages);
807         $guestno = html_writer::tag('p', get_string('guestsno', 'quiz'));
808         $liketologin = html_writer::tag('p', get_string('liketologin'));
809         $output .= $this->confirm($guestno."\n\n".$liketologin."\n", get_login_url(),
810                 get_referer(false));
811         return $output;
812     }
814     /**
815      * Outputs and error message for anyone who is not enrolle don the course
816      *
817      * @param int $course The course ID
818      * @param array $quiz Array contingin quiz data
819      * @param int $cm Course Module ID
820      * @param int $context The page contect ID
821      * @param array $messages Array containing any messages
822      */
823     public function view_page_notenrolled($course, $quiz, $cm, $context, $messages) {
824         global $CFG;
825         $output = '';
826         $output .= $this->view_information($quiz, $cm, $context, $messages);
827         $youneedtoenrol = html_writer::tag('p', get_string('youneedtoenrol', 'quiz'));
828         $button = html_writer::tag('p',
829                 $this->continue_button($CFG->wwwroot . '/course/view.php?id=' . $course->id));
830         $output .= $this->box($youneedtoenrol."\n\n".$button."\n", 'generalbox', 'notice');
831         return $output;
832     }
834     /**
835      * Output the page information
836      *
837      * @param object $quiz the quiz settings.
838      * @param object $cm the course_module object.
839      * @param object $context the quiz context.
840      * @param array $messages any access messages that should be described.
841      * @return string HTML to output.
842      */
843     public function view_information($quiz, $cm, $context, $messages) {
844         global $CFG;
846         $output = '';
848         // Print quiz name and description.
849         $output .= $this->heading(format_string($quiz->name));
850         $output .= $this->quiz_intro($quiz, $cm);
852         // Output any access messages.
853         if ($messages) {
854             $output .= $this->box($this->access_messages($messages), 'quizinfo');
855         }
857         // Show number of attempts summary to those who can view reports.
858         if (has_capability('mod/quiz:viewreports', $context)) {
859             if ($strattemptnum = $this->quiz_attempt_summary_link_to_reports($quiz, $cm,
860                     $context)) {
861                 $output .= html_writer::tag('div', $strattemptnum,
862                         array('class' => 'quizattemptcounts'));
863             }
864         }
865         return $output;
866     }
868     /**
869      * Output the quiz intro.
870      * @param object $quiz the quiz settings.
871      * @param object $cm the course_module object.
872      * @return string HTML to output.
873      */
874     public function quiz_intro($quiz, $cm) {
875         if (html_is_blank($quiz->intro)) {
876             return '';
877         }
879         return $this->box(format_module_intro('quiz', $quiz, $cm->id), 'generalbox', 'intro');
880     }
882     /**
883      * Generates the table heading.
884      */
885     public function view_table_heading() {
886         return $this->heading(get_string('summaryofattempts', 'quiz'), 3);
887     }
889     /**
890      * Generates the table of data
891      *
892      * @param array $quiz Array contining quiz data
893      * @param int $context The page context ID
894      * @param mod_quiz_view_object $viewobj
895      */
896     public function view_table($quiz, $context, $viewobj) {
897         if (!$viewobj->attempts) {
898             return '';
899         }
901         // Prepare table header.
902         $table = new html_table();
903         $table->attributes['class'] = 'generaltable quizattemptsummary';
904         $table->head = array();
905         $table->align = array();
906         $table->size = array();
907         if ($viewobj->attemptcolumn) {
908             $table->head[] = get_string('attemptnumber', 'quiz');
909             $table->align[] = 'center';
910             $table->size[] = '';
911         }
912         $table->head[] = get_string('attemptstate', 'quiz');
913         $table->align[] = 'left';
914         $table->size[] = '';
915         if ($viewobj->markcolumn) {
916             $table->head[] = get_string('marks', 'quiz') . ' / ' .
917                     quiz_format_grade($quiz, $quiz->sumgrades);
918             $table->align[] = 'center';
919             $table->size[] = '';
920         }
921         if ($viewobj->gradecolumn) {
922             $table->head[] = get_string('grade') . ' / ' .
923                     quiz_format_grade($quiz, $quiz->grade);
924             $table->align[] = 'center';
925             $table->size[] = '';
926         }
927         if ($viewobj->canreviewmine) {
928             $table->head[] = get_string('review', 'quiz');
929             $table->align[] = 'center';
930             $table->size[] = '';
931         }
932         if ($viewobj->feedbackcolumn) {
933             $table->head[] = get_string('feedback', 'quiz');
934             $table->align[] = 'left';
935             $table->size[] = '';
936         }
938         // One row for each attempt.
939         foreach ($viewobj->attemptobjs as $attemptobj) {
940             $attemptoptions = $attemptobj->get_display_options(true);
941             $row = array();
943             // Add the attempt number.
944             if ($viewobj->attemptcolumn) {
945                 if ($attemptobj->is_preview()) {
946                     $row[] = get_string('preview', 'quiz');
947                 } else {
948                     $row[] = $attemptobj->get_attempt_number();
949                 }
950             }
952             $row[] = $this->attempt_state($attemptobj);
954             if ($viewobj->markcolumn) {
955                 if ($attemptoptions->marks >= question_display_options::MARK_AND_MAX &&
956                         $attemptobj->is_finished()) {
957                     $row[] = quiz_format_grade($quiz, $attemptobj->get_sum_marks());
958                 } else {
959                     $row[] = '';
960                 }
961             }
963             // Ouside the if because we may be showing feedback but not grades.
964             $attemptgrade = quiz_rescale_grade($attemptobj->get_sum_marks(), $quiz, false);
966             if ($viewobj->gradecolumn) {
967                 if ($attemptoptions->marks >= question_display_options::MARK_AND_MAX &&
968                         $attemptobj->is_finished()) {
970                     // Highlight the highest grade if appropriate.
971                     if ($viewobj->overallstats && !$attemptobj->is_preview()
972                             && $viewobj->numattempts > 1 && !is_null($viewobj->mygrade)
973                             && $attemptgrade == $viewobj->mygrade
974                             && $quiz->grademethod == QUIZ_GRADEHIGHEST) {
975                         $table->rowclasses[$attemptobj->get_attempt_number()] = 'bestrow';
976                     }
978                     $row[] = quiz_format_grade($quiz, $attemptgrade);
979                 } else {
980                     $row[] = '';
981                 }
982             }
984             if ($viewobj->canreviewmine) {
985                 $row[] = $viewobj->accessmanager->make_review_link($attemptobj->get_attempt(),
986                         $attemptoptions, $this);
987             }
989             if ($viewobj->feedbackcolumn && $attemptobj->is_finished()) {
990                 if ($attemptoptions->overallfeedback) {
991                     $row[] = quiz_feedback_for_grade($attemptgrade, $quiz, $context);
992                 } else {
993                     $row[] = '';
994                 }
995             }
997             if ($attemptobj->is_preview()) {
998                 $table->data['preview'] = $row;
999             } else {
1000                 $table->data[$attemptobj->get_attempt_number()] = $row;
1001             }
1002         } // End of loop over attempts.
1004         $output = '';
1005         $output .= $this->view_table_heading();
1006         $output .= html_writer::table($table);
1007         return $output;
1008     }
1010     /**
1011      * Generate a brief textual desciption of the current state of an attempt.
1012      * @param quiz_attempt $attemptobj the attempt
1013      * @param int $timenow the time to use as 'now'.
1014      * @return string the appropriate lang string to describe the state.
1015      */
1016     public function attempt_state($attemptobj) {
1017         switch ($attemptobj->get_state()) {
1018             case quiz_attempt::IN_PROGRESS:
1019                 return get_string('stateinprogress', 'quiz');
1021             case quiz_attempt::OVERDUE:
1022                 return get_string('stateoverdue', 'quiz') . html_writer::tag('span',
1023                         get_string('stateoverduedetails', 'quiz',
1024                                 userdate($attemptobj->get_due_date())),
1025                         array('class' => 'statedetails'));
1027             case quiz_attempt::FINISHED:
1028                 return get_string('statefinished', 'quiz') . html_writer::tag('span',
1029                         get_string('statefinisheddetails', 'quiz',
1030                                 userdate($attemptobj->get_submitted_date())),
1031                         array('class' => 'statedetails'));
1033             case quiz_attempt::ABANDONED:
1034                 return get_string('stateabandoned', 'quiz');
1035         }
1036     }
1038     /**
1039      * Generates data pertaining to quiz results
1040      *
1041      * @param array $quiz Array containing quiz data
1042      * @param int $context The page context ID
1043      * @param int $cm The Course Module Id
1044      * @param mod_quiz_view_object $viewobj
1045      */
1046     public function view_result_info($quiz, $context, $cm, $viewobj) {
1047         $output = '';
1048         if (!$viewobj->numattempts && !$viewobj->gradecolumn && is_null($viewobj->mygrade)) {
1049             return $output;
1050         }
1051         $resultinfo = '';
1053         if ($viewobj->overallstats) {
1054             if ($viewobj->moreattempts) {
1055                 $a = new stdClass();
1056                 $a->method = quiz_get_grading_option_name($quiz->grademethod);
1057                 $a->mygrade = quiz_format_grade($quiz, $viewobj->mygrade);
1058                 $a->quizgrade = quiz_format_grade($quiz, $quiz->grade);
1059                 $resultinfo .= $this->heading(get_string('gradesofar', 'quiz', $a), 3);
1060             } else {
1061                 $a = new stdClass();
1062                 $a->grade = quiz_format_grade($quiz, $viewobj->mygrade);
1063                 $a->maxgrade = quiz_format_grade($quiz, $quiz->grade);
1064                 $a = get_string('outofshort', 'quiz', $a);
1065                 $resultinfo .= $this->heading(get_string('yourfinalgradeis', 'quiz', $a), 3);
1066             }
1067         }
1069         if ($viewobj->mygradeoverridden) {
1071             $resultinfo .= html_writer::tag('p', get_string('overriddennotice', 'grades'),
1072                     array('class' => 'overriddennotice'))."\n";
1073         }
1074         if ($viewobj->gradebookfeedback) {
1075             $resultinfo .= $this->heading(get_string('comment', 'quiz'), 3);
1076             $resultinfo .= html_writer::div($viewobj->gradebookfeedback, 'quizteacherfeedback') . "\n";
1077         }
1078         if ($viewobj->feedbackcolumn) {
1079             $resultinfo .= $this->heading(get_string('overallfeedback', 'quiz'), 3);
1080             $resultinfo .= html_writer::div(
1081                     quiz_feedback_for_grade($viewobj->mygrade, $quiz, $context),
1082                     'quizgradefeedback') . "\n";
1083         }
1085         if ($resultinfo) {
1086             $output .= $this->box($resultinfo, 'generalbox', 'feedback');
1087         }
1088         return $output;
1089     }
1091     /**
1092      * Output either a link to the review page for an attempt, or a button to
1093      * open the review in a popup window.
1094      *
1095      * @param moodle_url $url of the target page.
1096      * @param bool $reviewinpopup whether a pop-up is required.
1097      * @param array $popupoptions options to pass to the popup_action constructor.
1098      * @return string HTML to output.
1099      */
1100     public function review_link($url, $reviewinpopup, $popupoptions) {
1101         if ($reviewinpopup) {
1102             $button = new single_button($url, get_string('review', 'quiz'));
1103             $button->add_action(new popup_action('click', $url, 'quizpopup', $popupoptions));
1104             return $this->render($button);
1106         } else {
1107             return html_writer::link($url, get_string('review', 'quiz'),
1108                     array('title' => get_string('reviewthisattempt', 'quiz')));
1109         }
1110     }
1112     /**
1113      * Displayed where there might normally be a review link, to explain why the
1114      * review is not available at this time.
1115      * @param string $message optional message explaining why the review is not possible.
1116      * @return string HTML to output.
1117      */
1118     public function no_review_message($message) {
1119         return html_writer::nonempty_tag('span', $message,
1120                 array('class' => 'noreviewmessage'));
1121     }
1123     /**
1124      * Returns the same as {@link quiz_num_attempt_summary()} but wrapped in a link
1125      * to the quiz reports.
1126      *
1127      * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
1128      * @param object $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid
1129      * fields are used at the moment.
1130      * @param object $context the quiz context.
1131      * @param bool $returnzero if false (default), when no attempts have been made '' is returned
1132      * instead of 'Attempts: 0'.
1133      * @param int $currentgroup if there is a concept of current group where this method is being
1134      * called
1135      *         (e.g. a report) pass it in here. Default 0 which means no current group.
1136      * @return string HTML fragment for the link.
1137      */
1138     public function quiz_attempt_summary_link_to_reports($quiz, $cm, $context,
1139                                                           $returnzero = false, $currentgroup = 0) {
1140         global $CFG;
1141         $summary = quiz_num_attempt_summary($quiz, $cm, $returnzero, $currentgroup);
1142         if (!$summary) {
1143             return '';
1144         }
1146         require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
1147         $url = new moodle_url('/mod/quiz/report.php', array(
1148                 'id' => $cm->id, 'mode' => quiz_report_default_report($context)));
1149         return html_writer::link($url, $summary);
1150     }
1152     /**
1153      * Output a graph, or a message saying that GD is required.
1154      * @param moodle_url $url the URL of the graph.
1155      * @param string $title the title to display above the graph.
1156      * @return string HTML fragment for the graph.
1157      */
1158     public function graph(moodle_url $url, $title) {
1159         global $CFG;
1161         $graph = html_writer::empty_tag('img', array('src' => $url, 'alt' => $title));
1163         return $this->heading($title, 3) . html_writer::tag('div', $graph, array('class' => 'graph'));
1164     }
1167 class mod_quiz_links_to_other_attempts implements renderable {
1168     /**
1169      * @var array string attempt number => url, or null for the current attempt.
1170      */
1171     public $links = array();
1174 class mod_quiz_view_object {
1175     /** @var array $infomessages of messages with information to display about the quiz. */
1176     public $infomessages;
1177     /** @var array $attempts contains all the user's attempts at this quiz. */
1178     public $attempts;
1179     /** @var array $attemptobjs quiz_attempt objects corresponding to $attempts. */
1180     public $attemptobjs;
1181     /** @var quiz_access_manager $accessmanager contains various access rules. */
1182     public $accessmanager;
1183     /** @var bool $canreviewmine whether the current user has the capability to
1184      *       review their own attempts. */
1185     public $canreviewmine;
1186     /** @var bool $canedit whether the current user has the capability to edit the quiz. */
1187     public $canedit;
1188     /** @var moodle_url $editurl the URL for editing this quiz. */
1189     public $editurl;
1190     /** @var int $attemptcolumn contains the number of attempts done. */
1191     public $attemptcolumn;
1192     /** @var int $gradecolumn contains the grades of any attempts. */
1193     public $gradecolumn;
1194     /** @var int $markcolumn contains the marks of any attempt. */
1195     public $markcolumn;
1196     /** @var int $overallstats contains all marks for any attempt. */
1197     public $overallstats;
1198     /** @var string $feedbackcolumn contains any feedback for and attempt. */
1199     public $feedbackcolumn;
1200     /** @var string $timenow contains a timestamp in string format. */
1201     public $timenow;
1202     /** @var int $numattempts contains the total number of attempts. */
1203     public $numattempts;
1204     /** @var float $mygrade contains the user's final grade for a quiz. */
1205     public $mygrade;
1206     /** @var bool $moreattempts whether this user is allowed more attempts. */
1207     public $moreattempts;
1208     /** @var int $mygradeoverridden contains an overriden grade. */
1209     public $mygradeoverridden;
1210     /** @var string $gradebookfeedback contains any feedback for a gradebook. */
1211     public $gradebookfeedback;
1212     /** @var bool $unfinished contains 1 if an attempt is unfinished. */
1213     public $unfinished;
1214     /** @var object $lastfinishedattempt the last attempt from the attempts array. */
1215     public $lastfinishedattempt;
1216     /** @var array $preventmessages of messages telling the user why they can't
1217      *       attempt the quiz now. */
1218     public $preventmessages;
1219     /** @var string $buttontext caption for the start attempt button. If this is null, show no
1220      *      button, or if it is '' show a back to the course button. */
1221     public $buttontext;
1222     /** @var string $startattemptwarning alert to show the user before starting an attempt. */
1223     public $startattemptwarning;
1224     /** @var moodle_url $startattempturl URL to start an attempt. */
1225     public $startattempturl;
1226     /** @var moodle_url $startattempturl URL for any Back to the course button. */
1227     public $backtocourseurl;
1228     /** @var bool $showbacktocourse should we show a back to the course button? */
1229     public $showbacktocourse;
1230     /** @var bool whether the attempt must take place in a popup window. */
1231     public $popuprequired;
1232     /** @var array options to use for the popup window, if required. */
1233     public $popupoptions;
1234     /** @var bool $quizhasquestions whether the quiz has any questions. */
1235     public $quizhasquestions;