165ec10e47c678f14c2eb8294869e638cc92b23d
[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->notification($message);
103         $output .= $this->close_window_button();
104         $output .= $this->footer();
105         return $output;
106     }
108     /**
109      * Filters the summarydata array.
110      *
111      * @param array $summarydata contains row data for table
112      * @param int $page the current page number
113      * @return $summarydata containing filtered row data
114      */
115     protected function filter_review_summary_table($summarydata, $page) {
116         if ($page == 0) {
117             return $summarydata;
118         }
120         // Only show some of summary table on subsequent pages.
121         foreach ($summarydata as $key => $rowdata) {
122             if (!in_array($key, array('user', 'attemptlist'))) {
123                 unset($summarydata[$key]);
124             }
125         }
127         return $summarydata;
128     }
130     /**
131      * Outputs the table containing data from summary data array
132      *
133      * @param array $summarydata contains row data for table
134      * @param int $page contains the current page number
135      */
136     public function review_summary_table($summarydata, $page) {
137         $summarydata = $this->filter_review_summary_table($summarydata, $page);
138         if (empty($summarydata)) {
139             return '';
140         }
142         $output = '';
143         $output .= html_writer::start_tag('table', array(
144                 'class' => 'generaltable generalbox quizreviewsummary'));
145         $output .= html_writer::start_tag('tbody');
146         foreach ($summarydata as $rowdata) {
147             if ($rowdata['title'] instanceof renderable) {
148                 $title = $this->render($rowdata['title']);
149             } else {
150                 $title = $rowdata['title'];
151             }
153             if ($rowdata['content'] instanceof renderable) {
154                 $content = $this->render($rowdata['content']);
155             } else {
156                 $content = $rowdata['content'];
157             }
159             $output .= html_writer::tag('tr',
160                 html_writer::tag('th', $title, array('class' => 'cell', 'scope' => 'row')) .
161                         html_writer::tag('td', $content, array('class' => 'cell'))
162             );
163         }
165         $output .= html_writer::end_tag('tbody');
166         $output .= html_writer::end_tag('table');
167         return $output;
168     }
170     /**
171      * Renders each question
172      *
173      * @param quiz_attempt $attemptobj instance of quiz_attempt
174      * @param bool $reviewing
175      * @param array $slots array of intgers relating to questions
176      * @param int $page current page number
177      * @param bool $showall if true shows attempt on single page
178      * @param mod_quiz_display_options $displayoptions instance of mod_quiz_display_options
179      */
180     public function questions(quiz_attempt $attemptobj, $reviewing, $slots, $page, $showall,
181                               mod_quiz_display_options $displayoptions) {
182         $output = '';
183         foreach ($slots as $slot) {
184             $output .= $attemptobj->render_question($slot, $reviewing,
185                     $attemptobj->review_url($slot, $page, $showall));
186         }
187         return $output;
188     }
190     /**
191      * Renders the main bit of the review page.
192      *
193      * @param array $summarydata contain row data for table
194      * @param int $page current page number
195      * @param mod_quiz_display_options $displayoptions instance of mod_quiz_display_options
196      * @param $content contains each question
197      * @param quiz_attempt $attemptobj instance of quiz_attempt
198      * @param bool $showall if true display attempt on one page
199      */
200     public function review_form($page, $showall, $displayoptions, $content, $attemptobj) {
201         if ($displayoptions->flags != question_display_options::EDITABLE) {
202             return $content;
203         }
205         $this->page->requires->js_init_call('M.mod_quiz.init_review_form', null, false,
206                 quiz_get_js_module());
208         $output = '';
209         $output .= html_writer::start_tag('form', array('action' => $attemptobj->review_url(null,
210                 $page, $showall), 'method' => 'post', 'class' => 'questionflagsaveform'));
211         $output .= html_writer::start_tag('div');
212         $output .= $content;
213         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey',
214                 'value' => sesskey()));
215         $output .= html_writer::start_tag('div', array('class' => 'submitbtns'));
216         $output .= html_writer::empty_tag('input', array('type' => 'submit',
217                 'class' => 'questionflagsavebutton', 'name' => 'savingflags',
218                 'value' => get_string('saveflags', 'question')));
219         $output .= html_writer::end_tag('div');
220         $output .= html_writer::end_tag('div');
221         $output .= html_writer::end_tag('form');
223         return $output;
224     }
226     /**
227      * Returns either a liink or button
228      *
229      * @param $url contains a url for the review link
230      */
231     public function finish_review_link($url) {
232         if ($this->page->pagelayout == 'popup') {
233             // In a 'secure' popup window.
234             $this->page->requires->js_init_call('M.mod_quiz.secure_window.init_close_button',
235                     array($url), quiz_get_js_module());
236             return html_writer::empty_tag('input', array('type' => 'button',
237                     'value' => get_string('finishreview', 'quiz'),
238                     'id' => 'secureclosebutton'));
239         } else {
240             return html_writer::link($url, get_string('finishreview', 'quiz'));
241         }
242     }
244     /**
245      * Creates a next page arrow or the finishing link
246      *
247      * @param quiz_attempt $attemptobj instance of quiz_attempt
248      * @param int $page the current page
249      * @param bool $lastpage if true current page is the last page
250      */
251     public function review_next_navigation(quiz_attempt $attemptobj, $page, $lastpage) {
252         if ($lastpage) {
253             $nav = $this->finish_review_link($attemptobj->view_url());
254         } else {
255             $nav = link_arrow_right(get_string('next'), $attemptobj->review_url(null, $page + 1));
256         }
257         return html_writer::tag('div', $nav, array('class' => 'submitbtns'));
258     }
260     /**
261      * Return the HTML of the quiz timer.
262      * @return string HTML content.
263      */
264     public function countdown_timer(quiz_attempt $attemptobj, $timenow) {
266         $timeleft = $attemptobj->get_time_left($timenow);
267         if ($timeleft !== false) {
268             // Make sure the timer starts just above zero. If $timeleft was <= 0, then
269             // this will just have the effect of causing the quiz to be submitted immediately.
270             $timerstartvalue = max($timeleft, 1);
271             $this->initialise_timer($timerstartvalue);
272         }
274         return html_writer::tag('div', get_string('timeleft', 'quiz') . ' ' .
275                 html_writer::tag('span', '', array('id' => 'quiz-time-left')),
276                 array('id' => 'quiz-timer'));
277     }
279     /**
280      * Create a preview link
281      *
282      * @param $url contains a url to the given page
283      */
284     public function restart_preview_button($url) {
285         return $this->single_button($url, get_string('startnewpreview', 'quiz'));
286     }
288     /**
289      * Outputs the navigation block panel
290      *
291      * @param quiz_nav_panel_base $panel instance of quiz_nav_panel_base
292      */
293     public function navigation_panel(quiz_nav_panel_base $panel) {
295         $output = '';
296         $userpicture = $panel->user_picture();
297         if ($userpicture) {
298             $output .= html_writer::tag('div', $this->render($userpicture),
299                     array('id' => 'user-picture', 'class' => 'clearfix'));
300         }
301         $output .= $panel->render_before_button_bits($this);
303         $output .= html_writer::start_tag('div', array('class' => 'qn_buttons'));
304         foreach ($panel->get_question_buttons() as $button) {
305             $output .= $this->render($button);
306         }
307         $output .= html_writer::end_tag('div');
309         $output .= html_writer::tag('div', $panel->render_end_bits($this),
310                 array('class' => 'othernav'));
312         $this->page->requires->js_init_call('M.mod_quiz.nav.init', null, false,
313                 quiz_get_js_module());
315         return $output;
316     }
318     /**
319      * Returns the quizzes navigation button
320      *
321      * @param quiz_nav_question_button $button
322      */
323     protected function render_quiz_nav_question_button(quiz_nav_question_button $button) {
324         $classes = array('qnbutton', $button->stateclass, $button->navmethod);
325         $attributes = array();
327         if ($button->currentpage) {
328             $classes[] = 'thispage';
329             $attributes[] = get_string('onthispage', 'quiz');
330         }
332         // Flagged?
333         if ($button->flagged) {
334             $classes[] = 'flagged';
335             $flaglabel = get_string('flagged', 'question');
336         } else {
337             $flaglabel = '';
338         }
339         $attributes[] = html_writer::tag('span', $flaglabel, array('class' => 'flagstate'));
341         if (is_numeric($button->number)) {
342             $qnostring = 'questionnonav';
343         } else {
344             $qnostring = 'questionnonavinfo';
345         }
347         $a = new stdClass();
348         $a->number = $button->number;
349         $a->attributes = implode(' ', $attributes);
350         $tagcontents = html_writer::tag('span', '', array('class' => 'thispageholder')) .
351                         html_writer::tag('span', '', array('class' => 'trafficlight')) .
352                         get_string($qnostring, 'quiz', $a);
353         $tagattributes = array('class' => implode(' ', $classes), 'id' => $button->id,
354                                   'title' => $button->statestring);
356         if ($button->url) {
357             return html_writer::link($button->url, $tagcontents, $tagattributes);
358         } else {
359             return html_writer::tag('span', $tagcontents, $tagattributes);
360         }
361     }
363     /**
364      * outputs the link the other attempts.
365      *
366      * @param mod_quiz_links_to_other_attempts $links
367      */
368     protected function render_mod_quiz_links_to_other_attempts(
369             mod_quiz_links_to_other_attempts $links) {
370         $attemptlinks = array();
371         foreach ($links->links as $attempt => $url) {
372             if ($url) {
373                 $attemptlinks[] = html_writer::link($url, $attempt);
374             } else {
375                 $attemptlinks[] = html_writer::tag('strong', $attempt);
376             }
377         }
378         return implode(', ', $attemptlinks);
379     }
381     public function start_attempt_page(quiz $quizobj, mod_quiz_preflight_check_form $mform) {
382         $output = '';
383         $output .= $this->header();
384         $output .= $this->quiz_intro($quizobj->get_quiz(), $quizobj->get_cm());
385         ob_start();
386         $mform->display();
387         $output .= ob_get_clean();
388         $output .= $this->footer();
389         return $output;
390     }
392     /**
393      * Attempt Page
394      *
395      * @param quiz_attempt $attemptobj Instance of quiz_attempt
396      * @param int $page Current page number
397      * @param quiz_access_manager $accessmanager Instance of quiz_access_manager
398      * @param array $messages An array of messages
399      * @param array $slots Contains an array of integers that relate to questions
400      * @param int $id The ID of an attempt
401      * @param int $nextpage The number of the next page
402      */
403     public function attempt_page($attemptobj, $page, $accessmanager, $messages, $slots, $id,
404             $nextpage) {
405         $output = '';
406         $output .= $this->header();
407         $output .= $this->quiz_notices($messages);
408         $output .= $this->attempt_form($attemptobj, $page, $slots, $id, $nextpage);
409         $output .= $this->footer();
410         return $output;
411     }
413     /**
414      * Returns any notices.
415      *
416      * @param array $messages
417      */
418     public function quiz_notices($messages) {
419         if (!$messages) {
420             return '';
421         }
422         return $this->box($this->heading(get_string('accessnoticesheader', 'quiz'), 3) .
423                 $this->access_messages($messages), 'quizaccessnotices');
424     }
426     /**
427      * Ouputs the form for making an attempt
428      *
429      * @param quiz_attempt $attemptobj
430      * @param int $page Current page number
431      * @param array $slots Array of integers relating to questions
432      * @param int $id ID of the attempt
433      * @param int $nextpage Next page number
434      */
435     public function attempt_form($attemptobj, $page, $slots, $id, $nextpage) {
436         $output = '';
438         // Start the form.
439         $output .= html_writer::start_tag('form',
440                 array('action' => $attemptobj->processattempt_url(), 'method' => 'post',
441                 'enctype' => 'multipart/form-data', 'accept-charset' => 'utf-8',
442                 'id' => 'responseform'));
443         $output .= html_writer::start_tag('div');
445         // Print all the questions.
446         foreach ($slots as $slot) {
447             $output .= $attemptobj->render_question($slot, false,
448                     $attemptobj->attempt_url($slot, $page));
449         }
451         $output .= html_writer::start_tag('div', array('class' => 'submitbtns'));
452         $output .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'next',
453                 'value' => get_string('next')));
454         $output .= html_writer::end_tag('div');
456         // Some hidden fields to trach what is going on.
457         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'attempt',
458                 'value' => $attemptobj->get_attemptid()));
459         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'thispage',
460                 'value' => $page, 'id' => 'followingpage'));
461         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'nextpage',
462                 'value' => $nextpage));
463         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'timeup',
464                 'value' => '0', 'id' => 'timeup'));
465         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey',
466                 'value' => sesskey()));
467         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'scrollpos',
468                 'value' => '', 'id' => 'scrollpos'));
470         // Add a hidden field with questionids. Do this at the end of the form, so
471         // if you navigate before the form has finished loading, it does not wipe all
472         // the student's answers.
473         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'slots',
474                 'value' => implode(',', $slots)));
476         // Finish the form.
477         $output .= html_writer::end_tag('div');
478         $output .= html_writer::end_tag('form');
480         return $output;
481     }
483     /**
484      * Output the JavaScript required to initialise the countdown timer.
485      * @param int $timerstartvalue time remaining, in seconds.
486      */
487     public function initialise_timer($timerstartvalue) {
488         $this->page->requires->js_init_call('M.mod_quiz.timer.init',
489                 array($timerstartvalue), false, quiz_get_js_module());
490     }
492     /**
493      * Output a page with an optional message, and JavaScript code to close the
494      * current window and redirect the parent window to a new URL.
495      * @param moodle_url $url the URL to redirect the parent window to.
496      * @param string $message message to display before closing the window. (optional)
497      * @return string HTML to output.
498      */
499     public function close_attempt_popup($url, $message = '') {
500         $output = '';
501         $output .= $this->header();
502         $output .= $this->box_start();
504         if ($message) {
505             $output .= html_writer::tag('p', $message);
506             $output .= html_writer::tag('p', get_string('windowclosing', 'quiz'));
507             $delay = 5;
508         } else {
509             $output .= html_writer::tag('p', get_string('pleaseclose', 'quiz'));
510             $delay = 0;
511         }
512         $this->page->requires->js_function_call('M.mod_quiz.secure_window.close',
513                 array($url, $delay));
515         $output .= $this->box_end();
516         $output .= $this->footer();
517         return $output;
518     }
520     /**
521      * Print each message in an array, surrounded by &lt;p>, &lt;/p> tags.
522      *
523      * @param array $messages the array of message strings.
524      * @param bool $return if true, return a string, instead of outputting.
525      *
526      * @return string HTML to output.
527      */
528     public function access_messages($messages) {
529         $output = '';
530         foreach ($messages as $message) {
531             $output .= html_writer::tag('p', $message) . "\n";
532         }
533         return $output;
534     }
536     /*
537      * Summary Page
538      */
539     /**
540      * Create the summary page
541      *
542      * @param quiz_attempt $attemptobj
543      * @param mod_quiz_display_options $displayoptions
544      */
545     public function summary_page($attemptobj, $displayoptions) {
546         $output = '';
547         $output .= $this->header();
548         $output .= $this->heading(format_string($attemptobj->get_quiz_name()));
549         $output .= $this->heading(get_string('summaryofattempt', 'quiz'), 3);
550         $output .= $this->summary_table($attemptobj, $displayoptions);
551         $output .= $this->summary_page_controls($attemptobj);
552         $output .= $this->footer();
553         return $output;
554     }
556     /**
557      * Generates the table of summarydata
558      *
559      * @param quiz_attempt $attemptobj
560      * @param mod_quiz_display_options $displayoptions
561      */
562     public function summary_table($attemptobj, $displayoptions) {
563         // Prepare the summary table header.
564         $table = new html_table();
565         $table->attributes['class'] = 'generaltable quizsummaryofattempt boxaligncenter';
566         $table->head = array(get_string('question', 'quiz'), get_string('status', 'quiz'));
567         $table->align = array('left', 'left');
568         $table->size = array('', '');
569         $markscolumn = $displayoptions->marks >= question_display_options::MARK_AND_MAX;
570         if ($markscolumn) {
571             $table->head[] = get_string('marks', 'quiz');
572             $table->align[] = 'left';
573             $table->size[] = '';
574         }
575         $table->data = array();
577         // Get the summary info for each question.
578         $slots = $attemptobj->get_slots();
579         foreach ($slots as $slot) {
580             if (!$attemptobj->is_real_question($slot)) {
581                 continue;
582             }
583             $flag = '';
584             if ($attemptobj->is_question_flagged($slot)) {
585                 $flag = html_writer::empty_tag('img', array('src' => $this->pix_url('i/flagged'),
586                         'alt' => get_string('flagged', 'question'), 'class' => 'questionflag'));
587             }
588             if ($attemptobj->can_navigate_to($slot)) {
589                 $row = array(html_writer::link($attemptobj->attempt_url($slot),
590                         $attemptobj->get_question_number($slot) . $flag),
591                         $attemptobj->get_question_status($slot, $displayoptions->correctness));
592             } else {
593                 $row = array($attemptobj->get_question_number($slot) . $flag,
594                                 $attemptobj->get_question_status($slot, $displayoptions->correctness));
595             }
596             if ($markscolumn) {
597                 $row[] = $attemptobj->get_question_mark($slot);
598             }
599             $table->data[] = $row;
600             $table->rowclasses[] = $attemptobj->get_question_state_class(
601                     $slot, $displayoptions->correctness);
602         }
604         // Print the summary table.
605         $output = html_writer::table($table);
607         return $output;
608     }
610     /**
611      * Creates any controls a the page should have.
612      *
613      * @param quiz_attempt $attemptobj
614      */
615     public function summary_page_controls($attemptobj) {
616         $output = '';
618         // Return to place button.
619         if ($attemptobj->get_state() == quiz_attempt::IN_PROGRESS) {
620             $button = new single_button(
621                     new moodle_url($attemptobj->attempt_url(null, $attemptobj->get_currentpage())),
622                     get_string('returnattempt', 'quiz'));
623             $output .= $this->container($this->container($this->render($button),
624                     'controls'), 'submitbtns mdl-align');
625         }
627         // Finish attempt button.
628         $options = array(
629             'attempt' => $attemptobj->get_attemptid(),
630             'finishattempt' => 1,
631             'timeup' => 0,
632             'slots' => '',
633             'sesskey' => sesskey(),
634         );
636         $button = new single_button(
637                 new moodle_url($attemptobj->processattempt_url(), $options),
638                 get_string('submitallandfinish', 'quiz'));
639         $button->id = 'responseform';
640         if ($attemptobj->get_state() == quiz_attempt::IN_PROGRESS) {
641             $button->add_action(new confirm_action(get_string('confirmclose', 'quiz'), null,
642                     get_string('submitallandfinish', 'quiz')));
643         }
645         $duedate = $attemptobj->get_due_date();
646         $message = '';
647         if ($attemptobj->get_state() == quiz_attempt::OVERDUE) {
648             $message = get_string('overduemustbesubmittedby', 'quiz', userdate($duedate));
650         } else if ($duedate) {
651             $message = get_string('mustbesubmittedby', 'quiz', userdate($duedate));
652         }
654         $output .= $this->countdown_timer($attemptobj, time());
655         $output .= $this->container($message . $this->container(
656                 $this->render($button), 'controls'), 'submitbtns mdl-align');
658         return $output;
659     }
661     /*
662      * View Page
663      */
664     /**
665      * Generates the view page
666      *
667      * @param int $course The id of the course
668      * @param array $quiz Array conting quiz data
669      * @param int $cm Course Module ID
670      * @param int $context The page context ID
671      * @param array $infomessages information about this quiz
672      * @param mod_quiz_view_object $viewobj
673      * @param string $buttontext text for the start/continue attempt button, if
674      *      it should be shown.
675      * @param array $infomessages further information about why the student cannot
676      *      attempt this quiz now, if appicable this quiz
677      */
678     public function view_page($course, $quiz, $cm, $context, $viewobj) {
679         $output = '';
680         $output .= $this->view_information($quiz, $cm, $context, $viewobj->infomessages);
681         $output .= $this->view_table($quiz, $context, $viewobj);
682         $output .= $this->view_best_score($viewobj);
683         $output .= $this->view_result_info($quiz, $context, $cm, $viewobj);
684         $output .= $this->box($this->view_page_buttons($viewobj), 'quizattempt');
685         return $output;
686     }
688     /**
689      * Work out, and render, whatever buttons, and surrounding info, should appear
690      * at the end of the review page.
691      * @param mod_quiz_view_object $viewobj the information required to display
692      * the view page.
693      * @return string HTML to output.
694      */
695     public function view_page_buttons(mod_quiz_view_object $viewobj) {
696         $output = '';
698         if (!$viewobj->quizhasquestions) {
699             $output .= $this->no_questions_message($viewobj->canedit, $viewobj->editurl);
700         }
702         $output .= $this->access_messages($viewobj->preventmessages);
704         if ($viewobj->buttontext) {
705             $output .= $this->start_attempt_button($viewobj->buttontext,
706                     $viewobj->startattempturl, $viewobj->startattemptwarning,
707                     $viewobj->popuprequired, $viewobj->popupoptions);
709         } else if ($viewobj->buttontext === '') {
710             // We should show a 'back to the course' button.
711             $output .= $this->single_button($viewobj->backtocourseurl,
712                     get_string('backtocourse', 'quiz'), 'get',
713                     array('class' => 'continuebutton'));
714         }
716         return $output;
717     }
719     /**
720      * Generates the view attempt button
721      *
722      * @param int $course The course ID
723      * @param array $quiz Array containging quiz date
724      * @param int $cm The Course Module ID
725      * @param int $context The page Context ID
726      * @param mod_quiz_view_object $viewobj
727      * @param string $buttontext
728      */
729     public function start_attempt_button($buttontext, moodle_url $url,
730             $startattemptwarning, $popuprequired, $popupoptions) {
732         $button = new single_button($url, $buttontext);
733         $button->class .= ' quizstartbuttondiv';
735         $warning = '';
736         if ($popuprequired) {
737             $this->page->requires->js_module(quiz_get_js_module());
738             $this->page->requires->js('/mod/quiz/module.js');
739             $popupaction = new popup_action('click', $url, 'quizpopup', $popupoptions);
741             $button->class .= ' quizsecuremoderequired';
742             $button->add_action(new component_action('click',
743                     'M.mod_quiz.secure_window.start_attempt_action', array(
744                         'url' => $url->out(false),
745                         'windowname' => 'quizpopup',
746                         'options' => $popupaction->get_js_options(),
747                         'fullscreen' => true,
748                         'startattemptwarning' => $startattemptwarning,
749                     )));
751             $warning = html_writer::tag('noscript', $this->heading(get_string('noscript', 'quiz')));
753         } else if ($startattemptwarning) {
754             $button->add_action(new confirm_action($startattemptwarning, null,
755                     get_string('startattempt', 'quiz')));
756         }
758         return $this->render($button) . $warning;
759     }
761     /**
762      * Generate a message saying that this quiz has no questions, with a button to
763      * go to the edit page, if the user has the right capability.
764      * @param object $quiz the quiz settings.
765      * @param object $cm the course_module object.
766      * @param object $context the quiz context.
767      * @return string HTML to output.
768      */
769     public function no_questions_message($canedit, $editurl) {
770         $output = '';
771         $output .= $this->notification(get_string('noquestions', 'quiz'));
772         if ($canedit) {
773             $output .= $this->single_button($editurl, get_string('editquiz', 'quiz'), 'get');
774         }
776         return $output;
777     }
779     /**
780      * Outputs an error message for any guests accessing the quiz
781      *
782      * @param int $course The course ID
783      * @param array $quiz Array contingin quiz data
784      * @param int $cm Course Module ID
785      * @param int $context The page contect ID
786      * @param array $messages Array containing any messages
787      */
788     public function view_page_guest($course, $quiz, $cm, $context, $messages) {
789         $output = '';
790         $output .= $this->view_information($quiz, $cm, $context, $messages);
791         $guestno = html_writer::tag('p', get_string('guestsno', 'quiz'));
792         $liketologin = html_writer::tag('p', get_string('liketologin'));
793         $output .= $this->confirm($guestno."\n\n".$liketologin."\n", get_login_url(),
794                 get_referer(false));
795         return $output;
796     }
798     /**
799      * Outputs and error message for anyone who is not enrolle don the course
800      *
801      * @param int $course The course ID
802      * @param array $quiz Array contingin quiz data
803      * @param int $cm Course Module ID
804      * @param int $context The page contect ID
805      * @param array $messages Array containing any messages
806      */
807     public function view_page_notenrolled($course, $quiz, $cm, $context, $messages) {
808         global $CFG;
809         $output = '';
810         $output .= $this->view_information($quiz, $cm, $context, $messages);
811         $youneedtoenrol = html_writer::tag('p', get_string('youneedtoenrol', 'quiz'));
812         $button = html_writer::tag('p',
813                 $this->continue_button($CFG->wwwroot . '/course/view.php?id=' . $course->id));
814         $output .= $this->box($youneedtoenrol."\n\n".$button."\n", 'generalbox', 'notice');
815         return $output;
816     }
818     /**
819      * Output the page information
820      *
821      * @param object $quiz the quiz settings.
822      * @param object $cm the course_module object.
823      * @param object $context the quiz context.
824      * @param array $messages any access messages that should be described.
825      * @return string HTML to output.
826      */
827     public function view_information($quiz, $cm, $context, $messages) {
828         global $CFG;
830         $output = '';
831         // Print quiz name and description.
832         $output .= $this->heading(format_string($quiz->name));
833         if (trim(strip_tags($quiz->intro))) {
834             $output .= $this->box(format_module_intro('quiz', $quiz, $cm->id), 'generalbox',
835                     'intro');
836         }
838         $output .= $this->box($this->access_messages($messages), 'quizinfo');
840         // Show number of attempts summary to those who can view reports.
841         if (has_capability('mod/quiz:viewreports', $context)) {
842             if ($strattemptnum = $this->quiz_attempt_summary_link_to_reports($quiz, $cm,
843                     $context)) {
844                 $output .= html_writer::tag('div', $strattemptnum,
845                         array('class' => 'quizattemptcounts'));
846             }
847         }
848         return $output;
849     }
851     /**
852      * Output the quiz intro.
853      * @param object $quiz the quiz settings.
854      * @param object $cm the course_module object.
855      * @return string HTML to output.
856      */
857     public function quiz_intro($quiz, $cm) {
858         if (trim(strip_tags($quiz->intro))) {
859             return $this->box(format_module_intro('quiz', $quiz, $cm->id),
860                     'generalbox', 'intro');
862         } else {
863             return '';
864         }
865     }
867     /**
868      * Generates the table heading.
869      */
870     public function view_table_heading() {
871         return $this->heading(get_string('summaryofattempts', 'quiz'));
872     }
874     /**
875      * Generates the table of data
876      *
877      * @param array $quiz Array contining quiz data
878      * @param int $context The page context ID
879      * @param mod_quiz_view_object $viewobj
880      */
881     public function view_table($quiz, $context, $viewobj) {
882         if (!$viewobj->attempts) {
883             return '';
884         }
886         // Prepare table header.
887         $table = new html_table();
888         $table->attributes['class'] = 'generaltable quizattemptsummary';
889         $table->head = array();
890         $table->align = array();
891         $table->size = array();
892         if ($viewobj->attemptcolumn) {
893             $table->head[] = get_string('attemptnumber', 'quiz');
894             $table->align[] = 'center';
895             $table->size[] = '';
896         }
897         $table->head[] = get_string('attemptstate', 'quiz');
898         $table->align[] = 'left';
899         $table->size[] = '';
900         if ($viewobj->markcolumn) {
901             $table->head[] = get_string('marks', 'quiz') . ' / ' .
902                     quiz_format_grade($quiz, $quiz->sumgrades);
903             $table->align[] = 'center';
904             $table->size[] = '';
905         }
906         if ($viewobj->gradecolumn) {
907             $table->head[] = get_string('grade') . ' / ' .
908                     quiz_format_grade($quiz, $quiz->grade);
909             $table->align[] = 'center';
910             $table->size[] = '';
911         }
912         if ($viewobj->canreviewmine) {
913             $table->head[] = get_string('review', 'quiz');
914             $table->align[] = 'center';
915             $table->size[] = '';
916         }
917         if ($viewobj->feedbackcolumn) {
918             $table->head[] = get_string('feedback', 'quiz');
919             $table->align[] = 'left';
920             $table->size[] = '';
921         }
923         // One row for each attempt.
924         foreach ($viewobj->attemptobjs as $attemptobj) {
925             $attemptoptions = $attemptobj->get_display_options(true);
926             $row = array();
928             // Add the attempt number.
929             if ($viewobj->attemptcolumn) {
930                 if ($attemptobj->is_preview()) {
931                     $row[] = get_string('preview', 'quiz');
932                 } else {
933                     $row[] = $attemptobj->get_attempt_number();
934                 }
935             }
937             $row[] = $this->attempt_state($attemptobj);
939             if ($viewobj->markcolumn) {
940                 if ($attemptoptions->marks >= question_display_options::MARK_AND_MAX &&
941                         $attemptobj->is_finished()) {
942                     $row[] = quiz_format_grade($quiz, $attemptobj->get_sum_marks());
943                 } else {
944                     $row[] = '';
945                 }
946             }
948             // Ouside the if because we may be showing feedback but not grades.
949             $attemptgrade = quiz_rescale_grade($attemptobj->get_sum_marks(), $quiz, false);
951             if ($viewobj->gradecolumn) {
952                 if ($attemptoptions->marks >= question_display_options::MARK_AND_MAX &&
953                         $attemptobj->is_finished()) {
955                     // Highlight the highest grade if appropriate.
956                     if ($viewobj->overallstats && !$attemptobj->is_preview()
957                             && $viewobj->numattempts > 1 && !is_null($viewobj->mygrade)
958                             && $attemptgrade == $viewobj->mygrade
959                             && $quiz->grademethod == QUIZ_GRADEHIGHEST) {
960                         $table->rowclasses[$attemptobj->get_attempt_number()] = 'bestrow';
961                     }
963                     $row[] = quiz_format_grade($quiz, $attemptgrade);
964                 } else {
965                     $row[] = '';
966                 }
967             }
969             if ($viewobj->canreviewmine) {
970                 $row[] = $viewobj->accessmanager->make_review_link($attemptobj->get_attempt(),
971                         $attemptoptions, $this);
972             }
974             if ($viewobj->feedbackcolumn && $attemptobj->is_finished()) {
975                 if ($attemptoptions->overallfeedback) {
976                     $row[] = quiz_feedback_for_grade($attemptgrade, $quiz, $context);
977                 } else {
978                     $row[] = '';
979                 }
980             }
982             if ($attemptobj->is_preview()) {
983                 $table->data['preview'] = $row;
984             } else {
985                 $table->data[$attemptobj->get_attempt_number()] = $row;
986             }
987         } // End of loop over attempts.
989         $output = '';
990         $output .= $this->view_table_heading();
991         $output .= html_writer::table($table);
992         return $output;
993     }
995     /**
996      * Generate a brief textual desciption of the current state of an attempt.
997      * @param quiz_attempt $attemptobj the attempt
998      * @param int $timenow the time to use as 'now'.
999      * @return string the appropriate lang string to describe the state.
1000      */
1001     public function attempt_state($attemptobj) {
1002         switch ($attemptobj->get_state()) {
1003             case quiz_attempt::IN_PROGRESS:
1004                 return get_string('stateinprogress', 'quiz');
1006             case quiz_attempt::OVERDUE:
1007                 return get_string('stateoverdue', 'quiz') . html_writer::tag('span',
1008                         get_string('stateoverduedetails', 'quiz',
1009                                 userdate($attemptobj->get_due_date())),
1010                         array('class' => 'statedetails'));
1012             case quiz_attempt::FINISHED:
1013                 return get_string('statefinished', 'quiz') . html_writer::tag('span',
1014                         get_string('statefinisheddetails', 'quiz',
1015                                 userdate($attemptobj->get_submitted_date())),
1016                         array('class' => 'statedetails'));
1018             case quiz_attempt::ABANDONED:
1019                 return get_string('stateabandoned', 'quiz');
1020         }
1021     }
1023     /**
1024      * Prints the students best score
1025      *
1026      * @param mod_quiz_view_object $viewobj
1027      */
1028     public function view_best_score($viewobj) {
1029         $output = '';
1030         // Print information about the student's best score for this quiz if possible.
1031         if (!$viewobj->moreattempts) {
1032             $output .= $this->heading(get_string('nomoreattempts', 'quiz'));
1033         }
1034         return $output;
1035     }
1037     /**
1038      * Generates data pertaining to quiz results
1039      *
1040      * @param array $quiz Array containing quiz data
1041      * @param int $context The page context ID
1042      * @param int $cm The Course Module Id
1043      * @param mod_quiz_view_object $viewobj
1044      */
1045     public function view_result_info($quiz, $context, $cm, $viewobj) {
1046         $output = '';
1047         if (!$viewobj->numattempts && !$viewobj->gradecolumn && is_null($viewobj->mygrade)) {
1048             return $output;
1049         }
1050         $resultinfo = '';
1052         if ($viewobj->overallstats) {
1053             if ($viewobj->moreattempts) {
1054                 $a = new stdClass();
1055                 $a->method = quiz_get_grading_option_name($quiz->grademethod);
1056                 $a->mygrade = quiz_format_grade($quiz, $viewobj->mygrade);
1057                 $a->quizgrade = quiz_format_grade($quiz, $quiz->grade);
1058                 $resultinfo .= $this->heading(get_string('gradesofar', 'quiz', $a), 2, 'main');
1059             } else {
1060                 $a = new stdClass();
1061                 $a->grade = quiz_format_grade($quiz, $viewobj->mygrade);
1062                 $a->maxgrade = quiz_format_grade($quiz, $quiz->grade);
1063                 $a = get_string('outofshort', 'quiz', $a);
1064                 $resultinfo .= $this->heading(get_string('yourfinalgradeis', 'quiz', $a), 2,
1065                         'main');
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, 'main');
1076             $resultinfo .= '<p class="quizteacherfeedback">'.$viewobj->gradebookfeedback.
1077                     "</p>\n";
1078         }
1079         if ($viewobj->feedbackcolumn) {
1080             $resultinfo .= $this->heading(get_string('overallfeedback', 'quiz'), 3, 'main');
1081             $resultinfo .= html_writer::tag('p',
1082                     quiz_feedback_for_grade($viewobj->mygrade, $quiz, $context),
1083                     array('class' => '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     }
1154 class mod_quiz_links_to_other_attempts implements renderable {
1155     /**
1156      * @var array string attempt number => url, or null for the current attempt.
1157      */
1158     public $links = array();
1161 class mod_quiz_view_object {
1162     /** @var array $infomessages of messages with information to display about the quiz. */
1163     public $infomessages;
1164     /** @var array $attempts contains all the user's attempts at this quiz. */
1165     public $attempts;
1166     /** @var array $attemptobjs quiz_attempt objects corresponding to $attempts. */
1167     public $attemptobjs;
1168     /** @var quiz_access_manager $accessmanager contains various access rules. */
1169     public $accessmanager;
1170     /** @var bool $canreviewmine whether the current user has the capability to
1171      *       review their own attempts. */
1172     public $canreviewmine;
1173     /** @var bool $canedit whether the current user has the capability to edit the quiz. */
1174     public $canedit;
1175     /** @var moodle_url $editurl the URL for editing this quiz. */
1176     public $editurl;
1177     /** @var int $attemptcolumn contains the number of attempts done. */
1178     public $attemptcolumn;
1179     /** @var int $gradecolumn contains the grades of any attempts. */
1180     public $gradecolumn;
1181     /** @var int $markcolumn contains the marks of any attempt. */
1182     public $markcolumn;
1183     /** @var int $overallstats contains all marks for any attempt. */
1184     public $overallstats;
1185     /** @var string $feedbackcolumn contains any feedback for and attempt. */
1186     public $feedbackcolumn;
1187     /** @var string $timenow contains a timestamp in string format. */
1188     public $timenow;
1189     /** @var int $numattempts contains the total number of attempts. */
1190     public $numattempts;
1191     /** @var float $mygrade contains the user's final grade for a quiz. */
1192     public $mygrade;
1193     /** @var bool $moreattempts whether this user is allowed more attempts. */
1194     public $moreattempts;
1195     /** @var int $mygradeoverridden contains an overriden grade. */
1196     public $mygradeoverridden;
1197     /** @var string $gradebookfeedback contains any feedback for a gradebook. */
1198     public $gradebookfeedback;
1199     /** @var bool $unfinished contains 1 if an attempt is unfinished. */
1200     public $unfinished;
1201     /** @var object $lastfinishedattempt the last attempt from the attempts array. */
1202     public $lastfinishedattempt;
1203     /** @var array $preventmessages of messages telling the user why they can't
1204      *       attempt the quiz now. */
1205     public $preventmessages;
1206     /** @var string $buttontext caption for the start attempt button. If this is null, show no
1207      *      button, or if it is '' show a back to the course button. */
1208     public $buttontext;
1209     /** @var string $startattemptwarning alert to show the user before starting an attempt. */
1210     public $startattemptwarning;
1211     /** @var moodle_url $startattempturl URL to start an attempt. */
1212     public $startattempturl;
1213     /** @var moodle_url $startattempturl URL for any Back to the course button. */
1214     public $backtocourseurl;
1215     /** @var bool whether the attempt must take place in a popup window. */
1216     public $popuprequired;
1217     /** @var array options to use for the popup window, if required. */
1218     public $popupoptions;
1219     /** @var bool $quizhasquestions whether the quiz has any questions. */
1220     public $quizhasquestions;