From 5ef8b6978ed1e276a23ec868ad6ccd9952846d5e Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Mon, 19 Dec 2022 16:01:51 +0000 Subject: [PATCH] MDL-76614 quiz: mod_quiz_renderer => mod_quiz\output\renderer --- mod/quiz/attemptlib.php | 31 +- mod/quiz/classes/access_manager.php | 10 +- mod/quiz/classes/output/renderer.php | 1427 ++++++++++++++++++++++++++ mod/quiz/db/renamedclasses.php | 1 + mod/quiz/locallib.php | 1 - mod/quiz/renderer.php | 1421 +------------------------ mod/quiz/tests/locallib_test.php | 3 +- mod/quiz/upgrade.txt | 2 + mod/quiz/view.php | 3 +- 9 files changed, 1462 insertions(+), 1437 deletions(-) create mode 100644 mod/quiz/classes/output/renderer.php diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index 5813053aea6..bcc9d6f441a 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -29,6 +29,7 @@ defined('MOODLE_INTERNAL') || die(); use mod_quiz\access_manager; use mod_quiz\output\links_to_other_attempts; +use mod_quiz\output\renderer; use mod_quiz\question\bank\qbank_helper; use mod_quiz\question\display_options; @@ -1698,11 +1699,11 @@ class quiz_attempt { * * @param int $slot identifies the question in the attempt. * @param bool $reviewing is the being printed on an attempt or a review page. - * @param mod_quiz_renderer $renderer the quiz renderer. + * @param renderer $renderer the quiz renderer. * @param moodle_url $thispageurl the URL of the page this question is being printed on. * @return string HTML for the question in its current state. */ - public function render_question($slot, $reviewing, mod_quiz_renderer $renderer, $thispageurl = null) { + public function render_question($slot, $reviewing, renderer $renderer, $thispageurl = null) { if ($this->is_blocked_by_previous_question($slot)) { $placeholderqa = $this->make_blocked_question_placeholder($slot); @@ -1725,12 +1726,12 @@ class quiz_attempt { * @param int $slot identifies the question in the attempt. * @param bool $reviewing is the being printed on an attempt or a review page. * @param moodle_url $thispageurl the URL of the page this question is being printed on. - * @param mod_quiz_renderer $renderer the quiz renderer. + * @param renderer $renderer the quiz renderer. * @param int|null $seq the seq number of the past state to display. * @return string HTML fragment. */ protected function render_question_helper($slot, $reviewing, $thispageurl, - mod_quiz_renderer $renderer, $seq) { + renderer $renderer, $seq) { $originalslot = $this->get_original_slot($slot); $number = $this->get_question_number($originalslot); $displayoptions = $this->get_display_options_with_edit_link($reviewing, $slot, $thispageurl); @@ -1811,12 +1812,12 @@ class quiz_attempt { * @param int $slot the slot number of a question in this quiz attempt. * @param int $seq the seq number of the past state to display. * @param bool $reviewing is the being printed on an attempt or a review page. - * @param mod_quiz_renderer $renderer the quiz renderer. + * @param renderer $renderer the quiz renderer. * @param moodle_url $thispageurl the URL of the page this question is being printed on. * @return string HTML for the question in its current state. */ public function render_question_at_step($slot, $seq, $reviewing, - mod_quiz_renderer $renderer, $thispageurl = null) { + renderer $renderer, $thispageurl = null) { return $this->render_question_helper($slot, $reviewing, $thispageurl, $renderer, $seq); } @@ -1867,13 +1868,13 @@ class quiz_attempt { /** * Get the navigation panel object for this attempt. * - * @param mod_quiz_renderer $output the quiz renderer to use to output things. + * @param renderer $output the quiz renderer to use to output things. * @param string $panelclass The type of panel, quiz_attempt_nav_panel or quiz_review_nav_panel * @param int $page the current page number. * @param bool $showall whether we are showing the whole quiz on one page. (Used by review.php.) * @return block_contents the requested object. */ - public function get_navigation_panel(mod_quiz_renderer $output, + public function get_navigation_panel(renderer $output, $panelclass, $page, $showall = false) { $panel = new $panelclass($this, $this->get_display_options(true), $page, $showall); @@ -2845,19 +2846,19 @@ abstract class quiz_nav_panel_base { /** * Hook for subclasses to override. * - * @param mod_quiz_renderer $output the quiz renderer to use. + * @param renderer $output the quiz renderer to use. * @return string HTML to output. */ - public function render_before_button_bits(mod_quiz_renderer $output) { + public function render_before_button_bits(renderer $output) { return ''; } - abstract public function render_end_bits(mod_quiz_renderer $output); + abstract public function render_end_bits(renderer $output); /** * Render the restart preview button. * - * @param mod_quiz_renderer $output the quiz renderer to use. + * @param renderer $output the quiz renderer to use. * @return string HTML to output. */ protected function render_restart_preview_link($output) { @@ -2917,12 +2918,12 @@ class quiz_attempt_nav_panel extends quiz_nav_panel_base { } } - public function render_before_button_bits(mod_quiz_renderer $output) { + public function render_before_button_bits(renderer $output) { return html_writer::tag('div', get_string('navnojswarning', 'quiz'), array('id' => 'quiznojswarning')); } - public function render_end_bits(mod_quiz_renderer $output) { + public function render_end_bits(renderer $output) { if ($this->page == -1) { // Don't link from the summary page to itself. return ''; @@ -2946,7 +2947,7 @@ class quiz_review_nav_panel extends quiz_nav_panel_base { return $this->attemptobj->review_url($slot, -1, $this->showall, $this->page); } - public function render_end_bits(mod_quiz_renderer $output) { + public function render_end_bits(renderer $output) { $html = ''; if ($this->attemptobj->get_num_pages() > 1) { if ($this->showall) { diff --git a/mod/quiz/classes/access_manager.php b/mod/quiz/classes/access_manager.php index 33c1d62698a..c17893cd13f 100644 --- a/mod/quiz/classes/access_manager.php +++ b/mod/quiz/classes/access_manager.php @@ -21,7 +21,7 @@ use mod_quiz\form\preflight_check_form; use mod_quiz\local\access_rule_base; use mod_quiz\question\display_options; use mod_quiz_mod_form; -use mod_quiz_renderer; +use mod_quiz\output\renderer; use moodle_page; use moodle_url; use MoodleQuickForm; @@ -524,10 +524,10 @@ class access_manager { * * This method does not return; * - * @param mod_quiz_renderer $output the quiz renderer. + * @param renderer $output the quiz renderer. * @param string $message optional message to output while redirecting. */ - public function back_to_view_page(mod_quiz_renderer $output, string $message = ''): void { + public function back_to_view_page(renderer $output, string $message = ''): void { // Actually return type 'never' on the previous line, once 8.1 is our minimum PHP version. if ($this->attempt_must_be_in_popup()) { echo $output->close_attempt_popup(new moodle_url($this->quizobj->view_url()), $message); @@ -542,11 +542,11 @@ class access_manager { * * @param stdClass $attempt the attempt object * @param mixed $nolongerused not used any more. - * @param mod_quiz_renderer $output quiz renderer instance. + * @param renderer $output quiz renderer instance. * @return string some HTML, the $linktext either unmodified or wrapped in a * link to the review page. */ - public function make_review_link(stdClass $attempt, $nolongerused, mod_quiz_renderer $output): string { + public function make_review_link(stdClass $attempt, $nolongerused, renderer $output): string { // If the attempt is still open, don't link. if (in_array($attempt->state, [quiz_attempt::IN_PROGRESS, quiz_attempt::OVERDUE])) { diff --git a/mod/quiz/classes/output/renderer.php b/mod/quiz/classes/output/renderer.php new file mode 100644 index 00000000000..6d7bade579d --- /dev/null +++ b/mod/quiz/classes/output/renderer.php @@ -0,0 +1,1427 @@ +. + +namespace mod_quiz\output; + +use mod_quiz\access_manager; +use mod_quiz\form\preflight_check_form; +use mod_quiz\question\display_options; +use plugin_renderer_base; +use quiz_attempt; + +/** + * The main renderer for the quiz module. + * + * @package mod_quiz + * @category output + * @copyright 2011 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends plugin_renderer_base { + /** + * Builds the review page + * + * @param quiz_attempt $attemptobj an instance of quiz_attempt. + * @param array $slots an array of intgers relating to questions. + * @param int $page the current page number + * @param bool $showall whether to show entire attempt on one page. + * @param bool $lastpage if true the current page is the last page. + * @param display_options $displayoptions instance of display_options. + * @param array $summarydata contains all table data + * @return $output containing html data. + */ + public function review_page(quiz_attempt $attemptobj, $slots, $page, $showall, + $lastpage, display_options $displayoptions, + $summarydata) { + + $output = ''; + $output .= $this->header(); + $output .= $this->review_summary_table($summarydata, $page); + $output .= $this->review_form($page, $showall, $displayoptions, + $this->questions($attemptobj, true, $slots, $page, $showall, $displayoptions), + $attemptobj); + + $output .= $this->review_next_navigation($attemptobj, $page, $lastpage, $showall); + $output .= $this->footer(); + return $output; + } + + /** + * Renders the review question pop-up. + * + * @param quiz_attempt $attemptobj an instance of quiz_attempt. + * @param int $slot which question to display. + * @param int $seq which step of the question attempt to show. null = latest. + * @param display_options $displayoptions instance of display_options. + * @param array $summarydata contains all table data + * @return $output containing html data. + */ + public function review_question_page(quiz_attempt $attemptobj, $slot, $seq, + display_options $displayoptions, $summarydata) { + + $output = ''; + $output .= $this->header(); + $output .= $this->review_summary_table($summarydata, 0); + + if (!is_null($seq)) { + $output .= $attemptobj->render_question_at_step($slot, $seq, true, $this); + } else { + $output .= $attemptobj->render_question($slot, true, $this); + } + + $output .= $this->close_window_button(); + $output .= $this->footer(); + return $output; + } + + /** + * Renders the review question pop-up. + * + * @param quiz_attempt $attemptobj an instance of quiz_attempt. + * @param string $message Why the review is not allowed. + * @return string html to output. + */ + public function review_question_not_allowed(quiz_attempt $attemptobj, $message) { + $output = ''; + $output .= $this->header(); + $output .= $this->heading(format_string($attemptobj->get_quiz_name(), true, + array("context" => $attemptobj->get_quizobj()->get_context()))); + $output .= $this->notification($message); + $output .= $this->close_window_button(); + $output .= $this->footer(); + return $output; + } + + /** + * Filters the summarydata array. + * + * @param array $summarydata contains row data for table + * @param int $page the current page number + * @return $summarydata containing filtered row data + */ + protected function filter_review_summary_table($summarydata, $page) { + if ($page == 0) { + return $summarydata; + } + + // Only show some of summary table on subsequent pages. + foreach ($summarydata as $key => $rowdata) { + if (!in_array($key, array('user', 'attemptlist'))) { + unset($summarydata[$key]); + } + } + + return $summarydata; + } + + /** + * Outputs the table containing data from summary data array + * + * @param array $summarydata contains row data for table + * @param int $page contains the current page number + */ + public function review_summary_table($summarydata, $page) { + $summarydata = $this->filter_review_summary_table($summarydata, $page); + if (empty($summarydata)) { + return ''; + } + + $output = ''; + $output .= html_writer::start_tag('table', array( + 'class' => 'generaltable generalbox quizreviewsummary')); + $output .= html_writer::start_tag('tbody'); + foreach ($summarydata as $rowdata) { + if ($rowdata['title'] instanceof renderable) { + $title = $this->render($rowdata['title']); + } else { + $title = $rowdata['title']; + } + + if ($rowdata['content'] instanceof renderable) { + $content = $this->render($rowdata['content']); + } else { + $content = $rowdata['content']; + } + + $output .= html_writer::tag('tr', + html_writer::tag('th', $title, array('class' => 'cell', 'scope' => 'row')) . + html_writer::tag('td', $content, array('class' => 'cell')) + ); + } + + $output .= html_writer::end_tag('tbody'); + $output .= html_writer::end_tag('table'); + return $output; + } + + /** + * Renders each question + * + * @param quiz_attempt $attemptobj instance of quiz_attempt + * @param bool $reviewing + * @param array $slots array of intgers relating to questions + * @param int $page current page number + * @param bool $showall if true shows attempt on single page + * @param display_options $displayoptions instance of display_options + */ + public function questions(quiz_attempt $attemptobj, $reviewing, $slots, $page, $showall, + display_options $displayoptions) { + $output = ''; + foreach ($slots as $slot) { + $output .= $attemptobj->render_question($slot, $reviewing, $this, + $attemptobj->review_url($slot, $page, $showall)); + } + return $output; + } + + /** + * Renders the main bit of the review page. + * + * @param array $summarydata contain row data for table + * @param int $page current page number + * @param display_options $displayoptions instance of display_options + * @param $content contains each question + * @param quiz_attempt $attemptobj instance of quiz_attempt + * @param bool $showall if true display attempt on one page + */ + public function review_form($page, $showall, $displayoptions, $content, $attemptobj) { + if ($displayoptions->flags != question_display_options::EDITABLE) { + return $content; + } + + $this->page->requires->js_init_call('M.mod_quiz.init_review_form', null, false, + quiz_get_js_module()); + + $output = ''; + $output .= html_writer::start_tag('form', array('action' => $attemptobj->review_url(null, + $page, $showall), 'method' => 'post', 'class' => 'questionflagsaveform')); + $output .= html_writer::start_tag('div'); + $output .= $content; + $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', + 'value' => sesskey())); + $output .= html_writer::start_tag('div', array('class' => 'submitbtns')); + $output .= html_writer::empty_tag('input', array('type' => 'submit', + 'class' => 'questionflagsavebutton btn btn-secondary', 'name' => 'savingflags', + 'value' => get_string('saveflags', 'question'))); + $output .= html_writer::end_tag('div'); + $output .= html_writer::end_tag('div'); + $output .= html_writer::end_tag('form'); + + return $output; + } + + /** + * Returns either a liink or button + * + * @param quiz_attempt $attemptobj instance of quiz_attempt + */ + public function finish_review_link(quiz_attempt $attemptobj) { + $url = $attemptobj->view_url(); + + if ($attemptobj->get_access_manager(time())->attempt_must_be_in_popup()) { + $this->page->requires->js_init_call('M.mod_quiz.secure_window.init_close_button', + array($url), false, quiz_get_js_module()); + return html_writer::empty_tag('input', array('type' => 'button', + 'value' => get_string('finishreview', 'quiz'), + 'id' => 'secureclosebutton', + 'class' => 'mod_quiz-next-nav btn btn-primary')); + + } else { + return html_writer::link($url, get_string('finishreview', 'quiz'), + array('class' => 'mod_quiz-next-nav')); + } + } + + /** + * Creates the navigation links/buttons at the bottom of the reivew attempt page. + * + * Note, the name of this function is no longer accurate, but when the design + * changed, it was decided to keep the old name for backwards compatibility. + * + * @param quiz_attempt $attemptobj instance of quiz_attempt + * @param int $page the current page + * @param bool $lastpage if true current page is the last page + * @param bool|null $showall if true, the URL will be to review the entire attempt on one page, + * and $page will be ignored. If null, a sensible default will be chosen. + * + * @return string HTML fragment. + */ + public function review_next_navigation(quiz_attempt $attemptobj, $page, $lastpage, $showall = null) { + $nav = ''; + if ($page > 0) { + $nav .= link_arrow_left(get_string('navigateprevious', 'quiz'), + $attemptobj->review_url(null, $page - 1, $showall), false, 'mod_quiz-prev-nav'); + } + if ($lastpage) { + $nav .= $this->finish_review_link($attemptobj); + } else { + $nav .= link_arrow_right(get_string('navigatenext', 'quiz'), + $attemptobj->review_url(null, $page + 1, $showall), false, 'mod_quiz-next-nav'); + } + return html_writer::tag('div', $nav, array('class' => 'submitbtns')); + } + + /** + * Return the HTML of the quiz timer. + * @return string HTML content. + */ + public function countdown_timer(quiz_attempt $attemptobj, $timenow) { + + $timeleft = $attemptobj->get_time_left_display($timenow); + if ($timeleft !== false) { + $ispreview = $attemptobj->is_preview(); + $timerstartvalue = $timeleft; + if (!$ispreview) { + // Make sure the timer starts just above zero. If $timeleft was <= 0, then + // this will just have the effect of causing the quiz to be submitted immediately. + $timerstartvalue = max($timerstartvalue, 1); + } + $this->initialise_timer($timerstartvalue, $ispreview); + } + + + return $this->output->render_from_template('mod_quiz/timer', (object)[]); + } + + /** + * Create a preview link + * + * @param moodle_url $url contains a url to the given page + */ + public function restart_preview_button($url) { + return $this->single_button($url, get_string('startnewpreview', 'quiz')); + } + + /** + * Outputs the navigation block panel + * + * @param quiz_nav_panel_base $panel instance of quiz_nav_panel_base + */ + public function navigation_panel(quiz_nav_panel_base $panel) { + + $output = ''; + $userpicture = $panel->user_picture(); + if ($userpicture) { + $fullname = fullname($userpicture->user); + if ($userpicture->size === true) { + $fullname = html_writer::div($fullname); + } + $output .= html_writer::tag('div', $this->render($userpicture) . $fullname, + array('id' => 'user-picture', 'class' => 'clearfix')); + } + $output .= $panel->render_before_button_bits($this); + + $bcc = $panel->get_button_container_class(); + $output .= html_writer::start_tag('div', array('class' => "qn_buttons clearfix $bcc")); + foreach ($panel->get_question_buttons() as $button) { + $output .= $this->render($button); + } + $output .= html_writer::end_tag('div'); + + $output .= html_writer::tag('div', $panel->render_end_bits($this), + array('class' => 'othernav')); + + $this->page->requires->js_init_call('M.mod_quiz.nav.init', null, false, + quiz_get_js_module()); + + return $output; + } + + /** + * Display a quiz navigation button. + * + * @param quiz_nav_question_button $button + * @return string HTML fragment. + */ + protected function render_quiz_nav_question_button(quiz_nav_question_button $button) { + $classes = array('qnbutton', $button->stateclass, $button->navmethod, 'btn'); + $extrainfo = array(); + + if ($button->currentpage) { + $classes[] = 'thispage'; + $extrainfo[] = get_string('onthispage', 'quiz'); + } + + // Flagged? + if ($button->flagged) { + $classes[] = 'flagged'; + $flaglabel = get_string('flagged', 'question'); + } else { + $flaglabel = ''; + } + $extrainfo[] = html_writer::tag('span', $flaglabel, array('class' => 'flagstate')); + + if (is_numeric($button->number)) { + $qnostring = 'questionnonav'; + } else { + $qnostring = 'questionnonavinfo'; + } + + $a = new stdClass(); + $a->number = $button->number; + $a->attributes = implode(' ', $extrainfo); + $tagcontents = html_writer::tag('span', '', array('class' => 'thispageholder')) . + html_writer::tag('span', '', array('class' => 'trafficlight')) . + get_string($qnostring, 'quiz', $a); + $tagattributes = array('class' => implode(' ', $classes), 'id' => $button->id, + 'title' => $button->statestring, 'data-quiz-page' => $button->page); + + if ($button->url) { + return html_writer::link($button->url, $tagcontents, $tagattributes); + } else { + return html_writer::tag('span', $tagcontents, $tagattributes); + } + } + + /** + * Display a quiz navigation heading. + * + * @param quiz_nav_section_heading $heading the heading. + * @return string HTML fragment. + */ + protected function render_quiz_nav_section_heading(quiz_nav_section_heading $heading) { + if (empty($heading->heading)) { + $headingtext = get_string('sectionnoname', 'quiz'); + $class = ' dimmed_text'; + } else { + $headingtext = $heading->heading; + $class = ''; + } + return $this->heading($headingtext, 3, 'mod_quiz-section-heading' . $class); + } + + /** + * Renders a list of links the other attempts. + * + * @param links_to_other_attempts $links + * @return string HTML fragment. + */ + protected function render_links_to_other_attempts( + links_to_other_attempts $links) { + $attemptlinks = array(); + foreach ($links->links as $attempt => $url) { + if (!$url) { + $attemptlinks[] = html_writer::tag('strong', $attempt); + } else if ($url instanceof renderable) { + $attemptlinks[] = $this->render($url); + } else { + $attemptlinks[] = html_writer::link($url, $attempt); + } + } + return implode(', ', $attemptlinks); + } + + /** + * Render the 'start attempt' page. + * + * The student gets here if their interaction with the preflight check + * from fails in some way (e.g. they typed the wrong password). + * + * @param quiz $quizobj + * @param preflight_check_form $mform + * @return string + */ + public function start_attempt_page(quiz $quizobj, preflight_check_form $mform) { + $output = ''; + $output .= $this->header(); + $output .= $this->during_attempt_tertiary_nav($quizobj->view_url()); + $output .= $this->heading(format_string($quizobj->get_quiz_name(), true, + array("context" => $quizobj->get_context()))); + $output .= $this->quiz_intro($quizobj->get_quiz(), $quizobj->get_cm()); + $output .= $mform->render(); + $output .= $this->footer(); + return $output; + } + + /** + * Attempt Page + * + * @param quiz_attempt $attemptobj Instance of quiz_attempt + * @param int $page Current page number + * @param access_manager $accessmanager Instance of access_manager + * @param array $messages An array of messages + * @param array $slots Contains an array of integers that relate to questions + * @param int $id The ID of an attempt + * @param int $nextpage The number of the next page + * @return string HTML to output. + */ + public function attempt_page($attemptobj, $page, $accessmanager, $messages, $slots, $id, + $nextpage) { + $output = ''; + $output .= $this->header(); + $output .= $this->during_attempt_tertiary_nav($attemptobj->view_url()); + $output .= $this->quiz_notices($messages); + $output .= $this->countdown_timer($attemptobj, time()); + $output .= $this->attempt_form($attemptobj, $page, $slots, $id, $nextpage); + $output .= $this->footer(); + return $output; + } + + /** + * Render the tertiary navigation for pages during the attempt. + * + * @param string|moodle_url $quizviewurl url of the view.php page for this quiz. + * @return string HTML to output. + */ + public function during_attempt_tertiary_nav($quizviewurl): string { + $output = ''; + $output .= html_writer::start_div('container-fluid tertiary-navigation'); + $output .= html_writer::start_div('row'); + $output .= html_writer::start_div('navitem'); + $output .= html_writer::link($quizviewurl, get_string('back'), + ['class' => 'btn btn-secondary']); + $output .= html_writer::end_div(); + $output .= html_writer::end_div(); + $output .= html_writer::end_div(); + return $output; + } + + /** + * Returns any notices. + * + * @param array $messages + */ + public function quiz_notices($messages) { + if (!$messages) { + return ''; + } + return $this->notification( + html_writer::tag('p', get_string('accessnoticesheader', 'quiz')) . $this->access_messages($messages), + 'warning', + false + ); + } + + /** + * Ouputs the form for making an attempt + * + * @param quiz_attempt $attemptobj + * @param int $page Current page number + * @param array $slots Array of integers relating to questions + * @param int $id ID of the attempt + * @param int $nextpage Next page number + */ + public function attempt_form($attemptobj, $page, $slots, $id, $nextpage) { + $output = ''; + + // Start the form. + $output .= html_writer::start_tag('form', + array('action' => new moodle_url($attemptobj->processattempt_url(), + array('cmid' => $attemptobj->get_cmid())), 'method' => 'post', + 'enctype' => 'multipart/form-data', 'accept-charset' => 'utf-8', + 'id' => 'responseform')); + $output .= html_writer::start_tag('div'); + + // Print all the questions. + foreach ($slots as $slot) { + $output .= $attemptobj->render_question($slot, false, $this, + $attemptobj->attempt_url($slot, $page), $this); + } + + $navmethod = $attemptobj->get_quiz()->navmethod; + $output .= $this->attempt_navigation_buttons($page, $attemptobj->is_last_page($page), $navmethod); + + // Some hidden fields to trach what is going on. + $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'attempt', + 'value' => $attemptobj->get_attemptid())); + $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'thispage', + 'value' => $page, 'id' => 'followingpage')); + $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'nextpage', + 'value' => $nextpage)); + $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'timeup', + 'value' => '0', 'id' => 'timeup')); + $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', + 'value' => sesskey())); + $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'scrollpos', + 'value' => '', 'id' => 'scrollpos')); + + // Add a hidden field with questionids. Do this at the end of the form, so + // if you navigate before the form has finished loading, it does not wipe all + // the student's answers. + $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'slots', + 'value' => implode(',', $attemptobj->get_active_slots($page)))); + + // Finish the form. + $output .= html_writer::end_tag('div'); + $output .= html_writer::end_tag('form'); + + $output .= $this->connection_warning(); + + return $output; + } + + /** + * Display the prev/next buttons that go at the bottom of each page of the attempt. + * + * @param int $page the page number. Starts at 0 for the first page. + * @param bool $lastpage is this the last page in the quiz? + * @param string $navmethod Optional quiz attribute, 'free' (default) or 'sequential' + * @return string HTML fragment. + */ + protected function attempt_navigation_buttons($page, $lastpage, $navmethod = 'free') { + $output = ''; + + $output .= html_writer::start_tag('div', array('class' => 'submitbtns')); + if ($page > 0 && $navmethod == 'free') { + $output .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'previous', + 'value' => get_string('navigateprevious', 'quiz'), 'class' => 'mod_quiz-prev-nav btn btn-secondary', + 'id' => 'mod_quiz-prev-nav')); + $this->page->requires->js_call_amd('core_form/submit', 'init', ['mod_quiz-prev-nav']); + } + if ($lastpage) { + $nextlabel = get_string('endtest', 'quiz'); + } else { + $nextlabel = get_string('navigatenext', 'quiz'); + } + $output .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'next', + 'value' => $nextlabel, 'class' => 'mod_quiz-next-nav btn btn-primary', 'id' => 'mod_quiz-next-nav')); + $output .= html_writer::end_tag('div'); + $this->page->requires->js_call_amd('core_form/submit', 'init', ['mod_quiz-next-nav']); + + return $output; + } + + /** + * Render a button which allows students to redo a question in the attempt. + * + * @param int $slot the number of the slot to generate the button for. + * @param bool $disabled if true, output the button disabled. + * @return string HTML fragment. + */ + public function redo_question_button($slot, $disabled) { + $attributes = array('type' => 'submit', 'name' => 'redoslot' . $slot, + 'value' => get_string('redoquestion', 'quiz'), + 'class' => 'mod_quiz-redo_question_button btn btn-secondary'); + if ($disabled) { + $attributes['disabled'] = 'disabled'; + } + return html_writer::div(html_writer::empty_tag('input', $attributes)); + } + + /** + * Output the JavaScript required to initialise the countdown timer. + * @param int $timerstartvalue time remaining, in seconds. + */ + public function initialise_timer($timerstartvalue, $ispreview) { + $options = array($timerstartvalue, (bool)$ispreview); + $this->page->requires->js_init_call('M.mod_quiz.timer.init', $options, false, quiz_get_js_module()); + } + + /** + * Output a page with an optional message, and JavaScript code to close the + * current window and redirect the parent window to a new URL. + * @param moodle_url $url the URL to redirect the parent window to. + * @param string $message message to display before closing the window. (optional) + * @return string HTML to output. + */ + public function close_attempt_popup($url, $message = '') { + $output = ''; + $output .= $this->header(); + $output .= $this->box_start(); + + if ($message) { + $output .= html_writer::tag('p', $message); + $output .= html_writer::tag('p', get_string('windowclosing', 'quiz')); + $delay = 5; + } else { + $output .= html_writer::tag('p', get_string('pleaseclose', 'quiz')); + $delay = 0; + } + $this->page->requires->js_init_call('M.mod_quiz.secure_window.close', + array($url, $delay), false, quiz_get_js_module()); + + $output .= $this->box_end(); + $output .= $this->footer(); + return $output; + } + + /** + * Print each message in an array, surrounded by <p>, </p> tags. + * + * @param array $messages the array of message strings. + * @param bool $return if true, return a string, instead of outputting. + * + * @return string HTML to output. + */ + public function access_messages($messages) { + $output = ''; + foreach ($messages as $message) { + $output .= html_writer::tag('p', $message, ['class' => 'text-left']); + } + return $output; + } + + /* + * Summary Page + */ + /** + * Create the summary page + * + * @param quiz_attempt $attemptobj + * @param display_options $displayoptions + */ + public function summary_page($attemptobj, $displayoptions) { + $output = ''; + $output .= $this->header(); + $output .= $this->during_attempt_tertiary_nav($attemptobj->view_url()); + $output .= $this->heading(format_string($attemptobj->get_quiz_name())); + $output .= $this->heading(get_string('summaryofattempt', 'quiz'), 3); + $output .= $this->summary_table($attemptobj, $displayoptions); + $output .= $this->summary_page_controls($attemptobj); + $output .= $this->footer(); + return $output; + } + + /** + * Generates the table of summarydata + * + * @param quiz_attempt $attemptobj + * @param display_options $displayoptions + */ + public function summary_table($attemptobj, $displayoptions) { + // Prepare the summary table header. + $table = new html_table(); + $table->attributes['class'] = 'generaltable quizsummaryofattempt boxaligncenter'; + $table->head = array(get_string('question', 'quiz'), get_string('status', 'quiz')); + $table->align = array('left', 'left'); + $table->size = array('', ''); + $markscolumn = $displayoptions->marks >= question_display_options::MARK_AND_MAX; + if ($markscolumn) { + $table->head[] = get_string('marks', 'quiz'); + $table->align[] = 'left'; + $table->size[] = ''; + } + $tablewidth = count($table->align); + $table->data = array(); + + // Get the summary info for each question. + $slots = $attemptobj->get_slots(); + foreach ($slots as $slot) { + // Add a section headings if we need one here. + $heading = $attemptobj->get_heading_before_slot($slot); + if ($heading !== null) { + // There is a heading here. + $rowclasses = 'quizsummaryheading'; + if ($heading) { + $heading = format_string($heading); + } else if (count($attemptobj->get_quizobj()->get_sections()) > 1) { + // If this is the start of an unnamed section, and the quiz has more + // than one section, then add a default heading. + $heading = get_string('sectionnoname', 'quiz'); + $rowclasses .= ' dimmed_text'; + } + $cell = new html_table_cell(format_string($heading)); + $cell->header = true; + $cell->colspan = $tablewidth; + $table->data[] = array($cell); + $table->rowclasses[] = $rowclasses; + } + + // Don't display information items. + if (!$attemptobj->is_real_question($slot)) { + continue; + } + + // Real question, show it. + $flag = ''; + if ($attemptobj->is_question_flagged($slot)) { + // Quiz has custom JS manipulating these image tags - so we can't use the pix_icon method here. + $flag = html_writer::empty_tag('img', array('src' => $this->image_url('i/flagged'), + 'alt' => get_string('flagged', 'question'), 'class' => 'questionflag icon-post')); + } + if ($attemptobj->can_navigate_to($slot)) { + $row = array(html_writer::link($attemptobj->attempt_url($slot), + $attemptobj->get_question_number($slot) . $flag), + $attemptobj->get_question_status($slot, $displayoptions->correctness)); + } else { + $row = array($attemptobj->get_question_number($slot) . $flag, + $attemptobj->get_question_status($slot, $displayoptions->correctness)); + } + if ($markscolumn) { + $row[] = $attemptobj->get_question_mark($slot); + } + $table->data[] = $row; + $table->rowclasses[] = 'quizsummary' . $slot . ' ' . $attemptobj->get_question_state_class( + $slot, $displayoptions->correctness); + } + + // Print the summary table. + $output = html_writer::table($table); + + return $output; + } + + /** + * Creates any controls a the page should have. + * + * @param quiz_attempt $attemptobj + */ + public function summary_page_controls($attemptobj) { + $output = ''; + + // Return to place button. + if ($attemptobj->get_state() == quiz_attempt::IN_PROGRESS) { + $button = new single_button( + new moodle_url($attemptobj->attempt_url(null, $attemptobj->get_currentpage())), + get_string('returnattempt', 'quiz')); + $output .= $this->container($this->container($this->render($button), + 'controls'), 'submitbtns mdl-align'); + } + + // Finish attempt button. + $options = array( + 'attempt' => $attemptobj->get_attemptid(), + 'finishattempt' => 1, + 'timeup' => 0, + 'slots' => '', + 'cmid' => $attemptobj->get_cmid(), + 'sesskey' => sesskey(), + ); + + $button = new single_button( + new moodle_url($attemptobj->processattempt_url(), $options), + get_string('submitallandfinish', 'quiz')); + $button->id = 'responseform'; + $button->class = 'btn-finishattempt'; + $button->formid = 'frm-finishattempt'; + if ($attemptobj->get_state() == quiz_attempt::IN_PROGRESS) { + $totalunanswered = 0; + if ($attemptobj->get_quiz()->navmethod == 'free') { + // Only count the unanswered question if the navigation method is set to free. + $totalunanswered = $attemptobj->get_number_of_unanswered_questions(); + } + $this->page->requires->js_call_amd('mod_quiz/submission_confirmation', 'init', [$totalunanswered]); + } + $button->primary = true; + + $duedate = $attemptobj->get_due_date(); + $message = ''; + if ($attemptobj->get_state() == quiz_attempt::OVERDUE) { + $message = get_string('overduemustbesubmittedby', 'quiz', userdate($duedate)); + + } else if ($duedate) { + $message = get_string('mustbesubmittedby', 'quiz', userdate($duedate)); + } + + $output .= $this->countdown_timer($attemptobj, time()); + $output .= $this->container($message . $this->container( + $this->render($button), 'controls'), 'submitbtns mdl-align'); + + return $output; + } + + /* + * View Page + */ + /** + * Generates the view page + * + * @param stdClass $course the course settings row from the database. + * @param stdClass $quiz the quiz settings row from the database. + * @param stdClass $cm the course_module settings row from the database. + * @param context_module $context the quiz context. + * @param view_page $viewobj + * @return string HTML to display + */ + public function view_page($course, $quiz, $cm, $context, $viewobj) { + $output = ''; + + $output .= $this->view_page_tertiary_nav($viewobj); + $output .= $this->view_information($quiz, $cm, $context, $viewobj->infomessages); + $output .= $this->view_table($quiz, $context, $viewobj); + $output .= $this->view_result_info($quiz, $context, $cm, $viewobj); + $output .= $this->box($this->view_page_buttons($viewobj), 'quizattempt'); + return $output; + } + + /** + * Render the tertiary navigation for the view page. + * + * @param view_page $viewobj the information required to display the view page. + * @return string HTML to output. + */ + public function view_page_tertiary_nav(view_page $viewobj): string { + $content = ''; + + if ($viewobj->buttontext) { + $attemptbtn = $this->start_attempt_button($viewobj->buttontext, + $viewobj->startattempturl, $viewobj->preflightcheckform, + $viewobj->popuprequired, $viewobj->popupoptions); + $content .= $attemptbtn; + } + + if ($viewobj->canedit && !$viewobj->quizhasquestions) { + $content .= html_writer::link($viewobj->editurl, get_string('addquestion', 'quiz'), + ['class' => 'btn btn-secondary']); + } + + if ($content) { + return html_writer::div(html_writer::div($content, 'row'), 'container-fluid tertiary-navigation'); + } else { + return ''; + } + } + + /** + * Work out, and render, whatever buttons, and surrounding info, should appear + * at the end of the review page. + * + * @param view_page $viewobj the information required to display the view page. + * @return string HTML to output. + */ + public function view_page_buttons(view_page $viewobj) { + $output = ''; + + if (!$viewobj->quizhasquestions) { + $output .= html_writer::div( + $this->notification(get_string('noquestions', 'quiz'), 'warning', false), + 'text-left mb-3'); + } + $output .= $this->access_messages($viewobj->preventmessages); + + if ($viewobj->showbacktocourse) { + $output .= $this->single_button($viewobj->backtocourseurl, + get_string('backtocourse', 'quiz'), 'get', + array('class' => 'continuebutton')); + } + + return $output; + } + + /** + * Generates the view attempt button + * + * @param string $buttontext the label to display on the button. + * @param moodle_url $url The URL to POST to in order to start the attempt. + * @param preflight_check_form $preflightcheckform deprecated. + * @param bool $popuprequired whether the attempt needs to be opened in a pop-up. + * @param array $popupoptions the options to use if we are opening a popup. + * @return string HTML fragment. + */ + public function start_attempt_button($buttontext, moodle_url $url, + preflight_check_form $preflightcheckform = null, + $popuprequired = false, $popupoptions = null) { + + $button = new single_button($url, $buttontext, 'post', true); + $button->class .= ' quizstartbuttondiv'; + if ($popuprequired) { + $button->class .= ' quizsecuremoderequired'; + } + + $popupjsoptions = null; + if ($popuprequired && $popupoptions) { + $action = new popup_action('click', $url, 'popup', $popupoptions); + $popupjsoptions = $action->get_js_options(); + } + + if ($preflightcheckform) { + $checkform = $preflightcheckform->render(); + } else { + $checkform = null; + } + + $this->page->requires->js_call_amd('mod_quiz/preflightcheck', 'init', + array('.quizstartbuttondiv [type=submit]', get_string('startattempt', 'quiz'), + '#mod_quiz_preflight_form', $popupjsoptions)); + + return $this->render($button) . $checkform; + } + + /** + * Generate a message saying that this quiz has no questions, with a button to + * go to the edit page, if the user has the right capability. + * + * @param bool $canedit can the current user edit the quiz? + * @param moodle_url $editurl URL of the edit quiz page. + * @return string HTML to output. + * + * @deprecated since Moodle 4.0 MDL-71915 - please do not use this function any more. + */ + public function no_questions_message($canedit, $editurl) { + debugging('no_questions_message() is deprecated, please use generate_no_questions_message() instead.', DEBUG_DEVELOPER); + + $output = html_writer::start_tag('div', array('class' => 'card text-center mb-3')); + $output .= html_writer::start_tag('div', array('class' => 'card-body')); + + $output .= $this->notification(get_string('noquestions', 'quiz'), 'warning', false); + if ($canedit) { + $output .= $this->single_button($editurl, get_string('editquiz', 'quiz'), 'get'); + } + $output .= html_writer::end_tag('div'); + $output .= html_writer::end_tag('div'); + + return $output; + } + + /** + * Outputs an error message for any guests accessing the quiz + * + * @param stdClass $course the course settings row from the database. + * @param stdClass $quiz the quiz settings row from the database. + * @param stdClass $cm the course_module settings row from the database. + * @param context_module $context the quiz context. + * @param array $messages Array containing any messages + * @param view_page $viewobj + */ + public function view_page_guest($course, $quiz, $cm, $context, $messages, $viewobj) { + $output = ''; + $output .= $this->view_page_tertiary_nav($viewobj); + $output .= $this->view_information($quiz, $cm, $context, $messages); + $guestno = html_writer::tag('p', get_string('guestsno', 'quiz')); + $liketologin = html_writer::tag('p', get_string('liketologin')); + $referer = get_local_referer(false); + $output .= $this->confirm($guestno."\n\n".$liketologin."\n", get_login_url(), $referer); + return $output; + } + + /** + * Outputs and error message for anyone who is not enrolle don the course + * + * @param stdClass $course the course settings row from the database. + * @param stdClass $quiz the quiz settings row from the database. + * @param stdClass $cm the course_module settings row from the database. + * @param context_module $context the quiz context. + * @param array $messages Array containing any messages + * @param view_page $viewobj + */ + public function view_page_notenrolled($course, $quiz, $cm, $context, $messages, $viewobj) { + global $CFG; + $output = ''; + $output .= $this->view_page_tertiary_nav($viewobj); + $output .= $this->view_information($quiz, $cm, $context, $messages); + $youneedtoenrol = html_writer::tag('p', get_string('youneedtoenrol', 'quiz')); + $button = html_writer::tag('p', + $this->continue_button($CFG->wwwroot . '/course/view.php?id=' . $course->id)); + $output .= $this->box($youneedtoenrol."\n\n".$button."\n", 'generalbox', 'notice'); + return $output; + } + + /** + * Output the page information + * + * @param object $quiz the quiz settings. + * @param object $cm the course_module object. + * @param context $context the quiz context. + * @param array $messages any access messages that should be described. + * @param bool $quizhasquestions does quiz has questions added. + * @return string HTML to output. + */ + public function view_information($quiz, $cm, $context, $messages, bool $quizhasquestions = false) { + $output = ''; + + // Output any access messages. + if ($messages) { + $output .= $this->box($this->access_messages($messages), 'quizinfo'); + } + + // Show number of attempts summary to those who can view reports. + if (has_capability('mod/quiz:viewreports', $context)) { + if ($strattemptnum = $this->quiz_attempt_summary_link_to_reports($quiz, $cm, + $context)) { + $output .= html_writer::tag('div', $strattemptnum, + array('class' => 'quizattemptcounts')); + } + } + + if (has_any_capability(['mod/quiz:manageoverrides', 'mod/quiz:viewoverrides'], $context)) { + if ($overrideinfo = $this->quiz_override_summary_links($quiz, $cm)) { + $output .= html_writer::tag('div', $overrideinfo, ['class' => 'quizattemptcounts']); + } + } + + return $output; + } + + /** + * Output the quiz intro. + * @param object $quiz the quiz settings. + * @param object $cm the course_module object. + * @return string HTML to output. + */ + public function quiz_intro($quiz, $cm) { + if (html_is_blank($quiz->intro)) { + return ''; + } + + return $this->box(format_module_intro('quiz', $quiz, $cm->id), 'generalbox', 'intro'); + } + + /** + * Generates the table heading. + */ + public function view_table_heading() { + return $this->heading(get_string('summaryofattempts', 'quiz'), 3); + } + + /** + * Generates the table of data + * + * @param array $quiz Array contining quiz data + * @param int $context The page context ID + * @param view_page $viewobj + */ + public function view_table($quiz, $context, $viewobj) { + if (!$viewobj->attempts) { + return ''; + } + + // Prepare table header. + $table = new html_table(); + $table->attributes['class'] = 'generaltable quizattemptsummary'; + $table->head = array(); + $table->align = array(); + $table->size = array(); + if ($viewobj->attemptcolumn) { + $table->head[] = get_string('attemptnumber', 'quiz'); + $table->align[] = 'center'; + $table->size[] = ''; + } + $table->head[] = get_string('attemptstate', 'quiz'); + $table->align[] = 'left'; + $table->size[] = ''; + if ($viewobj->markcolumn) { + $table->head[] = get_string('marks', 'quiz') . ' / ' . + quiz_format_grade($quiz, $quiz->sumgrades); + $table->align[] = 'center'; + $table->size[] = ''; + } + if ($viewobj->gradecolumn) { + $table->head[] = get_string('gradenoun') . ' / ' . + quiz_format_grade($quiz, $quiz->grade); + $table->align[] = 'center'; + $table->size[] = ''; + } + if ($viewobj->canreviewmine) { + $table->head[] = get_string('review', 'quiz'); + $table->align[] = 'center'; + $table->size[] = ''; + } + if ($viewobj->feedbackcolumn) { + $table->head[] = get_string('feedback', 'quiz'); + $table->align[] = 'left'; + $table->size[] = ''; + } + + // One row for each attempt. + foreach ($viewobj->attemptobjs as $attemptobj) { + $attemptoptions = $attemptobj->get_display_options(true); + $row = array(); + + // Add the attempt number. + if ($viewobj->attemptcolumn) { + if ($attemptobj->is_preview()) { + $row[] = get_string('preview', 'quiz'); + } else { + $row[] = $attemptobj->get_attempt_number(); + } + } + + $row[] = $this->attempt_state($attemptobj); + + if ($viewobj->markcolumn) { + if ($attemptoptions->marks >= question_display_options::MARK_AND_MAX && + $attemptobj->is_finished()) { + $row[] = quiz_format_grade($quiz, $attemptobj->get_sum_marks()); + } else { + $row[] = ''; + } + } + + // Ouside the if because we may be showing feedback but not grades. + $attemptgrade = quiz_rescale_grade($attemptobj->get_sum_marks(), $quiz, false); + + if ($viewobj->gradecolumn) { + if ($attemptoptions->marks >= question_display_options::MARK_AND_MAX && + $attemptobj->is_finished()) { + + // Highlight the highest grade if appropriate. + if ($viewobj->overallstats && !$attemptobj->is_preview() + && $viewobj->numattempts > 1 && !is_null($viewobj->mygrade) + && $attemptobj->get_state() == quiz_attempt::FINISHED + && $attemptgrade == $viewobj->mygrade + && $quiz->grademethod == QUIZ_GRADEHIGHEST) { + $table->rowclasses[$attemptobj->get_attempt_number()] = 'bestrow'; + } + + $row[] = quiz_format_grade($quiz, $attemptgrade); + } else { + $row[] = ''; + } + } + + if ($viewobj->canreviewmine) { + $row[] = $viewobj->accessmanager->make_review_link($attemptobj->get_attempt(), + $attemptoptions, $this); + } + + if ($viewobj->feedbackcolumn && $attemptobj->is_finished()) { + if ($attemptoptions->overallfeedback) { + $row[] = quiz_feedback_for_grade($attemptgrade, $quiz, $context); + } else { + $row[] = ''; + } + } + + if ($attemptobj->is_preview()) { + $table->data['preview'] = $row; + } else { + $table->data[$attemptobj->get_attempt_number()] = $row; + } + } // End of loop over attempts. + + $output = ''; + $output .= $this->view_table_heading(); + $output .= html_writer::table($table); + return $output; + } + + /** + * Generate a brief textual desciption of the current state of an attempt. + * @param quiz_attempt $attemptobj the attempt + * @param int $timenow the time to use as 'now'. + * @return string the appropriate lang string to describe the state. + */ + public function attempt_state($attemptobj) { + switch ($attemptobj->get_state()) { + case quiz_attempt::IN_PROGRESS: + return get_string('stateinprogress', 'quiz'); + + case quiz_attempt::OVERDUE: + return get_string('stateoverdue', 'quiz') . html_writer::tag('span', + get_string('stateoverduedetails', 'quiz', + userdate($attemptobj->get_due_date())), + array('class' => 'statedetails')); + + case quiz_attempt::FINISHED: + return get_string('statefinished', 'quiz') . html_writer::tag('span', + get_string('statefinisheddetails', 'quiz', + userdate($attemptobj->get_submitted_date())), + array('class' => 'statedetails')); + + case quiz_attempt::ABANDONED: + return get_string('stateabandoned', 'quiz'); + } + } + + /** + * Generates data pertaining to quiz results + * + * @param array $quiz Array containing quiz data + * @param int $context The page context ID + * @param int $cm The Course Module Id + * @param view_page $viewobj + */ + public function view_result_info($quiz, $context, $cm, $viewobj) { + $output = ''; + if (!$viewobj->numattempts && !$viewobj->gradecolumn && is_null($viewobj->mygrade)) { + return $output; + } + $resultinfo = ''; + + if ($viewobj->overallstats) { + if ($viewobj->moreattempts) { + $a = new stdClass(); + $a->method = quiz_get_grading_option_name($quiz->grademethod); + $a->mygrade = quiz_format_grade($quiz, $viewobj->mygrade); + $a->quizgrade = quiz_format_grade($quiz, $quiz->grade); + $resultinfo .= $this->heading(get_string('gradesofar', 'quiz', $a), 3); + } else { + $a = new stdClass(); + $a->grade = quiz_format_grade($quiz, $viewobj->mygrade); + $a->maxgrade = quiz_format_grade($quiz, $quiz->grade); + $a = get_string('outofshort', 'quiz', $a); + $resultinfo .= $this->heading(get_string('yourfinalgradeis', 'quiz', $a), 3); + } + } + + if ($viewobj->mygradeoverridden) { + + $resultinfo .= html_writer::tag('p', get_string('overriddennotice', 'grades'), + array('class' => 'overriddennotice'))."\n"; + } + if ($viewobj->gradebookfeedback) { + $resultinfo .= $this->heading(get_string('comment', 'quiz'), 3); + $resultinfo .= html_writer::div($viewobj->gradebookfeedback, 'quizteacherfeedback') . "\n"; + } + if ($viewobj->feedbackcolumn) { + $resultinfo .= $this->heading(get_string('overallfeedback', 'quiz'), 3); + $resultinfo .= html_writer::div( + quiz_feedback_for_grade($viewobj->mygrade, $quiz, $context), + 'quizgradefeedback') . "\n"; + } + + if ($resultinfo) { + $output .= $this->box($resultinfo, 'generalbox', 'feedback'); + } + return $output; + } + + /** + * Output either a link to the review page for an attempt, or a button to + * open the review in a popup window. + * + * @param moodle_url $url of the target page. + * @param bool $reviewinpopup whether a pop-up is required. + * @param array $popupoptions options to pass to the popup_action constructor. + * @return string HTML to output. + */ + public function review_link($url, $reviewinpopup, $popupoptions) { + if ($reviewinpopup) { + $button = new single_button($url, get_string('review', 'quiz')); + $button->add_action(new popup_action('click', $url, 'quizpopup', $popupoptions)); + return $this->render($button); + + } else { + return html_writer::link($url, get_string('review', 'quiz'), + array('title' => get_string('reviewthisattempt', 'quiz'))); + } + } + + /** + * Displayed where there might normally be a review link, to explain why the + * review is not available at this time. + * @param string $message optional message explaining why the review is not possible. + * @return string HTML to output. + */ + public function no_review_message($message) { + return html_writer::nonempty_tag('span', $message, + array('class' => 'noreviewmessage')); + } + + /** + * Returns the same as {@link quiz_num_attempt_summary()} but wrapped in a link + * to the quiz reports. + * + * @param stdClass $quiz the quiz object. Only $quiz->id is used at the moment. + * @param stdClass $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid + * fields are used at the moment. + * @param context $context the quiz context. + * @param bool $returnzero if false (default), when no attempts have been made '' is returned + * instead of 'Attempts: 0'. + * @param int $currentgroup if there is a concept of current group where this method is being + * called (e.g. a report) pass it in here. Default 0 which means no current group. + * @return string HTML fragment for the link. + */ + public function quiz_attempt_summary_link_to_reports($quiz, $cm, $context, + $returnzero = false, $currentgroup = 0) { + global $CFG; + $summary = quiz_num_attempt_summary($quiz, $cm, $returnzero, $currentgroup); + if (!$summary) { + return ''; + } + + require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php'); + $url = new moodle_url('/mod/quiz/report.php', array( + 'id' => $cm->id, 'mode' => quiz_report_default_report($context))); + return html_writer::link($url, $summary); + } + + /** + * Render a summary of the number of group and user overrides, with corresponding links. + * + * @param stdClass $quiz the quiz settings. + * @param stdClass|cm_info $cm the cm object. + * @param int $currentgroup currently selected group, if there is one. + * @return string HTML fragment for the link. + */ + public function quiz_override_summary_links(stdClass $quiz, stdClass $cm, $currentgroup = 0): string { + + $baseurl = new moodle_url('/mod/quiz/overrides.php', ['cmid' => $cm->id]); + $counts = quiz_override_summary($quiz, $cm, $currentgroup); + + $links = []; + if ($counts['group']) { + $links[] = html_writer::link(new moodle_url($baseurl, ['mode' => 'group']), + get_string('overridessummarygroup', 'quiz', $counts['group'])); + } + if ($counts['user']) { + $links[] = html_writer::link(new moodle_url($baseurl, ['mode' => 'user']), + get_string('overridessummaryuser', 'quiz', $counts['user'])); + } + + if (!$links) { + return ''; + } + + $links = implode(', ', $links); + switch ($counts['mode']) { + case 'onegroup': + return get_string('overridessummarythisgroup', 'quiz', $links); + + case 'somegroups': + return get_string('overridessummaryyourgroups', 'quiz', $links); + + case 'allgroups': + return get_string('overridessummary', 'quiz', $links); + + default: + throw new coding_exception('Unexpected mode ' . $counts['mode']); + } + } + + /** + * Outputs a chart. + * + * @param \core\chart_base $chart The chart. + * @param string $title The title to display above the graph. + * @param array $attrs extra container html attributes. + * @return string HTML fragment for the graph. + */ + public function chart(\core\chart_base $chart, $title, $attrs = []) { + return $this->heading($title, 3) . html_writer::tag('div', + $this->render($chart), array_merge(['class' => 'graph'], $attrs)); + } + + /** + * Output a graph, or a message saying that GD is required. + * @param moodle_url $url the URL of the graph. + * @param string $title the title to display above the graph. + * @return string HTML fragment for the graph. + */ + public function graph(moodle_url $url, $title) { + global $CFG; + + $graph = html_writer::empty_tag('img', array('src' => $url, 'alt' => $title)); + + return $this->heading($title, 3) . html_writer::tag('div', $graph, array('class' => 'graph')); + } + + /** + * Output the connection warning messages, which are initially hidden, and + * only revealed by JavaScript if necessary. + */ + public function connection_warning() { + $options = array('filter' => false, 'newlines' => false); + $warning = format_text(get_string('connectionerror', 'quiz'), FORMAT_MARKDOWN, $options); + $ok = format_text(get_string('connectionok', 'quiz'), FORMAT_MARKDOWN, $options); + return html_writer::tag('div', $warning, + array('id' => 'connection-error', 'style' => 'display: none;', 'role' => 'alert')) . + html_writer::tag('div', $ok, array('id' => 'connection-ok', 'style' => 'display: none;', 'role' => 'alert')); + } + + + /** + * Deprecated version of render_links_to_other_attempts. + * + * @param links_to_other_attempts $links + * @return string HTML fragment. + * @deprecated since Moodle 4.2. Please use render_links_to_other_attempts instead. + * @todo MDL-76612 Final deprecation in Moodle 4.6 + */ + protected function render_mod_quiz_links_to_other_attempts(links_to_other_attempts $links) { + return $this->render_links_to_other_attempts($links); + } +} diff --git a/mod/quiz/db/renamedclasses.php b/mod/quiz/db/renamedclasses.php index fe85ebd6298..3d964cc4963 100644 --- a/mod/quiz/db/renamedclasses.php +++ b/mod/quiz/db/renamedclasses.php @@ -58,4 +58,5 @@ $renamedclasses = [ 'quiz_add_random_form' => 'mod_quiz\form\add_random_form', 'mod_quiz_links_to_other_attempts' => 'mod_quiz\output\links_to_other_attempts', 'mod_quiz_view_object' => 'mod_quiz\output\view_page', + 'mod_quiz_renderer' => 'mod_quiz\output\renderer', ]; diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index 17d5adbc0da..ef979eadd1e 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -31,7 +31,6 @@ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/quiz/lib.php'); -require_once($CFG->dirroot . '/mod/quiz/renderer.php'); require_once($CFG->dirroot . '/mod/quiz/attemptlib.php'); require_once($CFG->libdir . '/completionlib.php'); require_once($CFG->libdir . '/filelib.php'); diff --git a/mod/quiz/renderer.php b/mod/quiz/renderer.php index 2ed18c39ac7..49b549d0cf8 100644 --- a/mod/quiz/renderer.php +++ b/mod/quiz/renderer.php @@ -15,1420 +15,13 @@ // along with Moodle. If not, see . /** - * Defines the renderer for the quiz module. + * File only retained to prevent fatal errors in code that tries to require/include this. * - * @package mod_quiz - * @copyright 2011 The Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @todo MDL-76612 delete this file as part of Moodle 4.6 development. + * @deprecated This file is no longer required in Moodle 4.2+. */ - -defined('MOODLE_INTERNAL') || die(); - -use mod_quiz\access_manager; -use mod_quiz\form\preflight_check_form; -use mod_quiz\output\links_to_other_attempts; -use mod_quiz\question\display_options; - - -/** - * The renderer for the quiz module. - * - * @copyright 2011 The Open University - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class mod_quiz_renderer extends plugin_renderer_base { - /** - * Builds the review page - * - * @param quiz_attempt $attemptobj an instance of quiz_attempt. - * @param array $slots an array of intgers relating to questions. - * @param int $page the current page number - * @param bool $showall whether to show entire attempt on one page. - * @param bool $lastpage if true the current page is the last page. - * @param display_options $displayoptions instance of display_options. - * @param array $summarydata contains all table data - * @return $output containing html data. - */ - public function review_page(quiz_attempt $attemptobj, $slots, $page, $showall, - $lastpage, display_options $displayoptions, - $summarydata) { - - $output = ''; - $output .= $this->header(); - $output .= $this->review_summary_table($summarydata, $page); - $output .= $this->review_form($page, $showall, $displayoptions, - $this->questions($attemptobj, true, $slots, $page, $showall, $displayoptions), - $attemptobj); - - $output .= $this->review_next_navigation($attemptobj, $page, $lastpage, $showall); - $output .= $this->footer(); - return $output; - } - - /** - * Renders the review question pop-up. - * - * @param quiz_attempt $attemptobj an instance of quiz_attempt. - * @param int $slot which question to display. - * @param int $seq which step of the question attempt to show. null = latest. - * @param display_options $displayoptions instance of display_options. - * @param array $summarydata contains all table data - * @return $output containing html data. - */ - public function review_question_page(quiz_attempt $attemptobj, $slot, $seq, - display_options $displayoptions, $summarydata) { - - $output = ''; - $output .= $this->header(); - $output .= $this->review_summary_table($summarydata, 0); - - if (!is_null($seq)) { - $output .= $attemptobj->render_question_at_step($slot, $seq, true, $this); - } else { - $output .= $attemptobj->render_question($slot, true, $this); - } - - $output .= $this->close_window_button(); - $output .= $this->footer(); - return $output; - } - - /** - * Renders the review question pop-up. - * - * @param quiz_attempt $attemptobj an instance of quiz_attempt. - * @param string $message Why the review is not allowed. - * @return string html to output. - */ - public function review_question_not_allowed(quiz_attempt $attemptobj, $message) { - $output = ''; - $output .= $this->header(); - $output .= $this->heading(format_string($attemptobj->get_quiz_name(), true, - array("context" => $attemptobj->get_quizobj()->get_context()))); - $output .= $this->notification($message); - $output .= $this->close_window_button(); - $output .= $this->footer(); - return $output; - } - - /** - * Filters the summarydata array. - * - * @param array $summarydata contains row data for table - * @param int $page the current page number - * @return $summarydata containing filtered row data - */ - protected function filter_review_summary_table($summarydata, $page) { - if ($page == 0) { - return $summarydata; - } - - // Only show some of summary table on subsequent pages. - foreach ($summarydata as $key => $rowdata) { - if (!in_array($key, array('user', 'attemptlist'))) { - unset($summarydata[$key]); - } - } - - return $summarydata; - } - - /** - * Outputs the table containing data from summary data array - * - * @param array $summarydata contains row data for table - * @param int $page contains the current page number - */ - public function review_summary_table($summarydata, $page) { - $summarydata = $this->filter_review_summary_table($summarydata, $page); - if (empty($summarydata)) { - return ''; - } - - $output = ''; - $output .= html_writer::start_tag('table', array( - 'class' => 'generaltable generalbox quizreviewsummary')); - $output .= html_writer::start_tag('tbody'); - foreach ($summarydata as $rowdata) { - if ($rowdata['title'] instanceof renderable) { - $title = $this->render($rowdata['title']); - } else { - $title = $rowdata['title']; - } - - if ($rowdata['content'] instanceof renderable) { - $content = $this->render($rowdata['content']); - } else { - $content = $rowdata['content']; - } - - $output .= html_writer::tag('tr', - html_writer::tag('th', $title, array('class' => 'cell', 'scope' => 'row')) . - html_writer::tag('td', $content, array('class' => 'cell')) - ); - } - - $output .= html_writer::end_tag('tbody'); - $output .= html_writer::end_tag('table'); - return $output; - } - - /** - * Renders each question - * - * @param quiz_attempt $attemptobj instance of quiz_attempt - * @param bool $reviewing - * @param array $slots array of intgers relating to questions - * @param int $page current page number - * @param bool $showall if true shows attempt on single page - * @param display_options $displayoptions instance of display_options - */ - public function questions(quiz_attempt $attemptobj, $reviewing, $slots, $page, $showall, - display_options $displayoptions) { - $output = ''; - foreach ($slots as $slot) { - $output .= $attemptobj->render_question($slot, $reviewing, $this, - $attemptobj->review_url($slot, $page, $showall)); - } - return $output; - } - - /** - * Renders the main bit of the review page. - * - * @param array $summarydata contain row data for table - * @param int $page current page number - * @param display_options $displayoptions instance of display_options - * @param $content contains each question - * @param quiz_attempt $attemptobj instance of quiz_attempt - * @param bool $showall if true display attempt on one page - */ - public function review_form($page, $showall, $displayoptions, $content, $attemptobj) { - if ($displayoptions->flags != question_display_options::EDITABLE) { - return $content; - } - - $this->page->requires->js_init_call('M.mod_quiz.init_review_form', null, false, - quiz_get_js_module()); - - $output = ''; - $output .= html_writer::start_tag('form', array('action' => $attemptobj->review_url(null, - $page, $showall), 'method' => 'post', 'class' => 'questionflagsaveform')); - $output .= html_writer::start_tag('div'); - $output .= $content; - $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', - 'value' => sesskey())); - $output .= html_writer::start_tag('div', array('class' => 'submitbtns')); - $output .= html_writer::empty_tag('input', array('type' => 'submit', - 'class' => 'questionflagsavebutton btn btn-secondary', 'name' => 'savingflags', - 'value' => get_string('saveflags', 'question'))); - $output .= html_writer::end_tag('div'); - $output .= html_writer::end_tag('div'); - $output .= html_writer::end_tag('form'); - - return $output; - } - - /** - * Returns either a liink or button - * - * @param quiz_attempt $attemptobj instance of quiz_attempt - */ - public function finish_review_link(quiz_attempt $attemptobj) { - $url = $attemptobj->view_url(); - - if ($attemptobj->get_access_manager(time())->attempt_must_be_in_popup()) { - $this->page->requires->js_init_call('M.mod_quiz.secure_window.init_close_button', - array($url), false, quiz_get_js_module()); - return html_writer::empty_tag('input', array('type' => 'button', - 'value' => get_string('finishreview', 'quiz'), - 'id' => 'secureclosebutton', - 'class' => 'mod_quiz-next-nav btn btn-primary')); - - } else { - return html_writer::link($url, get_string('finishreview', 'quiz'), - array('class' => 'mod_quiz-next-nav')); - } - } - - /** - * Creates the navigation links/buttons at the bottom of the reivew attempt page. - * - * Note, the name of this function is no longer accurate, but when the design - * changed, it was decided to keep the old name for backwards compatibility. - * - * @param quiz_attempt $attemptobj instance of quiz_attempt - * @param int $page the current page - * @param bool $lastpage if true current page is the last page - * @param bool|null $showall if true, the URL will be to review the entire attempt on one page, - * and $page will be ignored. If null, a sensible default will be chosen. - * - * @return string HTML fragment. - */ - public function review_next_navigation(quiz_attempt $attemptobj, $page, $lastpage, $showall = null) { - $nav = ''; - if ($page > 0) { - $nav .= link_arrow_left(get_string('navigateprevious', 'quiz'), - $attemptobj->review_url(null, $page - 1, $showall), false, 'mod_quiz-prev-nav'); - } - if ($lastpage) { - $nav .= $this->finish_review_link($attemptobj); - } else { - $nav .= link_arrow_right(get_string('navigatenext', 'quiz'), - $attemptobj->review_url(null, $page + 1, $showall), false, 'mod_quiz-next-nav'); - } - return html_writer::tag('div', $nav, array('class' => 'submitbtns')); - } - - /** - * Return the HTML of the quiz timer. - * @return string HTML content. - */ - public function countdown_timer(quiz_attempt $attemptobj, $timenow) { - - $timeleft = $attemptobj->get_time_left_display($timenow); - if ($timeleft !== false) { - $ispreview = $attemptobj->is_preview(); - $timerstartvalue = $timeleft; - if (!$ispreview) { - // Make sure the timer starts just above zero. If $timeleft was <= 0, then - // this will just have the effect of causing the quiz to be submitted immediately. - $timerstartvalue = max($timerstartvalue, 1); - } - $this->initialise_timer($timerstartvalue, $ispreview); - } - - - return $this->output->render_from_template('mod_quiz/timer', (object)[]); - } - - /** - * Create a preview link - * - * @param moodle_url $url contains a url to the given page - */ - public function restart_preview_button($url) { - return $this->single_button($url, get_string('startnewpreview', 'quiz')); - } - - /** - * Outputs the navigation block panel - * - * @param quiz_nav_panel_base $panel instance of quiz_nav_panel_base - */ - public function navigation_panel(quiz_nav_panel_base $panel) { - - $output = ''; - $userpicture = $panel->user_picture(); - if ($userpicture) { - $fullname = fullname($userpicture->user); - if ($userpicture->size === true) { - $fullname = html_writer::div($fullname); - } - $output .= html_writer::tag('div', $this->render($userpicture) . $fullname, - array('id' => 'user-picture', 'class' => 'clearfix')); - } - $output .= $panel->render_before_button_bits($this); - - $bcc = $panel->get_button_container_class(); - $output .= html_writer::start_tag('div', array('class' => "qn_buttons clearfix $bcc")); - foreach ($panel->get_question_buttons() as $button) { - $output .= $this->render($button); - } - $output .= html_writer::end_tag('div'); - - $output .= html_writer::tag('div', $panel->render_end_bits($this), - array('class' => 'othernav')); - - $this->page->requires->js_init_call('M.mod_quiz.nav.init', null, false, - quiz_get_js_module()); - - return $output; - } - - /** - * Display a quiz navigation button. - * - * @param quiz_nav_question_button $button - * @return string HTML fragment. - */ - protected function render_quiz_nav_question_button(quiz_nav_question_button $button) { - $classes = array('qnbutton', $button->stateclass, $button->navmethod, 'btn'); - $extrainfo = array(); - - if ($button->currentpage) { - $classes[] = 'thispage'; - $extrainfo[] = get_string('onthispage', 'quiz'); - } - - // Flagged? - if ($button->flagged) { - $classes[] = 'flagged'; - $flaglabel = get_string('flagged', 'question'); - } else { - $flaglabel = ''; - } - $extrainfo[] = html_writer::tag('span', $flaglabel, array('class' => 'flagstate')); - - if (is_numeric($button->number)) { - $qnostring = 'questionnonav'; - } else { - $qnostring = 'questionnonavinfo'; - } - - $a = new stdClass(); - $a->number = $button->number; - $a->attributes = implode(' ', $extrainfo); - $tagcontents = html_writer::tag('span', '', array('class' => 'thispageholder')) . - html_writer::tag('span', '', array('class' => 'trafficlight')) . - get_string($qnostring, 'quiz', $a); - $tagattributes = array('class' => implode(' ', $classes), 'id' => $button->id, - 'title' => $button->statestring, 'data-quiz-page' => $button->page); - - if ($button->url) { - return html_writer::link($button->url, $tagcontents, $tagattributes); - } else { - return html_writer::tag('span', $tagcontents, $tagattributes); - } - } - - /** - * Display a quiz navigation heading. - * - * @param quiz_nav_section_heading $heading the heading. - * @return string HTML fragment. - */ - protected function render_quiz_nav_section_heading(quiz_nav_section_heading $heading) { - if (empty($heading->heading)) { - $headingtext = get_string('sectionnoname', 'quiz'); - $class = ' dimmed_text'; - } else { - $headingtext = $heading->heading; - $class = ''; - } - return $this->heading($headingtext, 3, 'mod_quiz-section-heading' . $class); - } - - /** - * Renders a list of links the other attempts. - * - * @param links_to_other_attempts $links - * @return string HTML fragment. - */ - protected function render_links_to_other_attempts( - links_to_other_attempts $links) { - $attemptlinks = array(); - foreach ($links->links as $attempt => $url) { - if (!$url) { - $attemptlinks[] = html_writer::tag('strong', $attempt); - } else if ($url instanceof renderable) { - $attemptlinks[] = $this->render($url); - } else { - $attemptlinks[] = html_writer::link($url, $attempt); - } - } - return implode(', ', $attemptlinks); - } - - /** - * Render the 'start attempt' page. - * - * The student gets here if their interaction with the preflight check - * from fails in some way (e.g. they typed the wrong password). - * - * @param quiz $quizobj - * @param preflight_check_form $mform - * @return string - */ - public function start_attempt_page(quiz $quizobj, preflight_check_form $mform) { - $output = ''; - $output .= $this->header(); - $output .= $this->during_attempt_tertiary_nav($quizobj->view_url()); - $output .= $this->heading(format_string($quizobj->get_quiz_name(), true, - array("context" => $quizobj->get_context()))); - $output .= $this->quiz_intro($quizobj->get_quiz(), $quizobj->get_cm()); - $output .= $mform->render(); - $output .= $this->footer(); - return $output; - } - - /** - * Attempt Page - * - * @param quiz_attempt $attemptobj Instance of quiz_attempt - * @param int $page Current page number - * @param access_manager $accessmanager Instance of access_manager - * @param array $messages An array of messages - * @param array $slots Contains an array of integers that relate to questions - * @param int $id The ID of an attempt - * @param int $nextpage The number of the next page - * @return string HTML to output. - */ - public function attempt_page($attemptobj, $page, $accessmanager, $messages, $slots, $id, - $nextpage) { - $output = ''; - $output .= $this->header(); - $output .= $this->during_attempt_tertiary_nav($attemptobj->view_url()); - $output .= $this->quiz_notices($messages); - $output .= $this->countdown_timer($attemptobj, time()); - $output .= $this->attempt_form($attemptobj, $page, $slots, $id, $nextpage); - $output .= $this->footer(); - return $output; - } - - /** - * Render the tertiary navigation for pages during the attempt. - * - * @param string|moodle_url $quizviewurl url of the view.php page for this quiz. - * @return string HTML to output. - */ - public function during_attempt_tertiary_nav($quizviewurl): string { - $output = ''; - $output .= html_writer::start_div('container-fluid tertiary-navigation'); - $output .= html_writer::start_div('row'); - $output .= html_writer::start_div('navitem'); - $output .= html_writer::link($quizviewurl, get_string('back'), - ['class' => 'btn btn-secondary']); - $output .= html_writer::end_div(); - $output .= html_writer::end_div(); - $output .= html_writer::end_div(); - return $output; - } - - /** - * Returns any notices. - * - * @param array $messages - */ - public function quiz_notices($messages) { - if (!$messages) { - return ''; - } - return $this->notification( - html_writer::tag('p', get_string('accessnoticesheader', 'quiz')) . $this->access_messages($messages), - 'warning', - false - ); - } - - /** - * Ouputs the form for making an attempt - * - * @param quiz_attempt $attemptobj - * @param int $page Current page number - * @param array $slots Array of integers relating to questions - * @param int $id ID of the attempt - * @param int $nextpage Next page number - */ - public function attempt_form($attemptobj, $page, $slots, $id, $nextpage) { - $output = ''; - - // Start the form. - $output .= html_writer::start_tag('form', - array('action' => new moodle_url($attemptobj->processattempt_url(), - array('cmid' => $attemptobj->get_cmid())), 'method' => 'post', - 'enctype' => 'multipart/form-data', 'accept-charset' => 'utf-8', - 'id' => 'responseform')); - $output .= html_writer::start_tag('div'); - - // Print all the questions. - foreach ($slots as $slot) { - $output .= $attemptobj->render_question($slot, false, $this, - $attemptobj->attempt_url($slot, $page), $this); - } - - $navmethod = $attemptobj->get_quiz()->navmethod; - $output .= $this->attempt_navigation_buttons($page, $attemptobj->is_last_page($page), $navmethod); - - // Some hidden fields to trach what is going on. - $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'attempt', - 'value' => $attemptobj->get_attemptid())); - $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'thispage', - 'value' => $page, 'id' => 'followingpage')); - $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'nextpage', - 'value' => $nextpage)); - $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'timeup', - 'value' => '0', 'id' => 'timeup')); - $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', - 'value' => sesskey())); - $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'scrollpos', - 'value' => '', 'id' => 'scrollpos')); - - // Add a hidden field with questionids. Do this at the end of the form, so - // if you navigate before the form has finished loading, it does not wipe all - // the student's answers. - $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'slots', - 'value' => implode(',', $attemptobj->get_active_slots($page)))); - - // Finish the form. - $output .= html_writer::end_tag('div'); - $output .= html_writer::end_tag('form'); - - $output .= $this->connection_warning(); - - return $output; - } - - /** - * Display the prev/next buttons that go at the bottom of each page of the attempt. - * - * @param int $page the page number. Starts at 0 for the first page. - * @param bool $lastpage is this the last page in the quiz? - * @param string $navmethod Optional quiz attribute, 'free' (default) or 'sequential' - * @return string HTML fragment. - */ - protected function attempt_navigation_buttons($page, $lastpage, $navmethod = 'free') { - $output = ''; - - $output .= html_writer::start_tag('div', array('class' => 'submitbtns')); - if ($page > 0 && $navmethod == 'free') { - $output .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'previous', - 'value' => get_string('navigateprevious', 'quiz'), 'class' => 'mod_quiz-prev-nav btn btn-secondary', - 'id' => 'mod_quiz-prev-nav')); - $this->page->requires->js_call_amd('core_form/submit', 'init', ['mod_quiz-prev-nav']); - } - if ($lastpage) { - $nextlabel = get_string('endtest', 'quiz'); - } else { - $nextlabel = get_string('navigatenext', 'quiz'); - } - $output .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'next', - 'value' => $nextlabel, 'class' => 'mod_quiz-next-nav btn btn-primary', 'id' => 'mod_quiz-next-nav')); - $output .= html_writer::end_tag('div'); - $this->page->requires->js_call_amd('core_form/submit', 'init', ['mod_quiz-next-nav']); - - return $output; - } - - /** - * Render a button which allows students to redo a question in the attempt. - * - * @param int $slot the number of the slot to generate the button for. - * @param bool $disabled if true, output the button disabled. - * @return string HTML fragment. - */ - public function redo_question_button($slot, $disabled) { - $attributes = array('type' => 'submit', 'name' => 'redoslot' . $slot, - 'value' => get_string('redoquestion', 'quiz'), - 'class' => 'mod_quiz-redo_question_button btn btn-secondary'); - if ($disabled) { - $attributes['disabled'] = 'disabled'; - } - return html_writer::div(html_writer::empty_tag('input', $attributes)); - } - - /** - * Output the JavaScript required to initialise the countdown timer. - * @param int $timerstartvalue time remaining, in seconds. - */ - public function initialise_timer($timerstartvalue, $ispreview) { - $options = array($timerstartvalue, (bool)$ispreview); - $this->page->requires->js_init_call('M.mod_quiz.timer.init', $options, false, quiz_get_js_module()); - } - - /** - * Output a page with an optional message, and JavaScript code to close the - * current window and redirect the parent window to a new URL. - * @param moodle_url $url the URL to redirect the parent window to. - * @param string $message message to display before closing the window. (optional) - * @return string HTML to output. - */ - public function close_attempt_popup($url, $message = '') { - $output = ''; - $output .= $this->header(); - $output .= $this->box_start(); - - if ($message) { - $output .= html_writer::tag('p', $message); - $output .= html_writer::tag('p', get_string('windowclosing', 'quiz')); - $delay = 5; - } else { - $output .= html_writer::tag('p', get_string('pleaseclose', 'quiz')); - $delay = 0; - } - $this->page->requires->js_init_call('M.mod_quiz.secure_window.close', - array($url, $delay), false, quiz_get_js_module()); - - $output .= $this->box_end(); - $output .= $this->footer(); - return $output; - } - - /** - * Print each message in an array, surrounded by <p>, </p> tags. - * - * @param array $messages the array of message strings. - * @param bool $return if true, return a string, instead of outputting. - * - * @return string HTML to output. - */ - public function access_messages($messages) { - $output = ''; - foreach ($messages as $message) { - $output .= html_writer::tag('p', $message, ['class' => 'text-left']); - } - return $output; - } - - /* - * Summary Page - */ - /** - * Create the summary page - * - * @param quiz_attempt $attemptobj - * @param display_options $displayoptions - */ - public function summary_page($attemptobj, $displayoptions) { - $output = ''; - $output .= $this->header(); - $output .= $this->during_attempt_tertiary_nav($attemptobj->view_url()); - $output .= $this->heading(format_string($attemptobj->get_quiz_name())); - $output .= $this->heading(get_string('summaryofattempt', 'quiz'), 3); - $output .= $this->summary_table($attemptobj, $displayoptions); - $output .= $this->summary_page_controls($attemptobj); - $output .= $this->footer(); - return $output; - } - - /** - * Generates the table of summarydata - * - * @param quiz_attempt $attemptobj - * @param display_options $displayoptions - */ - public function summary_table($attemptobj, $displayoptions) { - // Prepare the summary table header. - $table = new html_table(); - $table->attributes['class'] = 'generaltable quizsummaryofattempt boxaligncenter'; - $table->head = array(get_string('question', 'quiz'), get_string('status', 'quiz')); - $table->align = array('left', 'left'); - $table->size = array('', ''); - $markscolumn = $displayoptions->marks >= question_display_options::MARK_AND_MAX; - if ($markscolumn) { - $table->head[] = get_string('marks', 'quiz'); - $table->align[] = 'left'; - $table->size[] = ''; - } - $tablewidth = count($table->align); - $table->data = array(); - - // Get the summary info for each question. - $slots = $attemptobj->get_slots(); - foreach ($slots as $slot) { - // Add a section headings if we need one here. - $heading = $attemptobj->get_heading_before_slot($slot); - if ($heading !== null) { - // There is a heading here. - $rowclasses = 'quizsummaryheading'; - if ($heading) { - $heading = format_string($heading); - } else if (count($attemptobj->get_quizobj()->get_sections()) > 1) { - // If this is the start of an unnamed section, and the quiz has more - // than one section, then add a default heading. - $heading = get_string('sectionnoname', 'quiz'); - $rowclasses .= ' dimmed_text'; - } - $cell = new html_table_cell(format_string($heading)); - $cell->header = true; - $cell->colspan = $tablewidth; - $table->data[] = array($cell); - $table->rowclasses[] = $rowclasses; - } - - // Don't display information items. - if (!$attemptobj->is_real_question($slot)) { - continue; - } - - // Real question, show it. - $flag = ''; - if ($attemptobj->is_question_flagged($slot)) { - // Quiz has custom JS manipulating these image tags - so we can't use the pix_icon method here. - $flag = html_writer::empty_tag('img', array('src' => $this->image_url('i/flagged'), - 'alt' => get_string('flagged', 'question'), 'class' => 'questionflag icon-post')); - } - if ($attemptobj->can_navigate_to($slot)) { - $row = array(html_writer::link($attemptobj->attempt_url($slot), - $attemptobj->get_question_number($slot) . $flag), - $attemptobj->get_question_status($slot, $displayoptions->correctness)); - } else { - $row = array($attemptobj->get_question_number($slot) . $flag, - $attemptobj->get_question_status($slot, $displayoptions->correctness)); - } - if ($markscolumn) { - $row[] = $attemptobj->get_question_mark($slot); - } - $table->data[] = $row; - $table->rowclasses[] = 'quizsummary' . $slot . ' ' . $attemptobj->get_question_state_class( - $slot, $displayoptions->correctness); - } - - // Print the summary table. - $output = html_writer::table($table); - - return $output; - } - - /** - * Creates any controls a the page should have. - * - * @param quiz_attempt $attemptobj - */ - public function summary_page_controls($attemptobj) { - $output = ''; - - // Return to place button. - if ($attemptobj->get_state() == quiz_attempt::IN_PROGRESS) { - $button = new single_button( - new moodle_url($attemptobj->attempt_url(null, $attemptobj->get_currentpage())), - get_string('returnattempt', 'quiz')); - $output .= $this->container($this->container($this->render($button), - 'controls'), 'submitbtns mdl-align'); - } - - // Finish attempt button. - $options = array( - 'attempt' => $attemptobj->get_attemptid(), - 'finishattempt' => 1, - 'timeup' => 0, - 'slots' => '', - 'cmid' => $attemptobj->get_cmid(), - 'sesskey' => sesskey(), - ); - - $button = new single_button( - new moodle_url($attemptobj->processattempt_url(), $options), - get_string('submitallandfinish', 'quiz')); - $button->id = 'responseform'; - $button->class = 'btn-finishattempt'; - $button->formid = 'frm-finishattempt'; - if ($attemptobj->get_state() == quiz_attempt::IN_PROGRESS) { - $totalunanswered = 0; - if ($attemptobj->get_quiz()->navmethod == 'free') { - // Only count the unanswered question if the navigation method is set to free. - $totalunanswered = $attemptobj->get_number_of_unanswered_questions(); - } - $this->page->requires->js_call_amd('mod_quiz/submission_confirmation', 'init', [$totalunanswered]); - } - $button->primary = true; - - $duedate = $attemptobj->get_due_date(); - $message = ''; - if ($attemptobj->get_state() == quiz_attempt::OVERDUE) { - $message = get_string('overduemustbesubmittedby', 'quiz', userdate($duedate)); - - } else if ($duedate) { - $message = get_string('mustbesubmittedby', 'quiz', userdate($duedate)); - } - - $output .= $this->countdown_timer($attemptobj, time()); - $output .= $this->container($message . $this->container( - $this->render($button), 'controls'), 'submitbtns mdl-align'); - - return $output; - } - - /* - * View Page - */ - /** - * Generates the view page - * - * @param stdClass $course the course settings row from the database. - * @param stdClass $quiz the quiz settings row from the database. - * @param stdClass $cm the course_module settings row from the database. - * @param context_module $context the quiz context. - * @param view_page $viewobj - * @return string HTML to display - */ - public function view_page($course, $quiz, $cm, $context, $viewobj) { - $output = ''; - - $output .= $this->view_page_tertiary_nav($viewobj); - $output .= $this->view_information($quiz, $cm, $context, $viewobj->infomessages); - $output .= $this->view_table($quiz, $context, $viewobj); - $output .= $this->view_result_info($quiz, $context, $cm, $viewobj); - $output .= $this->box($this->view_page_buttons($viewobj), 'quizattempt'); - return $output; - } - - /** - * Render the tertiary navigation for the view page. - * - * @param view_page $viewobj the information required to display the view page. - * @return string HTML to output. - */ - public function view_page_tertiary_nav(view_page $viewobj): string { - $content = ''; - - if ($viewobj->buttontext) { - $attemptbtn = $this->start_attempt_button($viewobj->buttontext, - $viewobj->startattempturl, $viewobj->preflightcheckform, - $viewobj->popuprequired, $viewobj->popupoptions); - $content .= $attemptbtn; - } - - if ($viewobj->canedit && !$viewobj->quizhasquestions) { - $content .= html_writer::link($viewobj->editurl, get_string('addquestion', 'quiz'), - ['class' => 'btn btn-secondary']); - } - - if ($content) { - return html_writer::div(html_writer::div($content, 'row'), 'container-fluid tertiary-navigation'); - } else { - return ''; - } - } - - /** - * Work out, and render, whatever buttons, and surrounding info, should appear - * at the end of the review page. - * - * @param view_page $viewobj the information required to display the view page. - * @return string HTML to output. - */ - public function view_page_buttons(view_page $viewobj) { - $output = ''; - - if (!$viewobj->quizhasquestions) { - $output .= html_writer::div( - $this->notification(get_string('noquestions', 'quiz'), 'warning', false), - 'text-left mb-3'); - } - $output .= $this->access_messages($viewobj->preventmessages); - - if ($viewobj->showbacktocourse) { - $output .= $this->single_button($viewobj->backtocourseurl, - get_string('backtocourse', 'quiz'), 'get', - array('class' => 'continuebutton')); - } - - return $output; - } - - /** - * Generates the view attempt button - * - * @param string $buttontext the label to display on the button. - * @param moodle_url $url The URL to POST to in order to start the attempt. - * @param preflight_check_form $preflightcheckform deprecated. - * @param bool $popuprequired whether the attempt needs to be opened in a pop-up. - * @param array $popupoptions the options to use if we are opening a popup. - * @return string HTML fragment. - */ - public function start_attempt_button($buttontext, moodle_url $url, - preflight_check_form $preflightcheckform = null, - $popuprequired = false, $popupoptions = null) { - - $button = new single_button($url, $buttontext, 'post', true); - $button->class .= ' quizstartbuttondiv'; - if ($popuprequired) { - $button->class .= ' quizsecuremoderequired'; - } - - $popupjsoptions = null; - if ($popuprequired && $popupoptions) { - $action = new popup_action('click', $url, 'popup', $popupoptions); - $popupjsoptions = $action->get_js_options(); - } - - if ($preflightcheckform) { - $checkform = $preflightcheckform->render(); - } else { - $checkform = null; - } - - $this->page->requires->js_call_amd('mod_quiz/preflightcheck', 'init', - array('.quizstartbuttondiv [type=submit]', get_string('startattempt', 'quiz'), - '#mod_quiz_preflight_form', $popupjsoptions)); - - return $this->render($button) . $checkform; - } - - /** - * Generate a message saying that this quiz has no questions, with a button to - * go to the edit page, if the user has the right capability. - * - * @param bool $canedit can the current user edit the quiz? - * @param moodle_url $editurl URL of the edit quiz page. - * @return string HTML to output. - * - * @deprecated since Moodle 4.0 MDL-71915 - please do not use this function any more. - */ - public function no_questions_message($canedit, $editurl) { - debugging('no_questions_message() is deprecated, please use generate_no_questions_message() instead.', DEBUG_DEVELOPER); - - $output = html_writer::start_tag('div', array('class' => 'card text-center mb-3')); - $output .= html_writer::start_tag('div', array('class' => 'card-body')); - - $output .= $this->notification(get_string('noquestions', 'quiz'), 'warning', false); - if ($canedit) { - $output .= $this->single_button($editurl, get_string('editquiz', 'quiz'), 'get'); - } - $output .= html_writer::end_tag('div'); - $output .= html_writer::end_tag('div'); - - return $output; - } - - /** - * Outputs an error message for any guests accessing the quiz - * - * @param stdClass $course the course settings row from the database. - * @param stdClass $quiz the quiz settings row from the database. - * @param stdClass $cm the course_module settings row from the database. - * @param context_module $context the quiz context. - * @param array $messages Array containing any messages - * @param view_page $viewobj - */ - public function view_page_guest($course, $quiz, $cm, $context, $messages, $viewobj) { - $output = ''; - $output .= $this->view_page_tertiary_nav($viewobj); - $output .= $this->view_information($quiz, $cm, $context, $messages); - $guestno = html_writer::tag('p', get_string('guestsno', 'quiz')); - $liketologin = html_writer::tag('p', get_string('liketologin')); - $referer = get_local_referer(false); - $output .= $this->confirm($guestno."\n\n".$liketologin."\n", get_login_url(), $referer); - return $output; - } - - /** - * Outputs and error message for anyone who is not enrolle don the course - * - * @param stdClass $course the course settings row from the database. - * @param stdClass $quiz the quiz settings row from the database. - * @param stdClass $cm the course_module settings row from the database. - * @param context_module $context the quiz context. - * @param array $messages Array containing any messages - * @param view_page $viewobj - */ - public function view_page_notenrolled($course, $quiz, $cm, $context, $messages, $viewobj) { - global $CFG; - $output = ''; - $output .= $this->view_page_tertiary_nav($viewobj); - $output .= $this->view_information($quiz, $cm, $context, $messages); - $youneedtoenrol = html_writer::tag('p', get_string('youneedtoenrol', 'quiz')); - $button = html_writer::tag('p', - $this->continue_button($CFG->wwwroot . '/course/view.php?id=' . $course->id)); - $output .= $this->box($youneedtoenrol."\n\n".$button."\n", 'generalbox', 'notice'); - return $output; - } - - /** - * Output the page information - * - * @param object $quiz the quiz settings. - * @param object $cm the course_module object. - * @param context $context the quiz context. - * @param array $messages any access messages that should be described. - * @param bool $quizhasquestions does quiz has questions added. - * @return string HTML to output. - */ - public function view_information($quiz, $cm, $context, $messages, bool $quizhasquestions = false) { - $output = ''; - - // Output any access messages. - if ($messages) { - $output .= $this->box($this->access_messages($messages), 'quizinfo'); - } - - // Show number of attempts summary to those who can view reports. - if (has_capability('mod/quiz:viewreports', $context)) { - if ($strattemptnum = $this->quiz_attempt_summary_link_to_reports($quiz, $cm, - $context)) { - $output .= html_writer::tag('div', $strattemptnum, - array('class' => 'quizattemptcounts')); - } - } - - if (has_any_capability(['mod/quiz:manageoverrides', 'mod/quiz:viewoverrides'], $context)) { - if ($overrideinfo = $this->quiz_override_summary_links($quiz, $cm)) { - $output .= html_writer::tag('div', $overrideinfo, ['class' => 'quizattemptcounts']); - } - } - - return $output; - } - - /** - * Output the quiz intro. - * @param object $quiz the quiz settings. - * @param object $cm the course_module object. - * @return string HTML to output. - */ - public function quiz_intro($quiz, $cm) { - if (html_is_blank($quiz->intro)) { - return ''; - } - - return $this->box(format_module_intro('quiz', $quiz, $cm->id), 'generalbox', 'intro'); - } - - /** - * Generates the table heading. - */ - public function view_table_heading() { - return $this->heading(get_string('summaryofattempts', 'quiz'), 3); - } - - /** - * Generates the table of data - * - * @param array $quiz Array contining quiz data - * @param int $context The page context ID - * @param view_page $viewobj - */ - public function view_table($quiz, $context, $viewobj) { - if (!$viewobj->attempts) { - return ''; - } - - // Prepare table header. - $table = new html_table(); - $table->attributes['class'] = 'generaltable quizattemptsummary'; - $table->head = array(); - $table->align = array(); - $table->size = array(); - if ($viewobj->attemptcolumn) { - $table->head[] = get_string('attemptnumber', 'quiz'); - $table->align[] = 'center'; - $table->size[] = ''; - } - $table->head[] = get_string('attemptstate', 'quiz'); - $table->align[] = 'left'; - $table->size[] = ''; - if ($viewobj->markcolumn) { - $table->head[] = get_string('marks', 'quiz') . ' / ' . - quiz_format_grade($quiz, $quiz->sumgrades); - $table->align[] = 'center'; - $table->size[] = ''; - } - if ($viewobj->gradecolumn) { - $table->head[] = get_string('gradenoun') . ' / ' . - quiz_format_grade($quiz, $quiz->grade); - $table->align[] = 'center'; - $table->size[] = ''; - } - if ($viewobj->canreviewmine) { - $table->head[] = get_string('review', 'quiz'); - $table->align[] = 'center'; - $table->size[] = ''; - } - if ($viewobj->feedbackcolumn) { - $table->head[] = get_string('feedback', 'quiz'); - $table->align[] = 'left'; - $table->size[] = ''; - } - - // One row for each attempt. - foreach ($viewobj->attemptobjs as $attemptobj) { - $attemptoptions = $attemptobj->get_display_options(true); - $row = array(); - - // Add the attempt number. - if ($viewobj->attemptcolumn) { - if ($attemptobj->is_preview()) { - $row[] = get_string('preview', 'quiz'); - } else { - $row[] = $attemptobj->get_attempt_number(); - } - } - - $row[] = $this->attempt_state($attemptobj); - - if ($viewobj->markcolumn) { - if ($attemptoptions->marks >= question_display_options::MARK_AND_MAX && - $attemptobj->is_finished()) { - $row[] = quiz_format_grade($quiz, $attemptobj->get_sum_marks()); - } else { - $row[] = ''; - } - } - - // Ouside the if because we may be showing feedback but not grades. - $attemptgrade = quiz_rescale_grade($attemptobj->get_sum_marks(), $quiz, false); - - if ($viewobj->gradecolumn) { - if ($attemptoptions->marks >= question_display_options::MARK_AND_MAX && - $attemptobj->is_finished()) { - - // Highlight the highest grade if appropriate. - if ($viewobj->overallstats && !$attemptobj->is_preview() - && $viewobj->numattempts > 1 && !is_null($viewobj->mygrade) - && $attemptobj->get_state() == quiz_attempt::FINISHED - && $attemptgrade == $viewobj->mygrade - && $quiz->grademethod == QUIZ_GRADEHIGHEST) { - $table->rowclasses[$attemptobj->get_attempt_number()] = 'bestrow'; - } - - $row[] = quiz_format_grade($quiz, $attemptgrade); - } else { - $row[] = ''; - } - } - - if ($viewobj->canreviewmine) { - $row[] = $viewobj->accessmanager->make_review_link($attemptobj->get_attempt(), - $attemptoptions, $this); - } - - if ($viewobj->feedbackcolumn && $attemptobj->is_finished()) { - if ($attemptoptions->overallfeedback) { - $row[] = quiz_feedback_for_grade($attemptgrade, $quiz, $context); - } else { - $row[] = ''; - } - } - - if ($attemptobj->is_preview()) { - $table->data['preview'] = $row; - } else { - $table->data[$attemptobj->get_attempt_number()] = $row; - } - } // End of loop over attempts. - - $output = ''; - $output .= $this->view_table_heading(); - $output .= html_writer::table($table); - return $output; - } - - /** - * Generate a brief textual desciption of the current state of an attempt. - * @param quiz_attempt $attemptobj the attempt - * @param int $timenow the time to use as 'now'. - * @return string the appropriate lang string to describe the state. - */ - public function attempt_state($attemptobj) { - switch ($attemptobj->get_state()) { - case quiz_attempt::IN_PROGRESS: - return get_string('stateinprogress', 'quiz'); - - case quiz_attempt::OVERDUE: - return get_string('stateoverdue', 'quiz') . html_writer::tag('span', - get_string('stateoverduedetails', 'quiz', - userdate($attemptobj->get_due_date())), - array('class' => 'statedetails')); - - case quiz_attempt::FINISHED: - return get_string('statefinished', 'quiz') . html_writer::tag('span', - get_string('statefinisheddetails', 'quiz', - userdate($attemptobj->get_submitted_date())), - array('class' => 'statedetails')); - - case quiz_attempt::ABANDONED: - return get_string('stateabandoned', 'quiz'); - } - } - - /** - * Generates data pertaining to quiz results - * - * @param array $quiz Array containing quiz data - * @param int $context The page context ID - * @param int $cm The Course Module Id - * @param view_page $viewobj - */ - public function view_result_info($quiz, $context, $cm, $viewobj) { - $output = ''; - if (!$viewobj->numattempts && !$viewobj->gradecolumn && is_null($viewobj->mygrade)) { - return $output; - } - $resultinfo = ''; - - if ($viewobj->overallstats) { - if ($viewobj->moreattempts) { - $a = new stdClass(); - $a->method = quiz_get_grading_option_name($quiz->grademethod); - $a->mygrade = quiz_format_grade($quiz, $viewobj->mygrade); - $a->quizgrade = quiz_format_grade($quiz, $quiz->grade); - $resultinfo .= $this->heading(get_string('gradesofar', 'quiz', $a), 3); - } else { - $a = new stdClass(); - $a->grade = quiz_format_grade($quiz, $viewobj->mygrade); - $a->maxgrade = quiz_format_grade($quiz, $quiz->grade); - $a = get_string('outofshort', 'quiz', $a); - $resultinfo .= $this->heading(get_string('yourfinalgradeis', 'quiz', $a), 3); - } - } - - if ($viewobj->mygradeoverridden) { - - $resultinfo .= html_writer::tag('p', get_string('overriddennotice', 'grades'), - array('class' => 'overriddennotice'))."\n"; - } - if ($viewobj->gradebookfeedback) { - $resultinfo .= $this->heading(get_string('comment', 'quiz'), 3); - $resultinfo .= html_writer::div($viewobj->gradebookfeedback, 'quizteacherfeedback') . "\n"; - } - if ($viewobj->feedbackcolumn) { - $resultinfo .= $this->heading(get_string('overallfeedback', 'quiz'), 3); - $resultinfo .= html_writer::div( - quiz_feedback_for_grade($viewobj->mygrade, $quiz, $context), - 'quizgradefeedback') . "\n"; - } - - if ($resultinfo) { - $output .= $this->box($resultinfo, 'generalbox', 'feedback'); - } - return $output; - } - - /** - * Output either a link to the review page for an attempt, or a button to - * open the review in a popup window. - * - * @param moodle_url $url of the target page. - * @param bool $reviewinpopup whether a pop-up is required. - * @param array $popupoptions options to pass to the popup_action constructor. - * @return string HTML to output. - */ - public function review_link($url, $reviewinpopup, $popupoptions) { - if ($reviewinpopup) { - $button = new single_button($url, get_string('review', 'quiz')); - $button->add_action(new popup_action('click', $url, 'quizpopup', $popupoptions)); - return $this->render($button); - - } else { - return html_writer::link($url, get_string('review', 'quiz'), - array('title' => get_string('reviewthisattempt', 'quiz'))); - } - } - - /** - * Displayed where there might normally be a review link, to explain why the - * review is not available at this time. - * @param string $message optional message explaining why the review is not possible. - * @return string HTML to output. - */ - public function no_review_message($message) { - return html_writer::nonempty_tag('span', $message, - array('class' => 'noreviewmessage')); - } - - /** - * Returns the same as {@link quiz_num_attempt_summary()} but wrapped in a link - * to the quiz reports. - * - * @param stdClass $quiz the quiz object. Only $quiz->id is used at the moment. - * @param stdClass $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid - * fields are used at the moment. - * @param context $context the quiz context. - * @param bool $returnzero if false (default), when no attempts have been made '' is returned - * instead of 'Attempts: 0'. - * @param int $currentgroup if there is a concept of current group where this method is being - * called (e.g. a report) pass it in here. Default 0 which means no current group. - * @return string HTML fragment for the link. - */ - public function quiz_attempt_summary_link_to_reports($quiz, $cm, $context, - $returnzero = false, $currentgroup = 0) { - global $CFG; - $summary = quiz_num_attempt_summary($quiz, $cm, $returnzero, $currentgroup); - if (!$summary) { - return ''; - } - - require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php'); - $url = new moodle_url('/mod/quiz/report.php', array( - 'id' => $cm->id, 'mode' => quiz_report_default_report($context))); - return html_writer::link($url, $summary); - } - - /** - * Render a summary of the number of group and user overrides, with corresponding links. - * - * @param stdClass $quiz the quiz settings. - * @param stdClass|cm_info $cm the cm object. - * @param int $currentgroup currently selected group, if there is one. - * @return string HTML fragment for the link. - */ - public function quiz_override_summary_links(stdClass $quiz, stdClass $cm, $currentgroup = 0): string { - - $baseurl = new moodle_url('/mod/quiz/overrides.php', ['cmid' => $cm->id]); - $counts = quiz_override_summary($quiz, $cm, $currentgroup); - - $links = []; - if ($counts['group']) { - $links[] = html_writer::link(new moodle_url($baseurl, ['mode' => 'group']), - get_string('overridessummarygroup', 'quiz', $counts['group'])); - } - if ($counts['user']) { - $links[] = html_writer::link(new moodle_url($baseurl, ['mode' => 'user']), - get_string('overridessummaryuser', 'quiz', $counts['user'])); - } - - if (!$links) { - return ''; - } - - $links = implode(', ', $links); - switch ($counts['mode']) { - case 'onegroup': - return get_string('overridessummarythisgroup', 'quiz', $links); - - case 'somegroups': - return get_string('overridessummaryyourgroups', 'quiz', $links); - - case 'allgroups': - return get_string('overridessummary', 'quiz', $links); - - default: - throw new coding_exception('Unexpected mode ' . $counts['mode']); - } - } - - /** - * Outputs a chart. - * - * @param \core\chart_base $chart The chart. - * @param string $title The title to display above the graph. - * @param array $attrs extra container html attributes. - * @return string HTML fragment for the graph. - */ - public function chart(\core\chart_base $chart, $title, $attrs = []) { - return $this->heading($title, 3) . html_writer::tag('div', - $this->render($chart), array_merge(['class' => 'graph'], $attrs)); - } - - /** - * Output a graph, or a message saying that GD is required. - * @param moodle_url $url the URL of the graph. - * @param string $title the title to display above the graph. - * @return string HTML fragment for the graph. - */ - public function graph(moodle_url $url, $title) { - global $CFG; - - $graph = html_writer::empty_tag('img', array('src' => $url, 'alt' => $title)); - - return $this->heading($title, 3) . html_writer::tag('div', $graph, array('class' => 'graph')); - } - - /** - * Output the connection warning messages, which are initially hidden, and - * only revealed by JavaScript if necessary. - */ - public function connection_warning() { - $options = array('filter' => false, 'newlines' => false); - $warning = format_text(get_string('connectionerror', 'quiz'), FORMAT_MARKDOWN, $options); - $ok = format_text(get_string('connectionok', 'quiz'), FORMAT_MARKDOWN, $options); - return html_writer::tag('div', $warning, - array('id' => 'connection-error', 'style' => 'display: none;', 'role' => 'alert')) . - html_writer::tag('div', $ok, array('id' => 'connection-ok', 'style' => 'display: none;', 'role' => 'alert')); - } - - - /** - * Deprecated version of render_links_to_other_attempts. - * - * @param links_to_other_attempts $links - * @return string HTML fragment. - * @deprecated since Moodle 4.2. Please use render_links_to_other_attempts instead. - * @todo MDL-76612 Final deprecation in Moodle 4.6 - */ - protected function render_mod_quiz_links_to_other_attempts(links_to_other_attempts $links) { - return $this->render_links_to_other_attempts($links); - } -} +// Normally I would debug-output +// 'This file is no longer required in Moodle 4.2+. Please do not include/require it.' +// but this file gets automatically included by the renderer factories in a way that +// is hard to prevent, so not doing that here. diff --git a/mod/quiz/tests/locallib_test.php b/mod/quiz/tests/locallib_test.php index a5c1809461a..79a93f7a5f4 100644 --- a/mod/quiz/tests/locallib_test.php +++ b/mod/quiz/tests/locallib_test.php @@ -24,6 +24,7 @@ */ namespace mod_quiz; +use mod_quiz\output\renderer; use quiz_attempt; use mod_quiz\question\display_options; @@ -517,7 +518,7 @@ class locallib_test extends \advanced_testcase { $generator = $this->getDataGenerator(); /** @var mod_quiz_generator $quizgenerator */ $quizgenerator = $generator->get_plugin_generator('mod_quiz'); - /** @var mod_quiz_renderer $renderer */ + /** @var renderer $renderer */ $renderer = $PAGE->get_renderer('mod_quiz'); // Course with quiz and a group - plus some others, to verify they don't get counted. diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index c301d840a19..99eb7ce8815 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -42,6 +42,7 @@ This files describes API changes in the quiz code. - quiz_add_random_form => mod_quiz\form\add_random_form - mod_quiz_links_to_other_attempts => mod_quiz\output\links_to_other_attempts - mod_quiz_view_object => mod_quiz\output\view_page + - mod_quiz_renderer => mod_quiz\output\renderer * The following classes have been deprecated: - mod_quiz_overdue_attempt_updater - merged into mod_quiz\task\update_overdue_attempts @@ -59,6 +60,7 @@ This files describes API changes in the quiz code. - mod/quiz/cronlib.php - mod/quiz/override_form.php - mod/quiz/accessrule/accessrulebase.php + - mod/quiz/renderer.php === 4.1 === diff --git a/mod/quiz/view.php b/mod/quiz/view.php index 96badfebe86..67a27968e2d 100644 --- a/mod/quiz/view.php +++ b/mod/quiz/view.php @@ -24,6 +24,7 @@ */ use mod_quiz\access_manager; +use mod_quiz\output\renderer; use mod_quiz\output\view_page; require_once(__DIR__ . '/../../config.php'); @@ -149,7 +150,7 @@ if (html_is_blank($quiz->intro)) { $PAGE->activityheader->set_description(''); } $PAGE->add_body_class('limitedwidth'); -/** @var mod_quiz_renderer $output */ +/** @var renderer $output */ $output = $PAGE->get_renderer('mod_quiz'); // Print table with existing attempts.