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 30] [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 overview report class.
      19   *
      20   * @package   quiz_overview
      21   * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
      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/attemptsreport.php');
      29  require_once($CFG->dirroot . '/mod/quiz/report/overview/overview_options.php');
      30  require_once($CFG->dirroot . '/mod/quiz/report/overview/overview_form.php');
      31  require_once($CFG->dirroot . '/mod/quiz/report/overview/overview_table.php');
      32  
      33  
      34  /**
      35   * Quiz report subclass for the overview (grades) report.
      36   *
      37   * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
      38   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
      39   */
      40  class quiz_overview_report extends quiz_attempts_report {
      41  
      42      public function display($quiz, $cm, $course) {
      43          global $CFG, $DB, $OUTPUT, $PAGE;
      44  
      45          list($currentgroup, $students, $groupstudents, $allowed) =
      46                  $this->init('overview', 'quiz_overview_settings_form', $quiz, $cm, $course);
      47          $options = new quiz_overview_options('overview', $quiz, $cm, $course);
      48  
      49          if ($fromform = $this->form->get_data()) {
      50              $options->process_settings_from_form($fromform);
      51  
      52          } else {
      53              $options->process_settings_from_params();
      54          }
      55  
      56          $this->form->set_data($options->get_initial_form_data());
      57  
      58          if ($options->attempts == self::ALL_WITH) {
      59              // This option is only available to users who can access all groups in
      60              // groups mode, so setting allowed to empty (which means all quiz attempts
      61              // are accessible, is not a security porblem.
      62              $allowed = array();
      63          }
      64  
      65          // Load the required questions.
      66          $questions = quiz_report_get_significant_questions($quiz);
      67  
      68          // Prepare for downloading, if applicable.
      69          $courseshortname = format_string($course->shortname, true,
      70                  array('context' => context_course::instance($course->id)));
      71          $table = new quiz_overview_table($quiz, $this->context, $this->qmsubselect,
      72                  $options, $groupstudents, $students, $questions, $options->get_url());
      73          $filename = quiz_report_download_filename(get_string('overviewfilename', 'quiz_overview'),
      74                  $courseshortname, $quiz->name);
      75          $table->is_downloading($options->download, $filename,
      76                  $courseshortname . ' ' . format_string($quiz->name, true));
      77          if ($table->is_downloading()) {
      78              raise_memory_limit(MEMORY_EXTRA);
      79          }
      80  
      81          $this->course = $course; // Hack to make this available in process_actions.
      82          $this->process_actions($quiz, $cm, $currentgroup, $groupstudents, $allowed, $options->get_url());
      83  
      84          // Start output.
      85          if (!$table->is_downloading()) {
      86              // Only print headers if not asked to download data.
      87              $this->print_header_and_tabs($cm, $course, $quiz, $this->mode);
      88          }
      89  
      90          if ($groupmode = groups_get_activity_groupmode($cm)) {
      91              // Groups are being used, so output the group selector if we are not downloading.
      92              if (!$table->is_downloading()) {
      93                  groups_print_activity_menu($cm, $options->get_url());
      94              }
      95          }
      96  
      97          // Print information on the number of existing attempts.
      98          if (!$table->is_downloading()) {
      99              // Do not print notices when downloading.
     100              if ($strattemptnum = quiz_num_attempt_summary($quiz, $cm, true, $currentgroup)) {
     101                  echo '<div class="quizattemptcounts">' . $strattemptnum . '</div>';
     102              }
     103          }
     104  
     105          $hasquestions = quiz_has_questions($quiz->id);
     106          if (!$table->is_downloading()) {
     107              if (!$hasquestions) {
     108                  echo quiz_no_questions_message($quiz, $cm, $this->context);
     109              } else if (!$students) {
     110                  echo $OUTPUT->notification(get_string('nostudentsyet'));
     111              } else if ($currentgroup && !$groupstudents) {
     112                  echo $OUTPUT->notification(get_string('nostudentsingroup'));
     113              }
     114  
     115              // Print the display options.
     116              $this->form->display();
     117          }
     118  
     119          $hasstudents = $students && (!$currentgroup || $groupstudents);
     120          if ($hasquestions && ($hasstudents || $options->attempts == self::ALL_WITH)) {
     121              // Construct the SQL.
     122              $fields = $DB->sql_concat('u.id', "'#'", 'COALESCE(quiza.attempt, 0)') .
     123                      ' AS uniqueid, ';
     124  
     125              list($fields, $from, $where, $params) = $table->base_sql($allowed);
     126  
     127              $table->set_count_sql("SELECT COUNT(1) FROM $from WHERE $where", $params);
     128  
     129              // Test to see if there are any regraded attempts to be listed.
     130              $fields .= ", COALESCE((
     131                                  SELECT MAX(qqr.regraded)
     132                                    FROM {quiz_overview_regrades} qqr
     133                                   WHERE qqr.questionusageid = quiza.uniqueid
     134                            ), -1) AS regraded";
     135              if ($options->onlyregraded) {
     136                  $where .= " AND COALESCE((
     137                                      SELECT MAX(qqr.regraded)
     138                                        FROM {quiz_overview_regrades} qqr
     139                                       WHERE qqr.questionusageid = quiza.uniqueid
     140                                  ), -1) <> -1";
     141              }
     142              $table->set_sql($fields, $from, $where, $params);
     143  
     144              if (!$table->is_downloading()) {
     145                  // Output the regrade buttons.
     146                  if (has_capability('mod/quiz:regrade', $this->context)) {
     147                      $regradesneeded = $this->count_question_attempts_needing_regrade(
     148                              $quiz, $groupstudents);
     149                      if ($currentgroup) {
     150                          $a= new stdClass();
     151                          $a->groupname = groups_get_group_name($currentgroup);
     152                          $a->coursestudents = get_string('participants');
     153                          $a->countregradeneeded = $regradesneeded;
     154                          $regradealldrydolabel =
     155                                  get_string('regradealldrydogroup', 'quiz_overview', $a);
     156                          $regradealldrylabel =
     157                                  get_string('regradealldrygroup', 'quiz_overview', $a);
     158                          $regradealllabel =
     159                                  get_string('regradeallgroup', 'quiz_overview', $a);
     160                      } else {
     161                          $regradealldrydolabel =
     162                                  get_string('regradealldrydo', 'quiz_overview', $regradesneeded);
     163                          $regradealldrylabel =
     164                                  get_string('regradealldry', 'quiz_overview');
     165                          $regradealllabel =
     166                                  get_string('regradeall', 'quiz_overview');
     167                      }
     168                      $displayurl = new moodle_url($options->get_url(), array('sesskey' => sesskey()));
     169                      echo '<div class="mdl-align">';
     170                      echo '<form action="'.$displayurl->out_omit_querystring().'">';
     171                      echo '<div>';
     172                      echo html_writer::input_hidden_params($displayurl);
     173                      echo '<input type="submit" name="regradeall" value="'.$regradealllabel.'"/>';
     174                      echo '<input type="submit" name="regradealldry" value="' .
     175                              $regradealldrylabel . '"/>';
     176                      if ($regradesneeded) {
     177                          echo '<input type="submit" name="regradealldrydo" value="' .
     178                                  $regradealldrydolabel . '"/>';
     179                      }
     180                      echo '</div>';
     181                      echo '</form>';
     182                      echo '</div>';
     183                  }
     184                  // Print information on the grading method.
     185                  if ($strattempthighlight = quiz_report_highlighting_grading_method(
     186                          $quiz, $this->qmsubselect, $options->onlygraded)) {
     187                      echo '<div class="quizattemptcounts">' . $strattempthighlight . '</div>';
     188                  }
     189              }
     190  
     191              // Define table columns.
     192              $columns = array();
     193              $headers = array();
     194  
     195              if (!$table->is_downloading() && $options->checkboxcolumn) {
     196                  $columns[] = 'checkbox';
     197                  $headers[] = null;
     198              }
     199  
     200              $this->add_user_columns($table, $columns, $headers);
     201              $this->add_state_column($columns, $headers);
     202              $this->add_time_columns($columns, $headers);
     203  
     204              $this->add_grade_columns($quiz, $options->usercanseegrades, $columns, $headers, false);
     205  
     206              if (!$table->is_downloading() && has_capability('mod/quiz:regrade', $this->context) &&
     207                      $this->has_regraded_questions($from, $where, $params)) {
     208                  $columns[] = 'regraded';
     209                  $headers[] = get_string('regrade', 'quiz_overview');
     210              }
     211  
     212              if ($options->slotmarks) {
     213                  foreach ($questions as $slot => $question) {
     214                      // Ignore questions of zero length.
     215                      $columns[] = 'qsgrade' . $slot;
     216                      $header = get_string('qbrief', 'quiz', $question->number);
     217                      if (!$table->is_downloading()) {
     218                          $header .= '<br />';
     219                      } else {
     220                          $header .= ' ';
     221                      }
     222                      $header .= '/' . quiz_rescale_grade($question->maxmark, $quiz, 'question');
     223                      $headers[] = $header;
     224                  }
     225              }
     226  
     227              $this->set_up_table_columns($table, $columns, $headers, $this->get_base_url(), $options, false);
     228              $table->set_attribute('class', 'generaltable generalbox grades');
     229  
     230              $table->out($options->pagesize, true);
     231          }
     232  
     233          if (!$table->is_downloading() && $options->usercanseegrades) {
     234              $output = $PAGE->get_renderer('mod_quiz');
     235              if ($currentgroup && $groupstudents) {
     236                  list($usql, $params) = $DB->get_in_or_equal($groupstudents);
     237                  $params[] = $quiz->id;
     238                  if ($DB->record_exists_select('quiz_grades', "userid $usql AND quiz = ?",
     239                          $params)) {
     240                      $imageurl = new moodle_url('/mod/quiz/report/overview/overviewgraph.php',
     241                              array('id' => $quiz->id, 'groupid' => $currentgroup));
     242                      $graphname = get_string('overviewreportgraphgroup', 'quiz_overview',
     243                              groups_get_group_name($currentgroup));
     244                      echo $output->graph($imageurl, $graphname);
     245                  }
     246              }
     247  
     248              if ($DB->record_exists('quiz_grades', array('quiz'=> $quiz->id))) {
     249                  $imageurl = new moodle_url('/mod/quiz/report/overview/overviewgraph.php',
     250                          array('id' => $quiz->id));
     251                  $graphname = get_string('overviewreportgraph', 'quiz_overview');
     252                  echo $output->graph($imageurl, $graphname);
     253              }
     254          }
     255          return true;
     256      }
     257  
     258      protected function process_actions($quiz, $cm, $currentgroup, $groupstudents, $allowed, $redirecturl) {
     259          parent::process_actions($quiz, $cm, $currentgroup, $groupstudents, $allowed, $redirecturl);
     260  
     261          if (empty($currentgroup) || $groupstudents) {
     262              if (optional_param('regrade', 0, PARAM_BOOL) && confirm_sesskey()) {
     263                  if ($attemptids = optional_param_array('attemptid', array(), PARAM_INT)) {
     264                      $this->start_regrade($quiz, $cm);
     265                      $this->regrade_attempts($quiz, false, $groupstudents, $attemptids);
     266                      $this->finish_regrade($redirecturl);
     267                  }
     268              }
     269          }
     270  
     271          if (optional_param('regradeall', 0, PARAM_BOOL) && confirm_sesskey()) {
     272              $this->start_regrade($quiz, $cm);
     273              $this->regrade_attempts($quiz, false, $groupstudents);
     274              $this->finish_regrade($redirecturl);
     275  
     276          } else if (optional_param('regradealldry', 0, PARAM_BOOL) && confirm_sesskey()) {
     277              $this->start_regrade($quiz, $cm);
     278              $this->regrade_attempts($quiz, true, $groupstudents);
     279              $this->finish_regrade($redirecturl);
     280  
     281          } else if (optional_param('regradealldrydo', 0, PARAM_BOOL) && confirm_sesskey()) {
     282              $this->start_regrade($quiz, $cm);
     283              $this->regrade_attempts_needing_it($quiz, $groupstudents);
     284              $this->finish_regrade($redirecturl);
     285          }
     286      }
     287  
     288      /**
     289       * Check necessary capabilities, and start the display of the regrade progress page.
     290       * @param object $quiz the quiz settings.
     291       * @param object $cm the cm object for the quiz.
     292       */
     293      protected function start_regrade($quiz, $cm) {
     294          global $OUTPUT, $PAGE;
     295          require_capability('mod/quiz:regrade', $this->context);
     296          $this->print_header_and_tabs($cm, $this->course, $quiz, $this->mode);
     297      }
     298  
     299      /**
     300       * Finish displaying the regrade progress page.
     301       * @param moodle_url $nexturl where to send the user after the regrade.
     302       * @uses exit. This method never returns.
     303       */
     304      protected function finish_regrade($nexturl) {
     305          global $OUTPUT, $PAGE;
     306          echo $OUTPUT->heading(get_string('regradecomplete', 'quiz_overview'), 3);
     307          echo $OUTPUT->continue_button($nexturl);
     308          echo $OUTPUT->footer();
     309          die();
     310      }
     311  
     312      /**
     313       * Unlock the session and allow the regrading process to run in the background.
     314       */
     315      protected function unlock_session() {
     316          \core\session\manager::write_close();
     317          ignore_user_abort(true);
     318      }
     319  
     320      /**
     321       * Regrade a particular quiz attempt. Either for real ($dryrun = false), or
     322       * as a pretend regrade to see which fractions would change. The outcome is
     323       * stored in the quiz_overview_regrades table.
     324       *
     325       * Note, $attempt is not upgraded in the database. The caller needs to do that.
     326       * However, $attempt->sumgrades is updated, if this is not a dry run.
     327       *
     328       * @param object $attempt the quiz attempt to regrade.
     329       * @param bool $dryrun if true, do a pretend regrade, otherwise do it for real.
     330       * @param array $slots if null, regrade all questions, otherwise, just regrade
     331       *      the quetsions with those slots.
     332       */
     333      protected function regrade_attempt($attempt, $dryrun = false, $slots = null) {
     334          global $DB;
     335          // Need more time for a quiz with many questions.
     336          core_php_time_limit::raise(300);
     337  
     338          $transaction = $DB->start_delegated_transaction();
     339  
     340          $quba = question_engine::load_questions_usage_by_activity($attempt->uniqueid);
     341  
     342          if (is_null($slots)) {
     343              $slots = $quba->get_slots();
     344          }
     345  
     346          $finished = $attempt->state == quiz_attempt::FINISHED;
     347          foreach ($slots as $slot) {
     348              $qqr = new stdClass();
     349              $qqr->oldfraction = $quba->get_question_fraction($slot);
     350  
     351              $quba->regrade_question($slot, $finished);
     352  
     353              $qqr->newfraction = $quba->get_question_fraction($slot);
     354  
     355              if (abs($qqr->oldfraction - $qqr->newfraction) > 1e-7) {
     356                  $qqr->questionusageid = $quba->get_id();
     357                  $qqr->slot = $slot;
     358                  $qqr->regraded = empty($dryrun);
     359                  $qqr->timemodified = time();
     360                  $DB->insert_record('quiz_overview_regrades', $qqr, false);
     361              }
     362          }
     363  
     364          if (!$dryrun) {
     365              question_engine::save_questions_usage_by_activity($quba);
     366          }
     367  
     368          $transaction->allow_commit();
     369  
     370          // Really, PHP should not need this hint, but without this, we just run out of memory.
     371          $quba = null;
     372          $transaction = null;
     373          gc_collect_cycles();
     374      }
     375  
     376      /**
     377       * Regrade attempts for this quiz, exactly which attempts are regraded is
     378       * controlled by the parameters.
     379       * @param object $quiz the quiz settings.
     380       * @param bool $dryrun if true, do a pretend regrade, otherwise do it for real.
     381       * @param array $groupstudents blank for all attempts, otherwise regrade attempts
     382       * for these users.
     383       * @param array $attemptids blank for all attempts, otherwise only regrade
     384       * attempts whose id is in this list.
     385       */
     386      protected function regrade_attempts($quiz, $dryrun = false,
     387              $groupstudents = array(), $attemptids = array()) {
     388          global $DB;
     389          $this->unlock_session();
     390  
     391          $where = "quiz = ? AND preview = 0";
     392          $params = array($quiz->id);
     393  
     394          if ($groupstudents) {
     395              list($usql, $uparams) = $DB->get_in_or_equal($groupstudents);
     396              $where .= " AND userid $usql";
     397              $params = array_merge($params, $uparams);
     398          }
     399  
     400          if ($attemptids) {
     401              list($asql, $aparams) = $DB->get_in_or_equal($attemptids);
     402              $where .= " AND id $asql";
     403              $params = array_merge($params, $aparams);
     404          }
     405  
     406          $attempts = $DB->get_records_select('quiz_attempts', $where, $params);
     407          if (!$attempts) {
     408              return;
     409          }
     410  
     411          $this->clear_regrade_table($quiz, $groupstudents);
     412  
     413          $progressbar = new progress_bar('quiz_overview_regrade', 500, true);
     414          $a = array(
     415              'count' => count($attempts),
     416              'done'  => 0,
     417          );
     418          foreach ($attempts as $attempt) {
     419              $this->regrade_attempt($attempt, $dryrun);
     420              $a['done']++;
     421              $progressbar->update($a['done'], $a['count'],
     422                      get_string('regradingattemptxofy', 'quiz_overview', $a));
     423          }
     424  
     425          if (!$dryrun) {
     426              $this->update_overall_grades($quiz);
     427          }
     428      }
     429  
     430      /**
     431       * Regrade those questions in those attempts that are marked as needing regrading
     432       * in the quiz_overview_regrades table.
     433       * @param object $quiz the quiz settings.
     434       * @param array $groupstudents blank for all attempts, otherwise regrade attempts
     435       * for these users.
     436       */
     437      protected function regrade_attempts_needing_it($quiz, $groupstudents) {
     438          global $DB;
     439          $this->unlock_session();
     440  
     441          $where = "quiza.quiz = ? AND quiza.preview = 0 AND qqr.regraded = 0";
     442          $params = array($quiz->id);
     443  
     444          // Fetch all attempts that need regrading.
     445          if ($groupstudents) {
     446              list($usql, $uparams) = $DB->get_in_or_equal($groupstudents);
     447              $where .= " AND quiza.userid $usql";
     448              $params += $uparams;
     449          }
     450  
     451          $toregrade = $DB->get_records_sql("
     452                  SELECT quiza.uniqueid, qqr.slot
     453                  FROM {quiz_attempts} quiza
     454                  JOIN {quiz_overview_regrades} qqr ON qqr.questionusageid = quiza.uniqueid
     455                  WHERE $where", $params);
     456  
     457          if (!$toregrade) {
     458              return;
     459          }
     460  
     461          $attemptquestions = array();
     462          foreach ($toregrade as $row) {
     463              $attemptquestions[$row->uniqueid][] = $row->slot;
     464          }
     465          $attempts = $DB->get_records_list('quiz_attempts', 'uniqueid',
     466                  array_keys($attemptquestions));
     467  
     468          $this->clear_regrade_table($quiz, $groupstudents);
     469  
     470          $progressbar = new progress_bar('quiz_overview_regrade', 500, true);
     471          $a = array(
     472              'count' => count($attempts),
     473              'done'  => 0,
     474          );
     475          foreach ($attempts as $attempt) {
     476              $this->regrade_attempt($attempt, false, $attemptquestions[$attempt->uniqueid]);
     477              $a['done']++;
     478              $progressbar->update($a['done'], $a['count'],
     479                      get_string('regradingattemptxofy', 'quiz_overview', $a));
     480          }
     481  
     482          $this->update_overall_grades($quiz);
     483      }
     484  
     485      /**
     486       * Count the number of attempts in need of a regrade.
     487       * @param object $quiz the quiz settings.
     488       * @param array $groupstudents user ids. If this is given, only data relating
     489       * to these users is cleared.
     490       */
     491      protected function count_question_attempts_needing_regrade($quiz, $groupstudents) {
     492          global $DB;
     493  
     494          $usertest = '';
     495          $params = array();
     496          if ($groupstudents) {
     497              list($usql, $params) = $DB->get_in_or_equal($groupstudents);
     498              $usertest = "quiza.userid $usql AND ";
     499          }
     500  
     501          $params[] = $quiz->id;
     502          $sql = "SELECT COUNT(DISTINCT quiza.id)
     503                  FROM {quiz_attempts} quiza
     504                  JOIN {quiz_overview_regrades} qqr ON quiza.uniqueid = qqr.questionusageid
     505                  WHERE
     506                      $usertest
     507                      quiza.quiz = ? AND
     508                      quiza.preview = 0 AND
     509                      qqr.regraded = 0";
     510          return $DB->count_records_sql($sql, $params);
     511      }
     512  
     513      /**
     514       * Are there any pending regrades in the table we are going to show?
     515       * @param string $from tables used by the main query.
     516       * @param string $where where clause used by the main query.
     517       * @param array $params required by the SQL.
     518       * @return bool whether there are pending regrades.
     519       */
     520      protected function has_regraded_questions($from, $where, $params) {
     521          global $DB;
     522          return $DB->record_exists_sql("
     523                  SELECT 1
     524                    FROM {$from}
     525                    JOIN {quiz_overview_regrades} qor ON qor.questionusageid = quiza.uniqueid
     526                   WHERE {$where}", $params);
     527      }
     528  
     529      /**
     530       * Remove all information about pending/complete regrades from the database.
     531       * @param object $quiz the quiz settings.
     532       * @param array $groupstudents user ids. If this is given, only data relating
     533       * to these users is cleared.
     534       */
     535      protected function clear_regrade_table($quiz, $groupstudents) {
     536          global $DB;
     537  
     538          // Fetch all attempts that need regrading.
     539          $where = '';
     540          $params = array();
     541          if ($groupstudents) {
     542              list($usql, $params) = $DB->get_in_or_equal($groupstudents);
     543              $where = "userid $usql AND ";
     544          }
     545  
     546          $params[] = $quiz->id;
     547          $DB->delete_records_select('quiz_overview_regrades',
     548                  "questionusageid IN (
     549                      SELECT uniqueid
     550                      FROM {quiz_attempts}
     551                      WHERE $where quiz = ?
     552                  )", $params);
     553      }
     554  
     555      /**
     556       * Update the final grades for all attempts. This method is used following
     557       * a regrade.
     558       * @param object $quiz the quiz settings.
     559       * @param array $userids only update scores for these userids.
     560       * @param array $attemptids attemptids only update scores for these attempt ids.
     561       */
     562      protected function update_overall_grades($quiz) {
     563          quiz_update_all_attempt_sumgrades($quiz);
     564          quiz_update_all_final_grades($quiz);
     565          quiz_update_grades($quiz);
     566      }
     567  }
    

    Search This Site: