Differences Between: [Versions 402 and 403]
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\local\reports; 18 19 defined('MOODLE_INTERNAL') || die(); 20 21 require_once($CFG->libdir.'/tablelib.php'); 22 23 use coding_exception; 24 use context_module; 25 use html_writer; 26 use mod_quiz\quiz_attempt; 27 use moodle_url; 28 use popup_action; 29 use question_state; 30 use qubaid_condition; 31 use qubaid_join; 32 use qubaid_list; 33 use question_engine_data_mapper; 34 use stdClass; 35 36 /** 37 * Base class for the table used by a {@see attempts_report}. 38 * 39 * @package mod_quiz 40 * @copyright 2010 The Open University 41 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 42 */ 43 abstract class attempts_report_table extends \table_sql { 44 public $useridfield = 'userid'; 45 46 /** @var moodle_url the URL of this report. */ 47 protected $reporturl; 48 49 /** @var array the display options. */ 50 protected $displayoptions; 51 52 /** 53 * @var array information about the latest step of each question. 54 * Loaded by {@see load_question_latest_steps()}, if applicable. 55 */ 56 protected $lateststeps = null; 57 58 /** @var stdClass the quiz settings for the quiz we are reporting on. */ 59 protected $quiz; 60 61 /** @var context_module the quiz context. */ 62 protected $context; 63 64 /** @var string HTML fragment to select the first/best/last attempt, if appropriate. */ 65 protected $qmsubselect; 66 67 /** @var stdClass attempts_report_options the options affecting this report. */ 68 protected $options; 69 70 /** @var \core\dml\sql_join Contains joins, wheres, params to find students 71 * in the currently selected group, if applicable. 72 */ 73 protected $groupstudentsjoins; 74 75 /** @var \core\dml\sql_join Contains joins, wheres, params to find the students in the course. */ 76 protected $studentsjoins; 77 78 /** @var array the questions that comprise this quiz. */ 79 protected $questions; 80 81 /** @var bool whether to include the column with checkboxes to select each attempt. */ 82 protected $includecheckboxes; 83 84 /** @var string The toggle group name for the checkboxes in the checkbox column. */ 85 protected $togglegroup = 'quiz-attempts'; 86 87 /** @var string strftime format. */ 88 protected $strtimeformat; 89 90 /** @var bool|null used by {@see col_state()} to cache the has_capability result. */ 91 protected $canreopen = null; 92 93 /** 94 * Constructor. 95 * 96 * @param string $uniqueid 97 * @param stdClass $quiz 98 * @param context_module $context 99 * @param string $qmsubselect 100 * @param attempts_report_options $options 101 * @param \core\dml\sql_join $groupstudentsjoins Contains joins, wheres, params 102 * @param \core\dml\sql_join $studentsjoins Contains joins, wheres, params 103 * @param array $questions 104 * @param moodle_url $reporturl 105 */ 106 public function __construct($uniqueid, $quiz, $context, $qmsubselect, 107 attempts_report_options $options, \core\dml\sql_join $groupstudentsjoins, \core\dml\sql_join $studentsjoins, 108 $questions, $reporturl) { 109 parent::__construct($uniqueid); 110 $this->quiz = $quiz; 111 $this->context = $context; 112 $this->qmsubselect = $qmsubselect; 113 $this->groupstudentsjoins = $groupstudentsjoins; 114 $this->studentsjoins = $studentsjoins; 115 $this->questions = $questions; 116 $this->includecheckboxes = $options->checkboxcolumn; 117 $this->reporturl = $reporturl; 118 $this->options = $options; 119 } 120 121 /** 122 * Generate the display of the checkbox column. 123 * 124 * @param stdClass $attempt the table row being output. 125 * @return string HTML content to go inside the td. 126 */ 127 public function col_checkbox($attempt) { 128 global $OUTPUT; 129 130 if ($attempt->attempt) { 131 $checkbox = new \core\output\checkbox_toggleall($this->togglegroup, false, [ 132 'id' => "attemptid_{$attempt->attempt}", 133 'name' => 'attemptid[]', 134 'value' => $attempt->attempt, 135 'label' => get_string('selectattempt', 'quiz'), 136 'labelclasses' => 'accesshide', 137 ]); 138 return $OUTPUT->render($checkbox); 139 } else { 140 return ''; 141 } 142 } 143 144 /** 145 * Generate the display of the user's picture column. 146 * 147 * @param stdClass $attempt the table row being output. 148 * @return string HTML content to go inside the td. 149 */ 150 public function col_picture($attempt) { 151 global $OUTPUT; 152 $user = new stdClass(); 153 $additionalfields = explode(',', implode(',', \core_user\fields::get_picture_fields())); 154 $user = username_load_fields_from_object($user, $attempt, null, $additionalfields); 155 $user->id = $attempt->userid; 156 return $OUTPUT->user_picture($user); 157 } 158 159 /** 160 * Generate the display of the user's full name column. 161 * 162 * @param stdClass $attempt the table row being output. 163 * @return string HTML content to go inside the td. 164 */ 165 public function col_fullname($attempt) { 166 $html = parent::col_fullname($attempt); 167 if ($this->is_downloading() || empty($attempt->attempt)) { 168 return $html; 169 } 170 171 return $html . html_writer::empty_tag('br') . html_writer::link( 172 new moodle_url('/mod/quiz/review.php', ['attempt' => $attempt->attempt]), 173 get_string('reviewattempt', 'quiz'), ['class' => 'reviewlink']); 174 } 175 176 /** 177 * Generate the display of the attempt state column. 178 * 179 * @param stdClass $attempt the table row being output. 180 * @return string HTML content to go inside the td. 181 */ 182 public function col_state($attempt) { 183 if (is_null($attempt->attempt)) { 184 return '-'; 185 } 186 187 $display = quiz_attempt::state_name($attempt->state); 188 if ($this->is_downloading()) { 189 return $display; 190 } 191 192 $this->canreopen ??= has_capability('mod/quiz:reopenattempts', $this->context); 193 if ($attempt->state == quiz_attempt::ABANDONED && $this->canreopen) { 194 $display .= ' ' . html_writer::tag('button', get_string('reopenattempt', 'quiz'), [ 195 'type' => 'button', 196 'class' => 'btn btn-secondary', 197 'data-action' => 'reopen-attempt', 198 'data-attempt-id' => $attempt->attempt, 199 'data-after-action-url' => $this->reporturl->out_as_local_url(false), 200 ]); 201 } 202 203 return $display; 204 } 205 206 /** 207 * Generate the display of the start time column. 208 * 209 * @param stdClass $attempt the table row being output. 210 * @return string HTML content to go inside the td. 211 */ 212 public function col_timestart($attempt) { 213 if ($attempt->attempt) { 214 return userdate($attempt->timestart, $this->strtimeformat); 215 } else { 216 return '-'; 217 } 218 } 219 220 /** 221 * Generate the display of the finish time column. 222 * 223 * @param stdClass $attempt the table row being output. 224 * @return string HTML content to go inside the td. 225 */ 226 public function col_timefinish($attempt) { 227 if ($attempt->attempt && $attempt->timefinish) { 228 return userdate($attempt->timefinish, $this->strtimeformat); 229 } else { 230 return '-'; 231 } 232 } 233 234 /** 235 * Generate the display of the time taken column. 236 * 237 * @param stdClass $attempt the table row being output. 238 * @return string HTML content to go inside the td. 239 */ 240 public function col_duration($attempt) { 241 if ($attempt->timefinish) { 242 return format_time($attempt->timefinish - $attempt->timestart); 243 } else { 244 return '-'; 245 } 246 } 247 248 /** 249 * Generate the display of the feedback column. 250 * 251 * @param stdClass $attempt the table row being output. 252 * @return string HTML content to go inside the td. 253 */ 254 public function col_feedbacktext($attempt) { 255 if ($attempt->state != quiz_attempt::FINISHED) { 256 return '-'; 257 } 258 259 $feedback = quiz_report_feedback_for_grade( 260 quiz_rescale_grade($attempt->sumgrades, $this->quiz, false), 261 $this->quiz->id, $this->context); 262 263 if ($this->is_downloading()) { 264 $feedback = strip_tags($feedback); 265 } 266 267 return $feedback; 268 } 269 270 public function get_row_class($attempt) { 271 if ($this->qmsubselect && $attempt->gradedattempt) { 272 return 'gradedattempt'; 273 } else { 274 return ''; 275 } 276 } 277 278 /** 279 * Make a link to review an individual question in a popup window. 280 * 281 * @param string $data HTML fragment. The text to make into the link. 282 * @param stdClass $attempt data for the row of the table being output. 283 * @param int $slot the number used to identify this question within this usage. 284 */ 285 public function make_review_link($data, $attempt, $slot) { 286 global $OUTPUT, $CFG; 287 288 $flag = ''; 289 if ($this->is_flagged($attempt->usageid, $slot)) { 290 $flag = $OUTPUT->pix_icon('i/flagged', get_string('flagged', 'question'), 291 'moodle', ['class' => 'questionflag']); 292 } 293 294 $feedbackimg = ''; 295 $state = $this->slot_state($attempt, $slot); 296 if ($state && $state->is_finished() && $state != question_state::$needsgrading) { 297 $feedbackimg = $this->icon_for_fraction($this->slot_fraction($attempt, $slot)); 298 } 299 300 $output = html_writer::tag('span', $feedbackimg . html_writer::tag('span', 301 $data, ['class' => $state->get_state_class(true)]) . $flag, ['class' => 'que']); 302 303 $reviewparams = ['attempt' => $attempt->attempt, 'slot' => $slot]; 304 if (isset($attempt->try)) { 305 $reviewparams['step'] = $this->step_no_for_try($attempt->usageid, $slot, $attempt->try); 306 } 307 $url = new moodle_url('/mod/quiz/reviewquestion.php', $reviewparams); 308 $output = $OUTPUT->action_link($url, $output, 309 new popup_action('click', $url, 'reviewquestion', 310 ['height' => 450, 'width' => 650]), 311 ['title' => get_string('reviewresponse', 'quiz')]); 312 313 if (!empty($CFG->enableplagiarism)) { 314 require_once($CFG->libdir . '/plagiarismlib.php'); 315 $output .= plagiarism_get_links([ 316 'context' => $this->context->id, 317 'component' => 'qtype_'.$this->questions[$slot]->qtype, 318 'cmid' => $this->context->instanceid, 319 'area' => $attempt->usageid, 320 'itemid' => $slot, 321 'userid' => $attempt->userid]); 322 } 323 return $output; 324 } 325 326 /** 327 * Get the question attempt state for a particular question in a particular quiz attempt. 328 * 329 * @param stdClass $attempt the row data. 330 * @param int $slot indicates which question. 331 * @return question_state the state of that question. 332 */ 333 protected function slot_state($attempt, $slot) { 334 $stepdata = $this->lateststeps[$attempt->usageid][$slot]; 335 return question_state::get($stepdata->state); 336 } 337 338 /** 339 * Work out if a particular question in a particular attempt has been flagged. 340 * 341 * @param int $questionusageid used to identify the attempt of interest. 342 * @param int $slot identifies which question in the attempt to check. 343 * @return bool true if the question is flagged in the attempt. 344 */ 345 protected function is_flagged($questionusageid, $slot) { 346 $stepdata = $this->lateststeps[$questionusageid][$slot]; 347 return $stepdata->flagged; 348 } 349 350 /** 351 * Get the mark (out of 1) for the question in a particular slot. 352 * 353 * @param stdClass $attempt the row data 354 * @param int $slot which slot to check. 355 * @return float the score for this question on a scale of 0 - 1. 356 */ 357 protected function slot_fraction($attempt, $slot) { 358 $stepdata = $this->lateststeps[$attempt->usageid][$slot]; 359 return $stepdata->fraction; 360 } 361 362 /** 363 * Return an appropriate icon (green tick, red cross, etc.) for a grade. 364 * 365 * @param float $fraction grade on a scale 0..1. 366 * @return string html fragment. 367 */ 368 protected function icon_for_fraction($fraction) { 369 global $OUTPUT; 370 371 $feedbackclass = question_state::graded_state_for_fraction($fraction)->get_feedback_class(); 372 return $OUTPUT->pix_icon('i/grade_' . $feedbackclass, get_string($feedbackclass, 'question'), 373 'moodle', ['class' => 'icon']); 374 } 375 376 /** 377 * Load any extra data after main query. 378 * 379 * At this point you can call {@see get_qubaids_condition} to get the condition 380 * that limits the query to just the question usages shown in this report page or 381 * alternatively for all attempts if downloading a full report. 382 */ 383 protected function load_extra_data() { 384 $this->lateststeps = $this->load_question_latest_steps(); 385 } 386 387 /** 388 * Load information about the latest state of selected questions in selected attempts. 389 * 390 * The results are returned as a two-dimensional array $qubaid => $slot => $dataobject. 391 * 392 * @param qubaid_condition|null $qubaids used to restrict which usages are included 393 * in the query. See {@see qubaid_condition}. 394 * @return array of records. See the SQL in this function to see the fields available. 395 */ 396 protected function load_question_latest_steps(qubaid_condition $qubaids = null) { 397 if ($qubaids === null) { 398 $qubaids = $this->get_qubaids_condition(); 399 } 400 $dm = new question_engine_data_mapper(); 401 $latesstepdata = $dm->load_questions_usages_latest_steps( 402 $qubaids, array_keys($this->questions)); 403 404 $lateststeps = []; 405 foreach ($latesstepdata as $step) { 406 $lateststeps[$step->questionusageid][$step->slot] = $step; 407 } 408 409 return $lateststeps; 410 } 411 412 /** 413 * Does this report require loading any more data after the main query. 414 * 415 * @return bool should {@see query_db()} call {@see load_extra_data}? 416 */ 417 protected function requires_extra_data() { 418 return $this->requires_latest_steps_loaded(); 419 } 420 421 /** 422 * Does this report require the detailed information for each question from the question_attempts_steps table? 423 * 424 * @return bool should {@see load_extra_data} call {@see load_question_latest_steps}? 425 */ 426 protected function requires_latest_steps_loaded() { 427 return false; 428 } 429 430 /** 431 * Is this a column that depends on joining to the latest state information? 432 * 433 * If so, return the corresponding slot. If not, return false. 434 * 435 * @param string $column a column name 436 * @return int|false false if no, else a slot. 437 */ 438 protected function is_latest_step_column($column) { 439 return false; 440 } 441 442 /** 443 * Get any fields that might be needed when sorting on date for a particular slot. 444 * 445 * Note: these values are only used for sorting. The values displayed are taken 446 * from $this->lateststeps loaded in load_extra_data(). 447 * 448 * @param int $slot the slot for the column we want. 449 * @param string $alias the table alias for latest state information relating to that slot. 450 * @return string definitions of extra fields to add to the SELECT list of the query. 451 */ 452 protected function get_required_latest_state_fields($slot, $alias) { 453 return ''; 454 } 455 456 /** 457 * Contruct all the parts of the main database query. 458 * 459 * @param \core\dml\sql_join $allowedstudentsjoins (joins, wheres, params) defines allowed users for the report. 460 * @return array with 4 elements [$fields, $from, $where, $params] that can be used to 461 * build the actual database query. 462 */ 463 public function base_sql(\core\dml\sql_join $allowedstudentsjoins) { 464 global $DB; 465 466 // Please note this uniqueid column is not the same as quiza.uniqueid. 467 $fields = 'DISTINCT ' . $DB->sql_concat('u.id', "'#'", 'COALESCE(quiza.attempt, 0)') . ' AS uniqueid,'; 468 469 if ($this->qmsubselect) { 470 $fields .= "\n(CASE WHEN $this->qmsubselect THEN 1 ELSE 0 END) AS gradedattempt,"; 471 } 472 473 $userfieldsapi = \core_user\fields::for_identity($this->context)->with_name() 474 ->excluding('id', 'idnumber', 'picture', 'imagealt', 'institution', 'department', 'email'); 475 $userfields = $userfieldsapi->get_sql('u', true, '', '', false); 476 477 $fields .= ' 478 quiza.uniqueid AS usageid, 479 quiza.id AS attempt, 480 u.id AS userid, 481 u.idnumber, 482 u.picture, 483 u.imagealt, 484 u.institution, 485 u.department, 486 u.email,' . $userfields->selects . ', 487 quiza.state, 488 quiza.sumgrades, 489 quiza.timefinish, 490 quiza.timestart, 491 CASE WHEN quiza.timefinish = 0 THEN null 492 WHEN quiza.timefinish > quiza.timestart THEN quiza.timefinish - quiza.timestart 493 ELSE 0 END AS duration'; 494 // To explain that last bit, timefinish can be non-zero and less 495 // than timestart when you have two load-balanced servers with very 496 // badly synchronised clocks, and a student does a really quick attempt. 497 498 // This part is the same for all cases. Join the users and quiz_attempts tables. 499 $from = " {user} u"; 500 $from .= "\n{$userfields->joins}"; 501 $from .= "\nLEFT JOIN {quiz_attempts} quiza ON 502 quiza.userid = u.id AND quiza.quiz = :quizid"; 503 $params = array_merge($userfields->params, ['quizid' => $this->quiz->id]); 504 505 if ($this->qmsubselect && $this->options->onlygraded) { 506 $from .= " AND (quiza.state <> :finishedstate OR $this->qmsubselect)"; 507 $params['finishedstate'] = quiz_attempt::FINISHED; 508 } 509 510 switch ($this->options->attempts) { 511 case attempts_report::ALL_WITH: 512 // Show all attempts, including students who are no longer in the course. 513 $where = 'quiza.id IS NOT NULL AND quiza.preview = 0'; 514 break; 515 case attempts_report::ENROLLED_WITH: 516 // Show only students with attempts. 517 $from .= "\n" . $allowedstudentsjoins->joins; 518 $where = "quiza.preview = 0 AND quiza.id IS NOT NULL AND " . $allowedstudentsjoins->wheres; 519 $params = array_merge($params, $allowedstudentsjoins->params); 520 break; 521 case attempts_report::ENROLLED_WITHOUT: 522 // Show only students without attempts. 523 $from .= "\n" . $allowedstudentsjoins->joins; 524 $where = "quiza.id IS NULL AND " . $allowedstudentsjoins->wheres; 525 $params = array_merge($params, $allowedstudentsjoins->params); 526 break; 527 case attempts_report::ENROLLED_ALL: 528 // Show all students with or without attempts. 529 $from .= "\n" . $allowedstudentsjoins->joins; 530 $where = "(quiza.preview = 0 OR quiza.preview IS NULL) AND " . $allowedstudentsjoins->wheres; 531 $params = array_merge($params, $allowedstudentsjoins->params); 532 break; 533 } 534 535 if ($this->options->states) { 536 [$statesql, $stateparams] = $DB->get_in_or_equal($this->options->states, 537 SQL_PARAMS_NAMED, 'state'); 538 $params += $stateparams; 539 $where .= " AND (quiza.state $statesql OR quiza.state IS NULL)"; 540 } 541 542 return [$fields, $from, $where, $params]; 543 } 544 545 /** 546 * Lets subclasses modify the SQL after the count query has been created and before the full query is. 547 * 548 * @param string $fields SELECT list. 549 * @param string $from JOINs part of the SQL. 550 * @param string $where WHERE clauses. 551 * @param array $params Query params. 552 * @return array with 4 elements ($fields, $from, $where, $params) as from base_sql. 553 */ 554 protected function update_sql_after_count($fields, $from, $where, $params) { 555 return [$fields, $from, $where, $params]; 556 } 557 558 /** 559 * Set up the SQL queries (count rows, and get data). 560 * 561 * @param \core\dml\sql_join $allowedjoins (joins, wheres, params) defines allowed users for the report. 562 */ 563 public function setup_sql_queries($allowedjoins) { 564 [$fields, $from, $where, $params] = $this->base_sql($allowedjoins); 565 566 // The WHERE clause is vital here, because some parts of tablelib.php will expect to 567 // add bits like ' AND x = 1' on the end, and that needs to leave to valid SQL. 568 $this->set_count_sql("SELECT COUNT(1) FROM (SELECT $fields FROM $from WHERE $where) temp WHERE 1 = 1", $params); 569 570 [$fields, $from, $where, $params] = $this->update_sql_after_count($fields, $from, $where, $params); 571 $this->set_sql($fields, $from, $where, $params); 572 } 573 574 /** 575 * Add the information about the latest state of the question with slot 576 * $slot to the query. 577 * 578 * The extra information is added as a join to a 579 * 'table' with alias qa$slot, with columns that are a union of 580 * the columns of the question_attempts and question_attempts_states tables. 581 * 582 * @param int $slot the question to add information for. 583 */ 584 protected function add_latest_state_join($slot) { 585 $alias = 'qa' . $slot; 586 587 $fields = $this->get_required_latest_state_fields($slot, $alias); 588 if (!$fields) { 589 return; 590 } 591 592 // This condition roughly filters the list of attempts to be considered. 593 // It is only used in a sub-select to help crappy databases (see MDL-30122) 594 // therefore, it is better to use a very simple join, which may include 595 // too many records, than to do a super-accurate join. 596 $qubaids = new qubaid_join("{quiz_attempts} {$alias}quiza", "{$alias}quiza.uniqueid", 597 "{$alias}quiza.quiz = :{$alias}quizid", ["{$alias}quizid" => $this->sql->params['quizid']]); 598 599 $dm = new question_engine_data_mapper(); 600 [$inlineview, $viewparams] = $dm->question_attempt_latest_state_view($alias, $qubaids); 601 602 $this->sql->fields .= ",\n$fields"; 603 $this->sql->from .= "\nLEFT JOIN $inlineview ON " . 604 "$alias.questionusageid = quiza.uniqueid AND $alias.slot = :{$alias}slot"; 605 $this->sql->params[$alias . 'slot'] = $slot; 606 $this->sql->params = array_merge($this->sql->params, $viewparams); 607 } 608 609 /** 610 * Get an appropriate qubaid_condition for loading more data about the attempts we are displaying. 611 * 612 * @return qubaid_condition 613 */ 614 protected function get_qubaids_condition() { 615 if (is_null($this->rawdata)) { 616 throw new coding_exception( 617 'Cannot call get_qubaids_condition until the main data has been loaded.'); 618 } 619 620 if ($this->is_downloading()) { 621 // We want usages for all attempts. 622 return new qubaid_join("( 623 SELECT DISTINCT quiza.uniqueid 624 FROM " . $this->sql->from . " 625 WHERE " . $this->sql->where . " 626 ) quizasubquery", 'quizasubquery.uniqueid', 627 "1 = 1", $this->sql->params); 628 } 629 630 $qubaids = []; 631 foreach ($this->rawdata as $attempt) { 632 if ($attempt->usageid > 0) { 633 $qubaids[] = $attempt->usageid; 634 } 635 } 636 637 return new qubaid_list($qubaids); 638 } 639 640 public function query_db($pagesize, $useinitialsbar = true) { 641 $doneslots = []; 642 foreach ($this->get_sort_columns() as $column => $notused) { 643 $slot = $this->is_latest_step_column($column); 644 if ($slot && !in_array($slot, $doneslots)) { 645 $this->add_latest_state_join($slot); 646 $doneslots[] = $slot; 647 } 648 } 649 650 parent::query_db($pagesize, $useinitialsbar); 651 652 if ($this->requires_extra_data()) { 653 $this->load_extra_data(); 654 } 655 } 656 657 public function get_sort_columns() { 658 // Add attemptid as a final tie-break to the sort. This ensures that 659 // Attempts by the same student appear in order when just sorting by name. 660 $sortcolumns = parent::get_sort_columns(); 661 $sortcolumns['quiza.id'] = SORT_ASC; 662 return $sortcolumns; 663 } 664 665 public function wrap_html_start() { 666 if ($this->is_downloading() || !$this->includecheckboxes) { 667 return; 668 } 669 670 $url = $this->options->get_url(); 671 $url->param('sesskey', sesskey()); 672 673 echo '<div id="tablecontainer">'; 674 echo '<form id="attemptsform" method="post" action="' . $url->out_omit_querystring() . '">'; 675 676 echo html_writer::input_hidden_params($url); 677 echo '<div>'; 678 } 679 680 public function wrap_html_finish() { 681 global $PAGE; 682 if ($this->is_downloading() || !$this->includecheckboxes) { 683 return; 684 } 685 686 echo '<div id="commands">'; 687 $this->submit_buttons(); 688 echo '</div>'; 689 690 // Close the form. 691 echo '</div>'; 692 echo '</form></div>'; 693 } 694 695 /** 696 * Output any submit buttons required by the $this->includecheckboxes form. 697 */ 698 protected function submit_buttons() { 699 global $PAGE; 700 if (has_capability('mod/quiz:deleteattempts', $this->context)) { 701 $deletebuttonparams = [ 702 'type' => 'submit', 703 'class' => 'btn btn-secondary mr-1', 704 'id' => 'deleteattemptsbutton', 705 'name' => 'delete', 706 'value' => get_string('deleteselected', 'quiz_overview'), 707 'data-action' => 'toggle', 708 'data-togglegroup' => $this->togglegroup, 709 'data-toggle' => 'action', 710 'disabled' => true 711 ]; 712 echo html_writer::empty_tag('input', $deletebuttonparams); 713 $PAGE->requires->event_handler('#deleteattemptsbutton', 'click', 'M.util.show_confirm_dialog', 714 ['message' => get_string('deleteattemptcheck', 'quiz')]); 715 } 716 } 717 718 /** 719 * Generates the contents for the checkbox column header. 720 * 721 * It returns the HTML for a master \core\output\checkbox_toggleall component that selects/deselects all quiz attempts. 722 * 723 * @param string $columnname The name of the checkbox column. 724 * @return string 725 */ 726 public function checkbox_col_header(string $columnname) { 727 global $OUTPUT; 728 729 // Make sure to disable sorting on this column. 730 $this->no_sorting($columnname); 731 732 // Build the select/deselect all control. 733 $selectallid = $this->uniqueid . '-selectall-attempts'; 734 $selectalltext = get_string('selectall', 'quiz'); 735 $deselectalltext = get_string('selectnone', 'quiz'); 736 $mastercheckbox = new \core\output\checkbox_toggleall($this->togglegroup, true, [ 737 'id' => $selectallid, 738 'name' => $selectallid, 739 'value' => 1, 740 'label' => $selectalltext, 741 'labelclasses' => 'accesshide', 742 'selectall' => $selectalltext, 743 'deselectall' => $deselectalltext, 744 ]); 745 746 return $OUTPUT->render($mastercheckbox); 747 } 748 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body