MDL-74923 quiz: move report base classes to local folder

This commit is contained in:
Tim Hunt 2022-12-09 12:42:57 +00:00
parent 9efec07f77
commit 8cbc6acb17
28 changed files with 1687 additions and 1617 deletions

View File

@ -0,0 +1,383 @@
<?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/>.
namespace mod_quiz\local\reports;
use coding_exception;
use context_module;
use moodle_url;
use stdClass;
use table_sql;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/tablelib.php');
/**
* Base class for quiz reports that are basically a table with one row for each attempt.
*
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class attempts_report extends report_base {
/** @var int default page size for reports. */
const DEFAULT_PAGE_SIZE = 30;
/** @var string constant used for the options, means all users with attempts. */
const ALL_WITH = 'all_with';
/** @var string constant used for the options, means only enrolled users with attempts. */
const ENROLLED_WITH = 'enrolled_with';
/** @var string constant used for the options, means only enrolled users without attempts. */
const ENROLLED_WITHOUT = 'enrolled_without';
/** @var string constant used for the options, means all enrolled users. */
const ENROLLED_ALL = 'enrolled_any';
/** @var string the mode this report is. */
protected $mode;
/** @var context_module the quiz context. */
protected $context;
/** @var attempts_report_options_form The settings form to use. */
protected $form;
/** @var string SQL fragment for selecting the attempt that gave the final grade,
* if applicable. */
protected $qmsubselect;
/** @var boolean caches the results of {@link should_show_grades()}. */
protected $showgrades = null;
/**
* Initialise various aspects of this report.
*
* @param string $mode
* @param string $formclass
* @param object $quiz
* @param object $cm
* @param object $course
* @return array with four elements:
* 0 => integer the current group id (0 for none).
* 1 => \core\dml\sql_join Contains joins, wheres, params for all the students in this course.
* 2 => \core\dml\sql_join Contains joins, wheres, params for all the students in the current group.
* 3 => \core\dml\sql_join Contains joins, wheres, params for all the students to show in the report.
* Will be the same as either element 1 or 2.
*/
public function init($mode, $formclass, $quiz, $cm, $course) {
$this->mode = $mode;
$this->context = context_module::instance($cm->id);
list($currentgroup, $studentsjoins, $groupstudentsjoins, $allowedjoins) = $this->get_students_joins(
$cm, $course);
$this->qmsubselect = quiz_report_qm_filter_select($quiz);
$this->form = new $formclass($this->get_base_url(),
array('quiz' => $quiz, 'currentgroup' => $currentgroup, 'context' => $this->context));
return array($currentgroup, $studentsjoins, $groupstudentsjoins, $allowedjoins);
}
/**
* Get the base URL for this report.
* @return moodle_url the URL.
*/
protected function get_base_url() {
return new moodle_url('/mod/quiz/report.php',
array('id' => $this->context->instanceid, 'mode' => $this->mode));
}
/**
* Get sql fragments (joins) which can be used to build queries that
* will select an appropriate set of students to show in the reports.
*
* @param object $cm the course module.
* @param object $course the course settings.
* @return array with four elements:
* 0 => integer the current group id (0 for none).
* 1 => \core\dml\sql_join Contains joins, wheres, params for all the students in this course.
* 2 => \core\dml\sql_join Contains joins, wheres, params for all the students in the current group.
* 3 => \core\dml\sql_join Contains joins, wheres, params for all the students to show in the report.
* Will be the same as either element 1 or 2.
*/
protected function get_students_joins($cm, $course = null) {
$currentgroup = $this->get_current_group($cm, $course, $this->context);
$empty = new \core\dml\sql_join();
if ($currentgroup == self::NO_GROUPS_ALLOWED) {
return array($currentgroup, $empty, $empty, $empty);
}
$studentsjoins = get_enrolled_with_capabilities_join($this->context, '',
array('mod/quiz:attempt', 'mod/quiz:reviewmyattempts'));
if (empty($currentgroup)) {
return array($currentgroup, $studentsjoins, $empty, $studentsjoins);
}
// We have a currently selected group.
$groupstudentsjoins = get_enrolled_with_capabilities_join($this->context, '',
array('mod/quiz:attempt', 'mod/quiz:reviewmyattempts'), $currentgroup);
return array($currentgroup, $studentsjoins, $groupstudentsjoins, $groupstudentsjoins);
}
/**
* Outputs the things you commonly want at the top of a quiz report.
*
* Calls through to {@link print_header_and_tabs()} and then
* outputs the standard group selector, number of attempts summary,
* and messages to cover common cases when the report can't be shown.
*
* @param stdClass $cm the course_module information.
* @param stdClass $course the course settings.
* @param stdClass $quiz the quiz settings.
* @param attempts_report_options $options the current report settings.
* @param int $currentgroup the current group.
* @param bool $hasquestions whether there are any questions in the quiz.
* @param bool $hasstudents whether there are any relevant students.
*/
protected function print_standard_header_and_messages($cm, $course, $quiz,
$options, $currentgroup, $hasquestions, $hasstudents) {
global $OUTPUT;
$this->print_header_and_tabs($cm, $course, $quiz, $this->mode);
if (groups_get_activity_groupmode($cm)) {
// Groups are being used, so output the group selector if we are not downloading.
groups_print_activity_menu($cm, $options->get_url());
}
// Print information on the number of existing attempts.
if ($strattemptnum = quiz_num_attempt_summary($quiz, $cm, true, $currentgroup)) {
echo '<div class="quizattemptcounts">' . $strattemptnum . '</div>';
}
if (!$hasquestions) {
echo quiz_no_questions_message($quiz, $cm, $this->context);
} else if ($currentgroup == self::NO_GROUPS_ALLOWED) {
echo $OUTPUT->notification(get_string('notingroup'));
} else if (!$hasstudents) {
echo $OUTPUT->notification(get_string('nostudentsyet'));
} else if ($currentgroup && !$this->hasgroupstudents) {
echo $OUTPUT->notification(get_string('nostudentsingroup'));
}
}
/**
* Add all the user-related columns to the $columns and $headers arrays.
* @param table_sql $table the table being constructed.
* @param array $columns the list of columns. Added to.
* @param array $headers the columns headings. Added to.
*/
protected function add_user_columns($table, &$columns, &$headers) {
global $CFG;
if (!$table->is_downloading() && $CFG->grade_report_showuserimage) {
$columns[] = 'picture';
$headers[] = '';
}
if (!$table->is_downloading()) {
$columns[] = 'fullname';
$headers[] = get_string('name');
} else {
$columns[] = 'lastname';
$headers[] = get_string('lastname');
$columns[] = 'firstname';
$headers[] = get_string('firstname');
}
$extrafields = \core_user\fields::get_identity_fields($this->context);
foreach ($extrafields as $field) {
$columns[] = $field;
$headers[] = \core_user\fields::get_display_name($field);
}
}
/**
* Set the display options for the user-related columns in the table.
* @param table_sql $table the table being constructed.
*/
protected function configure_user_columns($table) {
$table->column_suppress('picture');
$table->column_suppress('fullname');
$extrafields = \core_user\fields::get_identity_fields($this->context);
foreach ($extrafields as $field) {
$table->column_suppress($field);
}
$table->column_class('picture', 'picture');
$table->column_class('lastname', 'bold');
$table->column_class('firstname', 'bold');
$table->column_class('fullname', 'bold');
}
/**
* Add the state column to the $columns and $headers arrays.
* @param array $columns the list of columns. Added to.
* @param array $headers the columns headings. Added to.
*/
protected function add_state_column(&$columns, &$headers) {
$columns[] = 'state';
$headers[] = get_string('attemptstate', 'quiz');
}
/**
* Add all the time-related columns to the $columns and $headers arrays.
* @param array $columns the list of columns. Added to.
* @param array $headers the columns headings. Added to.
*/
protected function add_time_columns(&$columns, &$headers) {
$columns[] = 'timestart';
$headers[] = get_string('startedon', 'quiz');
$columns[] = 'timefinish';
$headers[] = get_string('timecompleted', 'quiz');
$columns[] = 'duration';
$headers[] = get_string('attemptduration', 'quiz');
}
/**
* Add all the grade and feedback columns, if applicable, to the $columns
* and $headers arrays.
* @param object $quiz the quiz settings.
* @param bool $usercanseegrades whether the user is allowed to see grades for this quiz.
* @param array $columns the list of columns. Added to.
* @param array $headers the columns headings. Added to.
* @param bool $includefeedback whether to include the feedbacktext columns
*/
protected function add_grade_columns($quiz, $usercanseegrades, &$columns, &$headers, $includefeedback = true) {
if ($usercanseegrades) {
$columns[] = 'sumgrades';
$headers[] = get_string('grade', 'quiz') . '/' .
quiz_format_grade($quiz, $quiz->grade);
}
if ($includefeedback && quiz_has_feedback($quiz)) {
$columns[] = 'feedbacktext';
$headers[] = get_string('feedback', 'quiz');
}
}
/**
* Set up the table.
* @param table_sql $table the table being constructed.
* @param array $columns the list of columns.
* @param array $headers the columns headings.
* @param moodle_url $reporturl the URL of this report.
* @param attempts_report_options $options the display options.
* @param bool $collapsible whether to allow columns in the report to be collapsed.
*/
protected function set_up_table_columns($table, $columns, $headers, $reporturl,
attempts_report_options $options, $collapsible) {
$table->define_columns($columns);
$table->define_headers($headers);
$table->sortable(true, 'uniqueid');
$table->define_baseurl($options->get_url());
$this->configure_user_columns($table);
$table->no_sorting('feedbacktext');
$table->column_class('sumgrades', 'bold');
$table->set_attribute('id', 'attempts');
$table->collapsible($collapsible);
}
/**
* Process any submitted actions.
* @param object $quiz the quiz settings.
* @param object $cm the cm object for the quiz.
* @param int $currentgroup the currently selected group.
* @param \core\dml\sql_join $groupstudentsjoins (joins, wheres, params) the students in the current group.
* @param \core\dml\sql_join $allowedjoins (joins, wheres, params) the users whose attempt this user is allowed to modify.
* @param moodle_url $redirecturl where to redircet to after a successful action.
*/
protected function process_actions($quiz, $cm, $currentgroup, \core\dml\sql_join $groupstudentsjoins,
\core\dml\sql_join $allowedjoins, $redirecturl) {
if (empty($currentgroup) || $this->hasgroupstudents) {
if (optional_param('delete', 0, PARAM_BOOL) && confirm_sesskey()) {
if ($attemptids = optional_param_array('attemptid', array(), PARAM_INT)) {
require_capability('mod/quiz:deleteattempts', $this->context);
$this->delete_selected_attempts($quiz, $cm, $attemptids, $allowedjoins);
redirect($redirecturl);
}
}
}
}
/**
* Delete the quiz attempts
* @param object $quiz the quiz settings. Attempts that don't belong to
* this quiz are not deleted.
* @param object $cm the course_module object.
* @param array $attemptids the list of attempt ids to delete.
* @param \core\dml\sql_join $allowedjoins (joins, wheres, params) This list of userids that are visible in the report.
* Users can only delete attempts that they are allowed to see in the report.
* Empty means all users.
*/
protected function delete_selected_attempts($quiz, $cm, $attemptids, \core\dml\sql_join $allowedjoins) {
global $DB;
foreach ($attemptids as $attemptid) {
if (empty($allowedjoins->joins)) {
$sql = "SELECT quiza.*
FROM {quiz_attempts} quiza
JOIN {user} u ON u.id = quiza.userid
WHERE quiza.id = :attemptid";
} else {
$sql = "SELECT quiza.*
FROM {quiz_attempts} quiza
JOIN {user} u ON u.id = quiza.userid
{$allowedjoins->joins}
WHERE {$allowedjoins->wheres} AND quiza.id = :attemptid";
}
$params = $allowedjoins->params + array('attemptid' => $attemptid);
$attempt = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE);
if (!$attempt || $attempt->quiz != $quiz->id || $attempt->preview != 0) {
// Ensure the attempt exists, belongs to this quiz and belongs to
// a student included in the report. If not skip.
continue;
}
// Set the course module id before calling quiz_delete_attempt().
$quiz->cmid = $cm->id;
quiz_delete_attempt($attempt, $quiz);
}
}
/**
* Get information about which students to show in the report.
* @param object $cm the coures module.
* @param object $course the course settings.
* @return array with four elements:
* 0 => integer the current group id (0 for none).
* 1 => array ids of all the students in this course.
* 2 => array ids of all the students in the current group.
* 3 => array ids of all the students to show in the report. Will be the
* same as either element 1 or 2.
* @deprecated since Moodle 3.2 Please use get_students_joins() instead.
*/
protected function load_relevant_students($cm, $course = null) {
$msg = 'The function load_relevant_students() is deprecated. Please use get_students_joins() instead.';
throw new coding_exception($msg);
}
}

View File

@ -0,0 +1,285 @@
<?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/>.
namespace mod_quiz\local\reports;
use context_module;
use moodle_url;
use quiz_attempt;
use stdClass;
use mod_quiz\local\reports\attempts_report;
/**
* Base class for the options that control what is visible in an {@see attempts_report}.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempts_report_options {
/** @var string the report mode. */
public $mode;
/** @var object the settings for the quiz being reported on. */
public $quiz;
/** @var object the course module objects for the quiz being reported on. */
public $cm;
/** @var object the course settings for the course the quiz is in. */
public $course;
/**
* @var array form field name => corresponding quiz_attempt:: state constant.
*/
protected static $statefields = array(
'stateinprogress' => quiz_attempt::IN_PROGRESS,
'stateoverdue' => quiz_attempt::OVERDUE,
'statefinished' => quiz_attempt::FINISHED,
'stateabandoned' => quiz_attempt::ABANDONED,
);
/**
* @var string attempts_report::ALL_WITH, attempts_report::ENROLLED_WITH,
* attempts_report::ENROLLED_WITHOUT or attempts_report::ENROLLED_ALL
*/
public $attempts = attempts_report::ENROLLED_WITH;
/** @var int the currently selected group. 0 if no group is selected. */
public $group = 0;
/**
* @var array|null of quiz_attempt::IN_PROGRESS, etc. constants. null means
* no restriction.
*/
public $states = array(quiz_attempt::IN_PROGRESS, quiz_attempt::OVERDUE,
quiz_attempt::FINISHED, quiz_attempt::ABANDONED);
/**
* @var bool whether to show all finished attmepts, or just the one that gave
* the final grade for the user.
*/
public $onlygraded = false;
/** @var int Number of attempts to show per page. */
public $pagesize = attempts_report::DEFAULT_PAGE_SIZE;
/** @var string whether the data should be downloaded in some format, or '' to display it. */
public $download = '';
/** @var bool whether the current user has permission to see grades. */
public $usercanseegrades;
/** @var bool whether the report table should have a column of checkboxes. */
public $checkboxcolumn = false;
/**
* Constructor.
* @param string $mode which report these options are for.
* @param object $quiz the settings for the quiz being reported on.
* @param object $cm the course module objects for the quiz being reported on.
* @param object $coures the course settings for the coures this quiz is in.
*/
public function __construct($mode, $quiz, $cm, $course) {
$this->mode = $mode;
$this->quiz = $quiz;
$this->cm = $cm;
$this->course = $course;
$this->usercanseegrades = quiz_report_should_show_grades($quiz, context_module::instance($cm->id));
}
/**
* Get the URL parameters required to show the report with these options.
* @return array URL parameter name => value.
*/
protected function get_url_params() {
$params = array(
'id' => $this->cm->id,
'mode' => $this->mode,
'attempts' => $this->attempts,
'onlygraded' => $this->onlygraded,
);
if ($this->states) {
$params['states'] = implode('-', $this->states);
}
if (groups_get_activity_groupmode($this->cm, $this->course)) {
$params['group'] = $this->group;
}
return $params;
}
/**
* Get the URL to show the report with these options.
* @return moodle_url the URL.
*/
public function get_url() {
return new moodle_url('/mod/quiz/report.php', $this->get_url_params());
}
/**
* Process the data we get when the settings form is submitted. This includes
* updating the fields of this class, and updating the user preferences
* where appropriate.
* @param object $fromform The data from $mform->get_data() from the settings form.
*/
public function process_settings_from_form($fromform) {
$this->setup_from_form_data($fromform);
$this->resolve_dependencies();
$this->update_user_preferences();
}
/**
* Set up this preferences object using optional_param (using user_preferences
* to set anything not specified by the params.
*/
public function process_settings_from_params() {
$this->setup_from_user_preferences();
$this->setup_from_params();
$this->resolve_dependencies();
}
/**
* Get the current value of the settings to pass to the settings form.
*/
public function get_initial_form_data() {
$toform = new stdClass();
$toform->attempts = $this->attempts;
$toform->onlygraded = $this->onlygraded;
$toform->pagesize = $this->pagesize;
if ($this->states) {
foreach (self::$statefields as $field => $state) {
$toform->$field = in_array($state, $this->states);
}
}
return $toform;
}
/**
* Set the fields of this object from the form data.
* @param object $fromform The data from $mform->get_data() from the settings form.
*/
public function setup_from_form_data($fromform) {
$this->attempts = $fromform->attempts;
$this->group = groups_get_activity_group($this->cm, true);
$this->onlygraded = !empty($fromform->onlygraded);
$this->pagesize = $fromform->pagesize;
$this->states = array();
foreach (self::$statefields as $field => $state) {
if (!empty($fromform->$field)) {
$this->states[] = $state;
}
}
}
/**
* Set the fields of this object from the URL parameters.
*/
public function setup_from_params() {
$this->attempts = optional_param('attempts', $this->attempts, PARAM_ALPHAEXT);
$this->group = groups_get_activity_group($this->cm, true);
$this->onlygraded = optional_param('onlygraded', $this->onlygraded, PARAM_BOOL);
$this->pagesize = optional_param('pagesize', $this->pagesize, PARAM_INT);
$states = optional_param('states', '', PARAM_ALPHAEXT);
if (!empty($states)) {
$this->states = explode('-', $states);
}
$this->download = optional_param('download', $this->download, PARAM_ALPHA);
}
/**
* Set the fields of this object from the user's preferences.
* (For those settings that are backed by user-preferences).
*/
public function setup_from_user_preferences() {
$this->pagesize = get_user_preferences('quiz_report_pagesize', $this->pagesize);
}
/**
* Update the user preferences so they match the settings in this object.
* (For those settings that are backed by user-preferences).
*/
public function update_user_preferences() {
set_user_preference('quiz_report_pagesize', $this->pagesize);
}
/**
* Check the settings, and remove any 'impossible' combinations.
*/
public function resolve_dependencies() {
if ($this->group) {
// Default for when a group is selected.
if ($this->attempts === null || $this->attempts == attempts_report::ALL_WITH) {
$this->attempts = attempts_report::ENROLLED_WITH;
}
} else if (!$this->group && $this->course->id == SITEID) {
// Force report on front page to show all, unless a group is selected.
$this->attempts = attempts_report::ALL_WITH;
} else if (!in_array($this->attempts, array(attempts_report::ALL_WITH, attempts_report::ENROLLED_WITH,
attempts_report::ENROLLED_WITHOUT, attempts_report::ENROLLED_ALL))) {
$this->attempts = attempts_report::ENROLLED_WITH;
}
$cleanstates = array();
foreach (self::$statefields as $state) {
if (in_array($state, $this->states)) {
$cleanstates[] = $state;
}
}
$this->states = $cleanstates;
if (count($this->states) == count(self::$statefields)) {
// If all states have been selected, then there is no constraint
// required in the SQL, so clear the array.
$this->states = null;
}
if (!quiz_report_can_filter_only_graded($this->quiz)) {
// A grading mode like 'average' has been selected, so we cannot do
// the show the attempt that gave the final grade thing.
$this->onlygraded = false;
}
if ($this->attempts == attempts_report::ENROLLED_WITHOUT) {
$this->states = null;
$this->onlygraded = false;
}
if (!$this->is_showing_finished_attempts()) {
$this->onlygraded = false;
}
if ($this->pagesize < 1) {
$this->pagesize = attempts_report::DEFAULT_PAGE_SIZE;
}
}
/**
* Whether the options are such that finished attempts are being shown.
* @return boolean
*/
protected function is_showing_finished_attempts() {
return $this->states === null || in_array(quiz_attempt::FINISHED, $this->states);
}
}

View File

@ -0,0 +1,115 @@
<?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/>.
namespace mod_quiz\local\reports;
use html_writer;
use MoodleQuickForm;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/formslib.php');
/**
* Base class for the settings form for {@see attempts_report}s.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class attempts_report_options_form extends \moodleform {
protected function definition() {
$mform = $this->_form;
$mform->addElement('header', 'preferencespage',
get_string('reportwhattoinclude', 'quiz'));
$this->standard_attempt_fields($mform);
$this->other_attempt_fields($mform);
$mform->addElement('header', 'preferencesuser',
get_string('reportdisplayoptions', 'quiz'));
$this->standard_preference_fields($mform);
$this->other_preference_fields($mform);
$mform->addElement('submit', 'submitbutton',
get_string('showreport', 'quiz'));
}
protected function standard_attempt_fields(MoodleQuickForm $mform) {
$mform->addElement('select', 'attempts', get_string('reportattemptsfrom', 'quiz'), array(
attempts_report::ENROLLED_WITH => get_string('reportuserswith', 'quiz'),
attempts_report::ENROLLED_WITHOUT => get_string('reportuserswithout', 'quiz'),
attempts_report::ENROLLED_ALL => get_string('reportuserswithorwithout', 'quiz'),
attempts_report::ALL_WITH => get_string('reportusersall', 'quiz'),
));
$stategroup = array(
$mform->createElement('advcheckbox', 'stateinprogress', '',
get_string('stateinprogress', 'quiz')),
$mform->createElement('advcheckbox', 'stateoverdue', '',
get_string('stateoverdue', 'quiz')),
$mform->createElement('advcheckbox', 'statefinished', '',
get_string('statefinished', 'quiz')),
$mform->createElement('advcheckbox', 'stateabandoned', '',
get_string('stateabandoned', 'quiz')),
);
$mform->addGroup($stategroup, 'stateoptions',
get_string('reportattemptsthatare', 'quiz'), array(' '), false);
$mform->setDefault('stateinprogress', 1);
$mform->setDefault('stateoverdue', 1);
$mform->setDefault('statefinished', 1);
$mform->setDefault('stateabandoned', 1);
$mform->disabledIf('stateinprogress', 'attempts', 'eq', attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('stateoverdue', 'attempts', 'eq', attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('statefinished', 'attempts', 'eq', attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('stateabandoned', 'attempts', 'eq', attempts_report::ENROLLED_WITHOUT);
if (quiz_report_can_filter_only_graded($this->_customdata['quiz'])) {
$gm = html_writer::tag('span',
quiz_get_grading_option_name($this->_customdata['quiz']->grademethod),
array('class' => 'highlight'));
$mform->addElement('advcheckbox', 'onlygraded', '',
get_string('reportshowonlyfinished', 'quiz', $gm));
$mform->disabledIf('onlygraded', 'attempts', 'eq', attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('onlygraded', 'statefinished', 'notchecked');
}
}
protected function other_attempt_fields(MoodleQuickForm $mform) {
}
protected function standard_preference_fields(MoodleQuickForm $mform) {
$mform->addElement('text', 'pagesize', get_string('pagesize', 'quiz'));
$mform->setType('pagesize', PARAM_INT);
}
protected function other_preference_fields(MoodleQuickForm $mform) {
}
public function validation($data, $files) {
$errors = parent::validation($data, $files);
if ($data['attempts'] != attempts_report::ENROLLED_WITHOUT && !(
$data['stateinprogress'] || $data['stateoverdue'] || $data['statefinished'] || $data['stateabandoned'])) {
$errors['stateoptions'] = get_string('reportmustselectstate', 'quiz');
}
return $errors;
}
}

View File

@ -0,0 +1,707 @@
<?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/>.
namespace mod_quiz\local\reports;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/tablelib.php');
use coding_exception;
use context;
use html_writer;
use moodle_url;
use popup_action;
use question_state;
use qubaid_condition;
use qubaid_join;
use qubaid_list;
use question_engine_data_mapper;
use quiz_attempt;
use stdClass;
/**
* Base class for the table used by a {@see attempts_report}.
*
* @package mod_quiz
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class attempts_report_table extends \table_sql {
public $useridfield = 'userid';
/** @var moodle_url the URL of this report. */
protected $reporturl;
/** @var array the display options. */
protected $displayoptions;
/**
* @var array information about the latest step of each question.
* Loaded by {@link load_question_latest_steps()}, if applicable.
*/
protected $lateststeps = null;
/** @var object the quiz settings for the quiz we are reporting on. */
protected $quiz;
/** @var context the quiz context. */
protected $context;
/** @var string HTML fragment to select the first/best/last attempt, if appropriate. */
protected $qmsubselect;
/** @var object attempts_report_options the options affecting this report. */
protected $options;
/** @var \core\dml\sql_join Contains joins, wheres, params to find students
* in the currently selected group, if applicable.
*/
protected $groupstudentsjoins;
/** @var \core\dml\sql_join Contains joins, wheres, params to find the students in the course. */
protected $studentsjoins;
/** @var object the questions that comprise this quiz.. */
protected $questions;
/** @var bool whether to include the column with checkboxes to select each attempt. */
protected $includecheckboxes;
/** @var string The toggle group name for the checkboxes in the checkbox column. */
protected $togglegroup = 'quiz-attempts';
/**
* Constructor
* @param string $uniqueid
* @param object $quiz
* @param context $context
* @param string $qmsubselect
* @param attempts_report_options $options
* @param \core\dml\sql_join $groupstudentsjoins Contains joins, wheres, params
* @param \core\dml\sql_join $studentsjoins Contains joins, wheres, params
* @param array $questions
* @param moodle_url $reporturl
*/
public function __construct($uniqueid, $quiz, $context, $qmsubselect,
attempts_report_options $options, \core\dml\sql_join $groupstudentsjoins, \core\dml\sql_join $studentsjoins,
$questions, $reporturl) {
parent::__construct($uniqueid);
$this->quiz = $quiz;
$this->context = $context;
$this->qmsubselect = $qmsubselect;
$this->groupstudentsjoins = $groupstudentsjoins;
$this->studentsjoins = $studentsjoins;
$this->questions = $questions;
$this->includecheckboxes = $options->checkboxcolumn;
$this->reporturl = $reporturl;
$this->options = $options;
}
/**
* Generate the display of the checkbox column.
* @param object $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_checkbox($attempt) {
global $OUTPUT;
if ($attempt->attempt) {
$checkbox = new \core\output\checkbox_toggleall($this->togglegroup, false, [
'id' => "attemptid_{$attempt->attempt}",
'name' => 'attemptid[]',
'value' => $attempt->attempt,
'label' => get_string('selectattempt', 'quiz'),
'labelclasses' => 'accesshide',
]);
return $OUTPUT->render($checkbox);
} else {
return '';
}
}
/**
* Generate the display of the user's picture column.
* @param object $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_picture($attempt) {
global $OUTPUT;
$user = new stdClass();
$additionalfields = explode(',', implode(',', \core_user\fields::get_picture_fields()));
$user = username_load_fields_from_object($user, $attempt, null, $additionalfields);
$user->id = $attempt->userid;
return $OUTPUT->user_picture($user);
}
/**
* Generate the display of the user's full name column.
* @param object $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_fullname($attempt) {
$html = parent::col_fullname($attempt);
if ($this->is_downloading() || empty($attempt->attempt)) {
return $html;
}
return $html . html_writer::empty_tag('br') . html_writer::link(
new moodle_url('/mod/quiz/review.php', array('attempt' => $attempt->attempt)),
get_string('reviewattempt', 'quiz'), array('class' => 'reviewlink'));
}
/**
* Generate the display of the attempt state column.
* @param object $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_state($attempt) {
if (!is_null($attempt->attempt)) {
return quiz_attempt::state_name($attempt->state);
} else {
return '-';
}
}
/**
* Generate the display of the start time column.
* @param object $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_timestart($attempt) {
if ($attempt->attempt) {
return userdate($attempt->timestart, $this->strtimeformat);
} else {
return '-';
}
}
/**
* Generate the display of the finish time column.
* @param object $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_timefinish($attempt) {
if ($attempt->attempt && $attempt->timefinish) {
return userdate($attempt->timefinish, $this->strtimeformat);
} else {
return '-';
}
}
/**
* Generate the display of the time taken column.
* @param object $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_duration($attempt) {
if ($attempt->timefinish) {
return format_time($attempt->timefinish - $attempt->timestart);
} else {
return '-';
}
}
/**
* Generate the display of the feedback column.
* @param object $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_feedbacktext($attempt) {
if ($attempt->state != quiz_attempt::FINISHED) {
return '-';
}
$feedback = quiz_report_feedback_for_grade(
quiz_rescale_grade($attempt->sumgrades, $this->quiz, false),
$this->quiz->id, $this->context);
if ($this->is_downloading()) {
$feedback = strip_tags($feedback);
}
return $feedback;
}
public function get_row_class($attempt) {
if ($this->qmsubselect && $attempt->gradedattempt) {
return 'gradedattempt';
} else {
return '';
}
}
/**
* Make a link to review an individual question in a popup window.
*
* @param string $data HTML fragment. The text to make into the link.
* @param object $attempt data for the row of the table being output.
* @param int $slot the number used to identify this question within this usage.
*/
public function make_review_link($data, $attempt, $slot) {
global $OUTPUT, $CFG;
$flag = '';
if ($this->is_flagged($attempt->usageid, $slot)) {
$flag = $OUTPUT->pix_icon('i/flagged', get_string('flagged', 'question'),
'moodle', array('class' => 'questionflag'));
}
$feedbackimg = '';
$state = $this->slot_state($attempt, $slot);
if ($state && $state->is_finished() && $state != question_state::$needsgrading) {
$feedbackimg = $this->icon_for_fraction($this->slot_fraction($attempt, $slot));
}
$output = html_writer::tag('span', $feedbackimg . html_writer::tag('span',
$data, array('class' => $state->get_state_class(true))) . $flag, array('class' => 'que'));
$reviewparams = array('attempt' => $attempt->attempt, 'slot' => $slot);
if (isset($attempt->try)) {
$reviewparams['step'] = $this->step_no_for_try($attempt->usageid, $slot, $attempt->try);
}
$url = new moodle_url('/mod/quiz/reviewquestion.php', $reviewparams);
$output = $OUTPUT->action_link($url, $output,
new popup_action('click', $url, 'reviewquestion',
array('height' => 450, 'width' => 650)),
array('title' => get_string('reviewresponse', 'quiz')));
if (!empty($CFG->enableplagiarism)) {
require_once($CFG->libdir . '/plagiarismlib.php');
$output .= plagiarism_get_links([
'context' => $this->context->id,
'component' => 'qtype_'.$this->questions[$slot]->qtype,
'cmid' => $this->context->instanceid,
'area' => $attempt->usageid,
'itemid' => $slot,
'userid' => $attempt->userid]);
}
return $output;
}
/**
* @param object $attempt the row data
* @param int $slot
* @return question_state
*/
protected function slot_state($attempt, $slot) {
$stepdata = $this->lateststeps[$attempt->usageid][$slot];
return question_state::get($stepdata->state);
}
/**
* @param int $questionusageid
* @param int $slot
* @return bool
*/
protected function is_flagged($questionusageid, $slot) {
$stepdata = $this->lateststeps[$questionusageid][$slot];
return $stepdata->flagged;
}
/**
* @param object $attempt the row data
* @param int $slot
* @return float
*/
protected function slot_fraction($attempt, $slot) {
$stepdata = $this->lateststeps[$attempt->usageid][$slot];
return $stepdata->fraction;
}
/**
* Return an appropriate icon (green tick, red cross, etc.) for a grade.
* @param float $fraction grade on a scale 0..1.
* @return string html fragment.
*/
protected function icon_for_fraction($fraction) {
global $OUTPUT;
$feedbackclass = question_state::graded_state_for_fraction($fraction)->get_feedback_class();
return $OUTPUT->pix_icon('i/grade_' . $feedbackclass, get_string($feedbackclass, 'question'),
'moodle', array('class' => 'icon'));
}
/**
* Load any extra data after main query. At this point you can call {@link get_qubaids_condition} to get the condition that
* limits the query to just the question usages shown in this report page or alternatively for all attempts if downloading a
* full report.
*/
protected function load_extra_data() {
$this->lateststeps = $this->load_question_latest_steps();
}
/**
* Load information about the latest state of selected questions in selected attempts.
*
* The results are returned as an two dimensional array $qubaid => $slot => $dataobject
*
* @param qubaid_condition|null $qubaids used to restrict which usages are included
* in the query. See {@link qubaid_condition}.
* @return array of records. See the SQL in this function to see the fields available.
*/
protected function load_question_latest_steps(qubaid_condition $qubaids = null) {
if ($qubaids === null) {
$qubaids = $this->get_qubaids_condition();
}
$dm = new question_engine_data_mapper();
$latesstepdata = $dm->load_questions_usages_latest_steps(
$qubaids, array_keys($this->questions));
$lateststeps = array();
foreach ($latesstepdata as $step) {
$lateststeps[$step->questionusageid][$step->slot] = $step;
}
return $lateststeps;
}
/**
* Does this report require loading any more data after the main query. After the main query then
* you can use $this->get
*
* @return bool should {@link query_db()} call {@link load_extra_data}?
*/
protected function requires_extra_data() {
return $this->requires_latest_steps_loaded();
}
/**
* Does this report require the detailed information for each question from the
* question_attempts_steps table?
* @return bool should {@link load_extra_data} call {@link load_question_latest_steps}?
*/
protected function requires_latest_steps_loaded() {
return false;
}
/**
* Is this a column that depends on joining to the latest state information?
* If so, return the corresponding slot. If not, return false.
* @param string $column a column name
* @return int false if no, else a slot.
*/
protected function is_latest_step_column($column) {
return false;
}
/**
* Get any fields that might be needed when sorting on date for a particular slot.
*
* Note: these values are only used for sorting. The values displayed are taken
* from $this->lateststeps loaded in load_extra_data().
*
* @param int $slot the slot for the column we want.
* @param string $alias the table alias for latest state information relating to that slot.
* @return string definitions of extra fields to add to the SELECT list of the query.
*/
protected function get_required_latest_state_fields($slot, $alias) {
return '';
}
/**
* Contruct all the parts of the main database query.
* @param \core\dml\sql_join $allowedstudentsjoins (joins, wheres, params) defines allowed users for the report.
* @return array with 4 elements ($fields, $from, $where, $params) that can be used to
* build the actual database query.
*/
public function base_sql(\core\dml\sql_join $allowedstudentsjoins) {
global $DB;
// Please note this uniqueid column is not the same as quiza.uniqueid.
$fields = 'DISTINCT ' . $DB->sql_concat('u.id', "'#'", 'COALESCE(quiza.attempt, 0)') . ' AS uniqueid,';
if ($this->qmsubselect) {
$fields .= "\n(CASE WHEN $this->qmsubselect THEN 1 ELSE 0 END) AS gradedattempt,";
}
$userfieldsapi = \core_user\fields::for_identity($this->context)->with_name()
->excluding('id', 'idnumber', 'picture', 'imagealt', 'institution', 'department', 'email');
$userfields = $userfieldsapi->get_sql('u', true, '', '', false);
$fields .= '
quiza.uniqueid AS usageid,
quiza.id AS attempt,
u.id AS userid,
u.idnumber,
u.picture,
u.imagealt,
u.institution,
u.department,
u.email,' . $userfields->selects . ',
quiza.state,
quiza.sumgrades,
quiza.timefinish,
quiza.timestart,
CASE WHEN quiza.timefinish = 0 THEN null
WHEN quiza.timefinish > quiza.timestart THEN quiza.timefinish - quiza.timestart
ELSE 0 END AS duration';
// To explain that last bit, timefinish can be non-zero and less
// than timestart when you have two load-balanced servers with very
// badly synchronised clocks, and a student does a really quick attempt.
// This part is the same for all cases. Join the users and quiz_attempts tables.
$from = " {user} u";
$from .= "\n{$userfields->joins}";
$from .= "\nLEFT JOIN {quiz_attempts} quiza ON
quiza.userid = u.id AND quiza.quiz = :quizid";
$params = array_merge($userfields->params, ['quizid' => $this->quiz->id]);
if ($this->qmsubselect && $this->options->onlygraded) {
$from .= " AND (quiza.state <> :finishedstate OR $this->qmsubselect)";
$params['finishedstate'] = quiz_attempt::FINISHED;
}
switch ($this->options->attempts) {
case attempts_report::ALL_WITH:
// Show all attempts, including students who are no longer in the course.
$where = 'quiza.id IS NOT NULL AND quiza.preview = 0';
break;
case attempts_report::ENROLLED_WITH:
// Show only students with attempts.
$from .= "\n" . $allowedstudentsjoins->joins;
$where = "quiza.preview = 0 AND quiza.id IS NOT NULL AND " . $allowedstudentsjoins->wheres;
$params = array_merge($params, $allowedstudentsjoins->params);
break;
case attempts_report::ENROLLED_WITHOUT:
// Show only students without attempts.
$from .= "\n" . $allowedstudentsjoins->joins;
$where = "quiza.id IS NULL AND " . $allowedstudentsjoins->wheres;
$params = array_merge($params, $allowedstudentsjoins->params);
break;
case attempts_report::ENROLLED_ALL:
// Show all students with or without attempts.
$from .= "\n" . $allowedstudentsjoins->joins;
$where = "(quiza.preview = 0 OR quiza.preview IS NULL) AND " . $allowedstudentsjoins->wheres;
$params = array_merge($params, $allowedstudentsjoins->params);
break;
}
if ($this->options->states) {
list($statesql, $stateparams) = $DB->get_in_or_equal($this->options->states,
SQL_PARAMS_NAMED, 'state');
$params += $stateparams;
$where .= " AND (quiza.state $statesql OR quiza.state IS NULL)";
}
return array($fields, $from, $where, $params);
}
/**
* A chance for subclasses to modify the SQL after the count query has been generated,
* and before the full query is constructed.
* @param string $fields SELECT list.
* @param string $from JOINs part of the SQL.
* @param string $where WHERE clauses.
* @param array $params Query params.
* @return array with 4 elements ($fields, $from, $where, $params) as from base_sql.
*/
protected function update_sql_after_count($fields, $from, $where, $params) {
return [$fields, $from, $where, $params];
}
/**
* Set up the SQL queries (count rows, and get data).
*
* @param \core\dml\sql_join $allowedjoins (joins, wheres, params) defines allowed users for the report.
*/
public function setup_sql_queries($allowedjoins) {
list($fields, $from, $where, $params) = $this->base_sql($allowedjoins);
// The WHERE clause is vital here, because some parts of tablelib.php will expect to
// add bits like ' AND x = 1' on the end, and that needs to leave to valid SQL.
$this->set_count_sql("SELECT COUNT(1) FROM (SELECT $fields FROM $from WHERE $where) temp WHERE 1 = 1", $params);
list($fields, $from, $where, $params) = $this->update_sql_after_count($fields, $from, $where, $params);
$this->set_sql($fields, $from, $where, $params);
}
/**
* Add the information about the latest state of the question with slot
* $slot to the query.
*
* The extra information is added as a join to a
* 'table' with alias qa$slot, with columns that are a union of
* the columns of the question_attempts and question_attempts_states tables.
*
* @param int $slot the question to add information for.
*/
protected function add_latest_state_join($slot) {
$alias = 'qa' . $slot;
$fields = $this->get_required_latest_state_fields($slot, $alias);
if (!$fields) {
return;
}
// This condition roughly filters the list of attempts to be considered.
// It is only used in a subselect to help crappy databases (see MDL-30122)
// therefore, it is better to use a very simple join, which may include
// too many records, than to do a super-accurate join.
$qubaids = new qubaid_join("{quiz_attempts} {$alias}quiza", "{$alias}quiza.uniqueid",
"{$alias}quiza.quiz = :{$alias}quizid", array("{$alias}quizid" => $this->sql->params['quizid']));
$dm = new question_engine_data_mapper();
list($inlineview, $viewparams) = $dm->question_attempt_latest_state_view($alias, $qubaids);
$this->sql->fields .= ",\n$fields";
$this->sql->from .= "\nLEFT JOIN $inlineview ON " .
"$alias.questionusageid = quiza.uniqueid AND $alias.slot = :{$alias}slot";
$this->sql->params[$alias . 'slot'] = $slot;
$this->sql->params = array_merge($this->sql->params, $viewparams);
}
/**
* Get an appropriate qubaid_condition for loading more data about the
* attempts we are displaying.
* @return qubaid_condition
*/
protected function get_qubaids_condition() {
if (is_null($this->rawdata)) {
throw new coding_exception(
'Cannot call get_qubaids_condition until the main data has been loaded.');
}
if ($this->is_downloading()) {
// We want usages for all attempts.
return new qubaid_join("(
SELECT DISTINCT quiza.uniqueid
FROM " . $this->sql->from . "
WHERE " . $this->sql->where . "
) quizasubquery", 'quizasubquery.uniqueid',
"1 = 1", $this->sql->params);
}
$qubaids = array();
foreach ($this->rawdata as $attempt) {
if ($attempt->usageid > 0) {
$qubaids[] = $attempt->usageid;
}
}
return new qubaid_list($qubaids);
}
public function query_db($pagesize, $useinitialsbar = true) {
$doneslots = array();
foreach ($this->get_sort_columns() as $column => $notused) {
$slot = $this->is_latest_step_column($column);
if ($slot && !in_array($slot, $doneslots)) {
$this->add_latest_state_join($slot);
$doneslots[] = $slot;
}
}
parent::query_db($pagesize, $useinitialsbar);
if ($this->requires_extra_data()) {
$this->load_extra_data();
}
}
public function get_sort_columns() {
// Add attemptid as a final tie-break to the sort. This ensures that
// Attempts by the same student appear in order when just sorting by name.
$sortcolumns = parent::get_sort_columns();
$sortcolumns['quiza.id'] = SORT_ASC;
return $sortcolumns;
}
public function wrap_html_start() {
if ($this->is_downloading() || !$this->includecheckboxes) {
return;
}
$url = $this->options->get_url();
$url->param('sesskey', sesskey());
echo '<div id="tablecontainer">';
echo '<form id="attemptsform" method="post" action="' . $url->out_omit_querystring() . '">';
echo html_writer::input_hidden_params($url);
echo '<div>';
}
public function wrap_html_finish() {
global $PAGE;
if ($this->is_downloading() || !$this->includecheckboxes) {
return;
}
echo '<div id="commands">';
$this->submit_buttons();
echo '</div>';
// Close the form.
echo '</div>';
echo '</form></div>';
}
/**
* Output any submit buttons required by the $this->includecheckboxes form.
*/
protected function submit_buttons() {
global $PAGE;
if (has_capability('mod/quiz:deleteattempts', $this->context)) {
$deletebuttonparams = [
'type' => 'submit',
'class' => 'btn btn-secondary mr-1',
'id' => 'deleteattemptsbutton',
'name' => 'delete',
'value' => get_string('deleteselected', 'quiz_overview'),
'data-action' => 'toggle',
'data-togglegroup' => $this->togglegroup,
'data-toggle' => 'action',
'disabled' => true
];
echo html_writer::empty_tag('input', $deletebuttonparams);
$PAGE->requires->event_handler('#deleteattemptsbutton', 'click', 'M.util.show_confirm_dialog',
array('message' => get_string('deleteattemptcheck', 'quiz')));
}
}
/**
* Generates the contents for the checkbox column header.
*
* It returns the HTML for a master \core\output\checkbox_toggleall component that selects/deselects all quiz attempts.
*
* @param string $columnname The name of the checkbox column.
* @return string
*/
public function checkbox_col_header(string $columnname) {
global $OUTPUT;
// Make sure to disable sorting on this column.
$this->no_sorting($columnname);
// Build the select/deselect all control.
$selectallid = $this->uniqueid . '-selectall-attempts';
$selectalltext = get_string('selectall', 'quiz');
$deselectalltext = get_string('selectnone', 'quiz');
$mastercheckbox = new \core\output\checkbox_toggleall($this->togglegroup, true, [
'id' => $selectallid,
'name' => $selectallid,
'value' => 1,
'label' => $selectalltext,
'labelclasses' => 'accesshide',
'selectall' => $selectalltext,
'deselectall' => $deselectalltext,
]);
return $OUTPUT->render($mastercheckbox);
}
}

View File

@ -0,0 +1,94 @@
<?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/>.
namespace mod_quiz\local\reports;
use context;
use context_module;
use stdClass;
/**
* Base class for quiz report plugins.
*
* Doesn't do anything on its own -- it needs to be extended.
* This class displays quiz reports. Because it is called from
* within /mod/quiz/report.php you can assume that the page header
* and footer are taken care of.
*
* This file can refer to itself as report.php to pass variables
* to itself - all these will also be globally available. You must
* pass "id=$cm->id" or q=$quiz->id", and "mode=reportname".
*
* @package mod_quiz
* @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class report_base {
const NO_GROUPS_ALLOWED = -2;
/**
* Override this function to displays the report.
* @param stdClass $cm the course-module for this quiz.
* @param stdClass $course the coures we are in.
* @param stdClass $quiz this quiz.
*/
public abstract function display($cm, $course, $quiz);
/**
* Initialise some parts of $PAGE and start output.
*
* @param stdClass $cm the course_module information.
* @param stdClass $course the course settings.
* @param stdClass $quiz the quiz settings.
* @param string $reportmode the report name.
*/
public function print_header_and_tabs($cm, $course, $quiz, $reportmode = 'overview') {
global $PAGE, $OUTPUT, $CFG;
// Print the page header.
$PAGE->set_title($quiz->name);
$PAGE->set_heading($course->fullname);
echo $OUTPUT->header();
$context = context_module::instance($cm->id);
if (!$PAGE->has_secondary_navigation()) {
echo $OUTPUT->heading(format_string($quiz->name, true, array('context' => $context)));
}
if (!empty($CFG->enableplagiarism)) {
require_once($CFG->libdir . '/plagiarismlib.php');
echo plagiarism_update_status($course, $cm);
}
}
/**
* Get the current group for the user user looking at the report.
*
* @param object $cm the course_module information.
* @param object $course the course settings.
* @param context $context the quiz context.
* @return int the current group id, if applicable. 0 for all users,
* NO_GROUPS_ALLOWED if the user cannot see any group.
*/
public function get_current_group($cm, $course, $context) {
$groupmode = groups_get_activity_groupmode($cm, $course);
$currentgroup = groups_get_activity_group($cm, true);
if ($groupmode == SEPARATEGROUPS && !$currentgroup && !has_capability('moodle/site:accessallgroups', $context)) {
$currentgroup = self::NO_GROUPS_ALLOWED;
}
return $currentgroup;
}
}

View File

@ -46,4 +46,9 @@ $renamedclasses = [
'mod_quiz\adminpresets\adminpresets_review_setting',
'mod_quiz\adminpresets\adminpresets_mod_quiz_admin_setting_user_image' =>
'mod_quiz\adminpresets\adminpresets_user_image_setting',
'quiz_default_report' => 'mod_quiz\local\reports\report_base',
'quiz_attempts_report' => 'mod_quiz\local\reports\attempts_report',
'mod_quiz_attempts_report_form' => 'mod_quiz\local\reports\attempts_report_options_form',
'mod_quiz_attempts_report_options' => 'mod_quiz\local\reports\attempts_report_options',
'quiz_attempts_report_table' => 'mod_quiz\local\reports\attempts_report_table',
];

View File

@ -15,371 +15,11 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The file defines a base class that can be used to build a report like the
* overview or responses report, that has one row per attempt.
* File only retained to prevent fatal errors in code that tries to require/include this.
*
* @package mod_quiz
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @todo MDL-76612 delete this file as part of Moodle 4.6 development.
* @deprecated This file is no longer required in Moodle 4.2+.
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/tablelib.php');
/**
* Base class for quiz reports that are basically a table with one row for each attempt.
*
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class quiz_attempts_report extends quiz_default_report {
/** @var int default page size for reports. */
const DEFAULT_PAGE_SIZE = 30;
/** @var string constant used for the options, means all users with attempts. */
const ALL_WITH = 'all_with';
/** @var string constant used for the options, means only enrolled users with attempts. */
const ENROLLED_WITH = 'enrolled_with';
/** @var string constant used for the options, means only enrolled users without attempts. */
const ENROLLED_WITHOUT = 'enrolled_without';
/** @var string constant used for the options, means all enrolled users. */
const ENROLLED_ALL = 'enrolled_any';
/** @var string the mode this report is. */
protected $mode;
/** @var context_module the quiz context. */
protected $context;
/** @var mod_quiz_attempts_report_form The settings form to use. */
protected $form;
/** @var string SQL fragment for selecting the attempt that gave the final grade,
* if applicable. */
protected $qmsubselect;
/** @var boolean caches the results of {@link should_show_grades()}. */
protected $showgrades = null;
/**
* Initialise various aspects of this report.
*
* @param string $mode
* @param string $formclass
* @param object $quiz
* @param object $cm
* @param object $course
* @return array with four elements:
* 0 => integer the current group id (0 for none).
* 1 => \core\dml\sql_join Contains joins, wheres, params for all the students in this course.
* 2 => \core\dml\sql_join Contains joins, wheres, params for all the students in the current group.
* 3 => \core\dml\sql_join Contains joins, wheres, params for all the students to show in the report.
* Will be the same as either element 1 or 2.
*/
public function init($mode, $formclass, $quiz, $cm, $course) {
$this->mode = $mode;
$this->context = context_module::instance($cm->id);
list($currentgroup, $studentsjoins, $groupstudentsjoins, $allowedjoins) = $this->get_students_joins(
$cm, $course);
$this->qmsubselect = quiz_report_qm_filter_select($quiz);
$this->form = new $formclass($this->get_base_url(),
array('quiz' => $quiz, 'currentgroup' => $currentgroup, 'context' => $this->context));
return array($currentgroup, $studentsjoins, $groupstudentsjoins, $allowedjoins);
}
/**
* Get the base URL for this report.
* @return moodle_url the URL.
*/
protected function get_base_url() {
return new moodle_url('/mod/quiz/report.php',
array('id' => $this->context->instanceid, 'mode' => $this->mode));
}
/**
* Get sql fragments (joins) which can be used to build queries that
* will select an appropriate set of students to show in the reports.
*
* @param object $cm the course module.
* @param object $course the course settings.
* @return array with four elements:
* 0 => integer the current group id (0 for none).
* 1 => \core\dml\sql_join Contains joins, wheres, params for all the students in this course.
* 2 => \core\dml\sql_join Contains joins, wheres, params for all the students in the current group.
* 3 => \core\dml\sql_join Contains joins, wheres, params for all the students to show in the report.
* Will be the same as either element 1 or 2.
*/
protected function get_students_joins($cm, $course = null) {
$currentgroup = $this->get_current_group($cm, $course, $this->context);
$empty = new \core\dml\sql_join();
if ($currentgroup == self::NO_GROUPS_ALLOWED) {
return array($currentgroup, $empty, $empty, $empty);
}
$studentsjoins = get_enrolled_with_capabilities_join($this->context, '',
array('mod/quiz:attempt', 'mod/quiz:reviewmyattempts'));
if (empty($currentgroup)) {
return array($currentgroup, $studentsjoins, $empty, $studentsjoins);
}
// We have a currently selected group.
$groupstudentsjoins = get_enrolled_with_capabilities_join($this->context, '',
array('mod/quiz:attempt', 'mod/quiz:reviewmyattempts'), $currentgroup);
return array($currentgroup, $studentsjoins, $groupstudentsjoins, $groupstudentsjoins);
}
/**
* Outputs the things you commonly want at the top of a quiz report.
*
* Calls through to {@link print_header_and_tabs()} and then
* outputs the standard group selector, number of attempts summary,
* and messages to cover common cases when the report can't be shown.
*
* @param stdClass $cm the course_module information.
* @param stdClass $course the course settings.
* @param stdClass $quiz the quiz settings.
* @param mod_quiz_attempts_report_options $options the current report settings.
* @param int $currentgroup the current group.
* @param bool $hasquestions whether there are any questions in the quiz.
* @param bool $hasstudents whether there are any relevant students.
*/
protected function print_standard_header_and_messages($cm, $course, $quiz,
$options, $currentgroup, $hasquestions, $hasstudents) {
global $OUTPUT;
$this->print_header_and_tabs($cm, $course, $quiz, $this->mode);
if (groups_get_activity_groupmode($cm)) {
// Groups are being used, so output the group selector if we are not downloading.
groups_print_activity_menu($cm, $options->get_url());
}
// Print information on the number of existing attempts.
if ($strattemptnum = quiz_num_attempt_summary($quiz, $cm, true, $currentgroup)) {
echo '<div class="quizattemptcounts">' . $strattemptnum . '</div>';
}
if (!$hasquestions) {
echo quiz_no_questions_message($quiz, $cm, $this->context);
} else if ($currentgroup == self::NO_GROUPS_ALLOWED) {
echo $OUTPUT->notification(get_string('notingroup'));
} else if (!$hasstudents) {
echo $OUTPUT->notification(get_string('nostudentsyet'));
} else if ($currentgroup && !$this->hasgroupstudents) {
echo $OUTPUT->notification(get_string('nostudentsingroup'));
}
}
/**
* Add all the user-related columns to the $columns and $headers arrays.
* @param table_sql $table the table being constructed.
* @param array $columns the list of columns. Added to.
* @param array $headers the columns headings. Added to.
*/
protected function add_user_columns($table, &$columns, &$headers) {
global $CFG;
if (!$table->is_downloading() && $CFG->grade_report_showuserimage) {
$columns[] = 'picture';
$headers[] = '';
}
if (!$table->is_downloading()) {
$columns[] = 'fullname';
$headers[] = get_string('name');
} else {
$columns[] = 'lastname';
$headers[] = get_string('lastname');
$columns[] = 'firstname';
$headers[] = get_string('firstname');
}
$extrafields = \core_user\fields::get_identity_fields($this->context);
foreach ($extrafields as $field) {
$columns[] = $field;
$headers[] = \core_user\fields::get_display_name($field);
}
}
/**
* Set the display options for the user-related columns in the table.
* @param table_sql $table the table being constructed.
*/
protected function configure_user_columns($table) {
$table->column_suppress('picture');
$table->column_suppress('fullname');
$extrafields = \core_user\fields::get_identity_fields($this->context);
foreach ($extrafields as $field) {
$table->column_suppress($field);
}
$table->column_class('picture', 'picture');
$table->column_class('lastname', 'bold');
$table->column_class('firstname', 'bold');
$table->column_class('fullname', 'bold');
}
/**
* Add the state column to the $columns and $headers arrays.
* @param array $columns the list of columns. Added to.
* @param array $headers the columns headings. Added to.
*/
protected function add_state_column(&$columns, &$headers) {
$columns[] = 'state';
$headers[] = get_string('attemptstate', 'quiz');
}
/**
* Add all the time-related columns to the $columns and $headers arrays.
* @param array $columns the list of columns. Added to.
* @param array $headers the columns headings. Added to.
*/
protected function add_time_columns(&$columns, &$headers) {
$columns[] = 'timestart';
$headers[] = get_string('startedon', 'quiz');
$columns[] = 'timefinish';
$headers[] = get_string('timecompleted', 'quiz');
$columns[] = 'duration';
$headers[] = get_string('attemptduration', 'quiz');
}
/**
* Add all the grade and feedback columns, if applicable, to the $columns
* and $headers arrays.
* @param object $quiz the quiz settings.
* @param bool $usercanseegrades whether the user is allowed to see grades for this quiz.
* @param array $columns the list of columns. Added to.
* @param array $headers the columns headings. Added to.
* @param bool $includefeedback whether to include the feedbacktext columns
*/
protected function add_grade_columns($quiz, $usercanseegrades, &$columns, &$headers, $includefeedback = true) {
if ($usercanseegrades) {
$columns[] = 'sumgrades';
$headers[] = get_string('grade', 'quiz') . '/' .
quiz_format_grade($quiz, $quiz->grade);
}
if ($includefeedback && quiz_has_feedback($quiz)) {
$columns[] = 'feedbacktext';
$headers[] = get_string('feedback', 'quiz');
}
}
/**
* Set up the table.
* @param table_sql $table the table being constructed.
* @param array $columns the list of columns.
* @param array $headers the columns headings.
* @param moodle_url $reporturl the URL of this report.
* @param mod_quiz_attempts_report_options $options the display options.
* @param bool $collapsible whether to allow columns in the report to be collapsed.
*/
protected function set_up_table_columns($table, $columns, $headers, $reporturl,
mod_quiz_attempts_report_options $options, $collapsible) {
$table->define_columns($columns);
$table->define_headers($headers);
$table->sortable(true, 'uniqueid');
$table->define_baseurl($options->get_url());
$this->configure_user_columns($table);
$table->no_sorting('feedbacktext');
$table->column_class('sumgrades', 'bold');
$table->set_attribute('id', 'attempts');
$table->collapsible($collapsible);
}
/**
* Process any submitted actions.
* @param object $quiz the quiz settings.
* @param object $cm the cm object for the quiz.
* @param int $currentgroup the currently selected group.
* @param \core\dml\sql_join $groupstudentsjoins (joins, wheres, params) the students in the current group.
* @param \core\dml\sql_join $allowedjoins (joins, wheres, params) the users whose attempt this user is allowed to modify.
* @param moodle_url $redirecturl where to redircet to after a successful action.
*/
protected function process_actions($quiz, $cm, $currentgroup, \core\dml\sql_join $groupstudentsjoins,
\core\dml\sql_join $allowedjoins, $redirecturl) {
if (empty($currentgroup) || $this->hasgroupstudents) {
if (optional_param('delete', 0, PARAM_BOOL) && confirm_sesskey()) {
if ($attemptids = optional_param_array('attemptid', array(), PARAM_INT)) {
require_capability('mod/quiz:deleteattempts', $this->context);
$this->delete_selected_attempts($quiz, $cm, $attemptids, $allowedjoins);
redirect($redirecturl);
}
}
}
}
/**
* Delete the quiz attempts
* @param object $quiz the quiz settings. Attempts that don't belong to
* this quiz are not deleted.
* @param object $cm the course_module object.
* @param array $attemptids the list of attempt ids to delete.
* @param \core\dml\sql_join $allowedjoins (joins, wheres, params) This list of userids that are visible in the report.
* Users can only delete attempts that they are allowed to see in the report.
* Empty means all users.
*/
protected function delete_selected_attempts($quiz, $cm, $attemptids, \core\dml\sql_join $allowedjoins) {
global $DB;
foreach ($attemptids as $attemptid) {
if (empty($allowedjoins->joins)) {
$sql = "SELECT quiza.*
FROM {quiz_attempts} quiza
JOIN {user} u ON u.id = quiza.userid
WHERE quiza.id = :attemptid";
} else {
$sql = "SELECT quiza.*
FROM {quiz_attempts} quiza
JOIN {user} u ON u.id = quiza.userid
{$allowedjoins->joins}
WHERE {$allowedjoins->wheres} AND quiza.id = :attemptid";
}
$params = $allowedjoins->params + array('attemptid' => $attemptid);
$attempt = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE);
if (!$attempt || $attempt->quiz != $quiz->id || $attempt->preview != 0) {
// Ensure the attempt exists, belongs to this quiz and belongs to
// a student included in the report. If not skip.
continue;
}
// Set the course module id before calling quiz_delete_attempt().
$quiz->cmid = $cm->id;
quiz_delete_attempt($attempt, $quiz);
}
}
/**
* Get information about which students to show in the report.
* @param object $cm the coures module.
* @param object $course the course settings.
* @return array with four elements:
* 0 => integer the current group id (0 for none).
* 1 => array ids of all the students in this course.
* 2 => array ids of all the students in the current group.
* 3 => array ids of all the students to show in the report. Will be the
* same as either element 1 or 2.
* @deprecated since Moodle 3.2 Please use get_students_joins() instead.
*/
protected function load_relevant_students($cm, $course = null) {
$msg = 'The function load_relevant_students() is deprecated. Please use get_students_joins() instead.';
throw new coding_exception($msg);
}
}
debugging('This file is no longer required in Moodle 4.2+. Please do not include/require it.', DEBUG_DEVELOPER);

View File

@ -15,106 +15,11 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Base class for the settings form for {@link quiz_attempts_report}s.
* File only retained to prevent fatal errors in code that tries to require/include this.
*
* @package mod_quiz
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @todo MDL-76612 delete this file as part of Moodle 4.6 development.
* @deprecated This file is no longer required in Moodle 4.2+.
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/formslib.php');
/**
* Base class for the settings form for {@link quiz_attempts_report}s.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class mod_quiz_attempts_report_form extends moodleform {
protected function definition() {
$mform = $this->_form;
$mform->addElement('header', 'preferencespage',
get_string('reportwhattoinclude', 'quiz'));
$this->standard_attempt_fields($mform);
$this->other_attempt_fields($mform);
$mform->addElement('header', 'preferencesuser',
get_string('reportdisplayoptions', 'quiz'));
$this->standard_preference_fields($mform);
$this->other_preference_fields($mform);
$mform->addElement('submit', 'submitbutton',
get_string('showreport', 'quiz'));
}
protected function standard_attempt_fields(MoodleQuickForm $mform) {
$mform->addElement('select', 'attempts', get_string('reportattemptsfrom', 'quiz'), array(
quiz_attempts_report::ENROLLED_WITH => get_string('reportuserswith', 'quiz'),
quiz_attempts_report::ENROLLED_WITHOUT => get_string('reportuserswithout', 'quiz'),
quiz_attempts_report::ENROLLED_ALL => get_string('reportuserswithorwithout', 'quiz'),
quiz_attempts_report::ALL_WITH => get_string('reportusersall', 'quiz'),
));
$stategroup = array(
$mform->createElement('advcheckbox', 'stateinprogress', '',
get_string('stateinprogress', 'quiz')),
$mform->createElement('advcheckbox', 'stateoverdue', '',
get_string('stateoverdue', 'quiz')),
$mform->createElement('advcheckbox', 'statefinished', '',
get_string('statefinished', 'quiz')),
$mform->createElement('advcheckbox', 'stateabandoned', '',
get_string('stateabandoned', 'quiz')),
);
$mform->addGroup($stategroup, 'stateoptions',
get_string('reportattemptsthatare', 'quiz'), array(' '), false);
$mform->setDefault('stateinprogress', 1);
$mform->setDefault('stateoverdue', 1);
$mform->setDefault('statefinished', 1);
$mform->setDefault('stateabandoned', 1);
$mform->disabledIf('stateinprogress', 'attempts', 'eq', quiz_attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('stateoverdue', 'attempts', 'eq', quiz_attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('statefinished', 'attempts', 'eq', quiz_attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('stateabandoned', 'attempts', 'eq', quiz_attempts_report::ENROLLED_WITHOUT);
if (quiz_report_can_filter_only_graded($this->_customdata['quiz'])) {
$gm = html_writer::tag('span',
quiz_get_grading_option_name($this->_customdata['quiz']->grademethod),
array('class' => 'highlight'));
$mform->addElement('advcheckbox', 'onlygraded', '',
get_string('reportshowonlyfinished', 'quiz', $gm));
$mform->disabledIf('onlygraded', 'attempts', 'eq', quiz_attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('onlygraded', 'statefinished', 'notchecked');
}
}
protected function other_attempt_fields(MoodleQuickForm $mform) {
}
protected function standard_preference_fields(MoodleQuickForm $mform) {
$mform->addElement('text', 'pagesize', get_string('pagesize', 'quiz'));
$mform->setType('pagesize', PARAM_INT);
}
protected function other_preference_fields(MoodleQuickForm $mform) {
}
public function validation($data, $files) {
$errors = parent::validation($data, $files);
if ($data['attempts'] != quiz_attempts_report::ENROLLED_WITHOUT && !(
$data['stateinprogress'] || $data['stateoverdue'] || $data['statefinished'] || $data['stateabandoned'])) {
$errors['stateoptions'] = get_string('reportmustselectstate', 'quiz');
}
return $errors;
}
}
debugging('This file is no longer required in Moodle 4.2+. Please do not include/require it.', DEBUG_DEVELOPER);

View File

@ -15,277 +15,11 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Base class for the options that control what is visible in an {@link quiz_attempts_report}.
* File only retained to prevent fatal errors in code that tries to require/include this.
*
* @package mod_quiz
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @todo MDL-76612 delete this file as part of Moodle 4.6 development.
* @deprecated This file is no longer required in Moodle 4.2+.
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/formslib.php');
/**
* Base class for the options that control what is visible in an {@link quiz_attempts_report}.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mod_quiz_attempts_report_options {
/** @var string the report mode. */
public $mode;
/** @var object the settings for the quiz being reported on. */
public $quiz;
/** @var object the course module objects for the quiz being reported on. */
public $cm;
/** @var object the course settings for the course the quiz is in. */
public $course;
/**
* @var array form field name => corresponding quiz_attempt:: state constant.
*/
protected static $statefields = array(
'stateinprogress' => quiz_attempt::IN_PROGRESS,
'stateoverdue' => quiz_attempt::OVERDUE,
'statefinished' => quiz_attempt::FINISHED,
'stateabandoned' => quiz_attempt::ABANDONED,
);
/**
* @var string quiz_attempts_report::ALL_WITH or quiz_attempts_report::ENROLLED_WITH
* quiz_attempts_report::ENROLLED_WITHOUT or quiz_attempts_report::ENROLLED_ALL
*/
public $attempts = quiz_attempts_report::ENROLLED_WITH;
/** @var int the currently selected group. 0 if no group is selected. */
public $group = 0;
/**
* @var array|null of quiz_attempt::IN_PROGRESS, etc. constants. null means
* no restriction.
*/
public $states = array(quiz_attempt::IN_PROGRESS, quiz_attempt::OVERDUE,
quiz_attempt::FINISHED, quiz_attempt::ABANDONED);
/**
* @var bool whether to show all finished attmepts, or just the one that gave
* the final grade for the user.
*/
public $onlygraded = false;
/** @var int Number of attempts to show per page. */
public $pagesize = quiz_attempts_report::DEFAULT_PAGE_SIZE;
/** @var string whether the data should be downloaded in some format, or '' to display it. */
public $download = '';
/** @var bool whether the current user has permission to see grades. */
public $usercanseegrades;
/** @var bool whether the report table should have a column of checkboxes. */
public $checkboxcolumn = false;
/**
* Constructor.
* @param string $mode which report these options are for.
* @param object $quiz the settings for the quiz being reported on.
* @param object $cm the course module objects for the quiz being reported on.
* @param object $coures the course settings for the coures this quiz is in.
*/
public function __construct($mode, $quiz, $cm, $course) {
$this->mode = $mode;
$this->quiz = $quiz;
$this->cm = $cm;
$this->course = $course;
$this->usercanseegrades = quiz_report_should_show_grades($quiz, context_module::instance($cm->id));
}
/**
* Get the URL parameters required to show the report with these options.
* @return array URL parameter name => value.
*/
protected function get_url_params() {
$params = array(
'id' => $this->cm->id,
'mode' => $this->mode,
'attempts' => $this->attempts,
'onlygraded' => $this->onlygraded,
);
if ($this->states) {
$params['states'] = implode('-', $this->states);
}
if (groups_get_activity_groupmode($this->cm, $this->course)) {
$params['group'] = $this->group;
}
return $params;
}
/**
* Get the URL to show the report with these options.
* @return moodle_url the URL.
*/
public function get_url() {
return new moodle_url('/mod/quiz/report.php', $this->get_url_params());
}
/**
* Process the data we get when the settings form is submitted. This includes
* updating the fields of this class, and updating the user preferences
* where appropriate.
* @param object $fromform The data from $mform->get_data() from the settings form.
*/
public function process_settings_from_form($fromform) {
$this->setup_from_form_data($fromform);
$this->resolve_dependencies();
$this->update_user_preferences();
}
/**
* Set up this preferences object using optional_param (using user_preferences
* to set anything not specified by the params.
*/
public function process_settings_from_params() {
$this->setup_from_user_preferences();
$this->setup_from_params();
$this->resolve_dependencies();
}
/**
* Get the current value of the settings to pass to the settings form.
*/
public function get_initial_form_data() {
$toform = new stdClass();
$toform->attempts = $this->attempts;
$toform->onlygraded = $this->onlygraded;
$toform->pagesize = $this->pagesize;
if ($this->states) {
foreach (self::$statefields as $field => $state) {
$toform->$field = in_array($state, $this->states);
}
}
return $toform;
}
/**
* Set the fields of this object from the form data.
* @param object $fromform The data from $mform->get_data() from the settings form.
*/
public function setup_from_form_data($fromform) {
$this->attempts = $fromform->attempts;
$this->group = groups_get_activity_group($this->cm, true);
$this->onlygraded = !empty($fromform->onlygraded);
$this->pagesize = $fromform->pagesize;
$this->states = array();
foreach (self::$statefields as $field => $state) {
if (!empty($fromform->$field)) {
$this->states[] = $state;
}
}
}
/**
* Set the fields of this object from the URL parameters.
*/
public function setup_from_params() {
$this->attempts = optional_param('attempts', $this->attempts, PARAM_ALPHAEXT);
$this->group = groups_get_activity_group($this->cm, true);
$this->onlygraded = optional_param('onlygraded', $this->onlygraded, PARAM_BOOL);
$this->pagesize = optional_param('pagesize', $this->pagesize, PARAM_INT);
$states = optional_param('states', '', PARAM_ALPHAEXT);
if (!empty($states)) {
$this->states = explode('-', $states);
}
$this->download = optional_param('download', $this->download, PARAM_ALPHA);
}
/**
* Set the fields of this object from the user's preferences.
* (For those settings that are backed by user-preferences).
*/
public function setup_from_user_preferences() {
$this->pagesize = get_user_preferences('quiz_report_pagesize', $this->pagesize);
}
/**
* Update the user preferences so they match the settings in this object.
* (For those settings that are backed by user-preferences).
*/
public function update_user_preferences() {
set_user_preference('quiz_report_pagesize', $this->pagesize);
}
/**
* Check the settings, and remove any 'impossible' combinations.
*/
public function resolve_dependencies() {
if ($this->group) {
// Default for when a group is selected.
if ($this->attempts === null || $this->attempts == quiz_attempts_report::ALL_WITH) {
$this->attempts = quiz_attempts_report::ENROLLED_WITH;
}
} else if (!$this->group && $this->course->id == SITEID) {
// Force report on front page to show all, unless a group is selected.
$this->attempts = quiz_attempts_report::ALL_WITH;
} else if (!in_array($this->attempts, array(quiz_attempts_report::ALL_WITH, quiz_attempts_report::ENROLLED_WITH,
quiz_attempts_report::ENROLLED_WITHOUT, quiz_attempts_report::ENROLLED_ALL))) {
$this->attempts = quiz_attempts_report::ENROLLED_WITH;
}
$cleanstates = array();
foreach (self::$statefields as $state) {
if (in_array($state, $this->states)) {
$cleanstates[] = $state;
}
}
$this->states = $cleanstates;
if (count($this->states) == count(self::$statefields)) {
// If all states have been selected, then there is no constraint
// required in the SQL, so clear the array.
$this->states = null;
}
if (!quiz_report_can_filter_only_graded($this->quiz)) {
// A grading mode like 'average' has been selected, so we cannot do
// the show the attempt that gave the final grade thing.
$this->onlygraded = false;
}
if ($this->attempts == quiz_attempts_report::ENROLLED_WITHOUT) {
$this->states = null;
$this->onlygraded = false;
}
if (!$this->is_showing_finished_attempts()) {
$this->onlygraded = false;
}
if ($this->pagesize < 1) {
$this->pagesize = quiz_attempts_report::DEFAULT_PAGE_SIZE;
}
}
/**
* Whether the options are such that finished attempts are being shown.
* @return boolean
*/
protected function is_showing_finished_attempts() {
return $this->states === null || in_array(quiz_attempt::FINISHED, $this->states);
}
}
debugging('This file is no longer required in Moodle 4.2+. Please do not include/require it.', DEBUG_DEVELOPER);

View File

@ -15,687 +15,11 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Base class for the table used by a {@link quiz_attempts_report}.
* File only retained to prevent fatal errors in code that tries to require/include this.
*
* @package mod_quiz
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @todo MDL-76612 delete this file as part of Moodle 4.6 development.
* @deprecated This file is no longer required in Moodle 4.2+.
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/tablelib.php');
/**
* Base class for the table used by a {@link quiz_attempts_report}.
*
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class quiz_attempts_report_table extends table_sql {
public $useridfield = 'userid';
/** @var moodle_url the URL of this report. */
protected $reporturl;
/** @var array the display options. */
protected $displayoptions;
/**
* @var array information about the latest step of each question.
* Loaded by {@link load_question_latest_steps()}, if applicable.
*/
protected $lateststeps = null;
/** @var object the quiz settings for the quiz we are reporting on. */
protected $quiz;
/** @var context the quiz context. */
protected $context;
/** @var string HTML fragment to select the first/best/last attempt, if appropriate. */
protected $qmsubselect;
/** @var object mod_quiz_attempts_report_options the options affecting this report. */
protected $options;
/** @var \core\dml\sql_join Contains joins, wheres, params to find students
* in the currently selected group, if applicable.
*/
protected $groupstudentsjoins;
/** @var \core\dml\sql_join Contains joins, wheres, params to find the students in the course. */
protected $studentsjoins;
/** @var object the questions that comprise this quiz.. */
protected $questions;
/** @var bool whether to include the column with checkboxes to select each attempt. */
protected $includecheckboxes;
/** @var string The toggle group name for the checkboxes in the checkbox column. */
protected $togglegroup = 'quiz-attempts';
/**
* Constructor
* @param string $uniqueid
* @param object $quiz
* @param context $context
* @param string $qmsubselect
* @param mod_quiz_attempts_report_options $options
* @param \core\dml\sql_join $groupstudentsjoins Contains joins, wheres, params
* @param \core\dml\sql_join $studentsjoins Contains joins, wheres, params
* @param array $questions
* @param moodle_url $reporturl
*/
public function __construct($uniqueid, $quiz, $context, $qmsubselect,
mod_quiz_attempts_report_options $options, \core\dml\sql_join $groupstudentsjoins, \core\dml\sql_join $studentsjoins,
$questions, $reporturl) {
parent::__construct($uniqueid);
$this->quiz = $quiz;
$this->context = $context;
$this->qmsubselect = $qmsubselect;
$this->groupstudentsjoins = $groupstudentsjoins;
$this->studentsjoins = $studentsjoins;
$this->questions = $questions;
$this->includecheckboxes = $options->checkboxcolumn;
$this->reporturl = $reporturl;
$this->options = $options;
}
/**
* Generate the display of the checkbox column.
* @param object $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_checkbox($attempt) {
global $OUTPUT;
if ($attempt->attempt) {
$checkbox = new \core\output\checkbox_toggleall($this->togglegroup, false, [
'id' => "attemptid_{$attempt->attempt}",
'name' => 'attemptid[]',
'value' => $attempt->attempt,
'label' => get_string('selectattempt', 'quiz'),
'labelclasses' => 'accesshide',
]);
return $OUTPUT->render($checkbox);
} else {
return '';
}
}
/**
* Generate the display of the user's picture column.
* @param object $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_picture($attempt) {
global $OUTPUT;
$user = new stdClass();
$additionalfields = explode(',', implode(',', \core_user\fields::get_picture_fields()));
$user = username_load_fields_from_object($user, $attempt, null, $additionalfields);
$user->id = $attempt->userid;
return $OUTPUT->user_picture($user);
}
/**
* Generate the display of the user's full name column.
* @param object $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_fullname($attempt) {
$html = parent::col_fullname($attempt);
if ($this->is_downloading() || empty($attempt->attempt)) {
return $html;
}
return $html . html_writer::empty_tag('br') . html_writer::link(
new moodle_url('/mod/quiz/review.php', array('attempt' => $attempt->attempt)),
get_string('reviewattempt', 'quiz'), array('class' => 'reviewlink'));
}
/**
* Generate the display of the attempt state column.
* @param object $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_state($attempt) {
if (!is_null($attempt->attempt)) {
return quiz_attempt::state_name($attempt->state);
} else {
return '-';
}
}
/**
* Generate the display of the start time column.
* @param object $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_timestart($attempt) {
if ($attempt->attempt) {
return userdate($attempt->timestart, $this->strtimeformat);
} else {
return '-';
}
}
/**
* Generate the display of the finish time column.
* @param object $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_timefinish($attempt) {
if ($attempt->attempt && $attempt->timefinish) {
return userdate($attempt->timefinish, $this->strtimeformat);
} else {
return '-';
}
}
/**
* Generate the display of the time taken column.
* @param object $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_duration($attempt) {
if ($attempt->timefinish) {
return format_time($attempt->timefinish - $attempt->timestart);
} else {
return '-';
}
}
/**
* Generate the display of the feedback column.
* @param object $attempt the table row being output.
* @return string HTML content to go inside the td.
*/
public function col_feedbacktext($attempt) {
if ($attempt->state != quiz_attempt::FINISHED) {
return '-';
}
$feedback = quiz_report_feedback_for_grade(
quiz_rescale_grade($attempt->sumgrades, $this->quiz, false),
$this->quiz->id, $this->context);
if ($this->is_downloading()) {
$feedback = strip_tags($feedback);
}
return $feedback;
}
public function get_row_class($attempt) {
if ($this->qmsubselect && $attempt->gradedattempt) {
return 'gradedattempt';
} else {
return '';
}
}
/**
* Make a link to review an individual question in a popup window.
*
* @param string $data HTML fragment. The text to make into the link.
* @param object $attempt data for the row of the table being output.
* @param int $slot the number used to identify this question within this usage.
*/
public function make_review_link($data, $attempt, $slot) {
global $OUTPUT, $CFG;
$flag = '';
if ($this->is_flagged($attempt->usageid, $slot)) {
$flag = $OUTPUT->pix_icon('i/flagged', get_string('flagged', 'question'),
'moodle', array('class' => 'questionflag'));
}
$feedbackimg = '';
$state = $this->slot_state($attempt, $slot);
if ($state && $state->is_finished() && $state != question_state::$needsgrading) {
$feedbackimg = $this->icon_for_fraction($this->slot_fraction($attempt, $slot));
}
$output = html_writer::tag('span', $feedbackimg . html_writer::tag('span',
$data, array('class' => $state->get_state_class(true))) . $flag, array('class' => 'que'));
$reviewparams = array('attempt' => $attempt->attempt, 'slot' => $slot);
if (isset($attempt->try)) {
$reviewparams['step'] = $this->step_no_for_try($attempt->usageid, $slot, $attempt->try);
}
$url = new moodle_url('/mod/quiz/reviewquestion.php', $reviewparams);
$output = $OUTPUT->action_link($url, $output,
new popup_action('click', $url, 'reviewquestion',
array('height' => 450, 'width' => 650)),
array('title' => get_string('reviewresponse', 'quiz')));
if (!empty($CFG->enableplagiarism)) {
require_once($CFG->libdir . '/plagiarismlib.php');
$output .= plagiarism_get_links([
'context' => $this->context->id,
'component' => 'qtype_'.$this->questions[$slot]->qtype,
'cmid' => $this->context->instanceid,
'area' => $attempt->usageid,
'itemid' => $slot,
'userid' => $attempt->userid]);
}
return $output;
}
/**
* @param object $attempt the row data
* @param int $slot
* @return question_state
*/
protected function slot_state($attempt, $slot) {
$stepdata = $this->lateststeps[$attempt->usageid][$slot];
return question_state::get($stepdata->state);
}
/**
* @param int $questionusageid
* @param int $slot
* @return bool
*/
protected function is_flagged($questionusageid, $slot) {
$stepdata = $this->lateststeps[$questionusageid][$slot];
return $stepdata->flagged;
}
/**
* @param object $attempt the row data
* @param int $slot
* @return float
*/
protected function slot_fraction($attempt, $slot) {
$stepdata = $this->lateststeps[$attempt->usageid][$slot];
return $stepdata->fraction;
}
/**
* Return an appropriate icon (green tick, red cross, etc.) for a grade.
* @param float $fraction grade on a scale 0..1.
* @return string html fragment.
*/
protected function icon_for_fraction($fraction) {
global $OUTPUT;
$feedbackclass = question_state::graded_state_for_fraction($fraction)->get_feedback_class();
return $OUTPUT->pix_icon('i/grade_' . $feedbackclass, get_string($feedbackclass, 'question'),
'moodle', array('class' => 'icon'));
}
/**
* Load any extra data after main query. At this point you can call {@link get_qubaids_condition} to get the condition that
* limits the query to just the question usages shown in this report page or alternatively for all attempts if downloading a
* full report.
*/
protected function load_extra_data() {
$this->lateststeps = $this->load_question_latest_steps();
}
/**
* Load information about the latest state of selected questions in selected attempts.
*
* The results are returned as an two dimensional array $qubaid => $slot => $dataobject
*
* @param qubaid_condition|null $qubaids used to restrict which usages are included
* in the query. See {@link qubaid_condition}.
* @return array of records. See the SQL in this function to see the fields available.
*/
protected function load_question_latest_steps(qubaid_condition $qubaids = null) {
if ($qubaids === null) {
$qubaids = $this->get_qubaids_condition();
}
$dm = new question_engine_data_mapper();
$latesstepdata = $dm->load_questions_usages_latest_steps(
$qubaids, array_keys($this->questions));
$lateststeps = array();
foreach ($latesstepdata as $step) {
$lateststeps[$step->questionusageid][$step->slot] = $step;
}
return $lateststeps;
}
/**
* Does this report require loading any more data after the main query. After the main query then
* you can use $this->get
*
* @return bool should {@link query_db()} call {@link load_extra_data}?
*/
protected function requires_extra_data() {
return $this->requires_latest_steps_loaded();
}
/**
* Does this report require the detailed information for each question from the
* question_attempts_steps table?
* @return bool should {@link load_extra_data} call {@link load_question_latest_steps}?
*/
protected function requires_latest_steps_loaded() {
return false;
}
/**
* Is this a column that depends on joining to the latest state information?
* If so, return the corresponding slot. If not, return false.
* @param string $column a column name
* @return int false if no, else a slot.
*/
protected function is_latest_step_column($column) {
return false;
}
/**
* Get any fields that might be needed when sorting on date for a particular slot.
*
* Note: these values are only used for sorting. The values displayed are taken
* from $this->lateststeps loaded in load_extra_data().
*
* @param int $slot the slot for the column we want.
* @param string $alias the table alias for latest state information relating to that slot.
* @return string definitions of extra fields to add to the SELECT list of the query.
*/
protected function get_required_latest_state_fields($slot, $alias) {
return '';
}
/**
* Contruct all the parts of the main database query.
* @param \core\dml\sql_join $allowedstudentsjoins (joins, wheres, params) defines allowed users for the report.
* @return array with 4 elements ($fields, $from, $where, $params) that can be used to
* build the actual database query.
*/
public function base_sql(\core\dml\sql_join $allowedstudentsjoins) {
global $DB;
// Please note this uniqueid column is not the same as quiza.uniqueid.
$fields = 'DISTINCT ' . $DB->sql_concat('u.id', "'#'", 'COALESCE(quiza.attempt, 0)') . ' AS uniqueid,';
if ($this->qmsubselect) {
$fields .= "\n(CASE WHEN $this->qmsubselect THEN 1 ELSE 0 END) AS gradedattempt,";
}
$userfieldsapi = \core_user\fields::for_identity($this->context)->with_name()
->excluding('id', 'idnumber', 'picture', 'imagealt', 'institution', 'department', 'email');
$userfields = $userfieldsapi->get_sql('u', true, '', '', false);
$fields .= '
quiza.uniqueid AS usageid,
quiza.id AS attempt,
u.id AS userid,
u.idnumber,
u.picture,
u.imagealt,
u.institution,
u.department,
u.email,' . $userfields->selects . ',
quiza.state,
quiza.sumgrades,
quiza.timefinish,
quiza.timestart,
CASE WHEN quiza.timefinish = 0 THEN null
WHEN quiza.timefinish > quiza.timestart THEN quiza.timefinish - quiza.timestart
ELSE 0 END AS duration';
// To explain that last bit, timefinish can be non-zero and less
// than timestart when you have two load-balanced servers with very
// badly synchronised clocks, and a student does a really quick attempt.
// This part is the same for all cases. Join the users and quiz_attempts tables.
$from = " {user} u";
$from .= "\n{$userfields->joins}";
$from .= "\nLEFT JOIN {quiz_attempts} quiza ON
quiza.userid = u.id AND quiza.quiz = :quizid";
$params = array_merge($userfields->params, ['quizid' => $this->quiz->id]);
if ($this->qmsubselect && $this->options->onlygraded) {
$from .= " AND (quiza.state <> :finishedstate OR $this->qmsubselect)";
$params['finishedstate'] = quiz_attempt::FINISHED;
}
switch ($this->options->attempts) {
case quiz_attempts_report::ALL_WITH:
// Show all attempts, including students who are no longer in the course.
$where = 'quiza.id IS NOT NULL AND quiza.preview = 0';
break;
case quiz_attempts_report::ENROLLED_WITH:
// Show only students with attempts.
$from .= "\n" . $allowedstudentsjoins->joins;
$where = "quiza.preview = 0 AND quiza.id IS NOT NULL AND " . $allowedstudentsjoins->wheres;
$params = array_merge($params, $allowedstudentsjoins->params);
break;
case quiz_attempts_report::ENROLLED_WITHOUT:
// Show only students without attempts.
$from .= "\n" . $allowedstudentsjoins->joins;
$where = "quiza.id IS NULL AND " . $allowedstudentsjoins->wheres;
$params = array_merge($params, $allowedstudentsjoins->params);
break;
case quiz_attempts_report::ENROLLED_ALL:
// Show all students with or without attempts.
$from .= "\n" . $allowedstudentsjoins->joins;
$where = "(quiza.preview = 0 OR quiza.preview IS NULL) AND " . $allowedstudentsjoins->wheres;
$params = array_merge($params, $allowedstudentsjoins->params);
break;
}
if ($this->options->states) {
list($statesql, $stateparams) = $DB->get_in_or_equal($this->options->states,
SQL_PARAMS_NAMED, 'state');
$params += $stateparams;
$where .= " AND (quiza.state $statesql OR quiza.state IS NULL)";
}
return array($fields, $from, $where, $params);
}
/**
* A chance for subclasses to modify the SQL after the count query has been generated,
* and before the full query is constructed.
* @param string $fields SELECT list.
* @param string $from JOINs part of the SQL.
* @param string $where WHERE clauses.
* @param array $params Query params.
* @return array with 4 elements ($fields, $from, $where, $params) as from base_sql.
*/
protected function update_sql_after_count($fields, $from, $where, $params) {
return [$fields, $from, $where, $params];
}
/**
* Set up the SQL queries (count rows, and get data).
*
* @param \core\dml\sql_join $allowedjoins (joins, wheres, params) defines allowed users for the report.
*/
public function setup_sql_queries($allowedjoins) {
list($fields, $from, $where, $params) = $this->base_sql($allowedjoins);
// The WHERE clause is vital here, because some parts of tablelib.php will expect to
// add bits like ' AND x = 1' on the end, and that needs to leave to valid SQL.
$this->set_count_sql("SELECT COUNT(1) FROM (SELECT $fields FROM $from WHERE $where) temp WHERE 1 = 1", $params);
list($fields, $from, $where, $params) = $this->update_sql_after_count($fields, $from, $where, $params);
$this->set_sql($fields, $from, $where, $params);
}
/**
* Add the information about the latest state of the question with slot
* $slot to the query.
*
* The extra information is added as a join to a
* 'table' with alias qa$slot, with columns that are a union of
* the columns of the question_attempts and question_attempts_states tables.
*
* @param int $slot the question to add information for.
*/
protected function add_latest_state_join($slot) {
$alias = 'qa' . $slot;
$fields = $this->get_required_latest_state_fields($slot, $alias);
if (!$fields) {
return;
}
// This condition roughly filters the list of attempts to be considered.
// It is only used in a subselect to help crappy databases (see MDL-30122)
// therefore, it is better to use a very simple join, which may include
// too many records, than to do a super-accurate join.
$qubaids = new qubaid_join("{quiz_attempts} {$alias}quiza", "{$alias}quiza.uniqueid",
"{$alias}quiza.quiz = :{$alias}quizid", array("{$alias}quizid" => $this->sql->params['quizid']));
$dm = new question_engine_data_mapper();
list($inlineview, $viewparams) = $dm->question_attempt_latest_state_view($alias, $qubaids);
$this->sql->fields .= ",\n$fields";
$this->sql->from .= "\nLEFT JOIN $inlineview ON " .
"$alias.questionusageid = quiza.uniqueid AND $alias.slot = :{$alias}slot";
$this->sql->params[$alias . 'slot'] = $slot;
$this->sql->params = array_merge($this->sql->params, $viewparams);
}
/**
* Get an appropriate qubaid_condition for loading more data about the
* attempts we are displaying.
* @return qubaid_condition
*/
protected function get_qubaids_condition() {
if (is_null($this->rawdata)) {
throw new coding_exception(
'Cannot call get_qubaids_condition until the main data has been loaded.');
}
if ($this->is_downloading()) {
// We want usages for all attempts.
return new qubaid_join("(
SELECT DISTINCT quiza.uniqueid
FROM " . $this->sql->from . "
WHERE " . $this->sql->where . "
) quizasubquery", 'quizasubquery.uniqueid',
"1 = 1", $this->sql->params);
}
$qubaids = array();
foreach ($this->rawdata as $attempt) {
if ($attempt->usageid > 0) {
$qubaids[] = $attempt->usageid;
}
}
return new qubaid_list($qubaids);
}
public function query_db($pagesize, $useinitialsbar = true) {
$doneslots = array();
foreach ($this->get_sort_columns() as $column => $notused) {
$slot = $this->is_latest_step_column($column);
if ($slot && !in_array($slot, $doneslots)) {
$this->add_latest_state_join($slot);
$doneslots[] = $slot;
}
}
parent::query_db($pagesize, $useinitialsbar);
if ($this->requires_extra_data()) {
$this->load_extra_data();
}
}
public function get_sort_columns() {
// Add attemptid as a final tie-break to the sort. This ensures that
// Attempts by the same student appear in order when just sorting by name.
$sortcolumns = parent::get_sort_columns();
$sortcolumns['quiza.id'] = SORT_ASC;
return $sortcolumns;
}
public function wrap_html_start() {
if ($this->is_downloading() || !$this->includecheckboxes) {
return;
}
$url = $this->options->get_url();
$url->param('sesskey', sesskey());
echo '<div id="tablecontainer">';
echo '<form id="attemptsform" method="post" action="' . $url->out_omit_querystring() . '">';
echo html_writer::input_hidden_params($url);
echo '<div>';
}
public function wrap_html_finish() {
global $PAGE;
if ($this->is_downloading() || !$this->includecheckboxes) {
return;
}
echo '<div id="commands">';
$this->submit_buttons();
echo '</div>';
// Close the form.
echo '</div>';
echo '</form></div>';
}
/**
* Output any submit buttons required by the $this->includecheckboxes form.
*/
protected function submit_buttons() {
global $PAGE;
if (has_capability('mod/quiz:deleteattempts', $this->context)) {
$deletebuttonparams = [
'type' => 'submit',
'class' => 'btn btn-secondary mr-1',
'id' => 'deleteattemptsbutton',
'name' => 'delete',
'value' => get_string('deleteselected', 'quiz_overview'),
'data-action' => 'toggle',
'data-togglegroup' => $this->togglegroup,
'data-toggle' => 'action',
'disabled' => true
];
echo html_writer::empty_tag('input', $deletebuttonparams);
$PAGE->requires->event_handler('#deleteattemptsbutton', 'click', 'M.util.show_confirm_dialog',
array('message' => get_string('deleteattemptcheck', 'quiz')));
}
}
/**
* Generates the contents for the checkbox column header.
*
* It returns the HTML for a master \core\output\checkbox_toggleall component that selects/deselects all quiz attempts.
*
* @param string $columnname The name of the checkbox column.
* @return string
*/
public function checkbox_col_header(string $columnname) {
global $OUTPUT;
// Make sure to disable sorting on this column.
$this->no_sorting($columnname);
// Build the select/deselect all control.
$selectallid = $this->uniqueid . '-selectall-attempts';
$selectalltext = get_string('selectall', 'quiz');
$deselectalltext = get_string('selectnone', 'quiz');
$mastercheckbox = new \core\output\checkbox_toggleall($this->togglegroup, true, [
'id' => $selectallid,
'name' => $selectallid,
'value' => 1,
'label' => $selectalltext,
'labelclasses' => 'accesshide',
'selectall' => $selectalltext,
'deselectall' => $deselectalltext,
]);
return $OUTPUT->render($mastercheckbox);
}
}
debugging('This file is no longer required in Moodle 4.2+. Please do not include/require it.', DEBUG_DEVELOPER);

View File

@ -15,85 +15,11 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Base class for quiz report plugins.
* File only retained to prevent fatal errors in code that tries to require/include this.
*
* @package mod_quiz
* @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @todo MDL-76612 delete this file as part of Moodle 4.6 development.
* @deprecated This file is no longer required in Moodle 4.2+.
*/
defined('MOODLE_INTERNAL') || die();
/**
* Base class for quiz report plugins.
*
* Doesn't do anything on it's own -- it needs to be extended.
* This class displays quiz reports. Because it is called from
* within /mod/quiz/report.php you can assume that the page header
* and footer are taken care of.
*
* This file can refer to itself as report.php to pass variables
* to itself - all these will also be globally available. You must
* pass "id=$cm->id" or q=$quiz->id", and "mode=reportname".
*
* @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class quiz_default_report {
const NO_GROUPS_ALLOWED = -2;
/**
* Override this function to displays the report.
* @param $cm the course-module for this quiz.
* @param $course the coures we are in.
* @param $quiz this quiz.
*/
public abstract function display($cm, $course, $quiz);
/**
* Initialise some parts of $PAGE and start output.
*
* @param object $cm the course_module information.
* @param object $coures the course settings.
* @param object $quiz the quiz settings.
* @param string $reportmode the report name.
*/
public function print_header_and_tabs($cm, $course, $quiz, $reportmode = 'overview') {
global $PAGE, $OUTPUT, $CFG;
// Print the page header.
$PAGE->set_title($quiz->name);
$PAGE->set_heading($course->fullname);
echo $OUTPUT->header();
$context = context_module::instance($cm->id);
if (!$PAGE->has_secondary_navigation()) {
echo $OUTPUT->heading(format_string($quiz->name, true, array('context' => $context)));
}
if (!empty($CFG->enableplagiarism)) {
require_once($CFG->libdir . '/plagiarismlib.php');
echo plagiarism_update_status($course, $cm);
}
}
/**
* Get the current group for the user user looking at the report.
*
* @param object $cm the course_module information.
* @param object $coures the course settings.
* @param context $context the quiz context.
* @return int the current group id, if applicable. 0 for all users,
* NO_GROUPS_ALLOWED if the user cannot see any group.
*/
public function get_current_group($cm, $course, $context) {
$groupmode = groups_get_activity_groupmode($cm, $course);
$currentgroup = groups_get_activity_group($cm, true);
if ($groupmode == SEPARATEGROUPS && !$currentgroup && !has_capability('moodle/site:accessallgroups', $context)) {
$currentgroup = self::NO_GROUPS_ALLOWED;
}
return $currentgroup;
}
}
debugging('This file is no longer required in Moodle 4.2+. Please do not include/require it.', DEBUG_DEVELOPER);

View File

@ -14,20 +14,12 @@
// 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 manual grading report class.
*
* @package quiz_grading
* @copyright 2006 Gustav Delius
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use mod_quiz\local\reports\report_base;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/quiz/report/grading/gradingsettings_form.php');
/**
* Quiz report to help teachers manually grade questions that need it.
*
@ -38,7 +30,7 @@ require_once($CFG->dirroot . '/mod/quiz/report/grading/gradingsettings_form.php'
* @copyright 2006 Gustav Delius
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_grading_report extends quiz_default_report {
class quiz_grading_report extends report_base {
const DEFAULT_PAGE_SIZE = 5;
const DEFAULT_ORDER = 'random';

View File

@ -14,19 +14,8 @@
// 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 setting form for the quiz overview report.
*
* @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_form.php');
use mod_quiz\local\reports\attempts_report;
use mod_quiz\local\reports\attempts_report_options_form;
/**
* Quiz overview report settings form.
@ -34,13 +23,13 @@ require_once($CFG->dirroot . '/mod/quiz/report/attemptsreport_form.php');
* @copyright 2008 Jamie Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_overview_settings_form extends mod_quiz_attempts_report_form {
class quiz_overview_settings_form extends attempts_report_options_form {
protected function other_attempt_fields(MoodleQuickForm $mform) {
if (has_capability('mod/quiz:regrade', $this->_customdata['context'])) {
$mform->addElement('advcheckbox', 'onlyregraded', get_string('reportshowonly', 'quiz'),
get_string('optonlyregradedattempts', 'quiz_overview'));
$mform->disabledIf('onlyregraded', 'attempts', 'eq', quiz_attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('onlyregraded', 'attempts', 'eq', attempts_report::ENROLLED_WITHOUT);
}
}

View File

@ -14,19 +14,8 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Class to store the options for a {@link quiz_overview_report}.
*
* @package quiz_overview
* @copyright 2012 The Open University
* @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_options.php');
use mod_quiz\local\reports\attempts_report;
use mod_quiz\local\reports\attempts_report_options;
/**
* Class to store the options for a {@link quiz_overview_report}.
@ -34,7 +23,7 @@ require_once($CFG->dirroot . '/mod/quiz/report/attemptsreport_options.php');
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_overview_options extends mod_quiz_attempts_report_options {
class quiz_overview_options extends attempts_report_options {
/** @var bool whether to show only attempt that need regrading. */
public $onlyregraded = false;
@ -88,7 +77,7 @@ class quiz_overview_options extends mod_quiz_attempts_report_options {
public function resolve_dependencies() {
parent::resolve_dependencies();
if ($this->attempts == quiz_attempts_report::ENROLLED_WITHOUT) {
if ($this->attempts == attempts_report::ENROLLED_WITHOUT) {
$this->onlyregraded = false;
}
@ -100,6 +89,6 @@ class quiz_overview_options extends mod_quiz_attempts_report_options {
// if the user has permissions and if the report mode is showing attempts.
$this->checkboxcolumn = has_any_capability(
array('mod/quiz:regrade', 'mod/quiz:deleteattempts'), context_module::instance($this->cm->id))
&& ($this->attempts != quiz_attempts_report::ENROLLED_WITHOUT);
&& ($this->attempts != attempts_report::ENROLLED_WITHOUT);
}
}

View File

@ -14,19 +14,7 @@
// 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');
use mod_quiz\local\reports\attempts_report_table;
/**
* This is a table subclass for displaying the quiz grades report.
@ -34,7 +22,7 @@ require_once($CFG->dirroot . '/mod/quiz/report/attemptsreport_table.php');
* @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 {
class quiz_overview_table extends attempts_report_table {
protected $regradedqs = array();

View File

@ -22,11 +22,11 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use mod_quiz\local\reports\attempts_report;
use mod_quiz\question\bank\qbank_helper;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/quiz/report/attemptsreport.php');
require_once($CFG->dirroot . '/mod/quiz/report/overview/overview_options.php');
require_once($CFG->dirroot . '/mod/quiz/report/overview/overview_form.php');
require_once($CFG->dirroot . '/mod/quiz/report/overview/overview_table.php');
@ -38,7 +38,7 @@ require_once($CFG->dirroot . '/mod/quiz/report/overview/overview_table.php');
* @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_overview_report extends quiz_attempts_report {
class quiz_overview_report extends attempts_report {
/**
* @var bool whether there are actually students to show, given the options.

View File

@ -14,26 +14,23 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
use mod_quiz\local\reports\attempts_report;
/**
* Makes some protected methods of quiz_attempts_report public to facilitate testing.
* Makes some protected methods of attempts_report public to facilitate testing.
*
* @package quiz_overview
* @copyright 2020 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/report/attemptsreport.php');
/**
* Makes some protected methods of quiz_attempts_report public to facilitate testing.
* Makes some protected methods of attempts_report public to facilitate testing.
*
* @copyright 2020 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class testable_quiz_attempts_report extends quiz_attempts_report {
class testable_quiz_attempts_report extends attempts_report {
/**
* Override this function to displays the report.

View File

@ -21,7 +21,7 @@ use mod_quiz\external\submit_question_version;
use question_engine;
use quiz;
use quiz_attempt;
use quiz_attempts_report;
use mod_quiz\local\reports\attempts_report;
use quiz_overview_options;
use quiz_overview_report;
use quiz_overview_table;
@ -31,7 +31,6 @@ defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
require_once($CFG->dirroot . '/mod/quiz/report/default.php');
require_once($CFG->dirroot . '/mod/quiz/report/overview/report.php');
require_once($CFG->dirroot . '/mod/quiz/report/overview/overview_form.php');
require_once($CFG->dirroot . '/mod/quiz/report/overview/tests/helpers.php');
@ -176,7 +175,7 @@ class report_test extends \advanced_testcase {
// Set the options.
$reportoptions = new quiz_overview_options('overview', $quiz, $cm, null);
$reportoptions->attempts = quiz_attempts_report::ENROLLED_ALL;
$reportoptions->attempts = attempts_report::ENROLLED_ALL;
$reportoptions->onlygraded = true;
$reportoptions->states = array(quiz_attempt::IN_PROGRESS, quiz_attempt::OVERDUE, quiz_attempt::FINISHED);

View File

@ -14,18 +14,7 @@
// 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 responses table for showing last try at question.
*
* @package quiz_responses
* @copyright 2008 Jean-Michel Vedrine
* @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');
use mod_quiz\local\reports\attempts_report_table;
/**
@ -34,7 +23,7 @@ require_once($CFG->dirroot . '/mod/quiz/report/attemptsreport_table.php');
* @copyright 2008 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_last_responses_table extends quiz_attempts_report_table {
class quiz_last_responses_table extends attempts_report_table {
/**
* Constructor

View File

@ -22,10 +22,10 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use mod_quiz\local\reports\attempts_report;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/quiz/report/attemptsreport.php');
require_once($CFG->dirroot . '/mod/quiz/report/responses/responses_options.php');
require_once($CFG->dirroot . '/mod/quiz/report/responses/responses_form.php');
require_once($CFG->dirroot . '/mod/quiz/report/responses/last_responses_table.php');
@ -46,7 +46,7 @@ require_once($CFG->dirroot . '/mod/quiz/report/responses/first_or_all_responses_
* @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_responses_report extends quiz_attempts_report {
class quiz_responses_report extends attempts_report {
public function display($quiz, $cm, $course) {
global $OUTPUT, $DB;

View File

@ -14,19 +14,8 @@
// 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 setting form for the quiz responses report.
*
* @package quiz_responses
* @copyright 2008 Jean-Michel Vedrine
* @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_form.php');
use mod_quiz\local\reports\attempts_report;
use mod_quiz\local\reports\attempts_report_options_form;
/**
* Quiz responses report settings form.
@ -34,7 +23,7 @@ require_once($CFG->dirroot . '/mod/quiz/report/attemptsreport_form.php');
* @copyright 2008 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_responses_settings_form extends mod_quiz_attempts_report_form {
class quiz_responses_settings_form extends attempts_report_options_form {
protected function other_preference_fields(MoodleQuickForm $mform) {
$mform->addGroup(array(
@ -45,15 +34,15 @@ class quiz_responses_settings_form extends mod_quiz_attempts_report_form {
$mform->createElement('advcheckbox', 'right', '',
get_string('rightanswer', 'quiz_responses')),
), 'coloptions', get_string('showthe', 'quiz_responses'), array(' '), false);
$mform->disabledIf('qtext', 'attempts', 'eq', quiz_attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('resp', 'attempts', 'eq', quiz_attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('right', 'attempts', 'eq', quiz_attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('qtext', 'attempts', 'eq', attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('resp', 'attempts', 'eq', attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('right', 'attempts', 'eq', attempts_report::ENROLLED_WITHOUT);
}
public function validation($data, $files) {
$errors = parent::validation($data, $files);
if ($data['attempts'] != quiz_attempts_report::ENROLLED_WITHOUT && !(
if ($data['attempts'] != attempts_report::ENROLLED_WITHOUT && !(
$data['qtext'] || $data['resp'] || $data['right'])) {
$errors['coloptions'] = get_string('reportmustselectstate', 'quiz');
}
@ -70,7 +59,7 @@ class quiz_responses_settings_form extends mod_quiz_attempts_report_form {
question_attempt::ALL_TRIES => get_string('alltries', 'question'))
);
$mform->setDefault('whichtries', question_attempt::LAST_TRY);
$mform->disabledIf('whichtries', 'attempts', 'eq', quiz_attempts_report::ENROLLED_WITHOUT);
$mform->disabledIf('whichtries', 'attempts', 'eq', attempts_report::ENROLLED_WITHOUT);
}
}
}

View File

@ -14,19 +14,8 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Class to store the options for a {@link quiz_responses_report}.
*
* @package quiz_responses
* @copyright 2012 The Open University
* @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_options.php');
use mod_quiz\local\reports\attempts_report;
use mod_quiz\local\reports\attempts_report_options;
/**
* Class to store the options for a {@link quiz_responses_report}.
@ -34,7 +23,7 @@ require_once($CFG->dirroot . '/mod/quiz/report/attemptsreport_options.php');
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_responses_options extends mod_quiz_attempts_report_options {
class quiz_responses_options extends attempts_report_options {
/** @var bool whether to show the question text columns. */
public $showqtext = false;
@ -126,6 +115,6 @@ class quiz_responses_options extends mod_quiz_attempts_report_options {
// We only want to show the checkbox to delete attempts
// if the user has permissions and if the report mode is showing attempts.
$this->checkboxcolumn = has_capability('mod/quiz:deleteattempts', context_module::instance($this->cm->id))
&& ($this->attempts != quiz_attempts_report::ENROLLED_WITHOUT);
&& ($this->attempts != attempts_report::ENROLLED_WITHOUT);
}
}

View File

@ -23,7 +23,6 @@ defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/tests/attempt_walkthrough_from_csv_test.php');
require_once($CFG->dirroot . '/mod/quiz/report/default.php');
require_once($CFG->dirroot . '/mod/quiz/report/statistics/report.php');
require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');

View File

@ -25,9 +25,9 @@
defined('MOODLE_INTERNAL') || die();
use mod_quiz\local\reports\report_base;
use core_question\statistics\questions\all_calculated_for_qubaid_condition;
require_once($CFG->dirroot . '/mod/quiz/report/default.php');
require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_form.php');
require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_table.php');
@ -42,7 +42,7 @@ require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');
* @copyright 2008 Jamie Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_statistics_report extends quiz_default_report {
class quiz_statistics_report extends report_base {
/** @var context_module context of this quiz.*/
protected $context;

View File

@ -25,7 +25,6 @@ defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/tests/attempt_walkthrough_from_csv_test.php');
require_once($CFG->dirroot . '/mod/quiz/report/default.php');
require_once($CFG->dirroot . '/mod/quiz/report/statistics/report.php');
require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');

View File

@ -4,11 +4,31 @@ Overview of this plugin type at http://docs.moodle.org/dev/Quiz_reports
=== 4.2 ===
There was an ill-advised, never documented, API change in quiz_report_get_significant_questions
in Moodle 4.0. The API has now been reverted to how it was before 4.0. Hopefully this
will not cause anyone a problem. (The API revert did not require any changes in any automated tests
or standard quiz reports.)
* There was an ill-advised, never documented, API change in quiz_report_get_significant_questions
in Moodle 4.0. The API has now been reverted to how it was before 4.0. Hopefully this
will not cause anyone a problem. (The API revert did not require any changes in any automated tests
or standard quiz reports.)
* The quiz has a lot of old classes in lib.php files. These have now been moved into the classes folder,
and so are now in namespaces. Because of Moodle's class renaming support, your code should continue
working, but output deprecated warnings, so you probably want to update. This should mostly be
doable by adding use statements, and a search/replace.
- quiz_default_report => mod_quiz\local\reports\report_base
- quiz_attempts_report => mod_quiz\local\reports\attempts_report
- mod_quiz_attempts_report_form => mod_quiz\local\reports\attempts_report_options_form
- mod_quiz_attempts_report_options => mod_quiz\local\reports\attempts_report_options
- quiz_attempts_report_table => mod_quiz\local\reports\attempts_report_table
As part of the clean-up, the following files are no longer required, and if you try to
include them, you will get a debugging notices telling you not to:
- mod/quiz/report/attemptsreport.php
- mod/quiz/report/attemptsreport_form.php
- mod/quiz/report/attemptsreport_options.php
- mod/quiz/report/attemptsreport_table.php
- mod/quiz/report/default.php
>>>>>>> 40c14e4712e... MDL-74923 quiz: move report base classes to local folder
=== 3.9 ===
* Quiz report plugins defining capabilities used to require an extra string like

View File

@ -30,6 +30,19 @@ This files describes API changes in the quiz code.
mod_quiz\adminpresets\adminpresets_review_setting
- mod_quiz\adminpresets\adminpresets_mod_quiz_admin_setting_user_image =>
mod_quiz\adminpresets\adminpresets_user_image_setting
- quiz_default_report => mod_quiz\local\reports\report_base
- quiz_attempts_report => mod_quiz\local\reports\attempts_report
- mod_quiz_attempts_report_form => mod_quiz\local\reports\attempts_report_options_form
- mod_quiz_attempts_report_options => mod_quiz\local\reports\attempts_report_options
- quiz_attempts_report_table => mod_quiz\local\reports\attempts_report_table
As part of the clean-up, the following files are no longer required, and if you try to
include them, you will get a debugging notices telling you not to:
- mod/quiz/report/attemptsreport.php
- mod/quiz/report/attemptsreport_form.php
- mod/quiz/report/attemptsreport_options.php
- mod/quiz/report/attemptsreport_table.php
- mod/quiz/report/default.php
=== 4.1 ===

View File

@ -1210,7 +1210,7 @@ ORDER BY
/**
* Get a subquery that returns the latest step of every qa in some qubas.
* Currently, this is only used by the quiz reports. See
* {@link quiz_attempts_report_table::add_latest_state_join()}.
* {@see \mod_quiz\local\reports\attempts_report_table::add_latest_state_join()}.
*
* This method may be called publicly.
*