Search moodle.org's
Developer Documentation


  • Bug fixes for general core bugs in 2.8.x ended 9 November 2015 (12 months).
  • Bug fixes for security issues in 2.8.x ended 9 May 2016 (18 months).
  • minimum PHP 5.4.4 (always use latest PHP 5.4.x or 5.5.x on Windows - http://windows.php.net/download/), PHP 7 is NOT supported
  • Differences Between: [Versions 28 and 31] [Versions 28 and 32] [Versions 28 and 33] [Versions 28 and 34] [Versions 28 and 35] [Versions 28 and 36] [Versions 28 and 37]

       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: