<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file defines the quiz grades table.
*
* @package quiz_overview
* @copyright 2008 Jamie Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/quiz/report/attemptsreport_table.php');
/**
* This is a table subclass for displaying the quiz grades report.
*
* @copyright 2008 Jamie Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_overview_table extends quiz_attempts_report_table {
protected $regradedqs = array();
/**
* Constructor
* @param object $quiz
* @param context $context
* @param string $qmsubselect
* @param quiz_overview_options $options
* @param \core\dml\sql_join $groupstudentsjoins
* @param \core\dml\sql_join $studentsjoins
* @param array $questions
* @param moodle_url $reporturl
*/
public function __construct($quiz, $context, $qmsubselect,
quiz_overview_options $options, \core\dml\sql_join $groupstudentsjoins,
\core\dml\sql_join $studentsjoins, $questions, $reporturl) {
parent::__construct('mod-quiz-report-overview-report', $quiz , $context,
$qmsubselect, $options, $groupstudentsjoins, $studentsjoins, $questions, $reporturl);
}
public function build_table() {
global $DB;
if (!$this->rawdata) {
return;
}
$this->strtimeformat = str_replace(',', ' ', get_string('strftimedatetime'));
parent::build_table();
// End of adding the data from attempts. Now add averages at bottom.
$this->add_separator();
if (!empty($this->groupstudentsjoins->joins)) {
< $sql = "SELECT DISTINCT u.id
> $hasgroupstudents = $DB->record_exists_sql("
> SELECT 1
FROM {user} u
{$this->groupstudentsjoins->joins}
< WHERE {$this->groupstudentsjoins->wheres}";
< $groupstudents = $DB->get_records_sql($sql, $this->groupstudentsjoins->params);
< if ($groupstudents) {
> WHERE {$this->groupstudentsjoins->wheres}
> ", $this->groupstudentsjoins->params);
> if ($hasgroupstudents) {
$this->add_average_row(get_string('groupavg', 'grades'), $this->groupstudentsjoins);
}
}
if (!empty($this->studentsjoins->joins)) {
< $sql = "SELECT DISTINCT u.id
> $hasstudents = $DB->record_exists_sql("
> SELECT 1
FROM {user} u
{$this->studentsjoins->joins}
< WHERE {$this->studentsjoins->wheres}";
< $students = $DB->get_records_sql($sql, $this->studentsjoins->params);
< if ($students) {
> WHERE {$this->studentsjoins->wheres}
> " , $this->studentsjoins->params);
> if ($hasstudents) {
$this->add_average_row(get_string('overallaverage', 'grades'), $this->studentsjoins);
}
}
}
/**
* Calculate the average overall and question scores for a set of attempts at the quiz.
*
* @param string $label the title ot use for this row.
* @param \core\dml\sql_join $usersjoins to indicate a set of users.
* @return array of table cells that make up the average row.
*/
public function compute_average_row($label, \core\dml\sql_join $usersjoins) {
global $DB;
list($fields, $from, $where, $params) = $this->base_sql($usersjoins);
$record = $DB->get_record_sql("
SELECT AVG(quizaouter.sumgrades) AS grade, COUNT(quizaouter.sumgrades) AS numaveraged
FROM {quiz_attempts} quizaouter
JOIN (
SELECT DISTINCT quiza.id
FROM $from
WHERE $where
) relevant_attempt_ids ON quizaouter.id = relevant_attempt_ids.id
", $params);
$record->grade = quiz_rescale_grade($record->grade, $this->quiz, false);
if ($this->is_downloading()) {
$namekey = 'lastname';
} else {
$namekey = 'fullname';
}
$averagerow = array(
$namekey => $label,
'sumgrades' => $this->format_average($record),
'feedbacktext' => strip_tags(quiz_report_feedback_for_grade(
$record->grade, $this->quiz->id, $this->context))
);
if ($this->options->slotmarks) {
$dm = new question_engine_data_mapper();
$qubaids = new qubaid_join("{quiz_attempts} quizaouter
JOIN (
SELECT DISTINCT quiza.id
FROM $from
WHERE $where
) relevant_attempt_ids ON quizaouter.id = relevant_attempt_ids.id",
'quizaouter.uniqueid', '1 = 1', $params);
$avggradebyq = $dm->load_average_marks($qubaids, array_keys($this->questions));
$averagerow += $this->format_average_grade_for_questions($avggradebyq);
}
return $averagerow;
}
/**
* Add an average grade row for a set of users.
*
* @param string $label the title ot use for this row.
* @param \core\dml\sql_join $usersjoins (joins, wheres, params) for the users to average over.
*/
protected function add_average_row($label, \core\dml\sql_join $usersjoins) {
$averagerow = $this->compute_average_row($label, $usersjoins);
$this->add_data_keyed($averagerow);
}
/**
* Helper userd by {@link add_average_row()}.
* @param array $gradeaverages the raw grades.
* @return array the (partial) row of data.
*/
protected function format_average_grade_for_questions($gradeaverages) {
$row = array();
if (!$gradeaverages) {
$gradeaverages = array();
}
foreach ($this->questions as $question) {
if (isset($gradeaverages[$question->slot]) && $question->maxmark > 0) {
$record = $gradeaverages[$question->slot];
$record->grade = quiz_rescale_grade(
$record->averagefraction * $question->maxmark, $this->quiz, false);
} else {
$record = new stdClass();
$record->grade = null;
$record->numaveraged = 0;
}
$row['qsgrade' . $question->slot] = $this->format_average($record, true);
}
return $row;
}
/**
* Format an entry in an average row.
* @param object $record with fields grade and numaveraged.
* @param bool $question true if this is a question score, false if it is an overall score.
* @return string HTML fragment for an average score (with number of things included in the average).
*/
protected function format_average($record, $question = false) {
if (is_null($record->grade)) {
$average = '-';
} else if ($question) {
$average = quiz_format_question_grade($this->quiz, $record->grade);
} else {
$average = quiz_format_grade($this->quiz, $record->grade);
}
if ($this->download) {
return $average;
} else if (is_null($record->numaveraged) || $record->numaveraged == 0) {
return html_writer::tag('span', html_writer::tag('span',
$average, array('class' => 'average')), array('class' => 'avgcell'));
} else {
return html_writer::tag('span', html_writer::tag('span',
$average, array('class' => 'average')) . ' ' . html_writer::tag('span',
'(' . $record->numaveraged . ')', array('class' => 'count')),
array('class' => 'avgcell'));
}
}
protected function submit_buttons() {
if (has_capability('mod/quiz:regrade', $this->context)) {
$regradebuttonparams = [
'type' => 'submit',
'class' => 'btn btn-secondary mr-1',
'name' => 'regrade',
'value' => get_string('regradeselected', 'quiz_overview'),
'data-action' => 'toggle',
'data-togglegroup' => $this->togglegroup,
'data-toggle' => 'action',
'disabled' => true
];
echo html_writer::empty_tag('input', $regradebuttonparams);
}
parent::submit_buttons();
}
public function col_sumgrades($attempt) {
if ($attempt->state != quiz_attempt::FINISHED) {
return '-';
}
$grade = quiz_rescale_grade($attempt->sumgrades, $this->quiz);
if ($this->is_downloading()) {
return $grade;
}
if (isset($this->regradedqs[$attempt->usageid])) {
$newsumgrade = 0;
$oldsumgrade = 0;
foreach ($this->questions as $question) {
if (isset($this->regradedqs[$attempt->usageid][$question->slot])) {
$newsumgrade += $this->regradedqs[$attempt->usageid]
[$question->slot]->newfraction * $question->maxmark;
$oldsumgrade += $this->regradedqs[$attempt->usageid]
[$question->slot]->oldfraction * $question->maxmark;
} else {
$newsumgrade += $this->lateststeps[$attempt->usageid]
[$question->slot]->fraction * $question->maxmark;
$oldsumgrade += $this->lateststeps[$attempt->usageid]
[$question->slot]->fraction * $question->maxmark;
}
}
$newsumgrade = quiz_rescale_grade($newsumgrade, $this->quiz);
$oldsumgrade = quiz_rescale_grade($oldsumgrade, $this->quiz);
$grade = html_writer::tag('del', $oldsumgrade) . '/' .
html_writer::empty_tag('br') . $newsumgrade;
}
return html_writer::link(new moodle_url('/mod/quiz/review.php',
array('attempt' => $attempt->attempt)), $grade,
array('title' => get_string('reviewattempt', 'quiz')));
}
/**
* @param string $colname the name of the column.
* @param object $attempt the row of data - see the SQL in display() in
* mod/quiz/report/overview/report.php to see what fields are present,
* and what they are called.
* @return string the contents of the cell.
*/
public function other_cols($colname, $attempt) {
if (!preg_match('/^qsgrade(\d+)$/', $colname, $matches)) {
return parent::other_cols($colname, $attempt);
}
$slot = $matches[1];
$question = $this->questions[$slot];
if (!isset($this->lateststeps[$attempt->usageid][$slot])) {
return '-';
}
$stepdata = $this->lateststeps[$attempt->usageid][$slot];
$state = question_state::get($stepdata->state);
if ($question->maxmark == 0) {
$grade = '-';
} else if (is_null($stepdata->fraction)) {
if ($state == question_state::$needsgrading) {
$grade = get_string('requiresgrading', 'question');
} else {
$grade = '-';
}
} else {
$grade = quiz_rescale_grade(
$stepdata->fraction * $question->maxmark, $this->quiz, 'question');
}
if ($this->is_downloading()) {
return $grade;
}
if (isset($this->regradedqs[$attempt->usageid][$slot])) {
$gradefromdb = $grade;
$newgrade = quiz_rescale_grade(
$this->regradedqs[$attempt->usageid][$slot]->newfraction * $question->maxmark,
$this->quiz, 'question');
$oldgrade = quiz_rescale_grade(
$this->regradedqs[$attempt->usageid][$slot]->oldfraction * $question->maxmark,
$this->quiz, 'question');
$grade = html_writer::tag('del', $oldgrade) . '/' .
html_writer::empty_tag('br') . $newgrade;
}
return $this->make_review_link($grade, $attempt, $slot);
}
public function col_regraded($attempt) {
if ($attempt->regraded == '') {
return '';
} else if ($attempt->regraded == 0) {
return get_string('needed', 'quiz_overview');
} else if ($attempt->regraded == 1) {
return get_string('done', 'quiz_overview');
}
}
protected function update_sql_after_count($fields, $from, $where, $params) {
$fields .= ", COALESCE((
SELECT MAX(qqr.regraded)
FROM {quiz_overview_regrades} qqr
WHERE qqr.questionusageid = quiza.uniqueid
), -1) AS regraded";
if ($this->options->onlyregraded) {
$where .= " AND COALESCE((
SELECT MAX(qqr.regraded)
FROM {quiz_overview_regrades} qqr
WHERE qqr.questionusageid = quiza.uniqueid
), -1) <> -1";
}
return [$fields, $from, $where, $params];
}
protected function requires_latest_steps_loaded() {
return $this->options->slotmarks;
}
protected function is_latest_step_column($column) {
if (preg_match('/^qsgrade([0-9]+)/', $column, $matches)) {
return $matches[1];
}
return false;
}
protected function get_required_latest_state_fields($slot, $alias) {
return "$alias.fraction * $alias.maxmark AS qsgrade$slot";
}
public function query_db($pagesize, $useinitialsbar = true) {
parent::query_db($pagesize, $useinitialsbar);
if ($this->options->slotmarks && has_capability('mod/quiz:regrade', $this->context)) {
$this->regradedqs = $this->get_regraded_questions();
}
}
/**
* Get all the questions in all the attempts being displayed that need regrading.
* @return array A two dimensional array $questionusageid => $slot => $regradeinfo.
*/
protected function get_regraded_questions() {
global $DB;
$qubaids = $this->get_qubaids_condition();
$regradedqs = $DB->get_records_select('quiz_overview_regrades',
'questionusageid ' . $qubaids->usage_id_in(), $qubaids->usage_id_in_params());
return quiz_report_index_by_keys($regradedqs, array('questionusageid', 'slot'));
}
}