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