Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]
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 use mod_quiz\local\reports\report_base; 18 use mod_quiz\quiz_attempt; 19 20 defined('MOODLE_INTERNAL') || die(); 21 22 require_once($CFG->dirroot . '/mod/quiz/report/grading/gradingsettings_form.php'); 23 24 /** 25 * Quiz report to help teachers manually grade questions that need it. 26 * 27 * This report basically provides two screens: 28 * - List question that might need manual grading (or optionally all questions). 29 * - Provide an efficient UI to grade all attempts at a particular question. 30 * 31 * @copyright 2006 Gustav Delius 32 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 */ 34 class quiz_grading_report extends report_base { 35 const DEFAULT_PAGE_SIZE = 5; 36 const DEFAULT_ORDER = 'random'; 37 38 /** @var string Positive integer regular expression. */ 39 const REGEX_POSITIVE_INT = '/^[1-9]\d*$/'; 40 41 /** @var array URL parameters for what is being displayed when grading. */ 42 protected $viewoptions = []; 43 44 /** @var int the current group, 0 if none, or NO_GROUPS_ALLOWED. */ 45 protected $currentgroup; 46 47 /** @var array from quiz_report_get_significant_questions. */ 48 protected $questions; 49 50 /** @var stdClass the course settings. */ 51 protected $course; 52 53 /** @var stdClass the course_module settings. */ 54 protected $cm; 55 56 /** @var stdClass the quiz settings. */ 57 protected $quiz; 58 59 /** @var context the quiz context. */ 60 protected $context; 61 62 /** @var quiz_grading_renderer Renderer of Quiz Grading. */ 63 protected $renderer; 64 65 /** @var string fragment of SQL code to restrict to the relevant users. */ 66 protected $userssql; 67 68 /** @var array extra user fields. */ 69 protected $extrauserfields = []; 70 71 public function display($quiz, $cm, $course) { 72 73 $this->quiz = $quiz; 74 $this->cm = $cm; 75 $this->course = $course; 76 77 // Get the URL options. 78 $slot = optional_param('slot', null, PARAM_INT); 79 $questionid = optional_param('qid', null, PARAM_INT); 80 $grade = optional_param('grade', null, PARAM_ALPHA); 81 82 $includeauto = optional_param('includeauto', false, PARAM_BOOL); 83 if (!in_array($grade, ['all', 'needsgrading', 'autograded', 'manuallygraded'])) { 84 $grade = null; 85 } 86 $pagesize = optional_param('pagesize', 87 get_user_preferences('quiz_grading_pagesize', self::DEFAULT_PAGE_SIZE), 88 PARAM_INT); 89 $page = optional_param('page', 0, PARAM_INT); 90 $order = optional_param('order', 91 get_user_preferences('quiz_grading_order', self::DEFAULT_ORDER), 92 PARAM_ALPHAEXT); 93 94 // Assemble the options required to reload this page. 95 $optparams = ['includeauto', 'page']; 96 foreach ($optparams as $param) { 97 if ($$param) { 98 $this->viewoptions[$param] = $$param; 99 } 100 } 101 if (!data_submitted() && !preg_match(self::REGEX_POSITIVE_INT, $pagesize)) { 102 // We only validate if the user accesses the page via a cleaned-up GET URL here. 103 throw new moodle_exception('invalidpagesize'); 104 } 105 if ($pagesize != self::DEFAULT_PAGE_SIZE) { 106 $this->viewoptions['pagesize'] = $pagesize; 107 } 108 if ($order != self::DEFAULT_ORDER) { 109 $this->viewoptions['order'] = $order; 110 } 111 112 // Check permissions. 113 $this->context = context_module::instance($this->cm->id); 114 require_capability('mod/quiz:grade', $this->context); 115 $shownames = has_capability('quiz/grading:viewstudentnames', $this->context); 116 // Whether the current user can see custom user fields. 117 $showcustomfields = has_capability('quiz/grading:viewidnumber', $this->context); 118 $userfieldsapi = \core_user\fields::for_identity($this->context)->with_name(); 119 $customfields = []; 120 foreach ($userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]) as $field) { 121 $customfields[] = $field; 122 } 123 // Validate order. 124 $orderoptions = array_merge(['random', 'date', 'studentfirstname', 'studentlastname'], $customfields); 125 if (!in_array($order, $orderoptions)) { 126 $order = self::DEFAULT_ORDER; 127 } else if (!$shownames && ($order == 'studentfirstname' || $order == 'studentlastname')) { 128 $order = self::DEFAULT_ORDER; 129 } else if (!$showcustomfields && in_array($order, $customfields)) { 130 $order = self::DEFAULT_ORDER; 131 } 132 if ($order == 'random') { 133 $page = 0; 134 } 135 136 // Get the list of questions in this quiz. 137 $this->questions = quiz_report_get_significant_questions($quiz); 138 if ($slot && !array_key_exists($slot, $this->questions)) { 139 throw new moodle_exception('unknownquestion', 'quiz_grading'); 140 } 141 142 // Process any submitted data. 143 if ($data = data_submitted() && confirm_sesskey() && $this->validate_submitted_marks()) { 144 // Changes done to handle attempts being missed from grading due to redirecting to new page. 145 $attemptsgraded = $this->process_submitted_data(); 146 147 $nextpagenumber = $page + 1; 148 // If attempts need grading and one or more have now been graded, then page number should remain the same. 149 if ($grade == 'needsgrading' && $attemptsgraded) { 150 $nextpagenumber = $page; 151 } 152 153 redirect($this->grade_question_url($slot, $questionid, $grade, $nextpagenumber)); 154 } 155 156 // Get the group, and the list of significant users. 157 $this->currentgroup = $this->get_current_group($cm, $course, $this->context); 158 if ($this->currentgroup == self::NO_GROUPS_ALLOWED) { 159 $this->userssql = []; 160 } else { 161 $this->userssql = get_enrolled_sql($this->context, 162 ['mod/quiz:reviewmyattempts', 'mod/quiz:attempt'], $this->currentgroup); 163 } 164 165 $hasquestions = quiz_has_questions($this->quiz->id); 166 if (!$hasquestions) { 167 $this->print_header_and_tabs($cm, $course, $quiz, 'grading'); 168 echo $this->renderer->render_quiz_no_question_notification($quiz, $cm, $this->context); 169 return true; 170 } 171 172 if (!$slot) { 173 $this->display_index($includeauto); 174 return true; 175 } 176 177 // Display the grading UI for one question. 178 179 // Make sure there is something to do. 180 $counts = null; 181 $statecounts = $this->get_question_state_summary([$slot]); 182 foreach ($statecounts as $record) { 183 if ($record->questionid == $questionid) { 184 $counts = $record; 185 break; 186 } 187 } 188 189 // If not, redirect back to the list. 190 if (!$counts || $counts->$grade == 0) { 191 redirect($this->list_questions_url(), get_string('alldoneredirecting', 'quiz_grading')); 192 } 193 194 $this->display_grading_interface($slot, $questionid, $grade, 195 $pagesize, $page, $shownames, $showcustomfields, $order, $counts); 196 return true; 197 } 198 199 /** 200 * Get the JOIN conditions needed so we only show attempts by relevant users. 201 * 202 * @return qubaid_join 203 */ 204 protected function get_qubaids_condition() { 205 206 $where = "quiza.quiz = :mangrquizid AND 207 quiza.preview = 0 AND 208 quiza.state = :statefinished"; 209 $params = ['mangrquizid' => $this->cm->instance, 'statefinished' => quiz_attempt::FINISHED]; 210 211 $usersjoin = ''; 212 $currentgroup = groups_get_activity_group($this->cm, true); 213 $enrolleduserscount = count_enrolled_users($this->context, 214 ['mod/quiz:reviewmyattempts', 'mod/quiz:attempt'], $currentgroup); 215 if ($currentgroup) { 216 $userssql = get_enrolled_sql($this->context, 217 ['mod/quiz:reviewmyattempts', 'mod/quiz:attempt'], $currentgroup); 218 if ($enrolleduserscount < 1) { 219 $where .= ' AND quiza.userid = 0'; 220 } else { 221 $usersjoin = "JOIN ({$userssql[0]}) AS enr ON quiza.userid = enr.id"; 222 $params += $userssql[1]; 223 } 224 } 225 226 return new qubaid_join("{quiz_attempts} quiza $usersjoin ", 'quiza.uniqueid', $where, $params); 227 } 228 229 /** 230 * Load the quiz_attempts rows corresponding to a list of question_usage ids. 231 * 232 * @param int[] $qubaids the question_usage ids of the quiz_attempts to load. 233 * @return array quiz attempts, with added user name fields. 234 */ 235 protected function load_attempts_by_usage_ids($qubaids) { 236 global $DB; 237 238 list($asql, $params) = $DB->get_in_or_equal($qubaids); 239 $params[] = quiz_attempt::FINISHED; 240 $params[] = $this->quiz->id; 241 242 $fields = 'quiza.*, '; 243 $userfieldsapi = \core_user\fields::for_identity($this->context)->with_name(); 244 $userfieldssql = $userfieldsapi->get_sql('u', false, '', 'userid', false); 245 $fields .= $userfieldssql->selects; 246 foreach ($userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]) as $userfield) { 247 $this->extrauserfields[] = s($userfield); 248 } 249 $params = array_merge($userfieldssql->params, $params); 250 $attemptsbyid = $DB->get_records_sql(" 251 SELECT $fields 252 FROM {quiz_attempts} quiza 253 JOIN {user} u ON u.id = quiza.userid 254 {$userfieldssql->joins} 255 WHERE quiza.uniqueid $asql AND quiza.state = ? AND quiza.quiz = ?", 256 $params); 257 258 $attempts = []; 259 foreach ($attemptsbyid as $attempt) { 260 $attempts[$attempt->uniqueid] = $attempt; 261 } 262 return $attempts; 263 } 264 265 /** 266 * Get the URL of the front page of the report that lists all the questions. 267 * 268 * @return moodle_url the URL. 269 */ 270 protected function base_url() { 271 return new moodle_url('/mod/quiz/report.php', 272 ['id' => $this->cm->id, 'mode' => 'grading']); 273 } 274 275 /** 276 * Get the URL of the front page of the report that lists all the questions. 277 * 278 * @param bool $includeauto if not given, use the current setting, otherwise, 279 * force a particular value of includeauto in the URL. 280 * @return moodle_url the URL. 281 */ 282 protected function list_questions_url($includeauto = null) { 283 $url = $this->base_url(); 284 285 $url->params($this->viewoptions); 286 287 if (!is_null($includeauto)) { 288 $url->param('includeauto', $includeauto); 289 } 290 291 return $url; 292 } 293 294 /** 295 * Get the URL to grade a batch of question attempts. 296 * 297 * @param int $slot 298 * @param int $questionid 299 * @param string $grade 300 * @param int|bool $page = true, link to current page. false = omit page. 301 * number = link to specific page. 302 * @return moodle_url 303 */ 304 protected function grade_question_url($slot, $questionid, $grade, $page = true) { 305 $url = $this->base_url(); 306 $url->params(['slot' => $slot, 'qid' => $questionid, 'grade' => $grade]); 307 $url->params($this->viewoptions); 308 309 if (!$page) { 310 $url->remove_params('page'); 311 } else if (is_integer($page)) { 312 $url->param('page', $page); 313 } 314 315 return $url; 316 } 317 318 /** 319 * Renders the contents of one cell of the table on the index view. 320 * 321 * @param stdClass $counts counts of different types of attempt for this slot. 322 * @param string $type the type of count to format. 323 * @param string $gradestring get_string identifier for the grading link text, if required. 324 * @return string HTML. 325 */ 326 protected function format_count_for_table($counts, $type, $gradestring) { 327 $result = $counts->$type; 328 if ($counts->$type > 0) { 329 $gradeurl = $this->grade_question_url($counts->slot, $counts->questionid, $type); 330 $result .= $this->renderer->render_grade_link($counts, $type, $gradestring, $gradeurl); 331 } 332 return $result; 333 } 334 335 /** 336 * Display the report front page which summarises the number of attempts to grade. 337 * 338 * @param bool $includeauto whether to show automatically-graded questions. 339 */ 340 protected function display_index($includeauto) { 341 global $PAGE, $OUTPUT; 342 343 $this->print_header_and_tabs($this->cm, $this->course, $this->quiz, 'grading'); 344 345 if ($groupmode = groups_get_activity_groupmode($this->cm)) { 346 // Groups is being used. 347 groups_print_activity_menu($this->cm, $this->list_questions_url()); 348 } 349 // Get the current group for the user looking at the report. 350 $currentgroup = $this->get_current_group($this->cm, $this->course, $this->context); 351 if ($currentgroup == self::NO_GROUPS_ALLOWED) { 352 echo $OUTPUT->notification(get_string('notingroup')); 353 return; 354 } 355 $statecounts = $this->get_question_state_summary(array_keys($this->questions)); 356 if ($includeauto) { 357 $linktext = get_string('hideautomaticallygraded', 'quiz_grading'); 358 } else { 359 $linktext = get_string('alsoshowautomaticallygraded', 'quiz_grading'); 360 } 361 echo $this->renderer->render_display_index_heading($linktext, $this->list_questions_url(!$includeauto)); 362 $data = []; 363 $header = []; 364 365 $header[] = get_string('qno', 'quiz_grading'); 366 $header[] = get_string('qtypeveryshort', 'question'); 367 $header[] = get_string('questionname', 'quiz_grading'); 368 $header[] = get_string('tograde', 'quiz_grading'); 369 $header[] = get_string('alreadygraded', 'quiz_grading'); 370 if ($includeauto) { 371 $header[] = get_string('automaticallygraded', 'quiz_grading'); 372 } 373 $header[] = get_string('total', 'quiz_grading'); 374 375 foreach ($statecounts as $counts) { 376 if ($counts->all == 0) { 377 continue; 378 } 379 if (!$includeauto && $counts->needsgrading == 0 && $counts->manuallygraded == 0) { 380 continue; 381 } 382 383 $row = []; 384 385 $row[] = $this->questions[$counts->slot]->number; 386 387 $row[] = $PAGE->get_renderer('question', 'bank')->qtype_icon($this->questions[$counts->slot]->qtype); 388 389 $row[] = format_string($counts->name); 390 391 $row[] = $this->format_count_for_table($counts, 'needsgrading', 'grade'); 392 393 $row[] = $this->format_count_for_table($counts, 'manuallygraded', 'updategrade'); 394 395 if ($includeauto) { 396 $row[] = $this->format_count_for_table($counts, 'autograded', 'updategrade'); 397 } 398 399 $row[] = $this->format_count_for_table($counts, 'all', 'gradeall'); 400 401 $data[] = $row; 402 } 403 echo $this->renderer->render_questions_table($includeauto, $data, $header); 404 } 405 406 /** 407 * Display the UI for grading attempts at one question. 408 * 409 * @param int $slot identifies which question to grade. 410 * @param int $questionid identifies which question to grade. 411 * @param string $grade type of attempts to grade. 412 * @param int $pagesize number of questions to show per page. 413 * @param int $page current page number. 414 * @param bool $shownames whether student names should be shown. 415 * @param bool $showcustomfields whether custom field values should be shown. 416 * @param string $order preferred order of attempts. 417 * @param stdClass $counts object that stores the number of each type of attempt. 418 */ 419 protected function display_grading_interface($slot, $questionid, $grade, 420 $pagesize, $page, $shownames, $showcustomfields, $order, $counts) { 421 422 if ($pagesize * $page >= $counts->$grade) { 423 $page = 0; 424 } 425 426 // Prepare the options form. 427 $hidden = [ 428 'id' => $this->cm->id, 429 'mode' => 'grading', 430 'slot' => $slot, 431 'qid' => $questionid, 432 'page' => $page, 433 ]; 434 if (array_key_exists('includeauto', $this->viewoptions)) { 435 $hidden['includeauto'] = $this->viewoptions['includeauto']; 436 } 437 $mform = new quiz_grading_settings_form($hidden, $counts, $shownames, $showcustomfields, $this->context); 438 439 // Tell the form the current settings. 440 $settings = new stdClass(); 441 $settings->grade = $grade; 442 $settings->pagesize = $pagesize; 443 $settings->order = $order; 444 $mform->set_data($settings); 445 446 if ($mform->is_submitted()) { 447 if ($mform->is_validated()) { 448 // If the form was submitted and validated, save the user preferences, and 449 // redirect to a cleaned-up GET URL. 450 set_user_preference('quiz_grading_pagesize', $pagesize); 451 set_user_preference('quiz_grading_order', $order); 452 redirect($this->grade_question_url($slot, $questionid, $grade, $page)); 453 } else { 454 // Set the pagesize back to the previous value, so the report page can continue the render 455 // and the form can show the validation. 456 $pagesize = get_user_preferences('quiz_grading_pagesize', self::DEFAULT_PAGE_SIZE); 457 } 458 } 459 460 list($qubaids, $count) = $this->get_usage_ids_where_question_in_state( 461 $grade, $slot, $questionid, $order, $page, $pagesize); 462 $attempts = $this->load_attempts_by_usage_ids($qubaids); 463 464 // Question info. 465 $questioninfo = new stdClass(); 466 $questioninfo->number = $this->questions[$slot]->number; 467 $questioninfo->questionname = format_string($counts->name); 468 469 // Paging info. 470 $paginginfo = new stdClass(); 471 $paginginfo->from = $page * $pagesize + 1; 472 $paginginfo->to = min(($page + 1) * $pagesize, $count); 473 $paginginfo->of = $count; 474 $qubaidlist = implode(',', $qubaids); 475 476 $this->print_header_and_tabs($this->cm, $this->course, $this->quiz, 'grading'); 477 478 $gradequestioncontent = ''; 479 foreach ($qubaids as $qubaid) { 480 $attempt = $attempts[$qubaid]; 481 $quba = question_engine::load_questions_usage_by_activity($qubaid); 482 $displayoptions = quiz_get_review_options($this->quiz, $attempt, $this->context); 483 $displayoptions->generalfeedback = question_display_options::HIDDEN; 484 $displayoptions->history = question_display_options::HIDDEN; 485 $displayoptions->manualcomment = question_display_options::EDITABLE; 486 487 $gradequestioncontent .= $this->renderer->render_grade_question( 488 $quba, 489 $slot, 490 $displayoptions, 491 $this->questions[$slot]->number, 492 $this->get_question_heading($attempt, $shownames, $showcustomfields) 493 ); 494 } 495 496 $pagingbar = new stdClass(); 497 $pagingbar->count = $count; 498 $pagingbar->page = $page; 499 $pagingbar->pagesize = $pagesize; 500 $pagingbar->pagesize = $pagesize; 501 $pagingbar->order = $order; 502 $pagingbar->pagingurl = $this->grade_question_url($slot, $questionid, $grade, false); 503 504 $hiddeninputs = [ 505 'qubaids' => $qubaidlist, 506 'slots' => $slot, 507 'sesskey' => sesskey() 508 ]; 509 510 echo $this->renderer->render_grading_interface( 511 $questioninfo, 512 $this->list_questions_url(), 513 $mform, 514 $paginginfo, 515 $pagingbar, 516 $this->grade_question_url($slot, $questionid, $grade, $page), 517 $hiddeninputs, 518 $gradequestioncontent 519 ); 520 } 521 522 /** 523 * When saving a grading page, are all the submitted marks valid? 524 * 525 * @return bool true if all valid, else false. 526 */ 527 protected function validate_submitted_marks() { 528 529 $qubaids = optional_param('qubaids', null, PARAM_SEQUENCE); 530 if (!$qubaids) { 531 return false; 532 } 533 $qubaids = clean_param_array(explode(',', $qubaids), PARAM_INT); 534 535 $slots = optional_param('slots', '', PARAM_SEQUENCE); 536 if (!$slots) { 537 $slots = []; 538 } else { 539 $slots = explode(',', $slots); 540 } 541 542 foreach ($qubaids as $qubaid) { 543 foreach ($slots as $slot) { 544 if (!question_engine::is_manual_grade_in_range($qubaid, $slot)) { 545 return false; 546 } 547 } 548 } 549 550 return true; 551 } 552 553 /** 554 * Save all submitted marks to the database. 555 * 556 * @return bool returns true if some attempts or all are graded. False, if none of the attempts are graded. 557 */ 558 protected function process_submitted_data(): bool { 559 global $DB; 560 561 $qubaids = optional_param('qubaids', null, PARAM_SEQUENCE); 562 $assumedslotforevents = optional_param('slot', null, PARAM_INT); 563 564 if (!$qubaids) { 565 return false; 566 } 567 568 $qubaids = clean_param_array(explode(',', $qubaids), PARAM_INT); 569 $attempts = $this->load_attempts_by_usage_ids($qubaids); 570 $events = []; 571 572 $transaction = $DB->start_delegated_transaction(); 573 $attemptsgraded = false; 574 foreach ($qubaids as $qubaid) { 575 $attempt = $attempts[$qubaid]; 576 $attemptobj = new quiz_attempt($attempt, $this->quiz, $this->cm, $this->course); 577 578 // State of the attempt before grades are changed. 579 $attemptoldtstate = $attemptobj->get_question_state($assumedslotforevents); 580 581 $attemptobj->process_submitted_actions(time()); 582 583 // Get attempt state after grades are changed. 584 $attemptnewtstate = $attemptobj->get_question_state($assumedslotforevents); 585 586 // Check if any attempts are graded. 587 if (!$attemptsgraded && $attemptoldtstate->is_graded() != $attemptnewtstate->is_graded()) { 588 $attemptsgraded = true; 589 } 590 591 // Add the event we will trigger later. 592 $params = [ 593 'objectid' => $attemptobj->get_question_attempt($assumedslotforevents)->get_question_id(), 594 'courseid' => $attemptobj->get_courseid(), 595 'context' => context_module::instance($attemptobj->get_cmid()), 596 'other' => [ 597 'quizid' => $attemptobj->get_quizid(), 598 'attemptid' => $attemptobj->get_attemptid(), 599 'slot' => $assumedslotforevents, 600 ], 601 ]; 602 $events[] = \mod_quiz\event\question_manually_graded::create($params); 603 } 604 $transaction->allow_commit(); 605 606 // Trigger events for all the questions we manually marked. 607 foreach ($events as $event) { 608 $event->trigger(); 609 } 610 611 return $attemptsgraded; 612 } 613 614 /** 615 * Load information about the number of attempts at various questions in each 616 * summarystate. 617 * 618 * The results are returned as an two dimensional array $qubaid => $slot => $dataobject 619 * 620 * @param array $slots A list of slots for the questions you want to konw about. 621 * @return array The array keys are slot,qestionid. The values are objects with 622 * fields $slot, $questionid, $inprogress, $name, $needsgrading, $autograded, 623 * $manuallygraded and $all. 624 */ 625 protected function get_question_state_summary($slots) { 626 $dm = new question_engine_data_mapper(); 627 return $dm->load_questions_usages_question_state_summary( 628 $this->get_qubaids_condition(), $slots); 629 } 630 631 /** 632 * Get a list of usage ids where the question with slot $slot, and optionally 633 * also with question id $questionid, is in summary state $summarystate. Also 634 * return the total count of such states. 635 * 636 * Only a subset of the ids can be returned by using $orderby, $limitfrom and 637 * $limitnum. A special value 'random' can be passed as $orderby, in which case 638 * $limitfrom is ignored. 639 * 640 * @param int $slot The slot for the questions you want to konw about. 641 * @param int $questionid (optional) Only return attempts that were of this specific question. 642 * @param string $summarystate 'all', 'needsgrading', 'autograded' or 'manuallygraded'. 643 * @param string $orderby 'random', 'date', 'student' or 'idnumber'. 644 * @param int $page implements paging of the results. 645 * Ignored if $orderby = random or $pagesize is null. 646 * @param int $pagesize implements paging of the results. null = all. 647 * @return array with two elements, an array of usage ids, and a count of the total number. 648 */ 649 protected function get_usage_ids_where_question_in_state($summarystate, $slot, 650 $questionid = null, $orderby = 'random', $page = 0, $pagesize = null) { 651 $dm = new question_engine_data_mapper(); 652 $extraselect = ''; 653 if ($pagesize && $orderby != 'random') { 654 $limitfrom = $page * $pagesize; 655 } else { 656 $limitfrom = 0; 657 } 658 659 $qubaids = $this->get_qubaids_condition(); 660 661 $params = []; 662 $userfieldsapi = \core_user\fields::for_identity($this->context)->with_name(); 663 $userfieldssql = $userfieldsapi->get_sql('u', true, '', 'userid', true); 664 $params = array_merge($params, $userfieldssql->params); 665 $customfields = []; 666 foreach ($userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]) as $field) { 667 $customfields[] = $field; 668 } 669 if ($orderby === 'date') { 670 list($statetest, $params) = $dm->in_summary_state_test( 671 'manuallygraded', false, 'mangrstate'); 672 $extraselect = "( 673 SELECT MAX(sortqas.timecreated) 674 FROM {question_attempt_steps} sortqas 675 WHERE sortqas.questionattemptid = qa.id 676 AND sortqas.state $statetest 677 ) as tcreated"; 678 $orderby = "tcreated"; 679 } else if ($orderby === 'studentfirstname' || $orderby === 'studentlastname' || in_array($orderby, $customfields)) { 680 $qubaids->from .= " JOIN {user} u ON quiza.userid = u.id {$userfieldssql->joins}"; 681 // For name sorting, map orderby form value to 682 // actual column names; 'idnumber' maps naturally. 683 if ($orderby === "studentlastname") { 684 $orderby = "u.lastname, u.firstname"; 685 } else if ($orderby === "studentfirstname") { 686 $orderby = "u.firstname, u.lastname"; 687 } else if (in_array($orderby, $customfields)) { // Sort order by current custom user field. 688 $orderby = $userfieldssql->mappings[$orderby]; 689 } 690 } 691 692 return $dm->load_questions_usages_where_question_in_state($qubaids, $summarystate, 693 $slot, $questionid, $orderby, $params, $limitfrom, $pagesize, $extraselect); 694 } 695 696 /** 697 * Initialise some parts of $PAGE and start output. 698 * 699 * @param stdClass $cm the course_module information. 700 * @param stdClass $course the course settings. 701 * @param stdClass $quiz the quiz settings. 702 * @param string $reportmode the report name. 703 */ 704 public function print_header_and_tabs($cm, $course, $quiz, $reportmode = 'overview') { 705 global $PAGE; 706 $this->renderer = $PAGE->get_renderer('quiz_grading'); 707 parent::print_header_and_tabs($cm, $course, $quiz, $reportmode); 708 } 709 710 /** 711 * Get question heading. 712 * 713 * @param stdClass $attempt An instance of quiz_attempt. 714 * @param bool $shownames True to show the student first/lastnames. 715 * @param bool $showcustomfields Whether custom field values should be shown. 716 * @return string The string text for the question heading. 717 */ 718 protected function get_question_heading(stdClass $attempt, bool $shownames, bool $showcustomfields): string { 719 global $DB; 720 $a = new stdClass(); 721 $a->attempt = $attempt->attempt; 722 $a->fullname = fullname($attempt); 723 724 $customfields = []; 725 foreach ($this->extrauserfields as $field) { 726 if (strval($attempt->{$field}) !== '') { 727 $customfields[] = s($attempt->{$field}); 728 } 729 } 730 731 $a->customfields = implode(', ', $customfields); 732 733 if ($shownames && $showcustomfields) { 734 return get_string('gradingattemptwithcustomfields', 'quiz_grading', $a); 735 } else if ($shownames) { 736 return get_string('gradingattempt', 'quiz_grading', $a); 737 } else if ($showcustomfields) { 738 $a->fullname = $a->customfields; 739 return get_string('gradingattempt', 'quiz_grading', $a); 740 } else { 741 return ''; 742 } 743 } 744 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body