Search moodle.org's
Developer Documentation

   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  /**
  18   * This file defines the quiz manual grading report class.
  19   *
  20   * @package   quiz_grading
  21   * @copyright 2006 Gustav Delius
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  require_once($CFG->dirroot . '/mod/quiz/report/grading/gradingsettings_form.php');
  29  
  30  
  31  /**
  32   * Quiz report to help teachers manually grade questions that need it.
  33   *
  34   * This report basically provides two screens:
  35   * - List question that might need manual grading (or optionally all questions).
  36   * - Provide an efficient UI to grade all attempts at a particular question.
  37   *
  38   * @copyright 2006 Gustav Delius
  39   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  40   */
  41  class quiz_grading_report extends quiz_default_report {
  42      const DEFAULT_PAGE_SIZE = 5;
  43      const DEFAULT_ORDER = 'random';
  44  
  45      protected $viewoptions = array();
  46      protected $questions;
  47      protected $cm;
  48      protected $quiz;
  49      protected $context;
  50  
  51      public function display($quiz, $cm, $course) {
  52          global $CFG, $DB, $PAGE;
  53  
  54          $this->quiz = $quiz;
  55          $this->cm = $cm;
  56          $this->course = $course;
  57  
  58          // Get the URL options.
  59          $slot = optional_param('slot', null, PARAM_INT);
  60          $questionid = optional_param('qid', null, PARAM_INT);
  61          $grade = optional_param('grade', null, PARAM_ALPHA);
  62  
  63          $includeauto = optional_param('includeauto', false, PARAM_BOOL);
  64          if (!in_array($grade, array('all', 'needsgrading', 'autograded', 'manuallygraded'))) {
  65              $grade = null;
  66          }
  67          $pagesize = optional_param('pagesize', self::DEFAULT_PAGE_SIZE, PARAM_INT);
  68          $page = optional_param('page', 0, PARAM_INT);
  69          $order = optional_param('order', self::DEFAULT_ORDER, PARAM_ALPHA);
  70  
  71          // Assemble the options requried to reload this page.
  72          $optparams = array('includeauto', 'page');
  73          foreach ($optparams as $param) {
  74              if ($$param) {
  75                  $this->viewoptions[$param] = $$param;
  76              }
  77          }
  78          if ($pagesize != self::DEFAULT_PAGE_SIZE) {
  79              $this->viewoptions['pagesize'] = $pagesize;
  80          }
  81          if ($order != self::DEFAULT_ORDER) {
  82              $this->viewoptions['order'] = $order;
  83          }
  84  
  85          // Check permissions.
  86          $this->context = context_module::instance($cm->id);
  87          require_capability('mod/quiz:grade', $this->context);
  88          $shownames = has_capability('quiz/grading:viewstudentnames', $this->context);
  89          $showidnumbers = has_capability('quiz/grading:viewidnumber', $this->context);
  90  
  91          // Validate order.
  92          if (!in_array($order, array('random', 'date', 'studentfirstname', 'studentlastname', 'idnumber'))) {
  93              $order = self::DEFAULT_ORDER;
  94          } else if (!$shownames && ($order == 'studentfirstname' || $order == 'studentlastname')) {
  95              $order = self::DEFAULT_ORDER;
  96          } else if (!$showidnumbers && $order == 'idnumber') {
  97              $order = self::DEFAULT_ORDER;
  98          }
  99          if ($order == 'random') {
 100              $page = 0;
 101          }
 102  
 103          // Get the list of questions in this quiz.
 104          $this->questions = quiz_report_get_significant_questions($quiz);
 105          if ($slot && !array_key_exists($slot, $this->questions)) {
 106              throw new moodle_exception('unknownquestion', 'quiz_grading');
 107          }
 108  
 109          // Process any submitted data.
 110          if ($data = data_submitted() && confirm_sesskey() && $this->validate_submitted_marks()) {
 111              $this->process_submitted_data();
 112  
 113              redirect($this->grade_question_url($slot, $questionid, $grade, $page + 1));
 114          }
 115  
 116          // Get the group, and the list of significant users.
 117          $this->currentgroup = $this->get_current_group($cm, $course, $this->context);
 118          if ($this->currentgroup == self::NO_GROUPS_ALLOWED) {
 119              $this->users = array();
 120          } else {
 121              $this->users = get_users_by_capability($this->context,
 122                      array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), '', '', '', '',
 123                      $this->currentgroup, '', false);
 124          }
 125  
 126          $hasquestions = quiz_has_questions($quiz->id);
 127          $counts = null;
 128          if ($slot && $hasquestions) {
 129              // Make sure there is something to do.
 130              $statecounts = $this->get_question_state_summary(array($slot));
 131              foreach ($statecounts as $record) {
 132                  if ($record->questionid == $questionid) {
 133                      $counts = $record;
 134                      break;
 135                  }
 136              }
 137              // If not, redirect back to the list.
 138              if (!$counts || $counts->$grade == 0) {
 139                  redirect($this->list_questions_url(), get_string('alldoneredirecting', 'quiz_grading'));
 140              }
 141          }
 142  
 143          // Start output.
 144          $this->print_header_and_tabs($cm, $course, $quiz, 'grading');
 145  
 146          // What sort of page to display?
 147          if (!$hasquestions) {
 148              echo quiz_no_questions_message($quiz, $cm, $this->context);
 149  
 150          } else if (!$slot) {
 151              $this->display_index($includeauto);
 152  
 153          } else {
 154              $this->display_grading_interface($slot, $questionid, $grade,
 155                      $pagesize, $page, $shownames, $showidnumbers, $order, $counts);
 156          }
 157          return true;
 158      }
 159  
 160      protected function get_qubaids_condition() {
 161          global $DB;
 162  
 163          $where = "quiza.quiz = :mangrquizid AND
 164                  quiza.preview = 0 AND
 165                  quiza.state = :statefinished";
 166          $params = array('mangrquizid' => $this->cm->instance, 'statefinished' => quiz_attempt::FINISHED);
 167  
 168          $currentgroup = groups_get_activity_group($this->cm, true);
 169          if ($currentgroup) {
 170              $users = get_users_by_capability($this->context,
 171                      array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), 'u.id, u.id', '', '', '',
 172                      $currentgroup, '', false);
 173              if (empty($users)) {
 174                  $where .= ' AND quiza.userid = 0';
 175              } else {
 176                  list($usql, $uparam) = $DB->get_in_or_equal(array_keys($users),
 177                          SQL_PARAMS_NAMED, 'mangru');
 178                  $where .= ' AND quiza.userid ' . $usql;
 179                  $params += $uparam;
 180              }
 181          }
 182  
 183          return new qubaid_join('{quiz_attempts} quiza', 'quiza.uniqueid', $where, $params);
 184      }
 185  
 186      protected function load_attempts_by_usage_ids($qubaids) {
 187          global $DB;
 188  
 189          list($asql, $params) = $DB->get_in_or_equal($qubaids);
 190          $params[] = quiz_attempt::FINISHED;
 191          $params[] = $this->quiz->id;
 192  
 193          $fields = 'quiza.*, u.idnumber, ';
 194          $fields .= get_all_user_name_fields(true, 'u');
 195          $attemptsbyid = $DB->get_records_sql("
 196                  SELECT $fields
 197                  FROM {quiz_attempts} quiza
 198                  JOIN {user} u ON u.id = quiza.userid
 199                  WHERE quiza.uniqueid $asql AND quiza.state = ? AND quiza.quiz = ?",
 200                  $params);
 201  
 202          $attempts = array();
 203          foreach ($attemptsbyid as $attempt) {
 204              $attempts[$attempt->uniqueid] = $attempt;
 205          }
 206          return $attempts;
 207      }
 208  
 209      /**
 210       * Get the URL of the front page of the report that lists all the questions.
 211       * @param $includeauto if not given, use the current setting, otherwise,
 212       *      force a paricular value of includeauto in the URL.
 213       * @return string the URL.
 214       */
 215      protected function base_url() {
 216          return new moodle_url('/mod/quiz/report.php',
 217                  array('id' => $this->cm->id, 'mode' => 'grading'));
 218      }
 219  
 220      /**
 221       * Get the URL of the front page of the report that lists all the questions.
 222       * @param $includeauto if not given, use the current setting, otherwise,
 223       *      force a paricular value of includeauto in the URL.
 224       * @return string the URL.
 225       */
 226      protected function list_questions_url($includeauto = null) {
 227          $url = $this->base_url();
 228  
 229          $url->params($this->viewoptions);
 230  
 231          if (!is_null($includeauto)) {
 232              $url->param('includeauto', $includeauto);
 233          }
 234  
 235          return $url;
 236      }
 237  
 238      /**
 239       * @param int $slot
 240       * @param int $questionid
 241       * @param string $grade
 242       * @param mixed $page = true, link to current page. false = omit page.
 243       *      number = link to specific page.
 244       */
 245      protected function grade_question_url($slot, $questionid, $grade, $page = true) {
 246          $url = $this->base_url();
 247          $url->params(array('slot' => $slot, 'qid' => $questionid, 'grade' => $grade));
 248          $url->params($this->viewoptions);
 249  
 250          $options = $this->viewoptions;
 251          if (!$page) {
 252              $url->remove_params('page');
 253          } else if (is_integer($page)) {
 254              $url->param('page', $page);
 255          }
 256  
 257          return $url;
 258      }
 259  
 260      protected function format_count_for_table($counts, $type, $gradestring) {
 261          $result = $counts->$type;
 262          if ($counts->$type > 0) {
 263              $result .= ' ' . html_writer::link($this->grade_question_url(
 264                      $counts->slot, $counts->questionid, $type),
 265                      get_string($gradestring, 'quiz_grading'),
 266                      array('class' => 'gradetheselink'));
 267          }
 268          return $result;
 269      }
 270  
 271      protected function display_index($includeauto) {
 272          global $OUTPUT;
 273  
 274          if ($groupmode = groups_get_activity_groupmode($this->cm)) {
 275              // Groups is being used.
 276              groups_print_activity_menu($this->cm, $this->list_questions_url());
 277          }
 278  
 279          echo $OUTPUT->heading(get_string('questionsthatneedgrading', 'quiz_grading'), 3);
 280          if ($includeauto) {
 281              $linktext = get_string('hideautomaticallygraded', 'quiz_grading');
 282          } else {
 283              $linktext = get_string('alsoshowautomaticallygraded', 'quiz_grading');
 284          }
 285          echo html_writer::tag('p', html_writer::link($this->list_questions_url(!$includeauto),
 286                  $linktext), array('class' => 'toggleincludeauto'));
 287  
 288          $statecounts = $this->get_question_state_summary(array_keys($this->questions));
 289  
 290          $data = array();
 291          foreach ($statecounts as $counts) {
 292              if ($counts->all == 0) {
 293                  continue;
 294              }
 295              if (!$includeauto && $counts->needsgrading == 0 && $counts->manuallygraded == 0) {
 296                  continue;
 297              }
 298  
 299              $row = array();
 300  
 301              $row[] = $this->questions[$counts->slot]->number;
 302  
 303              $row[] = format_string($counts->name);
 304  
 305              $row[] = $this->format_count_for_table($counts, 'needsgrading', 'grade');
 306  
 307              $row[] = $this->format_count_for_table($counts, 'manuallygraded', 'updategrade');
 308  
 309              if ($includeauto) {
 310                  $row[] = $this->format_count_for_table($counts, 'autograded', 'updategrade');
 311              }
 312  
 313              $row[] = $this->format_count_for_table($counts, 'all', 'gradeall');
 314  
 315              $data[] = $row;
 316          }
 317  
 318          if (empty($data)) {
 319              echo $OUTPUT->notification(get_string('nothingfound', 'quiz_grading'));
 320              return;
 321          }
 322  
 323          $table = new html_table();
 324          $table->class = 'generaltable';
 325          $table->id = 'questionstograde';
 326  
 327          $table->head[] = get_string('qno', 'quiz_grading');
 328          $table->head[] = get_string('questionname', 'quiz_grading');
 329          $table->head[] = get_string('tograde', 'quiz_grading');
 330          $table->head[] = get_string('alreadygraded', 'quiz_grading');
 331          if ($includeauto) {
 332              $table->head[] = get_string('automaticallygraded', 'quiz_grading');
 333          }
 334          $table->head[] = get_string('total', 'quiz_grading');
 335  
 336          $table->data = $data;
 337          echo html_writer::table($table);
 338      }
 339  
 340      protected function display_grading_interface($slot, $questionid, $grade,
 341              $pagesize, $page, $shownames, $showidnumbers, $order, $counts) {
 342          global $OUTPUT;
 343  
 344          if ($pagesize * $page >= $counts->$grade) {
 345              $page = 0;
 346          }
 347  
 348          list($qubaids, $count) = $this->get_usage_ids_where_question_in_state(
 349                  $grade, $slot, $questionid, $order, $page, $pagesize);
 350          $attempts = $this->load_attempts_by_usage_ids($qubaids);
 351  
 352          // Prepare the form.
 353          $hidden = array(
 354              'id' => $this->cm->id,
 355              'mode' => 'grading',
 356              'slot' => $slot,
 357              'qid' => $questionid,
 358              'page' => $page,
 359          );
 360          if (array_key_exists('includeauto', $this->viewoptions)) {
 361              $hidden['includeauto'] = $this->viewoptions['includeauto'];
 362          }
 363          $mform = new quiz_grading_settings_form($hidden, $counts, $shownames, $showidnumbers);
 364  
 365          // Tell the form the current settings.
 366          $settings = new stdClass();
 367          $settings->grade = $grade;
 368          $settings->pagesize = $pagesize;
 369          $settings->order = $order;
 370          $mform->set_data($settings);
 371  
 372          // Print the heading and form.
 373          echo question_engine::initialise_js();
 374  
 375          $a = new stdClass();
 376          $a->number = $this->questions[$slot]->number;
 377          $a->questionname = format_string($counts->name);
 378          echo $OUTPUT->heading(get_string('gradingquestionx', 'quiz_grading', $a), 3);
 379          echo html_writer::tag('p', html_writer::link($this->list_questions_url(),
 380                  get_string('backtothelistofquestions', 'quiz_grading')),
 381                  array('class' => 'mdl-align'));
 382  
 383          $mform->display();
 384  
 385          // Paging info.
 386          $a = new stdClass();
 387          $a->from = $page * $pagesize + 1;
 388          $a->to = min(($page + 1) * $pagesize, $count);
 389          $a->of = $count;
 390          echo $OUTPUT->heading(get_string('gradingattemptsxtoyofz', 'quiz_grading', $a), 3);
 391  
 392          if ($count > $pagesize && $order != 'random') {
 393              echo $OUTPUT->paging_bar($count, $page, $pagesize,
 394                      $this->grade_question_url($slot, $questionid, $grade, false));
 395          }
 396  
 397          // Display the form with one section for each attempt.
 398          $sesskey = sesskey();
 399          $qubaidlist = implode(',', $qubaids);
 400          echo html_writer::start_tag('form', array('method' => 'post',
 401                  'action' => $this->grade_question_url($slot, $questionid, $grade, $page),
 402                  'class' => 'mform', 'id' => 'manualgradingform')) .
 403                  html_writer::start_tag('div') .
 404                  html_writer::input_hidden_params(new moodle_url('', array(
 405                  'qubaids' => $qubaidlist, 'slots' => $slot, 'sesskey' => $sesskey)));
 406  
 407          foreach ($qubaids as $qubaid) {
 408              $attempt = $attempts[$qubaid];
 409              $quba = question_engine::load_questions_usage_by_activity($qubaid);
 410              $displayoptions = quiz_get_review_options($this->quiz, $attempt, $this->context);
 411              $displayoptions->hide_all_feedback();
 412              $displayoptions->history = question_display_options::HIDDEN;
 413              $displayoptions->manualcomment = question_display_options::EDITABLE;
 414  
 415              $heading = $this->get_question_heading($attempt, $shownames, $showidnumbers);
 416              if ($heading) {
 417                  echo $OUTPUT->heading($heading, 4);
 418              }
 419              echo $quba->render_question($slot, $displayoptions, $this->questions[$slot]->number);
 420          }
 421  
 422          echo html_writer::tag('div', html_writer::empty_tag('input', array(
 423                  'type' => 'submit', 'value' => get_string('saveandnext', 'quiz_grading'))),
 424                  array('class' => 'mdl-align')) .
 425                  html_writer::end_tag('div') . html_writer::end_tag('form');
 426      }
 427  
 428      protected function get_question_heading($attempt, $shownames, $showidnumbers) {
 429          $a = new stdClass();
 430          $a->attempt = $attempt->attempt;
 431          $a->fullname = fullname($attempt);
 432          $a->idnumber = $attempt->idnumber;
 433  
 434          $showidnumbers &= !empty($attempt->idnumber);
 435  
 436          if ($shownames && $showidnumbers) {
 437              return get_string('gradingattemptwithidnumber', 'quiz_grading', $a);
 438          } else if ($shownames) {
 439              return get_string('gradingattempt', 'quiz_grading', $a);
 440          } else if ($showidnumbers) {
 441              $a->fullname = $attempt->idnumber;
 442              return get_string('gradingattempt', 'quiz_grading', $a);
 443          } else {
 444              return '';
 445          }
 446      }
 447  
 448      protected function validate_submitted_marks() {
 449  
 450          $qubaids = optional_param('qubaids', null, PARAM_SEQUENCE);
 451          if (!$qubaids) {
 452              return false;
 453          }
 454          $qubaids = clean_param_array(explode(',', $qubaids), PARAM_INT);
 455  
 456          $slots = optional_param('slots', '', PARAM_SEQUENCE);
 457          if (!$slots) {
 458              $slots = array();
 459          } else {
 460              $slots = explode(',', $slots);
 461          }
 462  
 463          foreach ($qubaids as $qubaid) {
 464              foreach ($slots as $slot) {
 465                  if (!question_engine::is_manual_grade_in_range($qubaid, $slot)) {
 466                      return false;
 467                  }
 468              }
 469          }
 470  
 471          return true;
 472      }
 473  
 474      protected function process_submitted_data() {
 475          global $DB;
 476  
 477          $qubaids = optional_param('qubaids', null, PARAM_SEQUENCE);
 478          $assumedslotforevents = optional_param('slot', null, PARAM_INT);
 479  
 480          if (!$qubaids) {
 481              return;
 482          }
 483  
 484          $qubaids = clean_param_array(explode(',', $qubaids), PARAM_INT);
 485          $attempts = $this->load_attempts_by_usage_ids($qubaids);
 486          $events = array();
 487  
 488          $transaction = $DB->start_delegated_transaction();
 489          foreach ($qubaids as $qubaid) {
 490              $attempt = $attempts[$qubaid];
 491              $attemptobj = new quiz_attempt($attempt, $this->quiz, $this->cm, $this->course);
 492              $attemptobj->process_submitted_actions(time());
 493  
 494              // Add the event we will trigger later.
 495              $params = array(
 496                  'objectid' => $attemptobj->get_question_attempt($assumedslotforevents)->get_question()->id,
 497                  'courseid' => $attemptobj->get_courseid(),
 498                  'context' => context_module::instance($attemptobj->get_cmid()),
 499                  'other' => array(
 500                      'quizid' => $attemptobj->get_quizid(),
 501                      'attemptid' => $attemptobj->get_attemptid(),
 502                      'slot' => $assumedslotforevents
 503                  )
 504              );
 505              $events[] = \mod_quiz\event\question_manually_graded::create($params);
 506          }
 507          $transaction->allow_commit();
 508  
 509          // Trigger events for all the questions we manually marked.
 510          foreach ($events as $event) {
 511              $event->trigger();
 512          }
 513      }
 514  
 515      /**
 516       * Load information about the number of attempts at various questions in each
 517       * summarystate.
 518       *
 519       * The results are returned as an two dimensional array $qubaid => $slot => $dataobject
 520       *
 521       * @param array $slots A list of slots for the questions you want to konw about.
 522       * @return array The array keys are slot,qestionid. The values are objects with
 523       * fields $slot, $questionid, $inprogress, $name, $needsgrading, $autograded,
 524       * $manuallygraded and $all.
 525       */
 526      protected function get_question_state_summary($slots) {
 527          $dm = new question_engine_data_mapper();
 528          return $dm->load_questions_usages_question_state_summary(
 529                  $this->get_qubaids_condition(), $slots);
 530      }
 531  
 532      /**
 533       * Get a list of usage ids where the question with slot $slot, and optionally
 534       * also with question id $questionid, is in summary state $summarystate. Also
 535       * return the total count of such states.
 536       *
 537       * Only a subset of the ids can be returned by using $orderby, $limitfrom and
 538       * $limitnum. A special value 'random' can be passed as $orderby, in which case
 539       * $limitfrom is ignored.
 540       *
 541       * @param int $slot The slot for the questions you want to konw about.
 542       * @param int $questionid (optional) Only return attempts that were of this specific question.
 543       * @param string $summarystate 'all', 'needsgrading', 'autograded' or 'manuallygraded'.
 544       * @param string $orderby 'random', 'date', 'student' or 'idnumber'.
 545       * @param int $page implements paging of the results.
 546       *      Ignored if $orderby = random or $pagesize is null.
 547       * @param int $pagesize implements paging of the results. null = all.
 548       */
 549      protected function get_usage_ids_where_question_in_state($summarystate, $slot,
 550              $questionid = null, $orderby = 'random', $page = 0, $pagesize = null) {
 551          global $CFG, $DB;
 552          $dm = new question_engine_data_mapper();
 553  
 554          if ($pagesize && $orderby != 'random') {
 555              $limitfrom = $page * $pagesize;
 556          } else {
 557              $limitfrom = 0;
 558          }
 559  
 560          $qubaids = $this->get_qubaids_condition();
 561  
 562          $params = array();
 563          if ($orderby == 'date') {
 564              list($statetest, $params) = $dm->in_summary_state_test(
 565                      'manuallygraded', false, 'mangrstate');
 566              $orderby = "(
 567                      SELECT MAX(sortqas.timecreated)
 568                      FROM {question_attempt_steps} sortqas
 569                      WHERE sortqas.questionattemptid = qa.id
 570                          AND sortqas.state $statetest
 571                      )";
 572          } else if ($orderby == 'studentfirstname' || $orderby == 'studentlastname' || $orderby == 'idnumber') {
 573              $qubaids->from .= " JOIN {user} u ON quiza.userid = u.id ";
 574              // For name sorting, map orderby form value to
 575              // actual column names; 'idnumber' maps naturally
 576              switch ($orderby) {
 577                  case "studentlastname":
 578                      $orderby = "u.lastname, u.firstname";
 579                      break;
 580                  case "studentfirstname":
 581                      $orderby = "u.firstname, u.lastname";
 582                      break;
 583              }
 584          }
 585  
 586          return $dm->load_questions_usages_where_question_in_state($qubaids, $summarystate,
 587                  $slot, $questionid, $orderby, $params, $limitfrom, $pagesize);
 588      }
 589  }

Search This Site: