first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
<?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/>.
/**
* File only retained to prevent fatal errors in code that tries to require/include this.
*
* @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();
debugging('This file is no longer required in Moodle 4.2+. Please do not include/require it.', DEBUG_DEVELOPER);
+25
View File
@@ -0,0 +1,25 @@
<?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/>.
/**
* File only retained to prevent fatal errors in code that tries to require/include this.
*
* @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();
debugging('This file is no longer required in Moodle 4.2+. Please do not include/require it.', DEBUG_DEVELOPER);
@@ -0,0 +1,25 @@
<?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/>.
/**
* File only retained to prevent fatal errors in code that tries to require/include this.
*
* @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();
debugging('This file is no longer required in Moodle 4.2+. Please do not include/require it.', DEBUG_DEVELOPER);
+25
View File
@@ -0,0 +1,25 @@
<?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/>.
/**
* File only retained to prevent fatal errors in code that tries to require/include this.
*
* @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();
debugging('This file is no longer required in Moodle 4.2+. Please do not include/require it.', DEBUG_DEVELOPER);
+25
View File
@@ -0,0 +1,25 @@
<?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/>.
/**
* File only retained to prevent fatal errors in code that tries to require/include this.
*
* @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();
debugging('This file is no longer required in Moodle 4.2+. Please do not include/require it.', DEBUG_DEVELOPER);
@@ -0,0 +1,89 @@
<?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/>.
/**
* Privacy subsystem implementation for quiz_grading.
*
* @package quiz_grading
* @copyright 2020 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace quiz_grading\privacy;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\writer;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy subsystem for quiz_grading.
*/
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\user_preference_provider {
/**
* Returns meta data about this system.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
$collection->add_user_preference('quiz_grading_pagesize', 'privacy:preference:pagesize');
$collection->add_user_preference('quiz_grading_order', 'privacy:preference:order');
return $collection;
}
/**
* Export all user preferences for the plugin.
*
* @param int $userid The userid of the user whose data is to be exported.
*/
public static function export_user_preferences(int $userid) {
// Page size.
$pagesize = get_user_preferences("quiz_grading_pagesize", null, $userid);
if ($pagesize !== null) {
writer::export_user_preference('quiz_grading', 'pagesize', $pagesize,
get_string('privacy:preference:pagesize', 'quiz_grading'));
}
// Attempt order.
$order = get_user_preferences("quiz_grading_order", null, $userid);
if ($order !== null) {
switch ($order) {
case 'random':
$order = get_string('random', 'quiz_grading');
break;
case 'date':
$order = get_string('date');
break;
case 'studentfirstname':
$order = get_string('studentfirstname', 'quiz_grading');
break;
case 'studentlastname':
$order = get_string('studentlastname', 'quiz_grading');
break;
default:
$order = \core_user\fields::get_display_name($order);
}
writer::export_user_preference('quiz_grading', 'order', $order,
get_string('privacy:preference:order', 'quiz_grading'));
}
}
}
+51
View File
@@ -0,0 +1,51 @@
<?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/>.
/**
* Capability definitions for the quiz manual grading report.
*
* @package quiz_grading
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$capabilities = [
// Is the user allowed to see the student's real names while grading?
'quiz/grading:viewstudentnames' => [
'captype' => 'read',
'contextlevel' => CONTEXT_MODULE,
'legacy' => [
'teacher' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
],
'clonepermissionsfrom' => 'mod/quiz:viewreports'
],
// Is the user allowed to see the student's identity fields while grading?
// Note that the name of this capability is now out-of-date, but to preserve
// backwards compatibility, the name was not changed when the functionality was updated.
'quiz/grading:viewidnumber' => [
'captype' => 'read',
'contextlevel' => CONTEXT_MODULE,
'legacy' => [
'teacher' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW
],
'clonepermissionsfrom' => 'mod/quiz:viewreports'
]
];
+40
View File
@@ -0,0 +1,40 @@
<?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/>.
/**
* Post-install script for the quiz manual grading report.
* @package quiz_grading
* @copyright 2013 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Post-install script
*/
function xmldb_quiz_grading_install() {
global $DB;
$record = new stdClass();
$record->name = 'grading';
$record->displayorder = '6000';
$record->capability = 'mod/quiz:grade';
$DB->insert_record('quiz_reports', $record);
}
@@ -0,0 +1,129 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file defines the setting form for the quiz grading report.
*
* @package quiz_grading
* @copyright 2010 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/formslib.php');
/**
* Quiz grading report settings form.
*
* @copyright 2010 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_grading_settings_form extends moodleform {
/** @var bool whether the user has asked to include automatically graded attempts. */
protected $includeauto;
/** @var array extra hidden fields to add to the form. Things like cm id. */
protected $hidden = [];
/** @var stdClass data about how many attempts of each type their are. */
protected $counts;
/** @var bool Whether the user has permission to see user names. */
protected $shownames;
/** @var bool $showcustomfields whether custom field values should be shown. */
protected $showcustomfields;
/** @var stdClass $context the quiz context. */
protected $context;
/**
* quiz_grading_settings_form constructor.
*
* @param array $hidden Array of options form.
* @param stdClass $counts object that stores the number of each type of attempt.
* @param bool $shownames whether student names should be shown.
* @param bool $showcustomfields whether custom field values should be shown.
* @param stdClass $context context object.
*/
public function __construct(array $hidden, stdClass $counts, bool $shownames, bool $showcustomfields, stdClass $context) {
global $CFG;
$this->includeauto = !empty($hidden['includeauto']);
$this->hidden = $hidden;
$this->counts = $counts;
$this->shownames = $shownames;
$this->showcustomfields = $showcustomfields;
$this->context = $context;
parent::__construct($CFG->wwwroot . '/mod/quiz/report.php');
}
protected function definition() {
$mform = $this->_form;
$mform->addElement('header', 'options', get_string('options', 'quiz_grading'));
$gradeoptions = [];
foreach (['needsgrading', 'manuallygraded', 'autograded', 'all'] as $type) {
if (empty($this->counts->$type)) {
continue;
}
if ($type == 'autograded' && !$this->includeauto) {
continue;
}
$gradeoptions[$type] = get_string('gradeattempts' . $type, 'quiz_grading',
$this->counts->$type);
}
$mform->addElement('select', 'grade', get_string('attemptstograde', 'quiz_grading'),
$gradeoptions);
$mform->addElement('text', 'pagesize', get_string('questionsperpage', 'quiz_grading'),
['size' => 3]);
$mform->addRule('pagesize', null, 'positiveint', null, 'client');
$mform->setType('pagesize', PARAM_INT);
$orderoptions = [
'random' => get_string('random', 'quiz_grading'),
'date' => get_string('date')
];
if ($this->shownames) {
$orderoptions['studentfirstname'] = get_string('firstname');
$orderoptions['studentlastname'] = get_string('lastname');
}
// If the current user can see custom user fields, add the custom user fields to the select menu.
if ($this->showcustomfields) {
$userfieldsapi = \core_user\fields::for_identity($this->context);
foreach ($userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]) as $field) {
$orderoptions[s($field)] = \core_user\fields::get_display_name(s($field));
}
}
$mform->addElement('select', 'order', get_string('orderattemptsby', 'quiz_grading'),
$orderoptions);
foreach ($this->hidden as $name => $value) {
$mform->addElement('hidden', $name, $value);
if ($name == 'mode') {
$mform->setType($name, PARAM_ALPHA);
} else {
$mform->setType($name, PARAM_INT);
}
}
$mform->addElement('submit', 'submitbutton', get_string('changeoptions', 'quiz_grading'));
}
}
@@ -0,0 +1 @@
grade,quiz_grading
@@ -0,0 +1,81 @@
<?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/>.
/**
* Strings for component 'quiz_grading', language 'en', branch 'MOODLE_20_STABLE'
*
* @package quiz_grading
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['alldoneredirecting'] = 'All selected attempts have been graded. Returning to the list of questions.';
$string['alreadygraded'] = 'Already graded';
$string['alsoshowautomaticallygraded'] = 'Also show questions that have been graded automatically';
$string['attemptstograde'] = 'Attempts to grade';
$string['automaticallygraded'] = 'Automatically graded';
$string['backtothelistofquestions'] = 'Back to the list of questions';
$string['cannotloadquestioninfo'] = 'Unable to load questiontype specific question information';
$string['cannotgradethisattempt'] = 'Cannot grade this attempt.';
$string['changeoptions'] = 'Change options';
$string['essayonly'] = 'The following questions need to be graded manually';
$string['invalidquestionid'] = 'Gradable question with ID {$a} not found';
$string['invalidattemptid'] = 'No such attempt ID exists';
$string['gradeall'] = 'grade all';
$string['gradeattemptsall'] = 'All ({$a})';
$string['gradeattemptsautograded'] = 'Those that have been graded automatically ({$a})';
$string['gradeattemptsmanuallygraded'] = 'Those that have previously been graded manually ({$a})';
$string['gradeattemptsneedsgrading'] = 'Those that need grading ({$a})';
$string['graded'] = '(graded)';
$string['gradenextungraded'] = 'Grade next {$a} ungraded attempts';
$string['gradeungraded'] = 'Grade all {$a} ungraded attempts';
$string['grading'] = 'Manual grading';
$string['grading:viewidnumber'] = 'See student identity fields while grading';
$string['grading:viewstudentnames'] = 'See student names while grading';
$string['gradingall'] = 'All {$a} attempts on this question.';
$string['gradingattempt'] = 'Attempt number {$a->attempt} for {$a->fullname}';
$string['gradingattemptwithcustomfields'] = 'Attempt number {$a->attempt} for {$a->fullname} ({$a->customfields})';
$string['gradingattemptsxtoyofz'] = 'Grading attempts {$a->from} to {$a->to} of {$a->of}';
$string['gradingnextungraded'] = 'Next {$a} ungraded attempts';
$string['gradingnotallowed'] = 'You do not have permission to manually grade responses in this quiz';
$string['gradingquestionx'] = 'Grading question {$a->number}: {$a->questionname}';
$string['gradingreport'] = 'Manual grading report';
$string['gradinguser'] = 'Attempts for {$a}';
$string['gradingungraded'] = '{$a} ungraded attempts';
$string['hideautomaticallygraded'] = 'Hide questions that have been graded automatically';
$string['inprogress'] = 'In progress';
$string['noquestionsfound'] = 'No manually graded questions found';
$string['nothingfound'] = 'Nothing to display';
$string['options'] = 'Options';
$string['orderattemptsby'] = 'Order attempts by';
$string['pluginname'] = 'Manual grading';
$string['privacy:preference:order'] = 'What order to show the attempts that need grading.';
$string['privacy:preference:pagesize'] = 'How many attempts to show on each page of the grading interface.';
$string['qno'] = 'Q #';
$string['questionname'] = 'Question name';
$string['questionsperpage'] = 'Questions per page';
$string['questionsthatneedgrading'] = 'Questions that need grading';
$string['questiontitle'] = 'Question {$a->number} : "{$a->name}" ({$a->openspan}{$a->gradedattempts}{$a->closespan} / {$a->totalattempts} attempts {$a->openspan}graded{$a->closespan}).';
$string['random'] = 'Random';
$string['saveandnext'] = 'Save and show next';
$string['showstudentnames'] = 'Show student names';
$string['tograde'] = 'To grade';
$string['total'] = 'Total';
$string['unknownquestion'] = 'Unknown question';
$string['updategrade'] = 'update grades';
// Deprecated since Moodle 4.4.
$string['grade'] = 'grade';
+212
View File
@@ -0,0 +1,212 @@
<?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/>.
/**
* Defines the renderer for the quiz_grading module.
*
* @package quiz_grading
* @copyright 2018 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* The renderer for the quiz_grading module.
*
* @copyright 2018 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_grading_renderer extends plugin_renderer_base {
/**
* Render no question notification.
*
* @param stdClass $quiz The quiz settings.
* @param stdClass $cm The course-module for this quiz.
* @param stdClass $context The quiz context.
* @return string The HTML for the no questions message.
*/
public function render_quiz_no_question_notification($quiz, $cm, $context) {
return quiz_no_questions_message($quiz, $cm, $context);
}
/**
* Render no question need to grade notification.
*
* @throws coding_exception
*/
public function render_quiz_no_grade_question_notification() {
return $this->notification(get_string('nothingfound', 'quiz_grading'), 'info', false);
}
/**
* Render index display.
*
* @param string $linktext The text of the link.
* @param moodle_url $listquestionurl Url of the page that list all questions.
* @return string The HTML for the display heading.
* @throws coding_exception
*/
public function render_display_index_heading($linktext, $listquestionurl) {
$output = '';
$output .= $this->heading(get_string('questionsthatneedgrading', 'quiz_grading'), 3);
$output .= html_writer::tag('p', html_writer::link($listquestionurl, $linktext), ['class' => 'toggleincludeauto']);
return $output;
}
/**
* Render questions list table.
*
* @param bool $includeauto True to show automatically graded questions.
* @param array $data List of questions.
* @param array $header List of table headers.
* @return string The HTML for the question table.
* @throws coding_exception
*/
public function render_questions_table($includeauto, $data, $header) {
if (empty($data)) {
return $this->render_quiz_no_grade_question_notification();
}
$output = '';
$table = new html_table();
$table->class = 'generaltable';
$table->id = 'questionstograde';
$table->head = $header;
$table->data = $data;
$output .= html_writer::table($table);
return $output;
}
/**
* Render grade link for question.
*
* @param stdClass $counts
* @param string $type Type of grade.
* @param string $string Lang string.
* @param string $component Lang string component.
* @param moodle_url $gradequestionurl Url to grade question.
* @return string The HTML for the question grade link.
* @throws coding_exception
*/
public function render_grade_link($counts, $type, $string, $component, $gradequestionurl) {
$output = '';
if ($counts->$type > 0) {
$output .= ' ' . html_writer::link(
$gradequestionurl,
get_string($string, $component),
['class' => 'gradetheselink']);
}
return $output;
}
/**
* Render grading page.
*
* @param stdClass $questioninfo Information of a question.
* @param moodle_url $listquestionsurl Url of the page that list all questions.
* @param quiz_grading_settings_form $filterform Question filter form.
* @param stdClass $paginginfo Pagination information.
* @param stdClass $pagingbar Pagination bar information.
* @param moodle_url $formaction Form submit url.
* @param array $hiddeninputs List of hidden input fields.
* @param string $gradequestioncontent HTML string of question content.
* @return string The HTML for the grading interface.
* @throws coding_exception
* @throws moodle_exception
*/
public function render_grading_interface($questioninfo, $listquestionsurl, $filterform, $paginginfo, $pagingbar, $formaction,
$hiddeninputs, $gradequestioncontent) {
$output = '';
$output .= question_engine::initialise_js();
$output .= $this->heading(get_string('gradingquestionx', 'quiz_grading', $questioninfo), 3);
$output .= html_writer::tag('p', html_writer::link($listquestionsurl,
get_string('backtothelistofquestions', 'quiz_grading')),
['class' => 'mdl-align']);
$output .= $filterform->render();
$output .= $this->heading(get_string('gradingattemptsxtoyofz', 'quiz_grading', $paginginfo), 3);
$output .= $this->render_paging_bar($pagingbar);
$output .= html_writer::start_tag('form', [
'method' => 'post',
'action' => $formaction,
'class' => 'mform',
'id' => 'manualgradingform'
]);
$output .= html_writer::start_tag('div');
$output .= html_writer::input_hidden_params(new moodle_url('', $hiddeninputs));
$output .= $gradequestioncontent;
$output .= html_writer::tag('div', html_writer::empty_tag('input', [
'type' => 'submit',
'class' => 'btn btn-primary',
'value' => get_string('saveandnext', 'quiz_grading')
]), ['class' => 'mdl-align']);
$output .= html_writer::end_tag('div') . html_writer::end_tag('form');
$output .= $this->render_paging_bar($pagingbar);
// Add the form change checker.
$this->page->requires->js_call_amd('core_form/changechecker', 'watchFormById', ['manualgradingform']);
return $output;
}
/**
* Render grade question content.
*
* @param question_usage_by_activity $questionusage The question usage that need to grade.
* @param int $slot the number used to identify this question within this usage.
* @param question_display_options $displayoptions the display options to use.
* @param int $questionnumber the number of the question to check.
* @param string $heading the question heading text.
* @return string The HTML for the question display.
*/
public function render_grade_question($questionusage, $slot, $displayoptions, $questionnumber, $heading) {
$output = '';
if ($heading) {
$output .= $this->heading($heading, 4);
}
$output .= $questionusage->render_question($slot, $displayoptions, $questionnumber);
return $output;
}
/**
* Render paging bar.
*
* @param object $pagingbar Pagination bar information.
* @return string The HTML for the question display.
*/
public function render_paging_bar(object $pagingbar): string {
if ($pagingbar->count > $pagingbar->pagesize && $pagingbar->order != 'random') {
return $this->paging_bar($pagingbar->count, $pagingbar->page, $pagingbar->pagesize, $pagingbar->pagingurl);
}
return '';
}
}
+745
View File
@@ -0,0 +1,745 @@
<?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/>.
use mod_quiz\local\reports\report_base;
use mod_quiz\quiz_attempt;
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.
*
* This report basically provides two screens:
* - List question that might need manual grading (or optionally all questions).
* - Provide an efficient UI to grade all attempts at a particular question.
*
* @copyright 2006 Gustav Delius
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_grading_report extends report_base {
const DEFAULT_PAGE_SIZE = 5;
const DEFAULT_ORDER = 'random';
/** @var string Positive integer regular expression. */
const REGEX_POSITIVE_INT = '/^[1-9]\d*$/';
/** @var array URL parameters for what is being displayed when grading. */
protected $viewoptions = [];
/** @var int the current group, 0 if none, or NO_GROUPS_ALLOWED. */
protected $currentgroup;
/** @var array from quiz_report_get_significant_questions. */
protected $questions;
/** @var stdClass the course settings. */
protected $course;
/** @var stdClass the course_module settings. */
protected $cm;
/** @var stdClass the quiz settings. */
protected $quiz;
/** @var context the quiz context. */
protected $context;
/** @var quiz_grading_renderer Renderer of Quiz Grading. */
protected $renderer;
/** @var string fragment of SQL code to restrict to the relevant users. */
protected $userssql;
/** @var array extra user fields. */
protected $extrauserfields = [];
public function display($quiz, $cm, $course) {
$this->quiz = $quiz;
$this->cm = $cm;
$this->course = $course;
// Get the URL options.
$slot = optional_param('slot', null, PARAM_INT);
$questionid = optional_param('qid', null, PARAM_INT);
$grade = optional_param('grade', null, PARAM_ALPHA);
$includeauto = optional_param('includeauto', false, PARAM_BOOL);
if (!in_array($grade, ['all', 'needsgrading', 'autograded', 'manuallygraded'])) {
$grade = null;
}
$pagesize = optional_param('pagesize',
get_user_preferences('quiz_grading_pagesize', self::DEFAULT_PAGE_SIZE),
PARAM_INT);
$page = optional_param('page', 0, PARAM_INT);
$order = optional_param('order',
get_user_preferences('quiz_grading_order', self::DEFAULT_ORDER),
PARAM_ALPHAEXT);
// Assemble the options required to reload this page.
$optparams = ['includeauto', 'page'];
foreach ($optparams as $param) {
if ($$param) {
$this->viewoptions[$param] = $$param;
}
}
if (!data_submitted() && !preg_match(self::REGEX_POSITIVE_INT, $pagesize)) {
// We only validate if the user accesses the page via a cleaned-up GET URL here.
throw new moodle_exception('invalidpagesize');
}
if ($pagesize != self::DEFAULT_PAGE_SIZE) {
$this->viewoptions['pagesize'] = $pagesize;
}
if ($order != self::DEFAULT_ORDER) {
$this->viewoptions['order'] = $order;
}
// Check permissions.
$this->context = context_module::instance($this->cm->id);
require_capability('mod/quiz:grade', $this->context);
$shownames = has_capability('quiz/grading:viewstudentnames', $this->context);
// Whether the current user can see custom user fields.
$showcustomfields = has_capability('quiz/grading:viewidnumber', $this->context);
$userfieldsapi = \core_user\fields::for_identity($this->context)->with_name();
$customfields = [];
foreach ($userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]) as $field) {
$customfields[] = $field;
}
// Validate order.
$orderoptions = array_merge(['random', 'date', 'studentfirstname', 'studentlastname'], $customfields);
if (!in_array($order, $orderoptions)) {
$order = self::DEFAULT_ORDER;
} else if (!$shownames && ($order == 'studentfirstname' || $order == 'studentlastname')) {
$order = self::DEFAULT_ORDER;
} else if (!$showcustomfields && in_array($order, $customfields)) {
$order = self::DEFAULT_ORDER;
}
if ($order == 'random') {
$page = 0;
}
// Get the list of questions in this quiz.
$this->questions = quiz_report_get_significant_questions($quiz);
if ($slot && !array_key_exists($slot, $this->questions)) {
throw new moodle_exception('unknownquestion', 'quiz_grading');
}
// Process any submitted data.
if ($data = data_submitted() && confirm_sesskey() && $this->validate_submitted_marks()) {
// Changes done to handle attempts being missed from grading due to redirecting to new page.
$attemptsgraded = $this->process_submitted_data();
$nextpagenumber = $page + 1;
// If attempts need grading and one or more have now been graded, then page number should remain the same.
if ($grade == 'needsgrading' && $attemptsgraded) {
$nextpagenumber = $page;
}
redirect($this->grade_question_url($slot, $questionid, $grade, $nextpagenumber));
}
// Get the group, and the list of significant users.
$this->currentgroup = $this->get_current_group($cm, $course, $this->context);
if ($this->currentgroup == self::NO_GROUPS_ALLOWED) {
$this->userssql = [];
} else {
$this->userssql = get_enrolled_sql($this->context,
['mod/quiz:reviewmyattempts', 'mod/quiz:attempt'], $this->currentgroup);
}
$hasquestions = quiz_has_questions($this->quiz->id);
if (!$hasquestions) {
$this->print_header_and_tabs($cm, $course, $quiz, 'grading');
echo $this->renderer->render_quiz_no_question_notification($quiz, $cm, $this->context);
return true;
}
if (!$slot) {
$this->display_index($includeauto);
return true;
}
// Display the grading UI for one question.
// Make sure there is something to do.
$counts = null;
$statecounts = $this->get_question_state_summary([$slot]);
foreach ($statecounts as $record) {
if ($record->questionid == $questionid) {
$counts = $record;
break;
}
}
// If not, redirect back to the list.
if (!$counts || $counts->$grade == 0) {
redirect($this->list_questions_url(), get_string('alldoneredirecting', 'quiz_grading'));
}
$this->display_grading_interface($slot, $questionid, $grade,
$pagesize, $page, $shownames, $showcustomfields, $order, $counts);
return true;
}
/**
* Get the JOIN conditions needed so we only show attempts by relevant users.
*
* @return qubaid_join
*/
protected function get_qubaids_condition() {
$where = "quiza.quiz = :mangrquizid AND
quiza.preview = 0 AND
quiza.state = :statefinished";
$params = ['mangrquizid' => $this->cm->instance, 'statefinished' => quiz_attempt::FINISHED];
$usersjoin = '';
$currentgroup = groups_get_activity_group($this->cm, true);
$enrolleduserscount = count_enrolled_users($this->context,
['mod/quiz:reviewmyattempts', 'mod/quiz:attempt'], $currentgroup);
if ($currentgroup) {
$userssql = get_enrolled_sql($this->context,
['mod/quiz:reviewmyattempts', 'mod/quiz:attempt'], $currentgroup);
if ($enrolleduserscount < 1) {
$where .= ' AND quiza.userid = 0';
} else {
$usersjoin = "JOIN ({$userssql[0]}) AS enr ON quiza.userid = enr.id";
$params += $userssql[1];
}
}
return new qubaid_join("{quiz_attempts} quiza $usersjoin ", 'quiza.uniqueid', $where, $params);
}
/**
* Load the quiz_attempts rows corresponding to a list of question_usage ids.
*
* @param int[] $qubaids the question_usage ids of the quiz_attempts to load.
* @return array quiz attempts, with added user name fields.
*/
protected function load_attempts_by_usage_ids($qubaids) {
global $DB;
list($asql, $params) = $DB->get_in_or_equal($qubaids);
$params[] = quiz_attempt::FINISHED;
$params[] = $this->quiz->id;
$fields = 'quiza.*, ';
$userfieldsapi = \core_user\fields::for_identity($this->context)->with_name();
$userfieldssql = $userfieldsapi->get_sql('u', false, '', 'userid', false);
$fields .= $userfieldssql->selects;
foreach ($userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]) as $userfield) {
$this->extrauserfields[] = s($userfield);
}
$params = array_merge($userfieldssql->params, $params);
$attemptsbyid = $DB->get_records_sql("
SELECT $fields
FROM {quiz_attempts} quiza
JOIN {user} u ON u.id = quiza.userid
{$userfieldssql->joins}
WHERE quiza.uniqueid $asql AND quiza.state = ? AND quiza.quiz = ?",
$params);
$attempts = [];
foreach ($attemptsbyid as $attempt) {
$attempts[$attempt->uniqueid] = $attempt;
}
return $attempts;
}
/**
* Get the URL of the front page of the report that lists all the questions.
*
* @return moodle_url the URL.
*/
protected function base_url() {
return new moodle_url('/mod/quiz/report.php',
['id' => $this->cm->id, 'mode' => 'grading']);
}
/**
* Get the URL of the front page of the report that lists all the questions.
*
* @param bool $includeauto if not given, use the current setting, otherwise,
* force a particular value of includeauto in the URL.
* @return moodle_url the URL.
*/
protected function list_questions_url($includeauto = null) {
$url = $this->base_url();
$url->params($this->viewoptions);
if (!is_null($includeauto)) {
$url->param('includeauto', $includeauto);
}
return $url;
}
/**
* Get the URL to grade a batch of question attempts.
*
* @param int $slot
* @param int $questionid
* @param string $grade
* @param int|bool $page = true, link to current page. false = omit page.
* number = link to specific page.
* @return moodle_url
*/
protected function grade_question_url($slot, $questionid, $grade, $page = true) {
$url = $this->base_url();
$url->params(['slot' => $slot, 'qid' => $questionid, 'grade' => $grade]);
$url->params($this->viewoptions);
if (!$page) {
$url->remove_params('page');
} else if (is_integer($page)) {
$url->param('page', $page);
}
return $url;
}
/**
* Renders the contents of one cell of the table on the index view.
*
* @param stdClass $counts counts of different types of attempt for this slot.
* @param string $type the type of count to format.
* @param string $string get_string identifier for the grading link text, if required.
* @param string $component get_string component identifier for the grading link text, if required.
* @return string HTML.
*/
protected function format_count_for_table($counts, $type, $string, $component) {
$result = $counts->$type;
if ($counts->$type > 0) {
$gradeurl = $this->grade_question_url($counts->slot, $counts->questionid, $type);
$result .= $this->renderer->render_grade_link($counts, $type, $string, $component, $gradeurl);
}
return $result;
}
/**
* Display the report front page which summarises the number of attempts to grade.
*
* @param bool $includeauto whether to show automatically-graded questions.
*/
protected function display_index($includeauto) {
global $PAGE, $OUTPUT;
$this->print_header_and_tabs($this->cm, $this->course, $this->quiz, 'grading');
if ($groupmode = groups_get_activity_groupmode($this->cm)) {
// Groups is being used.
groups_print_activity_menu($this->cm, $this->list_questions_url());
}
// Get the current group for the user looking at the report.
$currentgroup = $this->get_current_group($this->cm, $this->course, $this->context);
if ($currentgroup == self::NO_GROUPS_ALLOWED) {
echo $OUTPUT->notification(get_string('notingroup'));
return;
}
$statecounts = $this->get_question_state_summary(array_keys($this->questions));
if ($includeauto) {
$linktext = get_string('hideautomaticallygraded', 'quiz_grading');
} else {
$linktext = get_string('alsoshowautomaticallygraded', 'quiz_grading');
}
echo $this->renderer->render_display_index_heading($linktext, $this->list_questions_url(!$includeauto));
$data = [];
$header = [];
$header[] = get_string('qno', 'quiz_grading');
$header[] = get_string('qtypeveryshort', 'question');
$header[] = get_string('questionname', 'quiz_grading');
$header[] = get_string('tograde', 'quiz_grading');
$header[] = get_string('alreadygraded', 'quiz_grading');
if ($includeauto) {
$header[] = get_string('automaticallygraded', 'quiz_grading');
}
$header[] = get_string('total', 'quiz_grading');
foreach ($statecounts as $counts) {
if ($counts->all == 0) {
continue;
}
if (!$includeauto && $counts->needsgrading == 0 && $counts->manuallygraded == 0) {
continue;
}
$row = [];
$row[] = $this->questions[$counts->slot]->displaynumber;
$row[] = $PAGE->get_renderer('question', 'bank')->qtype_icon($this->questions[$counts->slot]->qtype);
$row[] = format_string($counts->name);
$row[] = $this->format_count_for_table($counts, 'needsgrading', 'gradeverb', 'moodle');
$row[] = $this->format_count_for_table($counts, 'manuallygraded', 'updategrade', 'quiz_grading');
if ($includeauto) {
$row[] = $this->format_count_for_table($counts, 'autograded', 'updategrade', 'quiz_grading');
}
$row[] = $this->format_count_for_table($counts, 'all', 'gradeall', 'quiz_grading');
$data[] = $row;
}
echo $this->renderer->render_questions_table($includeauto, $data, $header);
}
/**
* Display the UI for grading attempts at one question.
*
* @param int $slot identifies which question to grade.
* @param int $questionid identifies which question to grade.
* @param string $grade type of attempts to grade.
* @param int $pagesize number of questions to show per page.
* @param int $page current page number.
* @param bool $shownames whether student names should be shown.
* @param bool $showcustomfields whether custom field values should be shown.
* @param string $order preferred order of attempts.
* @param stdClass $counts object that stores the number of each type of attempt.
*/
protected function display_grading_interface($slot, $questionid, $grade,
$pagesize, $page, $shownames, $showcustomfields, $order, $counts) {
if ($pagesize * $page >= $counts->$grade) {
$page = 0;
}
// Prepare the options form.
$hidden = [
'id' => $this->cm->id,
'mode' => 'grading',
'slot' => $slot,
'qid' => $questionid,
'page' => $page,
];
if (array_key_exists('includeauto', $this->viewoptions)) {
$hidden['includeauto'] = $this->viewoptions['includeauto'];
}
$mform = new quiz_grading_settings_form($hidden, $counts, $shownames, $showcustomfields, $this->context);
// Tell the form the current settings.
$settings = new stdClass();
$settings->grade = $grade;
$settings->pagesize = $pagesize;
$settings->order = $order;
$mform->set_data($settings);
if ($mform->is_submitted()) {
if ($mform->is_validated()) {
// If the form was submitted and validated, save the user preferences, and
// redirect to a cleaned-up GET URL.
set_user_preference('quiz_grading_pagesize', $pagesize);
set_user_preference('quiz_grading_order', $order);
redirect($this->grade_question_url($slot, $questionid, $grade, $page));
} else {
// Set the pagesize back to the previous value, so the report page can continue the render
// and the form can show the validation.
$pagesize = get_user_preferences('quiz_grading_pagesize', self::DEFAULT_PAGE_SIZE);
}
}
list($qubaids, $count) = $this->get_usage_ids_where_question_in_state(
$grade, $slot, $questionid, $order, $page, $pagesize);
$attempts = $this->load_attempts_by_usage_ids($qubaids);
// Question info.
$questioninfo = new stdClass();
$questioninfo->number = $this->questions[$slot]->number;
$questioninfo->questionname = format_string($counts->name);
// Paging info.
$paginginfo = new stdClass();
$paginginfo->from = $page * $pagesize + 1;
$paginginfo->to = min(($page + 1) * $pagesize, $count);
$paginginfo->of = $count;
$qubaidlist = implode(',', $qubaids);
$this->print_header_and_tabs($this->cm, $this->course, $this->quiz, 'grading');
$gradequestioncontent = '';
foreach ($qubaids as $qubaid) {
$attempt = $attempts[$qubaid];
$quba = question_engine::load_questions_usage_by_activity($qubaid);
$displayoptions = quiz_get_review_options($this->quiz, $attempt, $this->context);
$displayoptions->generalfeedback = question_display_options::HIDDEN;
$displayoptions->history = question_display_options::HIDDEN;
$displayoptions->manualcomment = question_display_options::EDITABLE;
$gradequestioncontent .= $this->renderer->render_grade_question(
$quba,
$slot,
$displayoptions,
$this->questions[$slot]->number,
$this->get_question_heading($attempt, $shownames, $showcustomfields)
);
}
$pagingbar = new stdClass();
$pagingbar->count = $count;
$pagingbar->page = $page;
$pagingbar->pagesize = $pagesize;
$pagingbar->pagesize = $pagesize;
$pagingbar->order = $order;
$pagingbar->pagingurl = $this->grade_question_url($slot, $questionid, $grade, false);
$hiddeninputs = [
'qubaids' => $qubaidlist,
'slots' => $slot,
'sesskey' => sesskey()
];
echo $this->renderer->render_grading_interface(
$questioninfo,
$this->list_questions_url(),
$mform,
$paginginfo,
$pagingbar,
$this->grade_question_url($slot, $questionid, $grade, $page),
$hiddeninputs,
$gradequestioncontent
);
}
/**
* When saving a grading page, are all the submitted marks valid?
*
* @return bool true if all valid, else false.
*/
protected function validate_submitted_marks() {
$qubaids = optional_param('qubaids', null, PARAM_SEQUENCE);
if (!$qubaids) {
return false;
}
$qubaids = clean_param_array(explode(',', $qubaids), PARAM_INT);
$slots = optional_param('slots', '', PARAM_SEQUENCE);
if (!$slots) {
$slots = [];
} else {
$slots = explode(',', $slots);
}
foreach ($qubaids as $qubaid) {
foreach ($slots as $slot) {
if (!question_engine::is_manual_grade_in_range($qubaid, $slot)) {
return false;
}
}
}
return true;
}
/**
* Save all submitted marks to the database.
*
* @return bool returns true if some attempts or all are graded. False, if none of the attempts are graded.
*/
protected function process_submitted_data(): bool {
global $DB;
$qubaids = optional_param('qubaids', null, PARAM_SEQUENCE);
$assumedslotforevents = optional_param('slot', null, PARAM_INT);
if (!$qubaids) {
return false;
}
$qubaids = clean_param_array(explode(',', $qubaids), PARAM_INT);
$attempts = $this->load_attempts_by_usage_ids($qubaids);
$events = [];
$transaction = $DB->start_delegated_transaction();
$attemptsgraded = false;
foreach ($qubaids as $qubaid) {
$attempt = $attempts[$qubaid];
$attemptobj = new quiz_attempt($attempt, $this->quiz, $this->cm, $this->course);
// State of the attempt before grades are changed.
$attemptoldtstate = $attemptobj->get_question_state($assumedslotforevents);
$attemptobj->process_submitted_actions(time());
// Get attempt state after grades are changed.
$attemptnewtstate = $attemptobj->get_question_state($assumedslotforevents);
// Check if any attempts are graded.
if (!$attemptsgraded && $attemptoldtstate->is_graded() != $attemptnewtstate->is_graded()) {
$attemptsgraded = true;
}
// Add the event we will trigger later.
$params = [
'objectid' => $attemptobj->get_question_attempt($assumedslotforevents)->get_question_id(),
'courseid' => $attemptobj->get_courseid(),
'context' => context_module::instance($attemptobj->get_cmid()),
'other' => [
'quizid' => $attemptobj->get_quizid(),
'attemptid' => $attemptobj->get_attemptid(),
'slot' => $assumedslotforevents,
],
];
$events[] = \mod_quiz\event\question_manually_graded::create($params);
}
$transaction->allow_commit();
// Trigger events for all the questions we manually marked.
foreach ($events as $event) {
$event->trigger();
}
return $attemptsgraded;
}
/**
* Load information about the number of attempts at various questions in each
* summarystate.
*
* The results are returned as an two dimensional array $qubaid => $slot => $dataobject
*
* @param array $slots A list of slots for the questions you want to konw about.
* @return array The array keys are slot,qestionid. The values are objects with
* fields $slot, $questionid, $inprogress, $name, $needsgrading, $autograded,
* $manuallygraded and $all.
*/
protected function get_question_state_summary($slots) {
$dm = new question_engine_data_mapper();
return $dm->load_questions_usages_question_state_summary(
$this->get_qubaids_condition(), $slots);
}
/**
* Get a list of usage ids where the question with slot $slot, and optionally
* also with question id $questionid, is in summary state $summarystate. Also
* return the total count of such states.
*
* Only a subset of the ids can be returned by using $orderby, $limitfrom and
* $limitnum. A special value 'random' can be passed as $orderby, in which case
* $limitfrom is ignored.
*
* @param int $slot The slot for the questions you want to konw about.
* @param int $questionid (optional) Only return attempts that were of this specific question.
* @param string $summarystate 'all', 'needsgrading', 'autograded' or 'manuallygraded'.
* @param string $orderby 'random', 'date', 'student' or 'idnumber'.
* @param int $page implements paging of the results.
* Ignored if $orderby = random or $pagesize is null.
* @param int $pagesize implements paging of the results. null = all.
* @return array with two elements, an array of usage ids, and a count of the total number.
*/
protected function get_usage_ids_where_question_in_state($summarystate, $slot,
$questionid = null, $orderby = 'random', $page = 0, $pagesize = null) {
$dm = new question_engine_data_mapper();
$extraselect = '';
if ($pagesize && $orderby != 'random') {
$limitfrom = $page * $pagesize;
} else {
$limitfrom = 0;
}
$qubaids = $this->get_qubaids_condition();
$params = [];
$userfieldsapi = \core_user\fields::for_identity($this->context)->with_name();
$userfieldssql = $userfieldsapi->get_sql('u', true, '', 'userid', true);
$params = array_merge($params, $userfieldssql->params);
$customfields = [];
foreach ($userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]) as $field) {
$customfields[] = $field;
}
if ($orderby === 'date') {
list($statetest, $params) = $dm->in_summary_state_test(
'manuallygraded', false, 'mangrstate');
$extraselect = "(
SELECT MAX(sortqas.timecreated)
FROM {question_attempt_steps} sortqas
WHERE sortqas.questionattemptid = qa.id
AND sortqas.state $statetest
) as tcreated";
$orderby = "tcreated";
} else if ($orderby === 'studentfirstname' || $orderby === 'studentlastname' || in_array($orderby, $customfields)) {
$qubaids->from .= " JOIN {user} u ON quiza.userid = u.id {$userfieldssql->joins}";
// For name sorting, map orderby form value to
// actual column names; 'idnumber' maps naturally.
if ($orderby === "studentlastname") {
$orderby = "u.lastname, u.firstname";
} else if ($orderby === "studentfirstname") {
$orderby = "u.firstname, u.lastname";
} else if (in_array($orderby, $customfields)) { // Sort order by current custom user field.
$orderby = $userfieldssql->mappings[$orderby];
}
}
return $dm->load_questions_usages_where_question_in_state($qubaids, $summarystate,
$slot, $questionid, $orderby, $params, $limitfrom, $pagesize, $extraselect);
}
/**
* 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;
$this->renderer = $PAGE->get_renderer('quiz_grading');
parent::print_header_and_tabs($cm, $course, $quiz, $reportmode);
}
/**
* Get question heading.
*
* @param stdClass $attempt An instance of quiz_attempt.
* @param bool $shownames True to show the student first/lastnames.
* @param bool $showcustomfields Whether custom field values should be shown.
* @return string The string text for the question heading.
*/
protected function get_question_heading(stdClass $attempt, bool $shownames, bool $showcustomfields): string {
global $DB;
$a = new stdClass();
$a->attempt = $attempt->attempt;
$a->fullname = fullname($attempt);
$customfields = [];
foreach ($this->extrauserfields as $field) {
if (strval($attempt->{$field}) !== '') {
$customfields[] = s($attempt->{$field});
}
}
$a->customfields = implode(', ', $customfields);
if ($shownames && $showcustomfields) {
return get_string('gradingattemptwithcustomfields', 'quiz_grading', $a);
} else if ($shownames) {
return get_string('gradingattempt', 'quiz_grading', $a);
} else if ($showcustomfields) {
$a->fullname = $a->customfields;
return get_string('gradingattempt', 'quiz_grading', $a);
} else {
return '';
}
}
}
+15
View File
@@ -0,0 +1,15 @@
#page-mod-quiz-report #manualgradingform {
width: 100%;
}
#page-mod-quiz-report #manualgradingform.mform br {
clear: none;
}
#page-mod-quiz-report #manualgradingform.mform .clearfix:after {
clear: none;
}
#page-mod-quiz-report #manualgradingform .que {
margin-bottom: 0.7em;
}
@@ -0,0 +1,215 @@
@mod @mod_quiz @quiz @quiz_grading
Feature: Basic use of the Manual grading report
In order to easily find students attempts that need manual grading
As a teacher
I need to use the manual grading report
Background:
Given the following "custom profile fields" exist:
| datatype | shortname | name |
| text | username | Username |
| text | email | Email address |
| text | idnumber | ID number |
| text | frog | Favourite frog |
And the following config values are set as admin:
| showuseridentity | username,idnumber,email,profile_field_frog |
And the following "users" exist:
| username | firstname | lastname | email | idnumber | profile_field_frog |
| teacher1 | T1 | Teacher1 | teacher1@example.com | T1000 | |
| marker | M1 | Marker | marker@example.com | M1000 | |
| student1 | S1 | Student1 | student1@example.com | S1000 | little yellow frog |
| student2 | S2 | Student2 | student2@example.com | S2000 | |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| marker | C1 | teacher |
| student1 | C1 | student |
| student2 | C1 | student |
And the following "groupings" exist:
| name | course | idnumber |
| Tutor groups | C1 | tging |
And the following "groups" exist:
| name | course | idnumber |
| Tutor group | C1 | tg |
| Marker group | C1 | mg |
And the following "grouping groups" exist:
| grouping | group |
| tging | tg |
And the following "group members" exist:
| user | group |
| teacher1 | tg |
| student1 | tg |
| marker | mg |
| student2 | mg |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext | answer 1 | grade |
| Test questions | shortanswer | Short answer 001 | Where is the capital city of France? | Paris | 100% |
And the following "activities" exist:
| activity | name | course | idnumber | groupmode | grouping |
| quiz | Quiz 1 | C1 | quiz1 | 1 | tging |
| quiz | Quiz 2 | C1 | quiz2 | 1 | tging |
And quiz "Quiz 1" contains the following questions:
| question | page | displaynumber |
| Short answer 001 | 1 | 1a |
Scenario: Manual grading report without attempts
When I am on the "Quiz 1" "mod_quiz > Manual grading report" page logged in as "teacher1"
Then I should see "Separate groups (Tutor groups)"
And I should see "All participants"
And I should see "Quiz 1"
And I should see "Questions that need grading"
And I should see "Nothing to display"
And I follow "Also show questions that have been graded automatically"
And I should see "Nothing to display"
Scenario: Manual grading report with attempts
Given user "student1" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | Paris |
And I reload the page
When I am on the "Quiz 1" "mod_quiz > Manual grading report" page logged in as "teacher1"
Then I should see "Separate groups (Tutor groups)"
And I should see "All participants"
And I should see "Quiz 1"
And I follow "Also show questions that have been graded automatically"
And I should see "Short answer 001"
And "Short answer 001" row "To grade" column of "questionstograde" table should contain "0"
And "Short answer 001" row "Already graded" column of "questionstograde" table should contain "0"
And I should see "Short answer 001"
And "Short answer 001" row "To grade" column of "questionstograde" table should contain "0"
And "Short answer 001" row "Already graded" column of "questionstograde" table should contain "0"
# Go to the grading page.
And I click on "update grades" "link" in the "Short answer 001" "table_row"
And I should see "Grading attempts 1 to 1 of 1"
# Test the display options.
And I set the field "Order attempts by" to "ID number"
And I press "Change options"
# General feedback for Short answer 001 displays.
And I should see "That is a bad answer."
And I should see "The correct answer is: frog"
# Adjust the mark for Student1
And I set the field "Comment" to "I have adjusted your mark to 0.6"
And I set the field "Mark" to "0.6"
And I press "Save and show next"
And I should see "All selected attempts have been graded. Returning to the list of questions."
And "Short answer 001" row "Q #" column of "questionstograde" table should contain "1a"
And "Short answer 001" row "To grade" column of "questionstograde" table should contain "0"
And "Short answer 001" row "Already graded" column of "questionstograde" table should contain "1"
Scenario: Manual grading settings are remembered as user preferences
Given user "student1" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | Paris |
When I am on the "Quiz 1" "mod_quiz > Manual grading report" page logged in as "teacher1"
And I follow "Also show questions that have been graded automatically"
And I click on "update grades" "link" in the "Short answer 001" "table_row"
And I set the following fields to these values:
| Questions per page | 42 |
| Order attempts by | Date |
And I press "Change options"
And I log out
And I am on the "Quiz 1" "mod_quiz > Manual grading report" page logged in as "teacher1"
And I follow "Also show questions that have been graded automatically"
And I click on "update grades" "link" in the "Short answer 001" "table_row"
Then the following fields match these values:
| Questions per page | 42 |
| Order attempts by | Date |
@javascript
Scenario: Manual grading settings are validated
Given user "student1" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | Paris |
And I am on the "Quiz 1" "mod_quiz > Manual grading report" page logged in as "teacher1"
And I follow "Also show questions that have been graded automatically"
And I click on "update grades" "link" in the "Short answer 001" "table_row"
When I set the following fields to these values:
| Questions per page | 0 |
Then I should see "You must enter a whole number that is greater than 0."
And I set the following fields to these values:
| Questions per page | -1 |
And I press "Change options"
And I should see "You must enter a whole number that is greater than 0."
And I set the following fields to these values:
| Questions per page | abc |
And I press "Change options"
And I should see "You must enter a whole number that is greater than 0."
And I set the following fields to these values:
| Questions per page | 1 |
And I press "Change options"
@javascript
Scenario: Teacher can see user custom filed columns as additional user identity
Given user "student1" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | Paris |
When I am on the "Quiz 1" "mod_quiz > Manual grading report" page logged in as "teacher1"
And I follow "Also show questions that have been graded automatically"
And I click on "update grades" "link" in the "Short answer 001" "table_row"
Then I should see "Attempt number 1 for S1 Student1 (student1, S1000, student1@example.com, little yellow frog)"
And I should not see "You must enter a whole number that is greater than 0."
Scenario: A marker cannot access the report in separate group
Given user "student1" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | frog |
And user "student2" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | Duck |
When I am on the "Quiz 1" "mod_quiz > Manual grading report" page logged in as "marker"
Then I should see "Quiz 1"
And I should see "Separate groups: All participants"
Then I should see "Sorry, but you need to be part of a group to see this page."
@javascript
Scenario: Manual grading report with attempts to be graded
Given the following "questions" exist:
| questioncategory | qtype | name | user | questiontext |
| Test questions | essay | Essay Q1 | admin | Question 1 text |
And quiz "Quiz 2" contains the following questions:
| question | page |
| Essay Q1 | 1 |
When I am on the "Quiz 2" "mod_quiz > View" page logged in as "student1"
And I press "Attempt quiz"
And I set the field "Answer text Question 1" to "This is my attempt 1"
And I follow "Finish attempt ..."
And I press "Submit all and finish"
And I click on "Submit all and finish" "button" in the "Submit all your answers and finish?" "dialogue"
And I click on "Finish review" "link"
And I press "Re-attempt quiz"
And I set the field "Answer text Question 1" to "This is my attempt 2"
And I follow "Finish attempt ..."
And I press "Submit all and finish"
And I click on "Submit all and finish" "button" in the "Submit all your answers and finish?" "dialogue"
And I click on "Finish review" "link"
And I press "Re-attempt quiz"
And I set the field "Answer text Question 1" to "This is my attempt 3"
And I follow "Finish attempt ..."
And I press "Submit all and finish"
And I click on "Submit all and finish" "button" in the "Submit all your answers and finish?" "dialogue"
And I am on the "Quiz 2" "mod_quiz > Manual grading report" page logged in as "teacher1"
And I follow "Also show questions that have been graded automatically"
And I should see "Essay Q1"
And "Essay Q1" row "To grade" column of "questionstograde" table should contain "3"
And "Essay Q1" row "Already graded" column of "questionstograde" table should contain "0"
# Go to the grading page.
And I click on "Grade" "link" in the "Essay Q1" "table_row"
And I should see "Grading attempts 1 to 3 of 3"
And I set the following fields to these values:
| Questions per page | 1 |
| Order attempts by | ID number |
And I press "Change options"
And I should see "Grading attempts 1 to 1 of 3"
# Adjust the mark for Student1
And I set the field "Comment" to "I have adjusted your mark to 0.6"
And I set the field "Mark" to "0.6"
And I press "Save and show next"
Then I should see "Grading attempts 1 to 1 of 2"
And I press "Save and show next"
And I should see "Grading attempts 2 to 2 of 2"
@@ -0,0 +1,104 @@
<?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/>.
/**
* Privacy provider tests.
*
* @package quiz_grading
* @copyright 2020 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace quiz_grading\privacy;
use core_privacy\local\metadata\collection;
use quiz_grading\privacy\provider;
use core_privacy\local\request\writer;
use core_privacy\local\request\transform;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/question/engine/questionattempt.php');
/**
* Privacy provider tests class.
*/
class provider_test extends \core_privacy\tests\provider_testcase {
/**
* When no preference exists, there should be no export.
*/
public function test_preference_unset(): void {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
provider::export_user_preferences($USER->id);
$this->assertFalse(writer::with_context(\context_system::instance())->has_any_data());
}
/**
* Preference does exist.
*/
public function test_preference_bool_true(): void {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
set_user_preference('quiz_grading_pagesize', 42);
set_user_preference('quiz_grading_order', 'random');
provider::export_user_preferences($USER->id);
$writer = writer::with_context(\context_system::instance());
$this->assertTrue($writer->has_any_data());
$preferences = $writer->get_user_preferences('quiz_grading');
$this->assertNotEmpty($preferences->pagesize);
$this->assertEquals(42, $preferences->pagesize->value);
$this->assertNotEmpty($preferences->order);
$this->assertEquals('Random', $preferences->order->value);
}
/**
* Preference does exist using user custom fields.
*/
public function test_preference_bool_true_for_user_customfields(): void {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
$customfields = ['username', 'idnumber', 'email', 'profile_field_frog'];
foreach ($customfields as $customfield) {
set_user_preference('quiz_grading_order', $customfield);
provider::export_user_preferences($USER->id);
$writer = writer::with_context(\context_system::instance());
$this->assertTrue($writer->has_any_data());
$preferences = $writer->get_user_preferences('quiz_grading');
$this->assertNotEmpty($preferences->order);
$this->assertEquals(\core_user\fields::get_display_name($customfield), $preferences->order->value);
}
}
}
+29
View File
@@ -0,0 +1,29 @@
<?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/>.
/**
* Quiz grading report version information.
*
* @package quiz_grading
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->component = 'quiz_grading';
@@ -0,0 +1,78 @@
<?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/>.
/**
* Privacy Subsystem implementation for quiz_overview..
*
* @package quiz_overview
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace quiz_overview\privacy;
use \core_privacy\local\request\writer;
use \core_privacy\local\request\transform;
use \core_privacy\local\metadata\collection;
use \core_privacy\manager;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem implementation for quiz_overview..
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\user_preference_provider {
/**
* Returns meta data about this system.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
$collection->add_user_preference('quiz_overview_slotmarks', 'privacy:metadata:preference:quiz_overview_slotmarks');
return $collection;
}
/**
* Export all user preferences for the plugin.
*
* @param int $userid The userid of the user whose data is to be exported.
*/
public static function export_user_preferences(int $userid) {
$preference = get_user_preferences('quiz_overview_slotmarks', null, $userid);
if (null !== $preference) {
if (empty($preference)) {
$description = get_string('privacy:preference:slotmarks:no', 'quiz_overview');
} else {
$description = get_string('privacy:preference:slotmarks:yes', 'quiz_overview');
}
writer::export_user_preference(
'quiz_overview',
'slotmarks',
transform::yesno($preference),
$description
);
}
}
}
+39
View File
@@ -0,0 +1,39 @@
<?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/>.
/**
* Post-install script for the quiz grades report.
* @package quiz_overview
* @copyright 2013 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Post-install script
*/
function xmldb_quiz_overview_install() {
global $DB;
$record = new stdClass();
$record->name = 'overview';
$record->displayorder = '10000';
$DB->insert_record('quiz_reports', $record);
}
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="mod/quiz/report/overview/db" VERSION="20180319" COMMENT="XMLDB file for Moodle mod/quiz/report/overview"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<TABLE NAME="quiz_overview_regrades" COMMENT="This table records which question attempts need regrading and the grade they will be regraded to.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="questionusageid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Foreign key references question_usages.id, or equivalently quiz_attempt.uniqueid."/>
<FIELD NAME="slot" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Foreign key, references question_attempts.slot"/>
<FIELD NAME="newfraction" TYPE="number" LENGTH="12" NOTNULL="false" SEQUENCE="false" DECIMALS="7" COMMENT="The new fraction for this question_attempt after regrading."/>
<FIELD NAME="oldfraction" TYPE="number" LENGTH="12" NOTNULL="false" SEQUENCE="false" DECIMALS="7" COMMENT="The previous fraction for this question_attempt."/>
<FIELD NAME="regraded" TYPE="int" LENGTH="4" NOTNULL="true" SEQUENCE="false" COMMENT="set to 0 if element has just been regraded. Set to 1 if element has been marked as needing regrading."/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Timestamp of when this row was last modified."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="questionusageid-slot" TYPE="foreign" FIELDS="questionusageid, slot" REFTABLE="question_attempts" REFFIELDS="questionusageid, slot"/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>
+43
View File
@@ -0,0 +1,43 @@
<?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/>.
/**
* Quiz overview report upgrade script.
*
* @package quiz_overview
* @copyright 2008 Jamie Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Quiz overview report upgrade function.
* @param number $oldversion
*/
function xmldb_quiz_overview_upgrade($oldversion) {
// Automatically generated Moodle v4.1.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.2.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.4.0 release upgrade line.
// Put any upgrade step following this.
return true;
}
@@ -0,0 +1,80 @@
<?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/>.
/**
* Strings for component 'quiz_overview', language 'en', branch 'MOODLE_20_STABLE'
*
* @package quiz_overview
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['allattempts'] = 'Show all attempts';
$string['allattemptscontributetograde'] = 'All attempts contribute to final grade for user.';
$string['allstudents'] = 'Show all {$a}';
$string['attemptsonly'] = 'Show {$a} with attempts only';
$string['attemptsprepage'] = 'Attempts shown per page';
$string['deleteselected'] = 'Delete selected attempts';
$string['done'] = 'Done';
$string['err_failedtodeleteregrades'] = 'Failed to delete calculated attempt grades';
$string['err_failedtorecalculateattemptgrades'] = 'Failed to recalculate attempt grades';
$string['highlightinggraded'] = 'The user attempt that contributes to final grade is highlighted.';
$string['needed'] = 'Needed';
$string['noattemptsonly'] = 'Show / download {$a} with no attempts only';
$string['noattemptstoregrade'] = 'No attempts need regrading';
$string['nogradepermission'] = 'You don\'t have permission to grade this quiz.';
$string['onlyoneattemptallowed'] = 'Only one attempt per user allowed on this quiz.';
$string['optallattempts'] = 'all attempts';
$string['optallstudents'] = 'all {$a} who have or have not attempted the quiz';
$string['optattemptsonly'] = '{$a} who have attempted the quiz';
$string['optnoattemptsonly'] = '{$a} who have not attempted the quiz';
$string['optonlyregradedattempts'] = 'that have been regraded / are marked as needing regrading';
$string['overview'] = 'Grades';
$string['overviewdownload'] = 'Overview download';
$string['overviewfilename'] = 'grades';
$string['overviewreport'] = 'Grades report';
$string['overviewreportgraph'] = 'Overall number of students achieving grade ranges';
$string['overviewreportgraphgroup'] = 'Number of students in group \'{$a}\' achieving grade ranges';
$string['pagesize'] = 'Page size';
$string['pluginname'] = 'Grades';
$string['preferencespage'] = 'Preferences just for this page';
$string['preferencessave'] = 'Show report';
$string['preferencesuser'] = 'Your preferences for this report';
$string['privacy:metadata:preference:quiz_overview_slotmarks'] = 'Whether to show marks for each question slot.';
$string['privacy:preference:slotmarks:yes'] = 'Marks are shown alongside the question slot.';
$string['privacy:preference:slotmarks:no'] = 'Marks are not shown alongside the question slot.';
$string['regrade'] = 'Regrade';
$string['regradeall'] = 'Regrade all';
$string['regradealldry'] = 'Dry run a full regrade';
$string['regradealldrydo'] = 'Regrade attempts marked as needing regrading ({$a})';
$string['regradealldrydogroup'] = 'Regrade attempts ({$a->countregradeneeded}) marked as needing regrading in group \'{$a->groupname}\'';
$string['regradealldrygroup'] = 'Dry run a full regrade for group \'{$a->groupname}\'';
$string['regradeallgroup'] = 'Full regrade for group \'{$a->groupname}\'';
$string['regradecomplete'] = 'Regrade completed';
$string['regradedsuccessfullyxofy'] = 'Finished regrading ({$a->done}/{$a->count})';
$string['regradeheader'] = 'Regrading';
$string['regradeselected'] = 'Regrade selected attempts';
$string['regradingattemptissue'] = 'Slot {$a->slot}: {$a->reason}';
$string['regradingattemptxofy'] = 'Regrading attempt ({$a->done}/{$a->count})';
$string['regradingattemptxofyproblem'] = 'The following questions could not be regraded in attempt {$a->attemptnum} by {$a->name} (id {$a->attemptid})';
$string['regradingattemptxofywithdetails'] = 'Regrading attempt ({$a->done}/{$a->count}) - Attempt {$a->attemptnum} by {$a->name} (id {$a->attemptid})';
$string['show'] = 'Show / download';
$string['showattempts'] = 'Only show / download attempts';
$string['showdetailedmarks'] = 'Marks for each question';
$string['showinggraded'] = 'Showing only the attempt graded for each user.';
$string['showinggradedandungraded'] = 'Showing graded and ungraded attempts for each user. The one attempt for each user that is graded is highlighted. The grading method for this quiz is {$a}.';
$string['studentingroup'] = '\'{$a->coursestudent}\' in group \'{$a->groupname}\'';
$string['studentingrouplong'] = '\'{$a->coursestudent}\' in this group';
@@ -0,0 +1,45 @@
<?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/>.
use mod_quiz\local\reports\attempts_report;
use mod_quiz\local\reports\attempts_report_options_form;
/**
* Quiz overview report settings form.
*
* @copyright 2008 Jamie Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
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', attempts_report::ENROLLED_WITHOUT);
}
}
protected function other_preference_fields(MoodleQuickForm $mform) {
if (quiz_has_grades($this->_customdata['quiz'])) {
$mform->addElement('selectyesno', 'slotmarks',
get_string('showdetailedmarks', 'quiz_overview'));
} else {
$mform->addElement('hidden', 'slotmarks', 0);
$mform->setType('slotmarks', PARAM_INT);
}
}
}
@@ -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/>.
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}.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_overview_options extends attempts_report_options {
/** @var bool whether to show only attempt that need regrading. */
public $onlyregraded = false;
/** @var bool whether to show marks for each question (slot). */
public $slotmarks = true;
protected function get_url_params() {
$params = parent::get_url_params();
$params['onlyregraded'] = $this->onlyregraded;
$params['slotmarks'] = $this->slotmarks;
return $params;
}
public function get_initial_form_data() {
$toform = parent::get_initial_form_data();
$toform->onlyregraded = $this->onlyregraded;
$toform->slotmarks = $this->slotmarks;
return $toform;
}
public function setup_from_form_data($fromform) {
parent::setup_from_form_data($fromform);
$this->onlyregraded = !empty($fromform->onlyregraded);
$this->slotmarks = $fromform->slotmarks;
}
public function setup_from_params() {
parent::setup_from_params();
$this->onlyregraded = optional_param('onlyregraded', $this->onlyregraded, PARAM_BOOL);
$this->slotmarks = optional_param('slotmarks', $this->slotmarks, PARAM_BOOL);
}
public function setup_from_user_preferences() {
parent::setup_from_user_preferences();
$this->slotmarks = get_user_preferences('quiz_overview_slotmarks', $this->slotmarks);
}
public function update_user_preferences() {
parent::update_user_preferences();
if (quiz_has_grades($this->quiz)) {
set_user_preference('quiz_overview_slotmarks', $this->slotmarks);
}
}
public function resolve_dependencies() {
parent::resolve_dependencies();
if ($this->attempts == attempts_report::ENROLLED_WITHOUT) {
$this->onlyregraded = false;
}
if (!$this->usercanseegrades) {
$this->slotmarks = false;
}
// 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_any_capability(
['mod/quiz:regrade', 'mod/quiz:deleteattempts'], context_module::instance($this->cm->id))
&& ($this->attempts != attempts_report::ENROLLED_WITHOUT);
}
}
+374
View File
@@ -0,0 +1,374 @@
<?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/>.
use mod_quiz\local\reports\attempts_report_table;
use mod_quiz\quiz_attempt;
/**
* This is a table subclass for displaying the quiz grades report.
*
* @copyright 2008 Jamie Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_overview_table extends attempts_report_table {
/** @var array used to store information about which questoins have been regraded. */
protected $regradedqs = [];
/**
* Constructor
* @param stdClass $quiz
* @param context $context
* @param string $qmsubselect
* @param quiz_overview_options $options
* @param \core\dml\sql_join $groupstudentsjoins
* @param \core\dml\sql_join $studentsjoins
* @param array $questions
* @param moodle_url $reporturl
*/
public function __construct($quiz, $context, $qmsubselect,
quiz_overview_options $options, \core\dml\sql_join $groupstudentsjoins,
\core\dml\sql_join $studentsjoins, $questions, $reporturl) {
parent::__construct('mod-quiz-report-overview-report', $quiz , $context,
$qmsubselect, $options, $groupstudentsjoins, $studentsjoins, $questions, $reporturl);
}
public function build_table() {
global $DB;
if (!$this->rawdata) {
return;
}
$this->strtimeformat = str_replace(',', ' ', get_string('strftimedatetime'));
parent::build_table();
// End of adding the data from attempts. Now add averages at bottom.
$this->add_separator();
if (!empty($this->groupstudentsjoins->joins)) {
$hasgroupstudents = $DB->record_exists_sql("
SELECT 1
FROM {user} u
{$this->groupstudentsjoins->joins}
WHERE {$this->groupstudentsjoins->wheres}
", $this->groupstudentsjoins->params);
if ($hasgroupstudents) {
$this->add_average_row(get_string('groupavg', 'grades'), $this->groupstudentsjoins);
}
}
if (!empty($this->studentsjoins->joins)) {
$hasstudents = $DB->record_exists_sql("
SELECT 1
FROM {user} u
{$this->studentsjoins->joins}
WHERE {$this->studentsjoins->wheres}
" , $this->studentsjoins->params);
if ($hasstudents) {
$this->add_average_row(get_string('overallaverage', 'grades'), $this->studentsjoins);
}
}
}
/**
* Calculate the average overall and question scores for a set of attempts at the quiz.
*
* @param string $label the title ot use for this row.
* @param \core\dml\sql_join $usersjoins to indicate a set of users.
* @return array of table cells that make up the average row.
*/
public function compute_average_row($label, \core\dml\sql_join $usersjoins) {
global $DB;
list($fields, $from, $where, $params) = $this->base_sql($usersjoins);
$record = $DB->get_record_sql("
SELECT AVG(quizaouter.sumgrades) AS grade, COUNT(quizaouter.sumgrades) AS numaveraged
FROM {quiz_attempts} quizaouter
JOIN (
SELECT DISTINCT quiza.id
FROM $from
WHERE $where
) relevant_attempt_ids ON quizaouter.id = relevant_attempt_ids.id
", $params);
$record->grade = quiz_rescale_grade($record->grade, $this->quiz, false);
if ($this->is_downloading()) {
$namekey = 'lastname';
} else {
$namekey = 'fullname';
}
$averagerow = [
$namekey => $label,
'sumgrades' => $this->format_average($record),
'feedbacktext' => strip_tags(quiz_report_feedback_for_grade(
$record->grade, $this->quiz->id, $this->context))
];
if ($this->options->slotmarks) {
$dm = new question_engine_data_mapper();
$qubaids = new qubaid_join("{quiz_attempts} quizaouter
JOIN (
SELECT DISTINCT quiza.id
FROM $from
WHERE $where
) relevant_attempt_ids ON quizaouter.id = relevant_attempt_ids.id",
'quizaouter.uniqueid', '1 = 1', $params);
$avggradebyq = $dm->load_average_marks($qubaids, array_keys($this->questions));
$averagerow += $this->format_average_grade_for_questions($avggradebyq);
}
return $averagerow;
}
/**
* Add an average grade row for a set of users.
*
* @param string $label the title ot use for this row.
* @param \core\dml\sql_join $usersjoins (joins, wheres, params) for the users to average over.
*/
protected function add_average_row($label, \core\dml\sql_join $usersjoins) {
$averagerow = $this->compute_average_row($label, $usersjoins);
$this->add_data_keyed($averagerow);
}
/**
* Helper userd by {@link add_average_row()}.
* @param array $gradeaverages the raw grades.
* @return array the (partial) row of data.
*/
protected function format_average_grade_for_questions($gradeaverages) {
$row = [];
if (!$gradeaverages) {
$gradeaverages = [];
}
foreach ($this->questions as $question) {
if (isset($gradeaverages[$question->slot]) && $question->maxmark > 0) {
$record = $gradeaverages[$question->slot];
$record->grade = quiz_rescale_grade(
$record->averagefraction * $question->maxmark, $this->quiz, false);
} else {
$record = new stdClass();
$record->grade = null;
$record->numaveraged = 0;
}
$row['qsgrade' . $question->slot] = $this->format_average($record, true);
}
return $row;
}
/**
* Format an entry in an average row.
* @param stdClass $record with fields grade and numaveraged.
* @param bool $question true if this is a question score, false if it is an overall score.
* @return string HTML fragment for an average score (with number of things included in the average).
*/
protected function format_average($record, $question = false) {
if (is_null($record->grade)) {
$average = '-';
} else if ($question) {
$average = quiz_format_question_grade($this->quiz, $record->grade);
} else {
$average = quiz_format_grade($this->quiz, $record->grade);
}
if ($this->download) {
return $average;
} else if (is_null($record->numaveraged) || $record->numaveraged == 0) {
return html_writer::tag('span', html_writer::tag('span',
$average, ['class' => 'average']), ['class' => 'avgcell']);
} else {
return html_writer::tag('span', html_writer::tag('span',
$average, ['class' => 'average']) . ' ' . html_writer::tag('span',
'(' . $record->numaveraged . ')', ['class' => 'count']),
['class' => 'avgcell']);
}
}
protected function submit_buttons() {
if (has_capability('mod/quiz:regrade', $this->context)) {
$regradebuttonparams = [
'type' => 'submit',
'class' => 'btn btn-secondary mr-1',
'name' => 'regrade',
'value' => get_string('regradeselected', 'quiz_overview'),
'data-action' => 'toggle',
'data-togglegroup' => $this->togglegroup,
'data-toggle' => 'action',
'disabled' => true
];
echo html_writer::empty_tag('input', $regradebuttonparams);
}
parent::submit_buttons();
}
public function col_sumgrades($attempt) {
if ($attempt->state != quiz_attempt::FINISHED) {
return '-';
}
$grade = quiz_rescale_grade($attempt->sumgrades, $this->quiz);
if ($this->is_downloading()) {
return $grade;
}
if (isset($this->regradedqs[$attempt->usageid])) {
$newsumgrade = 0;
$oldsumgrade = 0;
foreach ($this->questions as $question) {
if (isset($this->regradedqs[$attempt->usageid][$question->slot])) {
$newsumgrade += $this->regradedqs[$attempt->usageid]
[$question->slot]->newfraction * $question->maxmark;
$oldsumgrade += $this->regradedqs[$attempt->usageid]
[$question->slot]->oldfraction * $question->maxmark;
} else {
$newsumgrade += $this->lateststeps[$attempt->usageid]
[$question->slot]->fraction * $question->maxmark;
$oldsumgrade += $this->lateststeps[$attempt->usageid]
[$question->slot]->fraction * $question->maxmark;
}
}
$newsumgrade = quiz_rescale_grade($newsumgrade, $this->quiz);
$oldsumgrade = quiz_rescale_grade($oldsumgrade, $this->quiz);
$grade = html_writer::tag('del', $oldsumgrade) . '/' .
html_writer::empty_tag('br') . $newsumgrade;
}
return html_writer::link(new moodle_url('/mod/quiz/review.php',
['attempt' => $attempt->attempt]), $grade,
['title' => get_string('reviewattempt', 'quiz')]);
}
/**
* @param string $colname the name of the column.
* @param stdClass $attempt the row of data - see the SQL in display() in
* mod/quiz/report/overview/report.php to see what fields are present,
* and what they are called.
* @return string the contents of the cell.
*/
public function other_cols($colname, $attempt) {
if (!preg_match('/^qsgrade(\d+)$/', $colname, $matches)) {
return parent::other_cols($colname, $attempt);
}
$slot = $matches[1];
$question = $this->questions[$slot];
if (!isset($this->lateststeps[$attempt->usageid][$slot])) {
return '-';
}
$stepdata = $this->lateststeps[$attempt->usageid][$slot];
$state = question_state::get($stepdata->state);
if ($question->maxmark == 0) {
$grade = '-';
} else if (is_null($stepdata->fraction)) {
if ($state == question_state::$needsgrading) {
$grade = get_string('requiresgrading', 'question');
} else {
$grade = '-';
}
} else {
$grade = quiz_rescale_grade(
$stepdata->fraction * $question->maxmark, $this->quiz, 'question');
}
if ($this->is_downloading()) {
return $grade;
}
if (isset($this->regradedqs[$attempt->usageid][$slot])) {
$gradefromdb = $grade;
$newgrade = quiz_rescale_grade(
$this->regradedqs[$attempt->usageid][$slot]->newfraction * $question->maxmark,
$this->quiz, 'question');
$oldgrade = quiz_rescale_grade(
$this->regradedqs[$attempt->usageid][$slot]->oldfraction * $question->maxmark,
$this->quiz, 'question');
$grade = html_writer::tag('del', $oldgrade) . '/' .
html_writer::empty_tag('br') . $newgrade;
}
return $this->make_review_link($grade, $attempt, $slot);
}
public function col_regraded($attempt) {
if ($attempt->regraded == '') {
return '';
} else if ($attempt->regraded == 0) {
return get_string('needed', 'quiz_overview');
} else if ($attempt->regraded == 1) {
return get_string('done', 'quiz_overview');
}
}
protected function update_sql_after_count($fields, $from, $where, $params) {
$fields .= ", COALESCE((
SELECT MAX(qqr.regraded)
FROM {quiz_overview_regrades} qqr
WHERE qqr.questionusageid = quiza.uniqueid
), -1) AS regraded";
if ($this->options->onlyregraded) {
$where .= " AND COALESCE((
SELECT MAX(qqr.regraded)
FROM {quiz_overview_regrades} qqr
WHERE qqr.questionusageid = quiza.uniqueid
), -1) <> -1";
}
return [$fields, $from, $where, $params];
}
protected function requires_latest_steps_loaded() {
return $this->options->slotmarks;
}
protected function is_latest_step_column($column) {
if (preg_match('/^qsgrade([0-9]+)/', $column, $matches)) {
return $matches[1];
}
return false;
}
protected function get_required_latest_state_fields($slot, $alias) {
return "$alias.fraction * $alias.maxmark AS qsgrade$slot";
}
public function query_db($pagesize, $useinitialsbar = true) {
parent::query_db($pagesize, $useinitialsbar);
if ($this->options->slotmarks && has_capability('mod/quiz:regrade', $this->context)) {
$this->regradedqs = $this->get_regraded_questions();
}
}
/**
* Get all the questions in all the attempts being displayed that need regrading.
* @return array A two dimensional array $questionusageid => $slot => $regradeinfo.
*/
protected function get_regraded_questions() {
global $DB;
$qubaids = $this->get_qubaids_condition();
$regradedqs = $DB->get_records_select('quiz_overview_regrades',
'questionusageid ' . $qubaids->usage_id_in(), $qubaids->usage_id_in_params());
return quiz_report_index_by_keys($regradedqs, ['questionusageid', 'slot']);
}
}
+708
View File
@@ -0,0 +1,708 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file defines the quiz overview report class.
*
* @package quiz_overview
* @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
* @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;
use mod_quiz\quiz_attempt;
use mod_quiz\quiz_settings;
defined('MOODLE_INTERNAL') || die();
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');
/**
* Quiz report subclass for the overview (grades) report.
*
* @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 attempts_report {
public function display($quiz, $cm, $course) {
global $DB, $PAGE;
list($currentgroup, $studentsjoins, $groupstudentsjoins, $allowedjoins) = $this->init(
'overview', 'quiz_overview_settings_form', $quiz, $cm, $course);
$options = new quiz_overview_options('overview', $quiz, $cm, $course);
if ($fromform = $this->form->get_data()) {
$options->process_settings_from_form($fromform);
} else {
$options->process_settings_from_params();
}
$this->form->set_data($options->get_initial_form_data());
// Load the required questions.
$questions = quiz_report_get_significant_questions($quiz);
// Prepare for downloading, if applicable.
$courseshortname = format_string($course->shortname, true,
['context' => context_course::instance($course->id)]);
$table = new quiz_overview_table($quiz, $this->context, $this->qmsubselect,
$options, $groupstudentsjoins, $studentsjoins, $questions, $options->get_url());
$filename = quiz_report_download_filename(get_string('overviewfilename', 'quiz_overview'),
$courseshortname, $quiz->name);
$table->is_downloading($options->download, $filename,
$courseshortname . ' ' . format_string($quiz->name, true));
if ($table->is_downloading()) {
raise_memory_limit(MEMORY_EXTRA);
}
$this->hasgroupstudents = false;
if (!empty($groupstudentsjoins->joins)) {
$sql = "SELECT DISTINCT u.id
FROM {user} u
$groupstudentsjoins->joins
WHERE $groupstudentsjoins->wheres";
$this->hasgroupstudents = $DB->record_exists_sql($sql, $groupstudentsjoins->params);
}
$hasstudents = false;
if (!empty($studentsjoins->joins)) {
$sql = "SELECT DISTINCT u.id
FROM {user} u
$studentsjoins->joins
WHERE $studentsjoins->wheres";
$hasstudents = $DB->record_exists_sql($sql, $studentsjoins->params);
}
if ($options->attempts == self::ALL_WITH) {
// This option is only available to users who can access all groups in
// groups mode, so setting allowed to empty (which means all quiz attempts
// are accessible, is not a security porblem.
$allowedjoins = new \core\dml\sql_join();
}
$this->process_actions($quiz, $cm, $currentgroup, $groupstudentsjoins, $allowedjoins, $options->get_url());
$hasquestions = quiz_has_questions($quiz->id);
// Start output.
if (!$table->is_downloading()) {
// Only print headers if not asked to download data.
$this->print_standard_header_and_messages($cm, $course, $quiz,
$options, $currentgroup, $hasquestions, $hasstudents);
// Print the display options.
$this->form->display();
}
$hasstudents = $hasstudents && (!$currentgroup || $this->hasgroupstudents);
if ($hasquestions && ($hasstudents || $options->attempts == self::ALL_WITH)) {
// Construct the SQL.
$table->setup_sql_queries($allowedjoins);
if (!$table->is_downloading()) {
// Output the regrade buttons.
if (has_capability('mod/quiz:regrade', $this->context)) {
$regradesneeded = $this->count_question_attempts_needing_regrade(
$quiz, $groupstudentsjoins);
if ($currentgroup) {
$a= new stdClass();
$a->groupname = format_string(groups_get_group_name($currentgroup), true, [
'context' => $this->context,
]);
$a->coursestudents = get_string('participants');
$a->countregradeneeded = $regradesneeded;
$regradealldrydolabel =
get_string('regradealldrydogroup', 'quiz_overview', $a);
$regradealldrylabel =
get_string('regradealldrygroup', 'quiz_overview', $a);
$regradealllabel =
get_string('regradeallgroup', 'quiz_overview', $a);
} else {
$regradealldrydolabel =
get_string('regradealldrydo', 'quiz_overview', $regradesneeded);
$regradealldrylabel =
get_string('regradealldry', 'quiz_overview');
$regradealllabel =
get_string('regradeall', 'quiz_overview');
}
$displayurl = new moodle_url($options->get_url(), ['sesskey' => sesskey()]);
echo '<div class="regradebuttons">';
echo '<form action="'.$displayurl->out_omit_querystring().'">';
echo '<div>';
echo html_writer::input_hidden_params($displayurl);
echo '<input type="submit" class="btn btn-secondary" name="regradeall" value="'.$regradealllabel.'"/>';
echo '<input type="submit" class="btn btn-secondary ml-1" name="regradealldry" value="' .
$regradealldrylabel . '"/>';
if ($regradesneeded) {
echo '<input type="submit" class="btn btn-secondary ml-1" name="regradealldrydo" value="' .
$regradealldrydolabel . '"/>';
}
echo '</div>';
echo '</form>';
echo '</div>';
}
// Print information on the grading method.
if ($strattempthighlight = quiz_report_highlighting_grading_method(
$quiz, $this->qmsubselect, $options->onlygraded)) {
echo '<div class="quizattemptcounts mt-3">' . $strattempthighlight . '</div>';
}
}
// Define table columns.
$columns = [];
$headers = [];
if (!$table->is_downloading() && $options->checkboxcolumn) {
$columnname = 'checkbox';
$columns[] = $columnname;
$headers[] = $table->checkbox_col_header($columnname);
}
$this->add_user_columns($table, $columns, $headers);
$this->add_state_column($columns, $headers);
$this->add_time_columns($columns, $headers);
$this->add_grade_columns($quiz, $options->usercanseegrades, $columns, $headers, false);
$this->add_grade_item_columns($options->usercanseegrades, $columns, $headers);
if (!$table->is_downloading() && has_capability('mod/quiz:regrade', $this->context) &&
$this->has_regraded_questions($table->sql->from, $table->sql->where, $table->sql->params)) {
$columns[] = 'regraded';
$headers[] = get_string('regrade', 'quiz_overview');
}
if ($options->slotmarks) {
foreach ($questions as $slot => $question) {
$columns[] = 'qsgrade' . $slot;
$header = get_string('qbrief', 'quiz', $question->displaynumber);
if (!$table->is_downloading()) {
$header .= '<br />';
} else {
$header .= ' ';
}
$header .= '/' . quiz_rescale_grade($question->maxmark, $quiz, 'question');
$headers[] = $header;
}
}
$this->set_up_table_columns($table, $columns, $headers, $this->get_base_url(), $options, false);
$table->set_attribute('class', 'generaltable generalbox grades');
$table->out($options->pagesize, true);
}
if (!$table->is_downloading() && $options->usercanseegrades) {
$output = $PAGE->get_renderer('mod_quiz');
list($bands, $bandwidth) = self::get_bands_count_and_width($quiz);
$labels = self::get_bands_labels($bands, $bandwidth, $quiz);
if ($currentgroup && $this->hasgroupstudents) {
$sql = "SELECT qg.id
FROM {quiz_grades} qg
JOIN {user} u on u.id = qg.userid
{$groupstudentsjoins->joins}
WHERE qg.quiz = $quiz->id AND {$groupstudentsjoins->wheres}";
if ($DB->record_exists_sql($sql, $groupstudentsjoins->params)) {
$data = quiz_report_grade_bands($bandwidth, $bands, $quiz->id, $groupstudentsjoins);
$chart = self::get_chart($labels, $data);
$groupname = format_string(groups_get_group_name($currentgroup), true, [
'context' => $this->context,
]);
$graphname = get_string('overviewreportgraphgroup', 'quiz_overview', $groupname);
// Numerical range data should display in LTR even for RTL languages.
echo $output->chart($chart, $graphname, ['dir' => 'ltr']);
}
}
if ($DB->record_exists('quiz_grades', ['quiz' => $quiz->id])) {
$data = quiz_report_grade_bands($bandwidth, $bands, $quiz->id, new \core\dml\sql_join());
$chart = self::get_chart($labels, $data);
$graphname = get_string('overviewreportgraph', 'quiz_overview');
// Numerical range data should display in LTR even for RTL languages.
echo $output->chart($chart, $graphname, ['dir' => 'ltr']);
}
}
return true;
}
/**
* Extends parent function processing any submitted actions.
*
* @param stdClass $quiz
* @param stdClass $cm
* @param int $currentgroup
* @param \core\dml\sql_join $groupstudentsjoins (joins, wheres, params)
* @param \core\dml\sql_join $allowedjoins (joins, wheres, params)
* @param moodle_url $redirecturl
*/
protected function process_actions($quiz, $cm, $currentgroup, \core\dml\sql_join $groupstudentsjoins,
\core\dml\sql_join $allowedjoins, $redirecturl) {
parent::process_actions($quiz, $cm, $currentgroup, $groupstudentsjoins, $allowedjoins, $redirecturl);
if (empty($currentgroup) || $this->hasgroupstudents) {
if (optional_param('regrade', 0, PARAM_BOOL) && confirm_sesskey()) {
if ($attemptids = optional_param_array('attemptid', [], PARAM_INT)) {
$this->start_regrade($quiz, $cm);
$this->regrade_attempts($quiz, false, $groupstudentsjoins, $attemptids);
$this->finish_regrade($redirecturl);
}
}
}
if (optional_param('regradeall', 0, PARAM_BOOL) && confirm_sesskey()) {
$this->start_regrade($quiz, $cm);
$this->regrade_attempts($quiz, false, $groupstudentsjoins);
$this->finish_regrade($redirecturl);
} else if (optional_param('regradealldry', 0, PARAM_BOOL) && confirm_sesskey()) {
$this->start_regrade($quiz, $cm);
$this->regrade_attempts($quiz, true, $groupstudentsjoins);
$this->finish_regrade($redirecturl);
} else if (optional_param('regradealldrydo', 0, PARAM_BOOL) && confirm_sesskey()) {
$this->start_regrade($quiz, $cm);
$this->regrade_attempts_needing_it($quiz, $groupstudentsjoins);
$this->finish_regrade($redirecturl);
}
}
/**
* Check necessary capabilities, and start the display of the regrade progress page.
* @param stdClass $quiz the quiz settings.
* @param stdClass $cm the cm object for the quiz.
*/
protected function start_regrade($quiz, $cm) {
require_capability('mod/quiz:regrade', $this->context);
$this->print_header_and_tabs(
$cm,
get_course($cm->course),
$quiz,
$this->mode
);
}
/**
* Finish displaying the regrade progress page.
* @param moodle_url $nexturl where to send the user after the regrade.
* @uses exit. This method never returns.
*/
protected function finish_regrade($nexturl) {
global $OUTPUT;
\core\notification::success(get_string('regradecomplete', 'quiz_overview'));
echo $OUTPUT->continue_button($nexturl);
echo $OUTPUT->footer();
die();
}
/**
* Unlock the session and allow the regrading process to run in the background.
*/
protected function unlock_session() {
\core\session\manager::write_close();
ignore_user_abort(true);
}
/**
* Regrade a particular quiz attempt. Either for real ($dryrun = false), or
* as a pretend regrade to see which fractions would change. The outcome is
* stored in the quiz_overview_regrades table.
*
* Note, $attempt is not upgraded in the database. The caller needs to do that.
* However, $attempt->sumgrades is updated, if this is not a dry run.
*
* @param stdClass $attempt the quiz attempt to regrade.
* @param bool $dryrun if true, do a pretend regrade, otherwise do it for real.
* @param array $slots if null, regrade all questions, otherwise, just regrade
* the questions with those slots.
* @return array messages array with keys slot number, and values reasons why that slot cannot be regraded.
*/
public function regrade_attempt($attempt, $dryrun = false, $slots = null): array {
global $DB;
// Need more time for a quiz with many questions.
core_php_time_limit::raise(300);
$transaction = $DB->start_delegated_transaction();
$quba = question_engine::load_questions_usage_by_activity($attempt->uniqueid);
$versioninformation = qbank_helper::get_version_information_for_questions_in_attempt(
$attempt, $this->context);
if (is_null($slots)) {
$slots = $quba->get_slots();
}
$messages = [];
$finished = $attempt->state == quiz_attempt::FINISHED;
foreach ($slots as $slot) {
$qqr = new stdClass();
$qqr->oldfraction = $quba->get_question_fraction($slot);
$otherquestionversion = question_bank::load_question($versioninformation[$slot]->newquestionid);
$message = $quba->validate_can_regrade_with_other_version($slot, $otherquestionversion);
if ($message) {
$messages[$slot] = $message;
continue;
}
$quba->regrade_question($slot, $finished, null, $otherquestionversion);
$qqr->newfraction = $quba->get_question_fraction($slot);
if (abs($qqr->oldfraction - $qqr->newfraction) > 1e-7) {
$qqr->questionusageid = $quba->get_id();
$qqr->slot = $slot;
$qqr->regraded = empty($dryrun);
$qqr->timemodified = time();
$DB->insert_record('quiz_overview_regrades', $qqr, false);
}
}
if (!$dryrun) {
question_engine::save_questions_usage_by_activity($quba);
$params = [
'objectid' => $attempt->id,
'relateduserid' => $attempt->userid,
'context' => $this->context,
'other' => [
'quizid' => $attempt->quiz
]
];
$event = \mod_quiz\event\attempt_regraded::create($params);
$event->trigger();
}
$transaction->allow_commit();
// Really, PHP should not need this hint, but without this, we just run out of memory.
$quba = null;
$transaction = null;
gc_collect_cycles();
return $messages;
}
/**
* Regrade attempts for this quiz, exactly which attempts are regraded is
* controlled by the parameters.
*
* @param stdClass $quiz the quiz settings.
* @param bool $dryrun if true, do a pretend regrade, otherwise do it for real.
* @param \core\dml\sql_join|null $groupstudentsjoins empty for all attempts, otherwise regrade attempts
* for these users.
* @param array $attemptids blank for all attempts, otherwise only regrade
* attempts whose id is in this list.
*/
protected function regrade_attempts($quiz, $dryrun = false,
core\dml\sql_join $groupstudentsjoins = null, $attemptids = []) {
global $DB;
$this->unlock_session();
$userfieldsapi = \core_user\fields::for_name();
$sql = "SELECT quiza.*, " . $userfieldsapi->get_sql('u', false, '', '', false)->selects . "
FROM {quiz_attempts} quiza
JOIN {user} u ON u.id = quiza.userid";
$where = "quiz = :qid AND preview = 0";
$params = ['qid' => $quiz->id];
if ($this->hasgroupstudents && !empty($groupstudentsjoins->joins)) {
$sql .= "\n{$groupstudentsjoins->joins}";
$where .= " AND {$groupstudentsjoins->wheres}";
$params += $groupstudentsjoins->params;
}
if ($attemptids) {
list($attemptidcondition, $attemptidparams) = $DB->get_in_or_equal($attemptids, SQL_PARAMS_NAMED);
$where .= " AND quiza.id $attemptidcondition";
$params += $attemptidparams;
}
$sql .= "\nWHERE {$where}";
$attempts = $DB->get_records_sql($sql, $params);
if (!$attempts) {
return;
}
$this->regrade_batch_of_attempts($quiz, $attempts, $dryrun, $groupstudentsjoins);
}
/**
* Regrade those questions in those attempts that are marked as needing regrading
* in the quiz_overview_regrades table.
* @param stdClass $quiz the quiz settings.
* @param \core\dml\sql_join $groupstudentsjoins empty for all attempts, otherwise regrade attempts
* for these users.
*/
protected function regrade_attempts_needing_it($quiz, \core\dml\sql_join $groupstudentsjoins) {
global $DB;
$this->unlock_session();
$join = '{quiz_overview_regrades} qqr ON qqr.questionusageid = quiza.uniqueid';
$where = "quiza.quiz = :qid AND quiza.preview = 0 AND qqr.regraded = 0";
$params = ['qid' => $quiz->id];
// Fetch all attempts that need regrading.
if ($this->hasgroupstudents && !empty($groupstudentsjoins->joins)) {
$join .= "\nJOIN {user} u ON u.id = quiza.userid
{$groupstudentsjoins->joins}";
$where .= " AND {$groupstudentsjoins->wheres}";
$params += $groupstudentsjoins->params;
}
$toregrade = $DB->get_recordset_sql("
SELECT quiza.uniqueid, qqr.slot
FROM {quiz_attempts} quiza
JOIN $join
WHERE $where", $params);
$attemptquestions = [];
foreach ($toregrade as $row) {
$attemptquestions[$row->uniqueid][] = $row->slot;
}
$toregrade->close();
if (!$attemptquestions) {
return;
}
list($uniqueidcondition, $params) = $DB->get_in_or_equal(array_keys($attemptquestions));
$userfieldsapi = \core_user\fields::for_name();
$attempts = $DB->get_records_sql("
SELECT quiza.*, " . $userfieldsapi->get_sql('u', false, '', '', false)->selects . "
FROM {quiz_attempts} quiza
JOIN {user} u ON u.id = quiza.userid
WHERE quiza.uniqueid $uniqueidcondition
", $params);
foreach ($attempts as $attempt) {
$attempt->regradeonlyslots = $attemptquestions[$attempt->uniqueid];
}
$this->regrade_batch_of_attempts($quiz, $attempts, false, $groupstudentsjoins);
}
/**
* This is a helper used by {@link regrade_attempts()} and
* {@link regrade_attempts_needing_it()}.
*
* Given an array of attempts, it regrades them all, or does a dry run.
* Each object in the attempts array must be a row from the quiz_attempts
* table, with the \core_user\fields::for_name() fields from the user table joined in.
* In addition, if $attempt->regradeonlyslots is set, then only those slots
* are regraded, otherwise all slots are regraded.
*
* @param stdClass $quiz the quiz settings.
* @param array $attempts of data from the quiz_attempts table, with extra data as above.
* @param bool $dryrun if true, do a pretend regrade, otherwise do it for real.
* @param \core\dml\sql_join $groupstudentsjoins empty for all attempts, otherwise regrade attempts
*/
protected function regrade_batch_of_attempts($quiz, array $attempts,
bool $dryrun, \core\dml\sql_join $groupstudentsjoins) {
global $OUTPUT;
$this->clear_regrade_table($quiz, $groupstudentsjoins);
$progressbar = new progress_bar('quiz_overview_regrade', 500, true);
$a = [
'count' => count($attempts),
'done' => 0,
];
foreach ($attempts as $attempt) {
$a['done']++;
$a['attemptnum'] = $attempt->attempt;
$a['name'] = fullname($attempt);
$a['attemptid'] = $attempt->id;
if (!isset($attempt->regradeonlyslots)) {
$attempt->regradeonlyslots = null;
}
$progressbar->update($a['done'], $a['count'],
get_string('regradingattemptxofywithdetails', 'quiz_overview', $a));
$messages = $this->regrade_attempt($attempt, $dryrun, $attempt->regradeonlyslots);
if ($messages) {
$items = [];
foreach ($messages as $slot => $message) {
$items[] = get_string('regradingattemptissue', 'quiz_overview',
['slot' => $slot, 'reason' => $message]);
}
echo $OUTPUT->notification(
html_writer::tag('p', get_string('regradingattemptxofyproblem', 'quiz_overview', $a)) .
html_writer::alist($items), \core\output\notification::NOTIFY_WARNING);
}
}
$progressbar->update($a['done'], $a['count'],
get_string('regradedsuccessfullyxofy', 'quiz_overview', $a));
if (!$dryrun) {
$this->update_overall_grades($quiz);
}
}
/**
* Count the number of attempts in need of a regrade.
*
* @param stdClass $quiz the quiz settings.
* @param \core\dml\sql_join $groupstudentsjoins (joins, wheres, params) If this is given, only data relating
* to these users is cleared.
* @return int the number of attempts.
*/
protected function count_question_attempts_needing_regrade($quiz, \core\dml\sql_join $groupstudentsjoins) {
global $DB;
$userjoin = '';
$usertest = '';
$params = [];
if ($this->hasgroupstudents) {
$userjoin = "JOIN {user} u ON u.id = quiza.userid
{$groupstudentsjoins->joins}";
$usertest = "{$groupstudentsjoins->wheres} AND u.id = quiza.userid AND ";
$params = $groupstudentsjoins->params;
}
$params['cquiz'] = $quiz->id;
$sql = "SELECT COUNT(DISTINCT quiza.id)
FROM {quiz_attempts} quiza
JOIN {quiz_overview_regrades} qqr ON quiza.uniqueid = qqr.questionusageid
$userjoin
WHERE
$usertest
quiza.quiz = :cquiz AND
quiza.preview = 0 AND
qqr.regraded = 0";
return $DB->count_records_sql($sql, $params);
}
/**
* Are there any pending regrades in the table we are going to show?
* @param string $from tables used by the main query.
* @param string $where where clause used by the main query.
* @param array $params required by the SQL.
* @return bool whether there are pending regrades.
*/
protected function has_regraded_questions($from, $where, $params) {
global $DB;
return $DB->record_exists_sql("
SELECT 1
FROM {$from}
JOIN {quiz_overview_regrades} qor ON qor.questionusageid = quiza.uniqueid
WHERE {$where}", $params);
}
/**
* Remove all information about pending/complete regrades from the database.
* @param stdClass $quiz the quiz settings.
* @param \core\dml\sql_join $groupstudentsjoins (joins, wheres, params). If this is given, only data relating
* to these users is cleared.
*/
protected function clear_regrade_table($quiz, \core\dml\sql_join $groupstudentsjoins) {
global $DB;
// Fetch all attempts that need regrading.
$select = "questionusageid IN (
SELECT uniqueid
FROM {quiz_attempts} quiza";
$where = "WHERE quiza.quiz = :qid";
$params = ['qid' => $quiz->id];
if ($this->hasgroupstudents && !empty($groupstudentsjoins->joins)) {
$select .= "\nJOIN {user} u ON u.id = quiza.userid
{$groupstudentsjoins->joins}";
$where .= " AND {$groupstudentsjoins->wheres}";
$params += $groupstudentsjoins->params;
}
$select .= "\n$where)";
$DB->delete_records_select('quiz_overview_regrades', $select, $params);
}
/**
* Update the final grades for all attempts. This method is used following a regrade.
*
* @param stdClass $quiz the quiz settings.
*/
protected function update_overall_grades($quiz) {
$gradecalculator = $this->quizobj->get_grade_calculator();
$gradecalculator->recompute_all_attempt_sumgrades();
$gradecalculator->recompute_all_final_grades();
quiz_update_grades($quiz);
}
/**
* Get the bands configuration for the quiz.
*
* This returns the configuration for having between 11 and 20 bars in
* a chart based on the maximum grade to be given on a quiz. The width of
* a band is the number of grade points it encapsulates.
*
* @param stdClass $quiz The quiz object.
* @return array Contains the number of bands, and their width.
*/
public static function get_bands_count_and_width($quiz) {
$bands = $quiz->grade;
while ($bands > 20 || $bands <= 10) {
if ($bands > 50) {
$bands /= 5;
} else if ($bands > 20) {
$bands /= 2;
}
if ($bands < 4) {
$bands *= 5;
} else if ($bands <= 10) {
$bands *= 2;
}
}
// See MDL-34589. Using doubles as array keys causes problems in PHP 5.4, hence the explicit cast to int.
$bands = (int) ceil($bands);
return [$bands, $quiz->grade / $bands];
}
/**
* Get the bands labels.
*
* @param int $bands The number of bands.
* @param int $bandwidth The band width.
* @param stdClass $quiz The quiz object.
* @return string[] The labels.
*/
public static function get_bands_labels($bands, $bandwidth, $quiz) {
$bandlabels = [];
for ($i = 1; $i <= $bands; $i++) {
$bandlabels[] = quiz_format_grade($quiz, ($i - 1) * $bandwidth) . ' - ' . quiz_format_grade($quiz, $i * $bandwidth);
}
return $bandlabels;
}
/**
* Get a chart.
*
* @param string[] $labels Chart labels.
* @param int[] $data The data.
* @return \core\chart_base
*/
protected static function get_chart($labels, $data) {
$chart = new \core\chart_bar();
$chart->set_labels($labels);
$chart->get_xaxis(0, true)->set_label(get_string('gradenoun'));
$yaxis = $chart->get_yaxis(0, true);
$yaxis->set_label(get_string('participants'));
$yaxis->set_stepsize(max(1, round(max($data) / 10)));
$series = new \core\chart_series(get_string('participants'), $data);
$chart->add_series($series);
return $chart;
}
}
@@ -0,0 +1,115 @@
@mod @mod_quiz @quiz @quiz_overview
Feature: Basic use of the Grades report
In order to easily get an overview of quiz attempts
As a teacher
I need to use the Grades report
Background:
Given the "multilang" filter is "on"
And the "multilang" filter applies to "content and headings"
And the following "custom profile fields" exist:
| datatype | shortname | name |
| text | fruit | Fruit |
And the following "users" exist:
| username | firstname | lastname | email | idnumber | profile_field_fruit |
| teacher1 | T1 | Teacher1 | teacher1@example.com | T1000 | |
| student1 | S1 | Student1 | student1@example.com | S1000 | Apple |
| student2 | S2 | Student2 | student2@example.com | S2000 | Banana |
| student3 | S3 | Student3 | student3@example.com | S3000 | Pear |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
| student3 | C1 | student |
And the following "groups" exist:
| course | idnumber | name |
| C1 | G1 | <span class="multilang" lang="en">English</span><span class="multilang" lang="es">Spanish</span> |
| C1 | G2 | Group 2 |
And the following "group members" exist:
| group | user |
| G1 | student1 |
| G1 | student2 |
| G2 | student3 |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "activities" exist:
| activity | name | intro | course | idnumber | groupmode |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | 2 |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | description | Intro | Welcome to this quiz |
| Test questions | truefalse | TF1 | First question |
| Test questions | truefalse | TF2 | Second question |
And quiz "Quiz 1" contains the following questions:
| question | page | maxmark | displaynumber |
| Intro | 1 | | |
| TF1 | 1 | | |
| TF2 | 1 | 3.0 | 2a |
And user "student1" has attempted "Quiz 1" with responses:
| slot | response |
| 2 | True |
| 3 | False |
And user "student2" has attempted "Quiz 1" with responses:
| slot | response |
| 2 | True |
| 3 | True |
@javascript
Scenario: Using the Grades report
# Basic check of the Grades report
When I am on the "Quiz 1" "quiz activity" page logged in as teacher1
And I navigate to "Results" in current page administration
Then I should see "Attempts: 2"
# Verify that the right columns are visible
And I should see "Q. 1"
And I should see "Q. 2a"
And I should not see "Q. 3"
# Check student1's grade
And I should see "25.00" in the "S1 Student1" "table_row"
# And student2's grade
And I should see "100.00" in the "S2 Student2" "table_row"
# Check changing the form parameters
And I set the field "Attempts from" to "enrolled users who have not attempted the quiz"
And I press "Show report"
# Note: teachers should not appear in the report.
# Check student3's grade
And I should see "-" in the "S3 Student3" "table_row"
And I set the field "Attempts from" to "enrolled users who have, or have not, attempted the quiz"
And I press "Show report"
# Check student1's grade
And I should see "25.00" in the "S1 Student1" "table_row"
# Check student2's grade
And I should see "100.00" in the "S2 Student2" "table_row"
# Check student3's grade
And I should see "-" in the "S3 Student3" "table_row"
And I set the field "Attempts from" to "all users who have attempted the quiz"
And I press "Show report"
# Check student1's grade
And I should see "25.00" in the "S1 Student1" "table_row"
# Check student2's grade
And I should see "100.00" in the "S2 Student2" "table_row"
# Verify groups are displayed correctly.
And I set the field "Visible groups" to "English"
And "Full regrade for group 'English'" "button" should exist
And "Dry run a full regrade for group 'English'" "button" should exist
And I should see "Number of students in group 'English' achieving grade ranges"
@javascript
Scenario: View custom user profile fields in the grades report
Given the following config values are set as admin:
| showuseridentity | email,profile_field_fruit |
And I am on the "Quiz 1" "quiz activity" page logged in as teacher1
And I navigate to "Results" in current page administration
Then I should see "Apple" in the "S1 Student1" "table_row"
And I should see "Banana" in the "S2 Student2" "table_row"
@@ -0,0 +1,54 @@
@mod @mod_quiz @quiz @quiz_overview
Feature: Grades report for a quiz with multiple grade items
In to get an overview of quiz attempt grade
As a teacher
I need the Grades report to show all grade items
Background:
Given the following "users" exist:
| username | firstname | lastname |
| student | Lorna | Lott |
| teacher | Mark | Allwright |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| student | C1 | student |
| teacher | C1 | teacher |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "activities" exist:
| activity | name | course |
| quiz | Test quiz | C1 |
@javascript
Scenario: Quiz grades report with multiple grade items
Given the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | Reading | Can you read this? |
| Test questions | truefalse | Listening | Can you hear this? |
And the following "mod_quiz > grade items" exist:
| quiz | name |
| Test quiz | Reading |
| Test quiz | Listening |
And quiz "Test quiz" contains the following questions:
| question | page | grade item |
| Reading | 1 | Reading |
| Listening | 1 | Listening |
And user "student" has attempted "Test quiz" with responses:
| slot | response |
| 1 | True |
| 2 | False |
When I am on the "Test quiz" "mod_quiz > Grades report" page logged in as teacher
Then "Lorna LottReview attempt" row "Grade/100.00Sort by Grade/100.00 Ascending" column of "attempts" table should contain "50.00"
And "Lorna LottReview attempt" row "Q. 1/50.00Sort by Q. 1/50.00 Ascending" column of "attempts" table should contain "0.00"
And "Lorna LottReview attempt" row "Q. 2/50.00Sort by Q. 2/50.00 Ascending" column of "attempts" table should contain "0.00"
And "Lorna LottReview attempt" row "Reading/1.00Sort by Reading/1.00 Ascending" column of "attempts" table should contain "1.00"
And "Lorna LottReview attempt" row "Listening/1.00Sort by Listening/1.00 Ascending" column of "attempts" table should contain "0.00"
And I follow "Reading/1.00Sort by Reading/1.00 Ascending"
# Main thing to check here is that sorting does not give a fatal error
And "Lorna LottReview attempt" row "Listening/1.00Sort by Listening/1.00 Ascending" column of "attempts" table should contain "0.00"
@@ -0,0 +1,220 @@
@mod @mod_quiz @quiz @quiz_overview @javascript
Feature: Regrading quiz attempts using the Grades report
In order to be able to correct mistakes I made setting up my quiz
As a teacher
I need to be able to re-grade attempts after editing questions
Background:
Given the following "users" exist:
| username | firstname | lastname |
| teacher | Mark | Allwright |
| student1 | Student | One |
| student2 | Student | Two |
| student3 | Student | Three |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
| student3 | C1 | student |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "activities" exist:
| activity | name | course | idnumber |
| quiz | Quiz for testing regrading | C1 | quiz1 |
And the following "questions" exist:
| questioncategory | qtype | name |
| Test questions | truefalse | TF |
| Test questions | shortanswer | SA |
And quiz "Quiz for testing regrading" contains the following questions:
| question | page | maxmark |
| TF | 1 | 5.0 |
| SA | 1 | 5.0 |
And user "student1" has attempted "Quiz for testing regrading" with responses:
| slot | response |
| 1 | True |
| 2 | frog |
And user "student2" has attempted "Quiz for testing regrading" with responses:
| slot | response |
| 1 | True |
| 2 | toad |
Scenario: Regrade all attempts
Given I am on the "Quiz for testing regrading" "quiz activity" page logged in as teacher
And I navigate to "Results" in current page administration
When I press "Regrade all"
# Note, the order is not defined, so we can only check part of the message.
# Also, nothing has changed in the quiz, so the regrade won't alter any scores,
# but this is still a useful test that the regrade process completes without errors.
Then I should see "Quiz for testing regrading"
And I should see "Finished regrading (2/2)"
And I should see "Regrade completed"
And I press "Continue"
# These next tests just serve to check we got back to the report.
And I should see "Quiz for testing regrading"
And I should see "Overall number of students achieving grade ranges"
Scenario: Regrade selected attempts
Given I am on the "Quiz for testing regrading" "quiz activity" page logged in as teacher
And I navigate to "Results" in current page administration
When I click on "Select attempt" "checkbox" in the "Student Two" "table_row"
And I press "Regrade selected attempts"
Then I should see "Quiz for testing regrading"
And I should see "Finished regrading (1/1)"
And I should see "Regrade completed"
And I press "Continue"
# These next tests just serve to check we got back to the report.
And I should see "Quiz for testing regrading"
And I should see "Overall number of students achieving grade ranges"
Scenario: Dry-run a full regrade, then regrade the attempts that will need it.
Given I am on the "Quiz for testing regrading" "mod_quiz > edit" page logged in as teacher
And I follow "Edit question SA"
And I set the field "id_fraction_1" to "50%"
And I press "id_submitbutton"
And I set the field "version" in the "TF" "list_item" to "v1"
And I set the field "version" in the "SA" "list_item" to "v2 (latest)"
And I follow "Attempts: 2"
And I press "Dry run a full regrade"
# Note, the order is not defined, so we can only check part of the message.
Then I should see "Quiz for testing regrading"
And I should see "Finished regrading (2/2)"
And I should see "Regrade completed"
And I press "Continue"
And "Student One" row "Regrade" column of "attempts" table should not contain "Needed"
And "Student TwoReview attempt" row "Regrade" column of "attempts" table should contain "Needed"
# In the following, the first number is strike-through, and the second is not, but Behat can't see that.
# At this point, it is showing what would change.
And "Student TwoReview attempt" row "Q. 2/50.00Sort by Q. 2/50.00 Ascending" column of "attempts" table should contain "40.00/25.00"
And "Student TwoReview attempt" row "Grade/100.00Sort by Grade/100.00 Ascending" column of "attempts" table should contain "90.00/75.00"
And I press "Regrade attempts marked as needing regrading (1)"
And I should see "Quiz for testing regrading"
And I should see "Finished regrading (1/1)"
And I should see "Regrade completed"
And I press "Continue"
# These next tests just serve to check we got back to the report.
And I should see "Quiz for testing regrading"
And I should see "Overall number of students achieving grade ranges"
# Now, both old-score strike-through and new score plain, are still shown, but now it indicates what did change.
And "Student TwoReview attempt" row "Q. 2/50.00Sort by Q. 2/50.00 Ascending" column of "attempts" table should contain "40.00/25.00"
And "Student TwoReview attempt" row "Grade/100.00Sort by Grade/100.00 Ascending" column of "attempts" table should contain "90.00/75.00"
And "Regrade attempts marked as needing regrading" "button" should not exist
Scenario: Regrade all attempts works against quiz selected question version
Given I am on the "Quiz for testing regrading" "quiz activity" page logged in as teacher
And I navigate to "Results" in current page administration
When I press "Dry run a full regrade"
Then I should see "Quiz for testing regrading"
And I should see "Finished regrading (2/2)"
And I should see "Regrade completed"
And I press "Continue"
And I should see "Quiz for testing regrading"
And I should see "Overall number of students achieving grade ranges"
And "Student One" row "Regrade" column of "attempts" table should not contain "Needed"
And I am on the "Quiz for testing regrading" "mod_quiz > question bank" page
And I choose "Edit question" action for "TF" in the question bank
And I set the field "Correct answer" to "False"
And I press "id_submitbutton"
And I am on the "Quiz for testing regrading" "mod_quiz > edit" page
And I set the field "version" in the "TF" "list_item" to "v2 (latest)"
And I navigate to "Results" in current page administration
And I press "Dry run a full regrade"
And I should see "Regrade completed"
And I press "Continue"
And "student1@example.com" row "Regrade" column of "attempts" table should contain "Needed"
And "Correct" "icon" should appear before "50.00/0.00" "text"
And I press "Regrade all"
And I should see "Regrade completed"
And I press "Continue"
Then "student1@example.com" row "Regrade" column of "attempts" table should contain "Done"
And "Student OneReview attempt" row "Q. 1/50.00Sort by Q. 1/50.00 Ascending" column of "attempts" table should contain "50.00/0.00"
And "Incorrect" "icon" should appear before "50.00/0.00" "text"
Scenario: Regrade all attempts works against quiz selected latest question version
Given I am on the "Quiz for testing regrading" "quiz activity" page logged in as teacher
And I navigate to "Results" in current page administration
And I click on "mod-quiz-report-overview-report-selectall-attempts" "checkbox"
And I click on "Delete selected attempts" "button"
And I click on "Yes" "button"
And I am on the "Quiz for testing regrading" "mod_quiz > edit" page
And I should see "(latest)" in the "TF" "list_item"
# Create multiple question versions.
And I am on the "Quiz for testing regrading" "mod_quiz > question bank" page
And I choose "Edit question" action for "TF" in the question bank
And I set the field "Correct answer" to "True"
And I press "id_submitbutton"
And I choose "Edit question" action for "TF" in the question bank
And I set the field "Question name" to "New version of TF"
And I set the field "Correct answer" to "False"
And I press "id_submitbutton"
And I am on the "Quiz for testing regrading" "mod_quiz > edit" page
And I should see "(latest)" in the "TF" "list_item"
And I click on "version" "select" in the "TF" "list_item"
And I should see "v1"
And I should see "v2"
And I should see "v3 (latest)"
# Set version that is going to be attempted to an older one.
And I set the field "version" in the "TF" "list_item" to "v1"
And user "student3" has attempted "Quiz for testing regrading" with responses:
| slot | response |
| 1 | True |
| 2 | toad |
And I am on the "Quiz for testing regrading" "mod_quiz > edit" page
And I set the field "version" in the "TF" "list_item" to "Always latest"
And I navigate to "Results" in current page administration
And I press "Regrade all"
And I should see "Finished regrading (1/1)"
And I should see "Regrade completed"
And I press "Continue"
Then "student3@example.com" row "Q. 1/50.00Sort by Q. 1/50.00 Ascending" column of "attempts" table should contain "50.00/0.00"
And "Incorrect" "icon" should appear before "50.00/0.00" "text"
Scenario: Regrade attempts should always regrade against latest random question version
Given I am on the "Quiz for testing regrading" "quiz activity" page logged in as teacher
And I navigate to "Results" in current page administration
And I click on "mod-quiz-report-overview-report-selectall-attempts" "checkbox"
And I click on "Delete selected attempts" "button"
And I click on "Yes" "button"
# Create multiple question versions.
And I am on the "Quiz for testing regrading" "mod_quiz > question bank" page
And I choose "Delete" action for "SA" in the question bank
And I press "Delete"
And I am on the "Quiz for testing regrading" "mod_quiz > edit" page
And I click on "Delete" "link" in the "TF" "list_item"
And I click on "Yes" "button" in the "Confirm" "dialogue"
And I click on "Delete" "link" in the "SA" "list_item"
And I click on "Yes" "button" in the "Confirm" "dialogue"
And I click on "Add" "link"
And I follow "a random question"
And I press "Add random question"
And I am on the "Quiz for testing regrading" "quiz activity" page logged in as student3
And I click on "Attempt quiz" "button"
And I should see "The answer is true."
And I set the field "True" to "1"
And I click on "Finish attempt ..." "button"
And I press "Submit all and finish"
And I click on "Submit" "button" in the "Submit all your answers and finish?" "dialogue"
And I am on the "Quiz for testing regrading" "mod_quiz > question bank" page logged in as teacher
And I choose "Edit question" action for "TF" in the question bank
And I set the field "Correct answer" to "False"
And I press "id_submitbutton"
And I navigate to "Results" in current page administration
And "student3@example.com" row "Q. 1/100.00Sort by Q. 1/100.00 Ascending" column of "attempts" table should contain "100.00"
And "Correct" "icon" should be visible
And I press "Regrade all"
And I should see "Finished regrading (1/1)"
And I should see "Regrade completed"
And I press "Continue"
Then "student3@example.com" row "Q. 1/100.00Sort by Q. 1/100.00 Ascending" column of "attempts" table should contain "100.00/0.00"
And "Incorrect" "icon" should be visible
@@ -0,0 +1,56 @@
@mod @mod_quiz @quiz @quiz_overview @javascript
Feature: Quiz regrade when not possible
In order avoid errors
As a teacher
I need the system to prevent impossible regrade scenarios
Background:
Given the following "users" exist:
| username | firstname | lastname |
| teacher | Mark | Allwright |
| student | Student | One |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher | C1 | editingteacher |
| student | C1 | student |
And the following "activities" exist:
| activity | name | course | idnumber |
| quiz | Quiz for testing regrading | C1 | quiz1 |
And the following "question categories" exist:
| contextlevel | reference | name |
| Activity module | quiz1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | template | name |
| Test questions | multichoice | one_of_four | MC |
And quiz "Quiz for testing regrading" contains the following questions:
| question | page | maxmark |
| MC | 1 | 10.0 |
And user "student" has attempted "Quiz for testing regrading" with responses:
| slot | response |
| 1 | B |
Scenario: Try a regrade after the question has been edited to have a different number of choices
# Edit the question so that V2 has the fourth choice removed.
Given I am on the "MC" "core_question > edit" page logged in as teacher
And I set the following fields to these values:
| Choice 4 | |
| id_feedback_3 | |
And I press "id_submitbutton"
# Try a regrade, and verify what happened is reported.
When I am on the "Quiz for testing regrading" "mod_quiz > grades report" page
And I press "Regrade all"
Then I should see "Quiz for testing regrading"
And I should see "The following questions could not be regraded in attempt 1 by Student One"
And I should see "Slot 1: The number of choices in the question has changed."
And I should see "Finished regrading (1/1)"
And I should see "Regrade completed"
And I press "Continue"
# These next tests just serve to check we got back to the report.
And I should see "Quiz for testing regrading"
And I should see "Overall number of students achieving grade ranges"
@@ -0,0 +1,49 @@
@mod @mod_quiz @quiz @quiz_overview @javascript
Feature: Reopening Never submitted quiz attempts
In order to cut some slack to students who forgot to submit their quiz attempt
As a teacher
I need to be able to reopen selected attempts.
Background:
Given the following "users" exist:
| username | firstname | lastname |
| teacher | Mark | Allwright |
| student | Freddy | Forgetful |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher | C1 | editingteacher |
| student | C1 | student |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name |
| Test questions | truefalse | TF |
And the following "activities" exist:
| activity | name | course | idnumber |
| quiz | Test quiz | C1 | quiz1 |
And quiz "Test quiz" contains the following questions:
| question | page |
| TF | 1 |
And user "student" has started an attempt at quiz "Test quiz"
And the attempt at "Test quiz" by "student" was never submitted
Scenario: Attempt can be reopened
Given I am on the "Test quiz" "mod_quiz > Grades report" page logged in as teacher
When I press "Reopen"
And I should see "This will reopen attempt 1 by Freddy Forgetful." in the "Reopen attempt?" "dialogue"
And I should see "The attempt will remain open and can be continued." in the "Reopen attempt?" "dialogue"
And I click on "Reopen" "button" in the "Reopen attempt?" "dialogue"
Then I should see "In progress" in the "Freddy Forgetful" "table_row"
And "Reopen" "button" should not exist
Scenario: Reopening an attempt can be cancelled and then nothing happens
Given I am on the "Test quiz" "mod_quiz > Grades report" page logged in as teacher
And I start watching to see if a new page loads
When I press "Reopen"
And I click on "Cancel" "button" in the "Reopen attempt?" "dialogue"
Then a new page should not have loaded since I started watching
And I should see "Never submitted" in the "Freddy Forgetful" "table_row"
@@ -0,0 +1,56 @@
<?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/>.
use mod_quiz\local\reports\attempts_report;
/**
* 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
*/
/**
* 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 attempts_report {
/**
* Override this function to displays the report.
* @param stdClass $cm the course-module for this quiz.
* @param stdClass $course the course we are in.
* @param stdClass $quiz this quiz.
*/
public function display($cm, $course, $quiz) {
}
/**
* Testable delete_selected_attempts function.
*
* @param stdClass $quiz
* @param stdClass $cm
* @param array $attemptids
* @param \core\dml\sql_join $allowedjoins
*/
public function delete_selected_attempts($quiz, $cm, $attemptids, \core\dml\sql_join $allowedjoins) {
parent::delete_selected_attempts($quiz, $cm, $attemptids, $allowedjoins);
}
}
@@ -0,0 +1,110 @@
<?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/>.
/**
* Privacy provider tests.
*
* @package quiz_overview
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace quiz_overview\privacy;
use core_privacy\local\metadata\collection;
use quiz_overview\privacy\provider;
use core_privacy\local\request\writer;
use core_privacy\local\request\transform;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy provider tests class.
*
* @package quiz_overview
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_test extends \core_privacy\tests\provider_testcase {
/**
* When no preference exists, there should be no export.
*/
public function test_preference_unset(): void {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
provider::export_user_preferences($USER->id);
$this->assertFalse(writer::with_context(\context_system::instance())->has_any_data());
}
/**
* Preference does exist.
*/
public function test_preference_yes(): void {
$this->resetAfterTest();
// Create test user, add some preferences.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
set_user_preference('quiz_overview_slotmarks', 1, $user);
// Switch to admin user (so we can validate preferences of the correct user are being exported).
$this->setAdminUser();
// Export test users preferences.
provider::export_user_preferences($user->id);
$writer = writer::with_context(\context_system::instance());
$this->assertTrue($writer->has_any_data());
$preferences = $writer->get_user_preferences('quiz_overview');
$this->assertNotEmpty($preferences->slotmarks);
$this->assertEquals(transform::yesno(1), $preferences->slotmarks->value);
$description = get_string('privacy:preference:slotmarks:yes', 'quiz_overview');
$this->assertEquals($description, $preferences->slotmarks->description);
}
/**
* Preference does exist and is no.
*/
public function test_preference_no(): void {
$this->resetAfterTest();
// Create test user, add some preferences.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
set_user_preference('quiz_overview_slotmarks', 0);
// Switch to admin user (so we can validate preferences of the correct user are being exported).
$this->setAdminUser();
// Export test users preferences.
provider::export_user_preferences($user->id);
$writer = writer::with_context(\context_system::instance());
$this->assertTrue($writer->has_any_data());
$preferences = $writer->get_user_preferences('quiz_overview');
$this->assertNotEmpty($preferences->slotmarks);
$this->assertEquals(transform::yesno(0), $preferences->slotmarks->value);
$description = get_string('privacy:preference:slotmarks:no', 'quiz_overview');
$this->assertEquals($description, $preferences->slotmarks->description);
}
}
@@ -0,0 +1,406 @@
<?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 quiz_overview;
use core_question\local\bank\question_version_status;
use mod_quiz\external\submit_question_version;
use mod_quiz\quiz_attempt;
use question_engine;
use mod_quiz\quiz_settings;
use mod_quiz\local\reports\attempts_report;
use quiz_overview_options;
use quiz_overview_report;
use quiz_overview_table;
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/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');
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* Tests for the quiz overview report.
*
* @package quiz_overview
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class report_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
/**
* Data provider for test_report_sql.
*
* @return array the data for the test sub-cases.
*/
public function report_sql_cases(): array {
return [[null], ['csv']]; // Only need to test on or off, not all download types.
}
/**
* Test how the report queries the database.
*
* @param string|null $isdownloading a download type, or null.
* @dataProvider report_sql_cases
*/
public function test_report_sql(?string $isdownloading): void {
global $DB;
$this->resetAfterTest();
// Create a course and a quiz.
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$quizgenerator = $generator->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => $course->id,
'grademethod' => QUIZ_GRADEHIGHEST, 'grade' => 100.0, 'sumgrades' => 10.0,
'attempts' => 10]);
// Add one question.
/** @var core_question_generator $questiongenerator */
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$q = $questiongenerator->create_question('essay', 'plain', ['category' => $cat->id]);
quiz_add_quiz_question($q->id, $quiz, 0 , 10);
// Create some students and enrol them in the course.
$student1 = $generator->create_user();
$student2 = $generator->create_user();
$student3 = $generator->create_user();
$generator->enrol_user($student1->id, $course->id);
$generator->enrol_user($student2->id, $course->id);
$generator->enrol_user($student3->id, $course->id);
// This line is not really necessary for the test asserts below,
// but what it does is add an extra user row returned by
// get_enrolled_with_capabilities_join because of a second enrolment.
// The extra row returned used to make $table->query_db complain
// about duplicate records. So this is really a test that an extra
// student enrolment does not cause duplicate records in this query.
$generator->enrol_user($student2->id, $course->id, null, 'self');
// Also create a user who should not appear in the reports,
// because they have a role with neither 'mod/quiz:attempt'
// nor 'mod/quiz:reviewmyattempts'.
$tutor = $generator->create_user();
$generator->enrol_user($tutor->id, $course->id, 'teacher');
// The test data.
$timestamp = 1234567890;
$attempts = [
[$quiz, $student1, 1, 0.0, quiz_attempt::FINISHED],
[$quiz, $student1, 2, 5.0, quiz_attempt::FINISHED],
[$quiz, $student1, 3, 8.0, quiz_attempt::FINISHED],
[$quiz, $student1, 4, null, quiz_attempt::ABANDONED],
[$quiz, $student1, 5, null, quiz_attempt::IN_PROGRESS],
[$quiz, $student2, 1, null, quiz_attempt::ABANDONED],
[$quiz, $student2, 2, null, quiz_attempt::ABANDONED],
[$quiz, $student2, 3, 7.0, quiz_attempt::FINISHED],
[$quiz, $student2, 4, null, quiz_attempt::ABANDONED],
[$quiz, $student2, 5, null, quiz_attempt::ABANDONED],
];
// Load it in to quiz attempts table.
foreach ($attempts as $attemptdata) {
list($quiz, $student, $attemptnumber, $sumgrades, $state) = $attemptdata;
$timestart = $timestamp + $attemptnumber * 3600;
$quizobj = quiz_settings::create($quiz->id, $student->id);
$quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
// Create the new attempt and initialize the question sessions.
$attempt = quiz_create_attempt($quizobj, $attemptnumber, null, $timestart, false, $student->id);
$attempt = quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timestamp);
$attempt = quiz_attempt_save_started($quizobj, $quba, $attempt);
// Process some responses from the student.
$attemptobj = quiz_attempt::create($attempt->id);
switch ($state) {
case quiz_attempt::ABANDONED:
$attemptobj->process_abandon($timestart + 300, false);
break;
case quiz_attempt::IN_PROGRESS:
// Do nothing.
break;
case quiz_attempt::FINISHED:
// Save answer and finish attempt.
$attemptobj->process_submitted_actions($timestart + 300, false, [
1 => ['answer' => 'My essay by ' . $student->firstname, 'answerformat' => FORMAT_PLAIN]]);
$attemptobj->process_finish($timestart + 600, false);
// Manually grade it.
$quba = $attemptobj->get_question_usage();
$quba->get_question_attempt(1)->manual_grade(
'Comment', $sumgrades, FORMAT_HTML, $timestart + 1200);
question_engine::save_questions_usage_by_activity($quba);
$update = new \stdClass();
$update->id = $attemptobj->get_attemptid();
$update->timemodified = $timestart + 1200;
$update->sumgrades = $quba->get_total_mark();
$DB->update_record('quiz_attempts', $update);
$attemptobj->get_quizobj()->get_grade_calculator()->recompute_final_grade($student->id);
break;
}
}
// Actually getting the SQL to run is quite hard. Do a minimal set up of
// some objects.
$context = \context_module::instance($quiz->cmid);
$cm = get_coursemodule_from_id('quiz', $quiz->cmid);
$qmsubselect = quiz_report_qm_filter_select($quiz);
$studentsjoins = get_enrolled_with_capabilities_join($context, '',
['mod/quiz:attempt', 'mod/quiz:reviewmyattempts']);
$empty = new \core\dml\sql_join();
// Set the options.
$reportoptions = new quiz_overview_options('overview', $quiz, $cm, null);
$reportoptions->attempts = attempts_report::ENROLLED_ALL;
$reportoptions->onlygraded = true;
$reportoptions->states = [quiz_attempt::IN_PROGRESS, quiz_attempt::OVERDUE, quiz_attempt::FINISHED];
// Now do a minimal set-up of the table class.
$q->slot = 1;
$q->maxmark = 10;
$table = new quiz_overview_table($quiz, $context, $qmsubselect, $reportoptions,
$empty, $studentsjoins, [1 => $q], null);
$table->download = $isdownloading; // Cannot call the is_downloading API, because it gives errors.
$table->define_columns(['fullname']);
$table->sortable(true, 'uniqueid');
$table->define_baseurl(new \moodle_url('/mod/quiz/report.php'));
$table->setup();
// Run the query.
$table->setup_sql_queries($studentsjoins);
$table->query_db(30, false);
// Should be 4 rows, matching count($table->rawdata) tested below.
// The count is only done if not downloading.
if (!$isdownloading) {
$this->assertEquals(4, $table->totalrows);
}
// Verify what was returned: Student 1's best and in progress attempts.
// Student 2's finshed attempt, and Student 3 with no attempt.
// The array key is {student id}#{attempt number}.
$this->assertEquals(4, count($table->rawdata));
$this->assertArrayHasKey($student1->id . '#3', $table->rawdata);
$this->assertEquals(1, $table->rawdata[$student1->id . '#3']->gradedattempt);
$this->assertArrayHasKey($student1->id . '#3', $table->rawdata);
$this->assertEquals(0, $table->rawdata[$student1->id . '#5']->gradedattempt);
$this->assertArrayHasKey($student2->id . '#3', $table->rawdata);
$this->assertEquals(1, $table->rawdata[$student2->id . '#3']->gradedattempt);
$this->assertArrayHasKey($student3->id . '#0', $table->rawdata);
$this->assertEquals(0, $table->rawdata[$student3->id . '#0']->gradedattempt);
// Check the calculation of averages.
$averagerow = $table->compute_average_row('overallaverage', $studentsjoins);
$this->assertStringContainsString('75.00', $averagerow['sumgrades']);
$this->assertStringContainsString('75.00', $averagerow['qsgrade1']);
if (!$isdownloading) {
$this->assertStringContainsString('(2)', $averagerow['sumgrades']);
$this->assertStringContainsString('(2)', $averagerow['qsgrade1']);
}
// Ensure that filtering by initial does not break it.
// This involves setting a private properly of the base class, which is
// only really possible using reflection :-(.
$reflectionobject = new \ReflectionObject($table);
while ($parent = $reflectionobject->getParentClass()) {
$reflectionobject = $parent;
}
$prefsproperty = $reflectionobject->getProperty('prefs');
$prefs = $prefsproperty->getValue($table);
$prefs['i_first'] = 'A';
$prefsproperty->setValue($table, $prefs);
list($fields, $from, $where, $params) = $table->base_sql($studentsjoins);
$table->set_count_sql("SELECT COUNT(1) FROM (SELECT $fields FROM $from WHERE $where) temp WHERE 1 = 1", $params);
$table->set_sql($fields, $from, $where, $params);
$table->query_db(30, false);
// Just verify that this does not cause a fatal error.
}
/**
* Bands provider.
* @return array
*/
public function get_bands_count_and_width_provider(): array {
return [
[10, [20, .5]],
[20, [20, 1]],
[30, [15, 2]],
// TODO MDL-55068 Handle bands better when grade is 50.
// [50, [10, 5]],
[100, [20, 5]],
[200, [20, 10]],
];
}
/**
* Test bands.
*
* @dataProvider get_bands_count_and_width_provider
* @param int $grade grade
* @param array $expected
*/
public function test_get_bands_count_and_width(int $grade, array $expected): void {
$this->resetAfterTest();
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(['course' => SITEID, 'grade' => $grade]);
$this->assertEquals($expected, quiz_overview_report::get_bands_count_and_width($quiz));
}
/**
* Test delete_selected_attempts function.
*/
public function test_delete_selected_attempts(): void {
$this->resetAfterTest();
$timestamp = 1234567890;
$timestart = $timestamp + 3600;
// Create a course and a quiz.
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$quizgenerator = $generator->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance([
'course' => $course->id,
'grademethod' => QUIZ_GRADEHIGHEST,
'grade' => 100.0,
'sumgrades' => 10.0,
'attempts' => 10
]);
// Add one question.
/** @var core_question_generator $questiongenerator */
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$q = $questiongenerator->create_question('essay', 'plain', ['category' => $cat->id]);
quiz_add_quiz_question($q->id, $quiz, 0 , 10);
// Create student and enrol them in the course.
// Note: we create two enrolments, to test the problem reported in MDL-67942.
$student = $generator->create_user();
$generator->enrol_user($student->id, $course->id);
$generator->enrol_user($student->id, $course->id, null, 'self');
$context = \context_module::instance($quiz->cmid);
$cm = get_coursemodule_from_id('quiz', $quiz->cmid);
$allowedjoins = get_enrolled_with_capabilities_join($context, '', ['mod/quiz:attempt', 'mod/quiz:reviewmyattempts']);
$quizattemptsreport = new \testable_quiz_attempts_report();
// Create the new attempt and initialize the question sessions.
$quizobj = quiz_settings::create($quiz->id, $student->id);
$quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
$attempt = quiz_create_attempt($quizobj, 1, null, $timestart, false, $student->id);
$attempt = quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timestamp);
$attempt = quiz_attempt_save_started($quizobj, $quba, $attempt);
// Delete the student's attempt.
$quizattemptsreport->delete_selected_attempts($quiz, $cm, [$attempt->id], $allowedjoins);
}
/**
* Test question regrade for selected versions.
*
* @covers ::regrade_question
*/
public function test_regrade_question(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$quiz = $this->create_test_quiz($course);
$cm = get_fast_modinfo($course->id)->get_cm($quiz->cmid);
$context = \context_module::instance($quiz->cmid);
/** @var core_question_generator $questiongenerator */
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
// Create a couple of questions.
$cat = $questiongenerator->create_question_category(['contextid' => $context->id]);
$q = $questiongenerator->create_question('shortanswer', null,
['category' => $cat->id, 'name' => 'Toad scores 0.8']);
// Create a version, the last one draft.
// Sadly, update_question is a bit dodgy, so it can't handle updating the answer score.
$q2 = $questiongenerator->update_question($q, null,
['name' => 'Toad now scores 1.0']);
$toadanswer = $DB->get_record_select('question_answers',
'question = ? AND ' . $DB->sql_compare_text('answer') . ' = ?',
[$q2->id, 'toad'], '*', MUST_EXIST);
$DB->set_field('question_answers', 'fraction', 1, ['id' => $toadanswer->id]);
// Add the question to the quiz.
quiz_add_quiz_question($q2->id, $quiz, 0, 10);
// Attempt the quiz, submitting response 'toad'.
$quizobj = quiz_settings::create($quiz->id);
$attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null);
$attemptobj = quiz_attempt::create($attempt->id);
$attemptobj->process_submitted_actions(time(), false, [1 => ['answer' => 'toad']]);
$attemptobj->process_finish(time(), false);
// We should be using 'always latest' version, which is currently v2, so should be right.
$this->assertEquals(10, $attemptobj->get_question_usage()->get_total_mark());
// Now change the quiz to use fixed version 1.
$slot = $quizobj->get_question($q2->id);
submit_question_version::execute($slot->slotid, 1);
// Regrade.
$report = new quiz_overview_report();
$report->init('overview', 'quiz_overview_settings_form', $quiz, $cm, $course);
$report->regrade_attempt($attempt);
// The mark should now be 8.
$attemptobj = quiz_attempt::create($attempt->id);
$this->assertEquals(8, $attemptobj->get_question_usage()->get_total_mark());
// Now add two more versions, the second of which is draft.
$q3 = $questiongenerator->update_question($q, null,
['name' => 'Toad now scores 0.5']);
$toadanswer = $DB->get_record_select('question_answers',
'question = ? AND ' . $DB->sql_compare_text('answer') . ' = ?',
[$q3->id, 'toad'], '*', MUST_EXIST);
$DB->set_field('question_answers', 'fraction', 0.5, ['id' => $toadanswer->id]);
$q4 = $questiongenerator->update_question($q, null,
['name' => 'Toad now scores 0.3',
'status' => question_version_status::QUESTION_STATUS_DRAFT]);
$toadanswer = $DB->get_record_select('question_answers',
'question = ? AND ' . $DB->sql_compare_text('answer') . ' = ?',
[$q4->id, 'toad'], '*', MUST_EXIST);
$DB->set_field('question_answers', 'fraction', 0.3, ['id' => $toadanswer->id]);
// Now change the quiz back to always latest and regrade again.
submit_question_version::execute($slot->slotid, 0);
$report->regrade_attempt($attempt);
// Score should now be 5, because v3 is the latest non-draft version.
$attemptobj = quiz_attempt::create($attempt->id);
$this->assertEquals(5, $attemptobj->get_question_usage()->get_total_mark());
}
}
+29
View File
@@ -0,0 +1,29 @@
<?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/>.
/**
* Quiz overview report version information.
*
* @package quiz_overview
* @copyright 2008 Jamie Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->component = 'quiz_overview';
+447
View File
@@ -0,0 +1,447 @@
<?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/>.
/**
* Helper functions for the quiz reports.
*
* @package mod_quiz
* @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/lib.php');
require_once($CFG->libdir . '/filelib.php');
use mod_quiz\question\display_options;
/**
* Takes an array of objects and constructs a multidimensional array keyed by
* the keys it finds on the object.
* @param array $datum an array of objects with properties on the object
* including the keys passed as the next param.
* @param array $keys Array of strings with the names of the properties on the
* objects in datum that you want to index the multidimensional array by.
* @param bool $keysunique If there is not only one object for each
* combination of keys you are using you should set $keysunique to true.
* Otherwise all the object will be added to a zero based array. So the array
* returned will have count($keys) + 1 indexs.
* @return array multidimensional array properly indexed.
*/
function quiz_report_index_by_keys($datum, $keys, $keysunique = true) {
if (!$datum) {
return [];
}
$key = array_shift($keys);
$datumkeyed = [];
foreach ($datum as $data) {
if ($keys || !$keysunique) {
$datumkeyed[$data->{$key}][]= $data;
} else {
$datumkeyed[$data->{$key}]= $data;
}
}
if ($keys) {
foreach ($datumkeyed as $datakey => $datakeyed) {
$datumkeyed[$datakey] = quiz_report_index_by_keys($datakeyed, $keys, $keysunique);
}
}
return $datumkeyed;
}
function quiz_report_unindex($datum) {
if (!$datum) {
return $datum;
}
$datumunkeyed = [];
foreach ($datum as $value) {
if (is_array($value)) {
$datumunkeyed = array_merge($datumunkeyed, quiz_report_unindex($value));
} else {
$datumunkeyed[] = $value;
}
}
return $datumunkeyed;
}
/**
* Are there any questions in this quiz?
* @param int $quizid the quiz id.
*/
function quiz_has_questions($quizid) {
global $DB;
return $DB->record_exists('quiz_slots', ['quizid' => $quizid]);
}
/**
* Get the slots of real questions (not descriptions) in this quiz, in order.
* @param stdClass $quiz the quiz.
* @return array of slot => objects with fields
* ->slot, ->id, ->qtype, ->length, ->number, ->maxmark, ->category (for random questions).
*/
function quiz_report_get_significant_questions($quiz) {
$quizobj = mod_quiz\quiz_settings::create($quiz->id);
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
$slots = $structure->get_slots();
$qsbyslot = [];
$number = 1;
foreach ($slots as $slot) {
// Ignore 'questions' of zero length.
if ($slot->length == 0) {
continue;
}
$slotreport = new \stdClass();
$slotreport->slot = $slot->slot;
$slotreport->id = $slot->questionid;
$slotreport->qtype = $slot->qtype;
$slotreport->length = $slot->length;
$slotreport->number = $number;
$slotreport->displaynumber = $slot->displaynumber ?? $number;
$number += $slot->length;
$slotreport->maxmark = $slot->maxmark;
$slotreport->category = $slot->category;
$qsbyslot[$slotreport->slot] = $slotreport;
}
return $qsbyslot;
}
/**
* @param stdClass $quiz the quiz settings.
* @return bool whether, for this quiz, it is possible to filter attempts to show
* only those that gave the final grade.
*/
function quiz_report_can_filter_only_graded($quiz) {
return $quiz->attempts != 1 && $quiz->grademethod != QUIZ_GRADEAVERAGE;
}
/**
* This is a wrapper for {@link quiz_report_grade_method_sql} that takes the whole quiz object instead of just the grading method
* as a param. See definition for {@link quiz_report_grade_method_sql} below.
*
* @param stdClass $quiz
* @param string $quizattemptsalias sql alias for 'quiz_attempts' table
* @return string sql to test if this is an attempt that will contribute towards the grade of the user
*/
function quiz_report_qm_filter_select($quiz, $quizattemptsalias = 'quiza') {
if ($quiz->attempts == 1) {
// This quiz only allows one attempt.
return '';
}
return quiz_report_grade_method_sql($quiz->grademethod, $quizattemptsalias);
}
/**
* Given a quiz grading method return sql to test if this is an
* attempt that will be contribute towards the grade of the user. Or return an
* empty string if the grading method is QUIZ_GRADEAVERAGE and thus all attempts
* contribute to final grade.
*
* @param string $grademethod quiz grading method.
* @param string $quizattemptsalias sql alias for 'quiz_attempts' table
* @return string sql to test if this is an attempt that will contribute towards the graded of the user
*/
function quiz_report_grade_method_sql($grademethod, $quizattemptsalias = 'quiza') {
switch ($grademethod) {
case QUIZ_GRADEHIGHEST :
return "($quizattemptsalias.state = 'finished' AND NOT EXISTS (
SELECT 1 FROM {quiz_attempts} qa2
WHERE qa2.quiz = $quizattemptsalias.quiz AND
qa2.userid = $quizattemptsalias.userid AND
qa2.state = 'finished' AND (
COALESCE(qa2.sumgrades, 0) > COALESCE($quizattemptsalias.sumgrades, 0) OR
(COALESCE(qa2.sumgrades, 0) = COALESCE($quizattemptsalias.sumgrades, 0) AND qa2.attempt < $quizattemptsalias.attempt)
)))";
case QUIZ_GRADEAVERAGE :
return '';
case QUIZ_ATTEMPTFIRST :
return "($quizattemptsalias.state = 'finished' AND NOT EXISTS (
SELECT 1 FROM {quiz_attempts} qa2
WHERE qa2.quiz = $quizattemptsalias.quiz AND
qa2.userid = $quizattemptsalias.userid AND
qa2.state = 'finished' AND
qa2.attempt < $quizattemptsalias.attempt))";
case QUIZ_ATTEMPTLAST :
return "($quizattemptsalias.state = 'finished' AND NOT EXISTS (
SELECT 1 FROM {quiz_attempts} qa2
WHERE qa2.quiz = $quizattemptsalias.quiz AND
qa2.userid = $quizattemptsalias.userid AND
qa2.state = 'finished' AND
qa2.attempt > $quizattemptsalias.attempt))";
}
}
/**
* Get the number of students whose score was in a particular band for this quiz.
* @param number $bandwidth the width of each band.
* @param int $bands the number of bands
* @param int $quizid the quiz id.
* @param \core\dml\sql_join $usersjoins (joins, wheres, params) to get enrolled users
* @return array band number => number of users with scores in that band.
*/
function quiz_report_grade_bands($bandwidth, $bands, $quizid, \core\dml\sql_join $usersjoins = null) {
global $DB;
if (!is_int($bands)) {
debugging('$bands passed to quiz_report_grade_bands must be an integer. (' .
gettype($bands) . ' passed.)', DEBUG_DEVELOPER);
$bands = (int) $bands;
}
if ($usersjoins && !empty($usersjoins->joins)) {
$userjoin = "JOIN {user} u ON u.id = qg.userid
{$usersjoins->joins}";
$usertest = $usersjoins->wheres;
$params = $usersjoins->params;
} else {
$userjoin = '';
$usertest = '1=1';
$params = [];
}
$sql = "
SELECT band, COUNT(1)
FROM (
SELECT FLOOR(qg.grade / :bandwidth) AS band
FROM {quiz_grades} qg
$userjoin
WHERE $usertest AND qg.quiz = :quizid
) subquery
GROUP BY
band
ORDER BY
band";
$params['quizid'] = $quizid;
$params['bandwidth'] = $bandwidth;
$data = $DB->get_records_sql_menu($sql, $params);
// We need to create array elements with values 0 at indexes where there is no element.
$data = $data + array_fill(0, $bands + 1, 0);
ksort($data);
// Place the maximum (perfect grade) into the last band i.e. make last
// band for example 9 <= g <=10 (where 10 is the perfect grade) rather than
// just 9 <= g <10.
$data[$bands - 1] += $data[$bands];
unset($data[$bands]);
// See MDL-60632. When a quiz participant achieves an overall negative grade the chart fails to render.
foreach ($data as $databand => $datanum) {
if ($databand < 0) {
$data["0"] += $datanum; // Add to band 0.
unset($data[$databand]); // Remove entry below 0.
}
}
return $data;
}
function quiz_report_highlighting_grading_method($quiz, $qmsubselect, $qmfilter) {
if ($quiz->attempts == 1) {
return '<p>' . get_string('onlyoneattemptallowed', 'quiz_overview') . '</p>';
} else if (!$qmsubselect) {
return '<p>' . get_string('allattemptscontributetograde', 'quiz_overview') . '</p>';
} else if ($qmfilter) {
return '<p>' . get_string('showinggraded', 'quiz_overview') . '</p>';
} else {
return '<p>' . get_string('showinggradedandungraded', 'quiz_overview',
'<span class="gradedattempt">' . quiz_get_grading_option_name($quiz->grademethod) .
'</span>') . '</p>';
}
}
/**
* Get the feedback text for a grade on this quiz. The feedback is
* processed ready for display.
*
* @param float $grade a grade on this quiz.
* @param int $quizid the id of the quiz object.
* @return string the comment that corresponds to this grade (empty string if there is not one.
*/
function quiz_report_feedback_for_grade($grade, $quizid, $context) {
global $DB;
static $feedbackcache = [];
if (!isset($feedbackcache[$quizid])) {
$feedbackcache[$quizid] = $DB->get_records('quiz_feedback', ['quizid' => $quizid]);
}
// With CBM etc, it is possible to get -ve grades, which would then not match
// any feedback. Therefore, we replace -ve grades with 0.
$grade = max($grade, 0);
$feedbacks = $feedbackcache[$quizid];
$feedbackid = 0;
$feedbacktext = '';
$feedbacktextformat = FORMAT_MOODLE;
foreach ($feedbacks as $feedback) {
if ($feedback->mingrade <= $grade && $grade < $feedback->maxgrade) {
$feedbackid = $feedback->id;
$feedbacktext = $feedback->feedbacktext;
$feedbacktextformat = $feedback->feedbacktextformat;
break;
}
}
// Clean the text, ready for display.
$formatoptions = new stdClass();
$formatoptions->noclean = true;
$feedbacktext = file_rewrite_pluginfile_urls($feedbacktext, 'pluginfile.php',
$context->id, 'mod_quiz', 'feedback', $feedbackid);
$feedbacktext = format_text($feedbacktext, $feedbacktextformat, $formatoptions);
return $feedbacktext;
}
/**
* Format a number as a percentage out of $quiz->sumgrades
* @param number $rawgrade the mark to format.
* @param stdClass $quiz the quiz settings
* @param bool $round whether to round the results ot $quiz->decimalpoints.
*/
function quiz_report_scale_summarks_as_percentage($rawmark, $quiz, $round = true) {
if ($quiz->sumgrades == 0) {
return '';
}
if (!is_numeric($rawmark)) {
return $rawmark;
}
$mark = $rawmark * 100 / $quiz->sumgrades;
if ($round) {
$mark = quiz_format_grade($quiz, $mark);
}
return get_string('percents', 'moodle', $mark);
}
/**
* Returns an array of reports to which the current user has access to.
* @return array reports are ordered as they should be for display in tabs.
*/
function quiz_report_list($context) {
global $DB;
static $reportlist = null;
if (!is_null($reportlist)) {
return $reportlist;
}
$reports = $DB->get_records('quiz_reports', null, 'displayorder DESC', 'name, capability');
$reportdirs = core_component::get_plugin_list('quiz');
// Order the reports tab in descending order of displayorder.
$reportcaps = [];
foreach ($reports as $key => $report) {
if (array_key_exists($report->name, $reportdirs)) {
$reportcaps[$report->name] = $report->capability;
}
}
// Add any other reports, which are on disc but not in the DB, on the end.
foreach ($reportdirs as $reportname => $notused) {
if (!isset($reportcaps[$reportname])) {
$reportcaps[$reportname] = null;
}
}
$reportlist = [];
foreach ($reportcaps as $name => $capability) {
if (empty($capability)) {
$capability = 'mod/quiz:viewreports';
}
if (has_capability($capability, $context)) {
$reportlist[] = $name;
}
}
return $reportlist;
}
/**
* Create a filename for use when downloading data from a quiz report. It is
* expected that this will be passed to flexible_table::is_downloading, which
* cleans the filename of bad characters and adds the file extension.
* @param string $report the type of report.
* @param string $courseshortname the course shortname.
* @param string $quizname the quiz name.
* @return string the filename.
*/
function quiz_report_download_filename($report, $courseshortname, $quizname) {
return $courseshortname . '-' . format_string($quizname, true) . '-' . $report;
}
/**
* Get the default report for the current user.
* @param stdClass $context the quiz context.
*/
function quiz_report_default_report($context) {
$reports = quiz_report_list($context);
return reset($reports);
}
/**
* Generate a message saying that this quiz has no questions, with a button to
* go to the edit page, if the user has the right capability.
* @param stdClass $quiz the quiz settings.
* @param stdClass $cm the course_module object.
* @param stdClass $context the quiz context.
* @return string HTML to output.
*/
function quiz_no_questions_message($quiz, $cm, $context) {
global $OUTPUT;
$output = '';
$output .= $OUTPUT->notification(get_string('noquestions', 'quiz'));
if (has_capability('mod/quiz:manage', $context)) {
$output .= $OUTPUT->single_button(new moodle_url('/mod/quiz/edit.php',
['cmid' => $cm->id]), get_string('editquiz', 'quiz'), 'get');
}
return $output;
}
/**
* Should the grades be displayed in this report. That depends on the quiz
* display options, and whether the quiz is graded.
* @param stdClass $quiz the quiz settings.
* @param context $context the quiz context.
* @return bool
*/
function quiz_report_should_show_grades($quiz, context $context) {
if ($quiz->timeclose && time() > $quiz->timeclose) {
$when = display_options::AFTER_CLOSE;
} else {
$when = display_options::LATER_WHILE_OPEN;
}
$reviewoptions = display_options::make_from_quiz($quiz, $when);
return quiz_has_grades($quiz) &&
($reviewoptions->marks >= question_display_options::MARK_AND_MAX ||
has_capability('moodle/grade:viewhidden', $context));
}
@@ -0,0 +1,99 @@
<?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/>.
/**
* Privacy Subsystem implementation for quiz_responses.
*
* @package quiz_responses
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace quiz_responses\privacy;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\writer;
use core_privacy\local\request\transform;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/question/engine/questionattempt.php');
/**
* Privacy Subsystem for quiz_responses with user preferences.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\user_preference_provider {
/**
* Returns meta data about this system.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
$collection->add_user_preference('quiz_report_responses_qtext', 'privacy:preference:qtext');
$collection->add_user_preference('quiz_report_responses_resp', 'privacy:preference:resp');
$collection->add_user_preference('quiz_report_responses_right', 'privacy:preference:right');
$collection->add_user_preference('quiz_report_responses_which_tries', 'privacy:preference:which_tries');
return $collection;
}
/**
* Export all user preferences for the plugin.
*
* @param int $userid The userid of the user whose data is to be exported.
*/
public static function export_user_preferences(int $userid) {
$preferences = [
'qtext',
'resp',
'right',
];
foreach ($preferences as $key) {
$preference = get_user_preferences("quiz_report_responses_{$key}", null, $userid);
if (null !== $preference) {
$desc = get_string("privacy:preference:{$key}", 'quiz_responses');
writer::export_user_preference('quiz_responses', $key, transform::yesno($preference), $desc);
}
}
$preference = get_user_preferences("quiz_report_responses_which_tries", null, $userid);
if (null !== $preference) {
switch($preference) {
case \question_attempt::FIRST_TRY:
$value = get_string("privacy:preference:which_tries:first", 'quiz_responses');
break;
case \question_attempt::LAST_TRY:
$value = get_string("privacy:preference:which_tries:last", 'quiz_responses');
break;
case \question_attempt::ALL_TRIES:
$value = get_string("privacy:preference:which_tries:all", 'quiz_responses');
break;
}
$desc = get_string("privacy:preference:which_tries", 'quiz_responses');
writer::export_user_preference('quiz_responses', 'which_tries', $value, $desc);
}
}
}
+39
View File
@@ -0,0 +1,39 @@
<?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/>.
/**
* Post-install script for the quiz responses report.
* @package quiz_responses
* @copyright 2013 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Post-install script
*/
function xmldb_quiz_responses_install() {
global $DB;
$record = new stdClass();
$record->name = 'responses';
$record->displayorder = '9000';
$DB->insert_record('quiz_reports', $record);
}
@@ -0,0 +1,300 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file defines the quiz responses table for showing first or all tries at a question.
*
* @package quiz_responses
* @copyright 2014 The Open University
* @author Jamie Pratt <me@jamiep.org>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use mod_quiz\quiz_attempt;
defined('MOODLE_INTERNAL') || die();
/**
* This is a table subclass for displaying the quiz responses report, showing first or all tries.
*
* @package quiz_responses
* @copyright 2014 The Open University
* @author Jamie Pratt <me@jamiep.org>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_first_or_all_responses_table extends quiz_last_responses_table {
/**
* The full question usage object for each try shown in report.
*
* @var question_usage_by_activity[]
*/
protected $questionusagesbyactivity;
protected function field_from_extra_data($tablerow, $slot, $field) {
$questionattempt = $this->get_question_attempt($tablerow->usageid, $slot);
switch($field) {
case 'questionsummary' :
return $questionattempt->get_question_summary();
case 'responsesummary' :
return $this->get_summary_after_try($tablerow, $slot);
case 'rightanswer' :
return $questionattempt->get_right_answer_summary();
default :
throw new coding_exception('Unknown question attempt field.');
}
}
protected function load_extra_data() {
if (count($this->rawdata) === 0) {
return;
}
$qubaids = $this->get_qubaids_condition();
$dm = new question_engine_data_mapper();
$this->questionusagesbyactivity = $dm->load_questions_usages_by_activity($qubaids);
// Insert an extra field in attempt data and extra rows where necessary.
$newrawdata = [];
foreach ($this->rawdata as $attempt) {
if (!isset($this->questionusagesbyactivity[$attempt->usageid])) {
// This is a user without attempts.
$attempt->try = 0;
$attempt->lasttryforallparts = true;
$newrawdata[] = $attempt;
continue;
}
// We have an attempt, which may require several rows.
$maxtriesinanyslot = 1;
foreach ($this->questionusagesbyactivity[$attempt->usageid]->get_slots() as $slot) {
$tries = $this->get_no_of_tries($attempt, $slot);
$maxtriesinanyslot = max($maxtriesinanyslot, $tries);
}
for ($try = 1; $try <= $maxtriesinanyslot; $try++) {
$newtablerow = clone($attempt);
$newtablerow->lasttryforallparts = ($try == $maxtriesinanyslot);
if ($try !== $maxtriesinanyslot) {
$newtablerow->state = quiz_attempt::IN_PROGRESS;
}
$newtablerow->try = $try;
$newrawdata[] = $newtablerow;
if ($this->options->whichtries == question_attempt::FIRST_TRY) {
break;
}
}
}
$this->rawdata = $newrawdata;
}
/**
* Return the question attempt object.
*
* @param int $questionusagesid
* @param int $slot
* @return question_attempt
*/
protected function get_question_attempt($questionusagesid, $slot) {
return $this->questionusagesbyactivity[$questionusagesid]->get_question_attempt($slot);
}
/**
* Find the state for $slot given after this try.
*
* @param stdClass $tablerow row data
* @param int $slot Slot number.
* @return question_state The question state after the attempt.
*/
protected function slot_state($tablerow, $slot) {
$qa = $this->get_question_attempt($tablerow->usageid, $slot);
$submissionsteps = $qa->get_steps_with_submitted_response_iterator();
$step = $submissionsteps[$tablerow->try];
if ($step === null) {
return null;
}
if ($this->is_last_try($tablerow, $slot, $tablerow->try)) {
// If this is the last try then the step with the try data does not contain the correct state. We need to
// use the last step's state, after the attempt has been finished.
return $qa->get_state();
}
return $step->get_state();
}
/**
* Get the summary of the response after the try.
*
* @param stdClass $tablerow row data
* @param int $slot Slot number.
* @return string summary for the question after this try.
*/
public function get_summary_after_try($tablerow, $slot) {
$qa = $this->get_question_attempt($tablerow->usageid, $slot);
if (!($qa->get_question(false) instanceof question_manually_gradable)) {
// No responses, and we cannot call summarise_response below.
return null;
}
$submissionsteps = $qa->get_steps_with_submitted_response_iterator();
$step = $submissionsteps[$tablerow->try];
if ($step === null) {
return null;
}
$qtdata = $step->get_qt_data();
return $qa->get_question()->summarise_response($qtdata);
}
/**
* Has this question usage been flagged?
*
* @param int $questionusageid Question usage id.
* @param int $slot Slot number
* @return bool Has it been flagged?
*/
protected function is_flagged($questionusageid, $slot) {
return $this->get_question_attempt($questionusageid, $slot)->is_flagged();
}
/**
* The grade for this slot after this try.
*
* @param stdClass $tablerow attempt data from db.
* @param int $slot Slot number.
* @return float The fraction.
*/
protected function slot_fraction($tablerow, $slot) {
$qa = $this->get_question_attempt($tablerow->usageid, $slot);
$submissionsteps = $qa->get_steps_with_submitted_response_iterator();
$step = $submissionsteps[$tablerow->try];
if ($step === null) {
return null;
}
if ($this->is_last_try($tablerow, $slot, $tablerow->try)) {
// If this is the last try then the step with the try data does not contain the correct fraction. We need to
// use the last step's fraction, after the attempt has been finished.
return $qa->get_fraction();
}
return $step->get_fraction();
}
/**
* Is this the last try in the question attempt?
*
* @param stdClass $tablerow attempt data from db.
* @param int $slot Slot number
* @param int $tryno try no
* @return bool Is it the last try?
*/
protected function is_last_try($tablerow, $slot, $tryno) {
return $tryno == $this->get_no_of_tries($tablerow, $slot);
}
/**
* How many tries were attempted at this question in this slot, during this usage?
*
* @param stdClass $tablerow attempt data from db.
* @param int $slot Slot number
* @return int the number of tries in the question attempt for slot $slot.
*/
public function get_no_of_tries($tablerow, $slot) {
return count($this->get_question_attempt($tablerow->usageid, $slot)->get_steps_with_submitted_response_iterator());
}
/**
* What is the step no this try was seen in?
*
* @param int $questionusageid The question usage id.
* @param int $slot Slot number
* @param int $tryno Try no
* @return int the step no or zero if not found
*/
protected function step_no_for_try($questionusageid, $slot, $tryno) {
$qa = $this->get_question_attempt($questionusageid, $slot);
return $qa->get_steps_with_submitted_response_iterator()->step_no_for_try($tryno);
}
public function col_checkbox($tablerow) {
if ($tablerow->try != 1) {
return '';
} else {
return parent::col_checkbox($tablerow);
}
}
/**
* Cell value function for email column. This extracts the contents for any cell in the email column from the row data.
*
* @param stdClass $tablerow Row data.
* @return string What to put in the cell for this column, for this row data.
*/
public function col_email($tablerow) {
if ($tablerow->try > 1) {
return '';
} else {
return $tablerow->email;
}
}
/**
* Cell value function for sumgrades column. This extracts the contents for any cell in the sumgrades column from the row data.
*
* @param stdClass $tablerow Row data.
* @return string What to put in the cell for this column, for this row data.
*/
public function col_sumgrades($tablerow) {
if ($tablerow->try == 0) {
// We are showing a user without a quiz attempt.
return '-';
} else if (!$tablerow->lasttryforallparts) {
// There are more rows to come for this quiz attempt, so we will show this later.
return '';
} else {
// Last row for this attempt. Now is the time to show attempt-related data.
return parent::col_sumgrades($tablerow);
}
}
public function col_state($tablerow) {
if ($tablerow->try == 0) {
// We are showing a user without a quiz attempt.
return '-';
} else if (!$tablerow->lasttryforallparts) {
// There are more rows to come for this quiz attempt, so we will show this later.
return '';
} else {
// Last row for this attempt. Now is the time to show attempt-related data.
return parent::col_state($tablerow);
}
}
public function get_row_class($tablerow) {
if ($this->options->whichtries == question_attempt::ALL_TRIES && $tablerow->lasttryforallparts) {
return 'lastrowforattempt';
} else {
return '';
}
}
public function make_review_link($data, $tablerow, $slot) {
if ($this->slot_state($tablerow, $slot) === null) {
return $data;
} else {
return parent::make_review_link($data, $tablerow, $slot);
}
}
}
@@ -0,0 +1,53 @@
<?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/>.
/**
* Strings for component 'quiz_responses', language 'en', branch 'MOODLE_20_STABLE'
*
* @package quiz_responses
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['cannotloadoptions'] = 'Could not load question options';
$string['include'] = 'Include the';
$string['mustselectcols'] = 'You must include something.';
$string['pagesize'] = 'Page size';
$string['pluginname'] = 'Responses';
$string['privacy:preference:qtext'] = 'Whether to show the question text columns.';
$string['privacy:preference:resp'] = 'Whether to show the students\' response columns.';
$string['privacy:preference:right'] = 'Whether to show the correct response columns.';
$string['privacy:preference:which_tries'] = 'Which tries to show responses from.';
$string['privacy:preference:which_tries:first'] = 'The first try at a question during an attempt by a user.';
$string['privacy:preference:which_tries:last'] = 'The last try at a question during an attempt by a user.';
$string['privacy:preference:which_tries:all'] = 'All tries at a question during an attempt by a user.';
$string['questiontext'] = 'question text';
$string['reportresponses'] = 'Responses';
$string['response'] = 'response';
$string['responses'] = 'Responses';
$string['responsesdownload'] = 'Responses download';
$string['responsesfilename'] = 'responses';
$string['responsesoptions'] = 'Responses options';
$string['responsesreport'] = 'Responses report';
$string['responsestitle'] = 'Responses';
$string['responsex'] = 'Response {$a}';
$string['rightanswer'] = 'right answer';
$string['rightanswerx'] = 'Right answer {$a}';
$string['showattempts'] = 'Only show / download attempts';
$string['showthe'] = 'Show the';
$string['summaryofquestiontext'] = 'Summary of the question';
$string['summaryofresponse'] = 'Summary of the response given';
$string['summaryofrightanswer'] = 'Summary of the right answer';
@@ -0,0 +1,147 @@
<?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/>.
use mod_quiz\local\reports\attempts_report_table;
use mod_quiz\quiz_attempt;
/**
* This is a table subclass for displaying the quiz responses report.
*
* @copyright 2008 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_last_responses_table extends attempts_report_table {
/**
* Constructor
* @param stdClass $quiz
* @param context $context
* @param string $qmsubselect
* @param quiz_responses_options $options
* @param \core\dml\sql_join $groupstudentsjoins
* @param \core\dml\sql_join $studentsjoins
* @param array $questions
* @param moodle_url $reporturl
*/
public function __construct($quiz, $context, $qmsubselect, quiz_responses_options $options,
\core\dml\sql_join $groupstudentsjoins, \core\dml\sql_join $studentsjoins, $questions, $reporturl) {
parent::__construct('mod-quiz-report-responses-report', $quiz, $context,
$qmsubselect, $options, $groupstudentsjoins, $studentsjoins, $questions, $reporturl);
}
public function build_table() {
if (!$this->rawdata) {
return;
}
$this->strtimeformat = str_replace(',', ' ', get_string('strftimedatetime'));
parent::build_table();
}
public function col_sumgrades($attempt) {
if ($attempt->state != quiz_attempt::FINISHED) {
return '-';
}
$grade = quiz_rescale_grade($attempt->sumgrades, $this->quiz);
if ($this->is_downloading()) {
return $grade;
}
$gradehtml = '<a href="review.php?q=' . $this->quiz->id . '&amp;attempt=' .
$attempt->attempt . '">' . $grade . '</a>';
return $gradehtml;
}
public function data_col($slot, $field, $attempt) {
if ($attempt->usageid == 0) {
return '-';
}
$value = $this->field_from_extra_data($attempt, $slot, $field);
if (is_null($value)) {
$summary = '-';
} else {
$summary = trim($value);
}
if ($this->is_downloading() && $this->is_downloading() != 'html') {
return $summary;
}
$summary = s($summary);
if ($this->is_downloading() || $field != 'responsesummary') {
return $summary;
}
return $this->make_review_link($summary, $attempt, $slot);
}
/**
* Column text from the extra data loaded in load_extra_data(), before html formatting etc.
*
* @param stdClass $attempt
* @param int $slot
* @param string $field
* @return string
*/
protected function field_from_extra_data($attempt, $slot, $field) {
if (!isset($this->lateststeps[$attempt->usageid][$slot])) {
return '-';
}
return $this->lateststeps[$attempt->usageid][$slot]->$field;
}
public function other_cols($colname, $attempt) {
if (preg_match('/^question(\d+)$/', $colname, $matches)) {
return $this->data_col($matches[1], 'questionsummary', $attempt);
} else if (preg_match('/^response(\d+)$/', $colname, $matches)) {
return $this->data_col($matches[1], 'responsesummary', $attempt);
} else if (preg_match('/^right(\d+)$/', $colname, $matches)) {
return $this->data_col($matches[1], 'rightanswer', $attempt);
} else {
return parent::other_cols($colname, $attempt);
}
}
protected function requires_extra_data() {
return true;
}
protected function is_latest_step_column($column) {
if (preg_match('/^(?:question|response|right)([0-9]+)/', $column, $matches)) {
return $matches[1];
}
return false;
}
/**
* Get any fields that might be needed when sorting on date for a particular slot.
* @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 sql fragment to alias fields.
*/
protected function get_required_latest_state_fields($slot, $alias) {
global $DB;
return $DB->sql_order_by_text("{$alias}.questionsummary") . " AS question{$slot},
" . $DB->sql_order_by_text("{$alias}.rightanswer") . " AS right{$slot},
" . $DB->sql_order_by_text("{$alias}.responsesummary") . " AS response{$slot}";
}
}
+193
View File
@@ -0,0 +1,193 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file defines the quiz responses report class.
*
* @package quiz_responses
* @copyright 2006 Jean-Michel Vedrine
* @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/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');
require_once($CFG->dirroot . '/mod/quiz/report/responses/first_or_all_responses_table.php');
/**
* Quiz report subclass for the responses report.
*
* This report lists some combination of
* * what question each student saw (this makes sense if random questions were used).
* * the response they gave,
* * and what the right answer is.
*
* Like the overview report, there are options for showing students with/without
* attempts, and for deleting selected attempts.
*
* @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 attempts_report {
public function display($quiz, $cm, $course) {
global $OUTPUT, $DB;
list($currentgroup, $studentsjoins, $groupstudentsjoins, $allowedjoins) = $this->init(
'responses', 'quiz_responses_settings_form', $quiz, $cm, $course);
$options = new quiz_responses_options('responses', $quiz, $cm, $course);
if ($fromform = $this->form->get_data()) {
$options->process_settings_from_form($fromform);
} else {
$options->process_settings_from_params();
}
$this->form->set_data($options->get_initial_form_data());
// Load the required questions.
$questions = quiz_report_get_significant_questions($quiz);
// Prepare for downloading, if applicable.
$courseshortname = format_string($course->shortname, true,
['context' => context_course::instance($course->id)]);
if ($options->whichtries === question_attempt::LAST_TRY) {
$tableclassname = 'quiz_last_responses_table';
} else {
$tableclassname = 'quiz_first_or_all_responses_table';
}
$table = new $tableclassname($quiz, $this->context, $this->qmsubselect,
$options, $groupstudentsjoins, $studentsjoins, $questions, $options->get_url());
$filename = quiz_report_download_filename(get_string('responsesfilename', 'quiz_responses'),
$courseshortname, $quiz->name);
$table->is_downloading($options->download, $filename,
$courseshortname . ' ' . format_string($quiz->name, true));
if ($table->is_downloading()) {
raise_memory_limit(MEMORY_EXTRA);
}
$this->hasgroupstudents = false;
if (!empty($groupstudentsjoins->joins)) {
$sql = "SELECT DISTINCT u.id
FROM {user} u
$groupstudentsjoins->joins
WHERE $groupstudentsjoins->wheres";
$this->hasgroupstudents = $DB->record_exists_sql($sql, $groupstudentsjoins->params);
}
$hasstudents = false;
if (!empty($studentsjoins->joins)) {
$sql = "SELECT DISTINCT u.id
FROM {user} u
$studentsjoins->joins
WHERE $studentsjoins->wheres";
$hasstudents = $DB->record_exists_sql($sql, $studentsjoins->params);
}
if ($options->attempts == self::ALL_WITH) {
// This option is only available to users who can access all groups in
// groups mode, so setting allowed to empty (which means all quiz attempts
// are accessible, is not a security problem.
$allowedjoins = new \core\dml\sql_join();
}
$this->process_actions($quiz, $cm, $currentgroup, $groupstudentsjoins, $allowedjoins, $options->get_url());
$hasquestions = quiz_has_questions($quiz->id);
// Start output.
if (!$table->is_downloading()) {
// Only print headers if not asked to download data.
$this->print_standard_header_and_messages($cm, $course, $quiz,
$options, $currentgroup, $hasquestions, $hasstudents);
// Print the display options.
$this->form->display();
}
$hasstudents = $hasstudents && (!$currentgroup || $this->hasgroupstudents);
if ($hasquestions && ($hasstudents || $options->attempts == self::ALL_WITH)) {
$table->setup_sql_queries($allowedjoins);
if (!$table->is_downloading()) {
// Print information on the grading method.
if ($strattempthighlight = quiz_report_highlighting_grading_method(
$quiz, $this->qmsubselect, $options->onlygraded)) {
echo '<div class="quizattemptcounts">' . $strattempthighlight . '</div>';
}
}
// Define table columns.
$columns = [];
$headers = [];
if (!$table->is_downloading() && $options->checkboxcolumn) {
$columnname = 'checkbox';
$columns[] = $columnname;
$headers[] = $table->checkbox_col_header($columnname);
}
$this->add_user_columns($table, $columns, $headers);
$this->add_state_column($columns, $headers);
if ($table->is_downloading()) {
$this->add_time_columns($columns, $headers);
}
$this->add_grade_columns($quiz, $options->usercanseegrades, $columns, $headers);
foreach ($questions as $id => $question) {
if ($options->showqtext) {
$columns[] = 'question' . $id;
$headers[] = get_string('questionx', 'question', $question->displaynumber);
}
if ($options->showresponses) {
$columns[] = 'response' . $id;
$headers[] = get_string('responsex', 'quiz_responses', $question->displaynumber);
}
if ($options->showright) {
$columns[] = 'right' . $id;
$headers[] = get_string('rightanswerx', 'quiz_responses', $question->displaynumber);
}
}
$table->define_columns($columns);
$table->define_headers($headers);
$table->sortable(true, 'uniqueid');
// Set up the table.
$table->define_baseurl($options->get_url());
$this->configure_user_columns($table);
$table->no_sorting('feedbacktext');
$table->column_class('sumgrades', 'bold');
$table->set_attribute('id', 'responses');
$table->collapsible(true);
$table->out($options->pagesize, true);
}
return true;
}
}
@@ -0,0 +1,65 @@
<?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/>.
use mod_quiz\local\reports\attempts_report;
use mod_quiz\local\reports\attempts_report_options_form;
/**
* Quiz responses report settings form.
*
* @copyright 2008 Jean-Michel Vedrine
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_responses_settings_form extends attempts_report_options_form {
protected function other_preference_fields(MoodleQuickForm $mform) {
$mform->addGroup([
$mform->createElement('advcheckbox', 'qtext', '',
get_string('questiontext', 'quiz_responses')),
$mform->createElement('advcheckbox', 'resp', '',
get_string('response', 'quiz_responses')),
$mform->createElement('advcheckbox', 'right', '',
get_string('rightanswer', 'quiz_responses')),
], 'coloptions', get_string('showthe', 'quiz_responses'), [' '], false);
$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'] != attempts_report::ENROLLED_WITHOUT && !(
$data['qtext'] || $data['resp'] || $data['right'])) {
$errors['coloptions'] = get_string('reportmustselectstate', 'quiz');
}
return $errors;
}
protected function other_attempt_fields(MoodleQuickForm $mform) {
parent::other_attempt_fields($mform);
if (quiz_allows_multiple_tries($this->_customdata['quiz'])) {
$mform->addElement('select', 'whichtries', get_string('whichtries', 'question'), [
question_attempt::FIRST_TRY => get_string('firsttry', 'question'),
question_attempt::LAST_TRY => get_string('lasttry', 'question'),
question_attempt::ALL_TRIES => get_string('alltries', 'question')]
);
$mform->setDefault('whichtries', question_attempt::LAST_TRY);
$mform->disabledIf('whichtries', 'attempts', 'eq', attempts_report::ENROLLED_WITHOUT);
}
}
}
@@ -0,0 +1,120 @@
<?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/>.
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}.
*
* @copyright 2012 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_responses_options extends attempts_report_options {
/** @var bool whether to show the question text columns. */
public $showqtext = false;
/** @var bool whether to show the students' response columns. */
public $showresponses = true;
/** @var bool whether to show the correct response columns. */
public $showright = false;
/** @var bool which try/tries to show responses from. */
public $whichtries = question_attempt::LAST_TRY;
protected function get_url_params() {
$params = parent::get_url_params();
$params['qtext'] = $this->showqtext;
$params['resp'] = $this->showresponses;
$params['right'] = $this->showright;
if (quiz_allows_multiple_tries($this->quiz)) {
$params['whichtries'] = $this->whichtries;
}
return $params;
}
public function get_initial_form_data() {
$toform = parent::get_initial_form_data();
$toform->qtext = $this->showqtext;
$toform->resp = $this->showresponses;
$toform->right = $this->showright;
if (quiz_allows_multiple_tries($this->quiz)) {
$toform->whichtries = $this->whichtries;
}
return $toform;
}
public function setup_from_form_data($fromform) {
parent::setup_from_form_data($fromform);
$this->showqtext = $fromform->qtext;
$this->showresponses = $fromform->resp;
$this->showright = $fromform->right;
if (quiz_allows_multiple_tries($this->quiz)) {
$this->whichtries = $fromform->whichtries;
}
}
public function setup_from_params() {
parent::setup_from_params();
$this->showqtext = optional_param('qtext', $this->showqtext, PARAM_BOOL);
$this->showresponses = optional_param('resp', $this->showresponses, PARAM_BOOL);
$this->showright = optional_param('right', $this->showright, PARAM_BOOL);
if (quiz_allows_multiple_tries($this->quiz)) {
$this->whichtries = optional_param('whichtries', $this->whichtries, PARAM_ALPHA);
}
}
public function setup_from_user_preferences() {
parent::setup_from_user_preferences();
$this->showqtext = get_user_preferences('quiz_report_responses_qtext', $this->showqtext);
$this->showresponses = get_user_preferences('quiz_report_responses_resp', $this->showresponses);
$this->showright = get_user_preferences('quiz_report_responses_right', $this->showright);
if (quiz_allows_multiple_tries($this->quiz)) {
$this->whichtries = get_user_preferences('quiz_report_responses_which_tries', $this->whichtries);
}
}
public function update_user_preferences() {
parent::update_user_preferences();
set_user_preference('quiz_report_responses_qtext', $this->showqtext);
set_user_preference('quiz_report_responses_resp', $this->showresponses);
set_user_preference('quiz_report_responses_right', $this->showright);
if (quiz_allows_multiple_tries($this->quiz)) {
set_user_preference('quiz_report_responses_which_tries', $this->whichtries);
}
}
public function resolve_dependencies() {
parent::resolve_dependencies();
if (!$this->showqtext && !$this->showresponses && !$this->showright) {
// We have to show at least something.
$this->showresponses = true;
}
// 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 != attempts_report::ENROLLED_WITHOUT);
}
}
@@ -0,0 +1,75 @@
@mod @mod_quiz @quiz @quiz_reponses
Feature: Basic use of the Responses report
In order to see how my students are progressing
As a teacher
I need to see all their quiz responses
Background: Using the Responses report
Given the following "users" exist:
| username | firstname | lastname |
| teacher | The | Teacher |
| student1 | Student | One |
| student2 | Student | Two |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "activities" exist:
| activity | name | intro | course | idnumber | preferredbehaviour |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | interactive |
And the following "questions" exist:
| questioncategory | qtype | name | template |
| Test questions | numerical | NQ | pi3tries |
And quiz "Quiz 1" contains the following questions:
| question | page | maxmark | displaynumber |
| NQ | 1 | 3.0 | 1a |
@javascript
Scenario: Report works when there are no attempts
When I am on the "Quiz 1" "mod_quiz > Responses report" page logged in as teacher
Then I should see "Attempts: 0"
And I should see "Nothing to display"
And I set the field "Attempts from" to "enrolled users who have not attempted the quiz"
@javascript
Scenario: Report works when there are attempts
Given user "student1" has started an attempt at quiz "Quiz 1"
And user "student1" has checked answers in their attempt at quiz "Quiz 1":
| slot | response |
| 1 | 1.0 |
And user "student1" has checked answers in their attempt at quiz "Quiz 1":
| slot | response |
| 1 | 3.0 |
And user "student1" has checked answers in their attempt at quiz "Quiz 1":
| slot | response |
| 1 | 3.14 |
And user "student1" has finished an attempt at quiz "Quiz 1"
When I am on the "Quiz 1" "mod_quiz > Responses report" page logged in as teacher
Then I should see "Attempts: 1"
And I should see "Student One"
And I should not see "Student Two"
And I set the field "Attempts from" to "enrolled users who have, or have not, attempted the quiz"
And I set the field "Which tries" to "All tries"
And I should see "Response 1a"
And I press "Show report"
And "Student OneReview attempt" row "Response 1aSort by Response 1a Ascending" column of "responses" table should contain "1.0"
And "Student OneReview attempt" row "Status" column of "responses" table should contain ""
And "Finished" row "Grade/100.00Sort by Grade/100.00 Ascending" column of "responses" table should contain "33.33"
And "Finished" row "Response 1aSort by Response 1a Ascending" column of "responses" table should contain "3.14"
And "Student Two" row "Status" column of "responses" table should contain "-"
And "Student Two" row "Response 1aSort by Response 1a Ascending" column of "responses" table should contain "-"
@javascript
Scenario: Report does not allow strange combinations of options
Given I am on the "Quiz 1" "mod_quiz > Responses report" page logged in as teacher
And the "Which tries" "select" should be enabled
When I set the field "Attempts from" to "enrolled users who have not attempted the quiz"
Then the "Which tries" "select" should be disabled
@@ -0,0 +1,7 @@
slot,type,which,cat,mark,overrides.hint.0.text,overrides.hint.0.format,overrides.hint.1.text,overrides.hint.1.format,overrides.hint.2.text,overrides.hint.2.format,overrides.hint.3.text,overrides.hint.3.format,overrides.shuffleanswers
1,random,,rand,1,"Hint 1",0,"Hint 2",0,"Hint 3",0,"Hint 4",0,0
,shortanswer,,rand,1,"Hint 1",0,"Hint 2",0,"Hint 3",0,"Hint 4",0,0
,numerical,,rand,1,"Hint 1",0,"Hint 2",0,"Hint 3",0,"Hint 4",0,0
2,calculatedsimple,sumwithvariants,maincat,1,"Hint 1",0,"Hint 2",0,"Hint 3",0,"Hint 4",0,0
3,match,,maincat,1,"Hint 1",0,"Hint 2",0,"Hint 3",0,"Hint 4",0,0
4,truefalse,,maincat,1,"",0,"",0,"",0,"",0,0
1 slot type which cat mark overrides.hint.0.text overrides.hint.0.format overrides.hint.1.text overrides.hint.1.format overrides.hint.2.text overrides.hint.2.format overrides.hint.3.text overrides.hint.3.format overrides.shuffleanswers
2 1 random rand 1 Hint 1 0 Hint 2 0 Hint 3 0 Hint 4 0 0
3 shortanswer rand 1 Hint 1 0 Hint 2 0 Hint 3 0 Hint 4 0 0
4 numerical rand 1 Hint 1 0 Hint 2 0 Hint 3 0 Hint 4 0 0
5 2 calculatedsimple sumwithvariants maincat 1 Hint 1 0 Hint 2 0 Hint 3 0 Hint 4 0 0
6 3 match maincat 1 Hint 1 0 Hint 2 0 Hint 3 0 Hint 4 0 0
7 4 truefalse maincat 1 0 0 0 0 0
+2
View File
@@ -0,0 +1,2 @@
testnumber,preferredbehaviour
00,interactive
1 testnumber preferredbehaviour
2 00 interactive
@@ -0,0 +1,10 @@
quizattempt,submittedstepno,slot.1.fraction,slot.1.responsesummary,slot.1.state,slot.2.responsesummary,slot.2.fraction,slot.2.state,slot.3.responsesummary,slot.3.fraction,slot.3.state,slot.4.responsesummary,slot.4.fraction,slot.4.state
1,1,,toad,todo,19.4,1.0,gradedright,"frog -> mammal; cat -> mammal; newt -> amphibian",,todo,True,1.0,gradedright
1,2,1.0,frog,gradedright,,,,"frog -> amphibian; cat -> mammal; newt -> amphibian",0.8888889,gradedright,,,
2,1,,frog,todo,-0.6,,todo,"frog -> amphibian; cat -> mammal; newt -> amphibian",,todo,True,,todo
3,1,0,3.142,gradedwrong,19.4,1.0,gradedright,"frog -> amphibian; cat -> mammal; newt -> amphibian",1.0,gradedright,False,0.0,gradedwrong
4,1,,2,todo,9.4,1.0,gradedright,"frog -> mammal; cat -> amphibian; newt -> amphibian",0.3333333,gradedpartial,False,0.0,gradedwrong
4,2,0,3.142,gradedwrong,,,,,,,,,
5,1,1.0,frog,gradedright,7.1,1.0,gradedright,"frog -> amphibian; cat -> mammal; newt -> amphibian",1.0,gradedright,True,1.0,gradedright
6,1,0,3.1,gradedwrong,9.1,1.0,gradedright,"frog -> amphibian; cat -> mammal; newt -> amphibian",1.0,gradedright,True,1.0,gradedright
7,1,0,2.5,gradedwrong,-0.2,0.0,gradedwrong,"frog -> amphibian; cat -> mammal; newt -> amphibian",1.0,gradedright,False,0,gradedwrong
1 quizattempt submittedstepno slot.1.fraction slot.1.responsesummary slot.1.state slot.2.responsesummary slot.2.fraction slot.2.state slot.3.responsesummary slot.3.fraction slot.3.state slot.4.responsesummary slot.4.fraction slot.4.state
2 1 1 toad todo 19.4 1.0 gradedright frog -> mammal; cat -> mammal; newt -> amphibian todo True 1.0 gradedright
3 1 2 1.0 frog gradedright frog -> amphibian; cat -> mammal; newt -> amphibian 0.8888889 gradedright
4 2 1 frog todo -0.6 todo frog -> amphibian; cat -> mammal; newt -> amphibian todo True todo
5 3 1 0 3.142 gradedwrong 19.4 1.0 gradedright frog -> amphibian; cat -> mammal; newt -> amphibian 1.0 gradedright False 0.0 gradedwrong
6 4 1 2 todo 9.4 1.0 gradedright frog -> mammal; cat -> amphibian; newt -> amphibian 0.3333333 gradedpartial False 0.0 gradedwrong
7 4 2 0 3.142 gradedwrong
8 5 1 1.0 frog gradedright 7.1 1.0 gradedright frog -> amphibian; cat -> mammal; newt -> amphibian 1.0 gradedright True 1.0 gradedright
9 6 1 0 3.1 gradedwrong 9.1 1.0 gradedright frog -> amphibian; cat -> mammal; newt -> amphibian 1.0 gradedright True 1.0 gradedright
10 7 1 0 2.5 gradedwrong -0.2 0.0 gradedwrong frog -> amphibian; cat -> mammal; newt -> amphibian 1.0 gradedright False 0 gradedwrong
+18
View File
@@ -0,0 +1,18 @@
quizattempt,firstname,lastname,randqs.1,responses.1.answer,responses.1.-submit,responses.1.-tryagain,variants.2,responses.2.answer,responses.2.-submit,responses.2.-tryagain,responses.3.frog,responses.3.cat,responses.3.newt,responses.3.-submit,responses.3.-tryagain,responses.4.answer,responses.4.-submit,responses.4.-tryagain,finished
1,John,Jones,shortanswer,toad,1,0,4,19.4,0,0,mammal,mammal,amphibian,0,0,1,0,0,0
1,John,Jones,shortanswer,toad,0,1,4,19.4,0,0,mammal,mammal,amphibian,0,0,1,0,0,0
1,John,Jones,shortanswer,frog,1,0,4,19.4,0,0,mammal,mammal,amphibian,0,0,1,0,0,0
1,John,Jones,shortanswer,toad,0,0,4,19.4,0,0,mammal,mammal,amphibian,1,0,1,0,0,0
1,John,Jones,shortanswer,toad,0,0,4,19.4,0,0,mammal,mammal,amphibian,0,1,1,0,0,0
1,John,Jones,shortanswer,frog,0,0,4,19.4,0,0,amphibian,mammal,amphibian,1,0,1,0,0,1
2,John,Smith,shortanswer,frog,0,0,6,-0.6,0,0,amphibian,mammal,amphibian,0,0,1,0,0,0
3,John,Vicars,numerical,3.142,0,0,4,19.4,0,0,amphibian,mammal,amphibian,0,0,0,0,0,1
4,John,Pacino,numerical,2,1,0,6,9.4,0,0,mammal,amphibian,amphibian,0,0,0,0,0,0
4,John,Pacino,numerical,2,0,1,6,9.4,0,0,mammal,amphibian,amphibian,0,0,0,0,0,0
4,John,Pacino,numerical,3.142,1,0,6,9.4,0,0,mammal,amphibian,amphibian,0,0,0,0,0,0
4,John,Pacino,numerical,3.142,0,0,6,9.4,0,0,mammal,amphibian,amphibian,0,0,0,1,0,0
4,John,Pacino,numerical,3.142,0,0,6,9.4,0,0,mammal,amphibian,amphibian,0,0,0,0,1,0
4,John,Pacino,numerical,3.142,0,0,6,9.4,0,0,mammal,amphibian,amphibian,0,0,1,1,0,1
5,John,Deniro,shortanswer,frog,0,0,9,7.1,0,0,amphibian,mammal,amphibian,0,0,1,0,0,1
6,John,Banks,numerical,3.1,0,0,7,9.1,0,0,amphibian,mammal,amphibian,0,0,1,0,0,1
7,John,Asimov,numerical,2.5,0,0,3,-0.2,0,0,amphibian,mammal,amphibian,0,0,0,0,0,1
1 quizattempt firstname lastname randqs.1 responses.1.answer responses.1.-submit responses.1.-tryagain variants.2 responses.2.answer responses.2.-submit responses.2.-tryagain responses.3.frog responses.3.cat responses.3.newt responses.3.-submit responses.3.-tryagain responses.4.answer responses.4.-submit responses.4.-tryagain finished
2 1 John Jones shortanswer toad 1 0 4 19.4 0 0 mammal mammal amphibian 0 0 1 0 0 0
3 1 John Jones shortanswer toad 0 1 4 19.4 0 0 mammal mammal amphibian 0 0 1 0 0 0
4 1 John Jones shortanswer frog 1 0 4 19.4 0 0 mammal mammal amphibian 0 0 1 0 0 0
5 1 John Jones shortanswer toad 0 0 4 19.4 0 0 mammal mammal amphibian 1 0 1 0 0 0
6 1 John Jones shortanswer toad 0 0 4 19.4 0 0 mammal mammal amphibian 0 1 1 0 0 0
7 1 John Jones shortanswer frog 0 0 4 19.4 0 0 amphibian mammal amphibian 1 0 1 0 0 1
8 2 John Smith shortanswer frog 0 0 6 -0.6 0 0 amphibian mammal amphibian 0 0 1 0 0 0
9 3 John Vicars numerical 3.142 0 0 4 19.4 0 0 amphibian mammal amphibian 0 0 0 0 0 1
10 4 John Pacino numerical 2 1 0 6 9.4 0 0 mammal amphibian amphibian 0 0 0 0 0 0
11 4 John Pacino numerical 2 0 1 6 9.4 0 0 mammal amphibian amphibian 0 0 0 0 0 0
12 4 John Pacino numerical 3.142 1 0 6 9.4 0 0 mammal amphibian amphibian 0 0 0 0 0 0
13 4 John Pacino numerical 3.142 0 0 6 9.4 0 0 mammal amphibian amphibian 0 0 0 1 0 0
14 4 John Pacino numerical 3.142 0 0 6 9.4 0 0 mammal amphibian amphibian 0 0 0 0 1 0
15 4 John Pacino numerical 3.142 0 0 6 9.4 0 0 mammal amphibian amphibian 0 0 1 1 0 1
16 5 John Deniro shortanswer frog 0 0 9 7.1 0 0 amphibian mammal amphibian 0 0 1 0 0 1
17 6 John Banks numerical 3.1 0 0 7 9.1 0 0 amphibian mammal amphibian 0 0 1 0 0 1
18 7 John Asimov numerical 2.5 0 0 3 -0.2 0 0 amphibian mammal amphibian 0 0 0 0 0 1
@@ -0,0 +1,140 @@
<?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/>.
/**
* Privacy provider tests.
*
* @package quiz_responses
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace quiz_responses\privacy;
use core_privacy\local\metadata\collection;
use quiz_responses\privacy\provider;
use core_privacy\local\request\writer;
use core_privacy\local\request\transform;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/question/engine/questionattempt.php');
/**
* Privacy provider tests class.
*
* @package quiz_responses
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_test extends \core_privacy\tests\provider_testcase {
/**
* When no preference exists, there should be no export.
*/
public function test_preference_unset(): void {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
provider::export_user_preferences($USER->id);
$this->assertFalse(writer::with_context(\context_system::instance())->has_any_data());
}
/**
* Preference does exist.
*/
public function test_preference_bool_true(): void {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
set_user_preference('quiz_report_responses_qtext', true);
set_user_preference('quiz_report_responses_resp', true);
set_user_preference('quiz_report_responses_right', true);
provider::export_user_preferences($USER->id);
$writer = writer::with_context(\context_system::instance());
$this->assertTrue($writer->has_any_data());
$preferences = $writer->get_user_preferences('quiz_responses');
$this->assertNotEmpty($preferences->qtext);
$this->assertEquals(transform::yesno(1), $preferences->qtext->value);
$this->assertNotEmpty($preferences->resp);
$this->assertEquals(transform::yesno(1), $preferences->resp->value);
$this->assertNotEmpty($preferences->right);
$this->assertEquals(transform::yesno(1), $preferences->right->value);
}
/**
* Preference does exist.
*/
public function test_preference_bool_false(): void {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
set_user_preference('quiz_report_responses_qtext', false);
set_user_preference('quiz_report_responses_resp', false);
set_user_preference('quiz_report_responses_right', false);
provider::export_user_preferences($USER->id);
$writer = writer::with_context(\context_system::instance());
$this->assertTrue($writer->has_any_data());
$preferences = $writer->get_user_preferences('quiz_responses');
$this->assertNotEmpty($preferences->qtext);
$this->assertEquals(transform::yesno(0), $preferences->qtext->value);
$this->assertNotEmpty($preferences->resp);
$this->assertEquals(transform::yesno(0), $preferences->resp->value);
$this->assertNotEmpty($preferences->right);
$this->assertEquals(transform::yesno(0), $preferences->right->value);
}
/**
* Preference does exist.
*/
public function test_preference_bool_which_first(): void {
global $USER;
$this->resetAfterTest();
$this->setAdminUser();
set_user_preference('quiz_report_responses_which_tries', \question_attempt::FIRST_TRY);
provider::export_user_preferences($USER->id);
$writer = writer::with_context(\context_system::instance());
$this->assertTrue($writer->has_any_data());
$preferences = $writer->get_user_preferences('quiz_responses');
$expected = get_string("privacy:preference:which_tries:first", 'quiz_responses');
$this->assertNotEmpty($preferences->which_tries);
$this->assertEquals($expected, $preferences->which_tries->value);
}
}
@@ -0,0 +1,128 @@
<?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 quiz_responses;
use mod_quiz\quiz_attempt;
use question_bank;
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/statistics/report.php');
require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
/**
* Quiz attempt walk through using data from csv file.
*
* @package quiz_responses
* @category test
* @copyright 2013 The Open University
* @author Jamie Pratt <me@jamiep.org>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class responses_from_steps_walkthrough_test extends \mod_quiz\attempt_walkthrough_from_csv_test {
protected function get_full_path_of_csv_file(string $setname, string $test): string {
// Overridden here so that __DIR__ points to the path of this file.
return __DIR__."/fixtures/{$setname}{$test}.csv";
}
/**
* @var string[] names of the files which contain the test data.
*/
protected $files = ['questions', 'steps', 'responses'];
/**
* Create a quiz add questions to it, walk through quiz attempts and then check results.
*
* @param array $quizsettings settings to override default settings for quiz created by generator. Taken from quizzes.csv.
* @param array $csvdata of data read from csv file "questionsXX.csv", "stepsXX.csv" and "responsesXX.csv".
* @dataProvider get_data_for_walkthrough
*/
public function test_walkthrough_from_csv($quizsettings, $csvdata): void {
$this->resetAfterTest(true);
question_bank::get_qtype('random')->clear_caches_before_testing();
$this->create_quiz($quizsettings, $csvdata['questions']);
$quizattemptids = $this->walkthrough_attempts($csvdata['steps']);
foreach ($csvdata['responses'] as $responsesfromcsv) {
$responses = $this->explode_dot_separated_keys_to_make_subindexs($responsesfromcsv);
if (!isset($quizattemptids[$responses['quizattempt']])) {
throw new \coding_exception("There is no quizattempt {$responses['quizattempt']}!");
}
$this->assert_response_test($quizattemptids[$responses['quizattempt']], $responses);
}
}
protected function assert_response_test($quizattemptid, $responses) {
$quizattempt = quiz_attempt::create($quizattemptid);
foreach ($responses['slot'] as $slot => $tests) {
$slothastests = false;
foreach ($tests as $test) {
if ('' !== $test) {
$slothastests = true;
}
}
if (!$slothastests) {
continue;
}
$qa = $quizattempt->get_question_attempt($slot);
$stepswithsubmit = $qa->get_steps_with_submitted_response_iterator();
$step = $stepswithsubmit[$responses['submittedstepno']];
if (null === $step) {
throw new \coding_exception("There is no step no {$responses['submittedstepno']} ".
"for slot $slot in quizattempt {$responses['quizattempt']}!");
}
foreach (['responsesummary', 'fraction', 'state'] as $column) {
if (isset($tests[$column]) && $tests[$column] != '') {
switch($column) {
case 'responsesummary' :
$actual = $qa->get_question()->summarise_response($step->get_qt_data());
break;
case 'fraction' :
if (count($stepswithsubmit) == $responses['submittedstepno']) {
// If this is the last step then we need to look at the fraction after the question has been
// finished.
$actual = $qa->get_fraction();
} else {
$actual = $step->get_fraction();
}
break;
case 'state' :
if (count($stepswithsubmit) == $responses['submittedstepno']) {
// If this is the last step then we need to look at the state after the question has been
// finished.
$state = $qa->get_state();
} else {
$state = $step->get_state();
}
$actual = substr(get_class($state), strlen('question_state_'));
}
$expected = $tests[$column];
$failuremessage = "Error in quizattempt {$responses['quizattempt']} in $column, slot $slot, ".
"submittedstepno {$responses['submittedstepno']}";
$this->assertEquals($expected, $actual, $failuremessage);
}
}
}
}
}
+30
View File
@@ -0,0 +1,30 @@
<?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/>.
/**
* Quiz responses report version information.
*
* @package quiz_responses
* @copyright 2011 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->component = 'quiz_responses';
@@ -0,0 +1,248 @@
<?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 quiz_statistics;
defined('MOODLE_INTERNAL') || die();
/**
* The statistics calculator returns an instance of this class which contains the calculated statistics.
*
* These quiz statistics calculations are described here :
*
* http://docs.moodle.org/dev/Quiz_statistics_calculations#Test_statistics
*
* @package quiz_statistics
* @copyright 2013 The Open University
* @author James Pratt me@jamiep.org
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class calculated {
/**
* @param string $whichattempts which attempts to use, represented internally as one of the constants as used in
* $quiz->grademethod ie.
* QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
* we calculate stats based on which attempts would affect the grade for each student,
* the default null value is used when constructing an instance whose values will be
* populated from a db record.
*/
public function __construct($whichattempts = null) {
if ($whichattempts !== null) {
$this->whichattempts = $whichattempts;
}
}
/**
* @var int which attempts we are calculating calculate stats from.
*/
public $whichattempts;
/* Following stats all described here : http://docs.moodle.org/dev/Quiz_statistics_calculations#Test_statistics */
public $firstattemptscount = 0;
public $allattemptscount = 0;
public $lastattemptscount = 0;
public $highestattemptscount = 0;
public $firstattemptsavg;
public $allattemptsavg;
public $lastattemptsavg;
public $highestattemptsavg;
public $median;
public $standarddeviation;
public $skewness;
public $kurtosis;
public $cic;
public $errorratio;
public $standarderror;
/**
* @var int time these stats where calculated and cached.
*/
public $timemodified;
/**
* Count of attempts selected by $this->whichattempts
*
* @return int
*/
public function s() {
return $this->get_field('count');
}
/**
* Average grade for the attempts selected by $this->whichattempts
*
* @return float
*/
public function avg() {
return $this->get_field('avg');
}
/**
* Get the right field name to fetch a stat for these attempts that is calculated for more than one $whichattempts (count or
* avg).
*
* @param string $field name of field
* @return int|float
*/
protected function get_field($field) {
$fieldname = calculator::using_attempts_string_id($this->whichattempts).$field;
return $this->{$fieldname};
}
/**
* @param $course
* @param $cm
* @param $quiz
* @return array to display in table or spreadsheet.
*/
public function get_formatted_quiz_info_data($course, $cm, $quiz) {
// You can edit this array to control which statistics are displayed.
$todisplay = ['firstattemptscount' => 'number',
'allattemptscount' => 'number',
'firstattemptsavg' => 'summarks_as_percentage',
'allattemptsavg' => 'summarks_as_percentage',
'lastattemptsavg' => 'summarks_as_percentage',
'highestattemptsavg' => 'summarks_as_percentage',
'median' => 'summarks_as_percentage',
'standarddeviation' => 'summarks_as_percentage',
'skewness' => 'number_format',
'kurtosis' => 'number_format',
'cic' => 'number_format_percent',
'errorratio' => 'number_format_percent',
'standarderror' => 'summarks_as_percentage'];
// General information about the quiz.
$quizinfo = [];
$quizinfo[get_string('quizname', 'quiz_statistics')] = format_string($quiz->name);
$quizinfo[get_string('coursename', 'quiz_statistics')] = format_string($course->fullname);
if ($cm->idnumber) {
$quizinfo[get_string('idnumbermod')] = $cm->idnumber;
}
if ($quiz->timeopen) {
$quizinfo[get_string('quizopen', 'quiz')] = userdate($quiz->timeopen);
}
if ($quiz->timeclose) {
$quizinfo[get_string('quizclose', 'quiz')] = userdate($quiz->timeclose);
}
if ($quiz->timeopen && $quiz->timeclose) {
$quizinfo[get_string('duration', 'quiz_statistics')] =
format_time($quiz->timeclose - $quiz->timeopen);
}
// The statistics.
foreach ($todisplay as $property => $format) {
if (!isset($this->$property) || !$format) {
continue;
}
$value = $this->$property;
switch ($format) {
case 'summarks_as_percentage':
$formattedvalue = quiz_report_scale_summarks_as_percentage($value, $quiz);
break;
case 'number_format_percent':
$formattedvalue = quiz_format_grade($quiz, $value) . '%';
break;
case 'number_format':
// 2 extra decimal places, since not a percentage,
// and we want the same number of sig figs.
$formattedvalue = format_float($value, $quiz->decimalpoints + 2);
break;
case 'number':
$formattedvalue = $value + 0;
break;
default:
$formattedvalue = $value;
}
$quizinfo[get_string($property, 'quiz_statistics',
calculator::using_attempts_lang_string($this->whichattempts))] = $formattedvalue;
}
return $quizinfo;
}
/**
* @var array of names of properties of this class that are cached in db record.
*/
protected $fieldsindb = ['whichattempts', 'firstattemptscount', 'allattemptscount', 'firstattemptsavg', 'allattemptsavg',
'lastattemptscount', 'highestattemptscount', 'lastattemptsavg', 'highestattemptsavg',
'median', 'standarddeviation', 'skewness',
'kurtosis', 'cic', 'errorratio', 'standarderror'];
/**
* Cache the stats contained in this class.
*
* @param $qubaids \qubaid_condition
*/
public function cache($qubaids) {
global $DB;
$toinsert = new \stdClass();
foreach ($this->fieldsindb as $field) {
$toinsert->{$field} = $this->{$field};
}
$toinsert->hashcode = $qubaids->get_hash_code();
$toinsert->timemodified = time();
// Fix up some dodgy data.
if (isset($toinsert->errorratio) && is_nan($toinsert->errorratio)) {
$toinsert->errorratio = null;
}
if (isset($toinsert->standarderror) && is_nan($toinsert->standarderror)) {
$toinsert->standarderror = null;
}
// Delete older statistics before we save the new ones.
$transaction = $DB->start_delegated_transaction();
$DB->delete_records('quiz_statistics', ['hashcode' => $qubaids->get_hash_code()]);
// Store the data.
$DB->insert_record('quiz_statistics', $toinsert);
$transaction->allow_commit();
}
/**
* Given a record from 'quiz_statistics' table load the data into the properties of this class.
*
* @param $record \stdClass from db.
*/
public function populate_from_record($record) {
foreach ($this->fieldsindb as $field) {
$this->$field = $record->$field;
}
$this->timemodified = $record->timemodified;
}
}
@@ -0,0 +1,294 @@
<?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 quiz_statistics;
defined('MOODLE_INTERNAL') || die();
/**
* Class to calculate and also manage caching of quiz statistics.
*
* These quiz statistics calculations are described here :
*
* http://docs.moodle.org/dev/Quiz_statistics_calculations#Test_statistics
*
* @package quiz_statistics
* @copyright 2013 The Open University
* @author James Pratt me@jamiep.org
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class calculator {
/**
* @var \core\progress\base
*/
protected $progress;
public function __construct(\core\progress\base $progress = null) {
if ($progress === null) {
$progress = new \core\progress\none();
}
$this->progress = $progress;
}
/**
* Compute the quiz statistics.
*
* @param int $quizid the quiz id.
* @param int $whichattempts which attempts to use, represented internally as one of the constants as used in
* $quiz->grademethod ie.
* QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
* we calculate stats based on which attempts would affect the grade for each student.
* @param \core\dml\sql_join $groupstudentsjoins Contains joins, wheres, params for students in this group.
* @param int $p number of positions (slots).
* @param float $sumofmarkvariance sum of mark variance, calculated as part of question statistics
* @return calculated $quizstats The statistics for overall attempt scores.
*/
public function calculate($quizid, $whichattempts, \core\dml\sql_join $groupstudentsjoins, $p, $sumofmarkvariance) {
$this->progress->start_progress('', 3);
$quizstats = new calculated($whichattempts);
$countsandaverages = $this->attempt_counts_and_averages($quizid, $groupstudentsjoins);
$this->progress->progress(1);
foreach ($countsandaverages as $propertyname => $value) {
$quizstats->{$propertyname} = $value;
}
$s = $quizstats->s();
if ($s != 0) {
// Recalculate sql again this time possibly including test for first attempt.
list($fromqa, $whereqa, $qaparams) =
quiz_statistics_attempts_sql($quizid, $groupstudentsjoins, $whichattempts);
$quizstats->median = $this->median($s, $fromqa, $whereqa, $qaparams);
$this->progress->progress(2);
if ($s > 1) {
$powers = $this->sum_of_powers_of_difference_to_mean($quizstats->avg(), $fromqa, $whereqa, $qaparams);
$this->progress->progress(3);
$quizstats->standarddeviation = sqrt($powers->power2 / ($s - 1));
// Skewness.
if ($s > 2) {
// See http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise#Skewness_and_Kurtosis.
$m2 = $powers->power2 / $s;
$m3 = $powers->power3 / $s;
$m4 = $powers->power4 / $s;
$k2 = $s * $m2 / ($s - 1);
$k3 = $s * $s * $m3 / (($s - 1) * ($s - 2));
if ($k2 != 0) {
$quizstats->skewness = $k3 / (pow($k2, 3 / 2));
// Kurtosis.
if ($s > 3) {
$k4 = $s * $s * ((($s + 1) * $m4) - (3 * ($s - 1) * $m2 * $m2)) / (($s - 1) * ($s - 2) * ($s - 3));
$quizstats->kurtosis = $k4 / ($k2 * $k2);
}
if ($p > 1) {
$quizstats->cic = (100 * $p / ($p - 1)) * (1 - ($sumofmarkvariance / $k2));
$quizstats->errorratio = 100 * sqrt(1 - ($quizstats->cic / 100));
$quizstats->standarderror = $quizstats->errorratio *
$quizstats->standarddeviation / 100;
}
}
}
}
$quizstats->cache(quiz_statistics_qubaids_condition($quizid, $groupstudentsjoins, $whichattempts));
}
$this->progress->end_progress();
return $quizstats;
}
/**
* @var int previously, the time after which statistics are automatically recomputed.
* @deprecated since Moodle 4.3. Use of pre-computed stats is no longer time-limited.
* @todo MDL-78091 Final deprecation in Moodle 4.7
*/
const TIME_TO_CACHE = 900; // 15 minutes.
/**
* Load cached statistics from the database.
*
* @param \qubaid_condition $qubaids
* @return calculated|false The statistics for overall attempt scores or false if not cached.
*/
public function get_cached($qubaids) {
global $DB;
$lastcalculatedtime = $this->get_last_calculated_time($qubaids);
if (!$lastcalculatedtime) {
return false;
}
$fromdb = $DB->get_record('quiz_statistics', ['hashcode' => $qubaids->get_hash_code(),
'timemodified' => $lastcalculatedtime]);
$stats = new calculated();
$stats->populate_from_record($fromdb);
return $stats;
}
/**
* Find time of non-expired statistics in the database.
*
* @param $qubaids \qubaid_condition
* @return int|bool Time of cached record that matches this qubaid_condition or false is non found.
*/
public function get_last_calculated_time($qubaids) {
global $DB;
$lastcalculatedtime = $DB->get_field('quiz_statistics', 'COALESCE(MAX(timemodified), 0)',
['hashcode' => $qubaids->get_hash_code()]);
if ($lastcalculatedtime) {
return $lastcalculatedtime;
} else {
return false;
}
}
/**
* Given a particular quiz grading method return a lang string describing which attempts contribute to grade.
*
* Note internally we use the grading method constants to represent which attempts we are calculating statistics for, each
* grading method corresponds to different attempts for each user.
*
* @param int $whichattempts which attempts to use, represented internally as one of the constants as used in
* $quiz->grademethod ie.
* QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
* we calculate stats based on which attempts would affect the grade for each student.
* @return string the appropriate lang string to describe this option.
*/
public static function using_attempts_lang_string($whichattempts) {
return get_string(static::using_attempts_string_id($whichattempts), 'quiz_statistics');
}
/**
* Given a particular quiz grading method return a string id for use as a field name prefix in mdl_quiz_statistics or to
* fetch the appropriate language string describing which attempts contribute to grade.
*
* Note internally we use the grading method constants to represent which attempts we are calculating statistics for, each
* grading method corresponds to different attempts for each user.
*
* @param int $whichattempts which attempts to use, represented internally as one of the constants as used in
* $quiz->grademethod ie.
* QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
* we calculate stats based on which attempts would affect the grade for each student.
* @return string the string id for this option.
*/
public static function using_attempts_string_id($whichattempts) {
switch ($whichattempts) {
case QUIZ_ATTEMPTFIRST :
return 'firstattempts';
case QUIZ_GRADEHIGHEST :
return 'highestattempts';
case QUIZ_ATTEMPTLAST :
return 'lastattempts';
case QUIZ_GRADEAVERAGE :
return 'allattempts';
}
}
/**
* Calculating count and mean of marks for first and ALL attempts by students.
*
* See : http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise
* #Calculating_MEAN_of_grades_for_all_attempts_by_students
* @param int $quizid
* @param \core\dml\sql_join $groupstudentsjoins Contains joins, wheres, params for students in this group.
* @return \stdClass with properties with count and avg with prefixes firstattempts, highestattempts, etc.
*/
protected function attempt_counts_and_averages($quizid, \core\dml\sql_join $groupstudentsjoins) {
global $DB;
$attempttotals = new \stdClass();
foreach (array_keys(quiz_get_grading_options()) as $which) {
list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $groupstudentsjoins, $which);
$fromdb = $DB->get_record_sql("SELECT COUNT(*) AS rcount, AVG(sumgrades) AS average FROM $fromqa WHERE $whereqa",
$qaparams);
$fieldprefix = static::using_attempts_string_id($which);
$attempttotals->{$fieldprefix.'avg'} = $fromdb->average;
$attempttotals->{$fieldprefix.'count'} = $fromdb->rcount;
}
return $attempttotals;
}
/**
* Median mark.
*
* http://docs.moodle.org/dev/Quiz_statistics_calculations#Median_Score
*
* @param $s integer count of attempts
* @param $fromqa string
* @param $whereqa string
* @param $qaparams string
* @return float
*/
protected function median($s, $fromqa, $whereqa, $qaparams) {
global $DB;
if ($s % 2 == 0) {
// An even number of attempts.
$limitoffset = $s / 2 - 1;
$limit = 2;
} else {
$limitoffset = floor($s / 2);
$limit = 1;
}
$sql = "SELECT quiza.id, quiza.sumgrades
FROM $fromqa
WHERE $whereqa
ORDER BY sumgrades";
$medianmarks = $DB->get_records_sql_menu($sql, $qaparams, $limitoffset, $limit);
return array_sum($medianmarks) / count($medianmarks);
}
/**
* Fetch the sum of squared, cubed and to the power 4 differences between sumgrade and it's mean.
*
* Explanation here : http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise
* #Calculating_Standard_Deviation.2C_Skewness_and_Kurtosis_of_grades_for_all_attempts_by_students
*
* @param $mean
* @param $fromqa
* @param $whereqa
* @param $qaparams
* @return stdClass with properties power2, power3, power4
*/
protected function sum_of_powers_of_difference_to_mean($mean, $fromqa, $whereqa, $qaparams) {
global $DB;
$sql = "SELECT
SUM(POWER((quiza.sumgrades - $mean), 2)) AS power2,
SUM(POWER((quiza.sumgrades - $mean), 3)) AS power3,
SUM(POWER((quiza.sumgrades - $mean), 4)) AS power4
FROM $fromqa
WHERE $whereqa";
$params = ['mean1' => $mean, 'mean2' => $mean, 'mean3' => $mean] + $qaparams;
return $DB->get_record_sql($sql, $params, MUST_EXIST);
}
}
@@ -0,0 +1,49 @@
<?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 quiz_statistics\event\observer;
use core\check\performance\debugging;
use quiz_statistics\task\recalculate;
/**
* Event observer for \mod_quiz\event\attempt_submitted
*
* @package quiz_statistics
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @deprecated Since Moodle 4.4 MDL-80099.
* @todo Final deprecation in Moodle 4.8 MDL-80956.
*/
class attempt_submitted {
/**
* Queue an ad-hoc task to recalculate statistics for the quiz.
*
* This will defer running the task for 1 hour, to give other attempts in progress
* a chance to submit.
*
* @param \mod_quiz\event\attempt_submitted $event
* @return void
* @deprecated Since Moodle 4.4 MDL-80099
*/
public static function process(\mod_quiz\event\attempt_submitted $event): void {
debugging('quiz_statistics\event\observer\attempt_submitted event observer has been deprecated in favour of ' .
'the quiz_statistics\hook_callbacks::quiz_attempt_submitted_or_deleted hook callback.', DEBUG_DEVELOPER);
$data = $event->get_data();
recalculate::queue_future_run($data['other']['quizid']);
}
}
@@ -0,0 +1,69 @@
<?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 quiz_statistics;
use core\dml\sql_join;
use mod_quiz\hook\attempt_state_changed;
use mod_quiz\hook\structure_modified;
use mod_quiz\quiz_attempt;
use quiz_statistics\task\recalculate;
/**
* Hook callbacks
*
* @package quiz_statistics
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class hook_callbacks {
/**
* Clear the statistics cache for the quiz where the structure was modified.
*
* @param structure_modified $hook The structure_modified hook containing the new structure.
* @return void
*/
public static function quiz_structure_modified(structure_modified $hook) {
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');
require_once($CFG->dirroot . '/mod/quiz/report/statistics/report.php');
$quiz = $hook->get_structure()->get_quiz();
$qubaids = quiz_statistics_qubaids_condition(
$quiz->id,
new sql_join(),
$quiz->grademethod
);
$report = new \quiz_statistics_report();
$report->clear_cached_data($qubaids);
}
/**
* Queue a statistics recalculation when an attempt is submitted or deleting.
*
* @param attempt_state_changed $hook
* @return bool True if a task was queued.
*/
public static function quiz_attempt_submitted_or_deleted(attempt_state_changed $hook): bool {
$originalattempt = $hook->get_original_attempt();
$updatedattempt = $hook->get_updated_attempt();
if (is_null($updatedattempt) || $updatedattempt->state === quiz_attempt::FINISHED) {
// Only recalculate on deletion or submission.
return recalculate::queue_future_run($originalattempt->quiz);
}
return false;
}
}
@@ -0,0 +1,46 @@
<?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/>.
/**
* Privacy Subsystem implementation for quiz_statistics.
*
* @package quiz_statistics
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace quiz_statistics\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for quiz_statistics implementing null_provider.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,44 @@
<?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 quiz_statistics;
use quiz_statistics\task\recalculate;
/**
* Queue a statistics recalculation when an attempt is deleted.
*
* @package quiz_statistics
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @deprecated Since Moodle 4.4 MDL-80099.
* @todo Final deprecation in Moodle 4.8 MDL-80956.
*/
class quiz_attempt_deleted {
/**
* Queue a recalculation.
*
* @param int $quizid The quiz the attempt belongs to.
* @return void
* @deprecated Since Moodle 4.4 MDL-80099.
*/
public static function callback(int $quizid): void {
debugging('quiz_statistics\quiz_attempt_deleted callback class has been deprecated in favour of ' .
'the quiz_statistics\hook_callbacks::quiz_attempt_submitted_or_deleted hook callback.', DEBUG_DEVELOPER);
recalculate::queue_future_run($quizid);
}
}
@@ -0,0 +1,117 @@
<?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 quiz_statistics\task;
use core\dml\sql_join;
use mod_quiz\quiz_attempt;
use quiz_statistics_report;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');
require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
require_once($CFG->dirroot . '/mod/quiz/report/statistics/report.php');
/**
* Re-calculate question statistics.
*
* @package quiz_statistics
* @copyright 2022 Catalyst IT Australia Pty Ltd
* @author Nathan Nguyen <nathannguyen@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class recalculate extends \core\task\adhoc_task {
/**
* The time to delay queued runs by, to prevent repeated recalculations.
*/
const DELAY = HOURSECS;
/**
* Create a new instance of the task.
*
* This sets the properties so that only one task will be queued at a time for a given quiz.
*
* @param int $quizid
* @return recalculate
*/
public static function instance(int $quizid): recalculate {
$task = new self();
$task->set_component('quiz_statistics');
$task->set_custom_data((object)[
'quizid' => $quizid,
]);
return $task;
}
public function get_name(): string {
return get_string('recalculatetask', 'quiz_statistics');
}
public function execute(): void {
global $DB;
$dateformat = get_string('strftimedatetimeshortaccurate', 'core_langconfig');
$data = $this->get_custom_data();
$quiz = $DB->get_record('quiz', ['id' => $data->quizid]);
if (!$quiz) {
mtrace('Could not find quiz with ID ' . $data->quizid . '.');
return;
}
$course = $DB->get_record('course', ['id' => $quiz->course]);
if (!$course) {
mtrace('Could not find course with ID ' . $quiz->course . '.');
return;
}
$attemptcount = $DB->count_records('quiz_attempts', ['quiz' => $data->quizid, 'state' => quiz_attempt::FINISHED]);
if ($attemptcount === 0) {
mtrace('Could not find any finished attempts for course with ID ' . $data->quizid . '.');
return;
}
mtrace("Re-calculating statistics for quiz {$quiz->name} ({$quiz->id}) " .
"from course {$course->shortname} ({$course->id}) with {$attemptcount} attempts, start time " .
userdate(time(), $dateformat) . " ...");
$qubaids = quiz_statistics_qubaids_condition(
$quiz->id,
new sql_join(),
$quiz->grademethod
);
$report = new quiz_statistics_report();
$report->clear_cached_data($qubaids);
$report->calculate_questions_stats_for_question_bank($quiz->id);
mtrace(' Calculations completed at ' . userdate(time(), $dateformat) . '.');
}
/**
* Queue an instance of this task to happen after a delay.
*
* Multiple events may happen over a short period that require a recalculation. Rather than
* run the recalculation each time, this will queue a single run of the task for a given quiz,
* within the delay period.
*
* @param int $quizid The quiz to run the recalculation for.
* @return bool true of the task was queued.
*/
public static function queue_future_run(int $quizid): bool {
$task = self::instance($quizid);
$task->set_next_run_time(time() + self::DELAY);
return \core\task\manager::queue_adhoc_task($task, true);
}
}
@@ -0,0 +1,53 @@
<?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 quiz_statistics\tests;
/**
* Test helper functions for statistics
*
* @package quiz_statistics
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class statistics_helper {
/**
* Run any ad-hoc recalculation tasks that have been scheduled.
*
* We need a special function to do this as the tasks are deferred by one hour,
* so we need to pass a custom $timestart argument.
*
* @param bool $discardoutput Capture and discard output from executed tasks?
* @return void
*/
public static function run_pending_recalculation_tasks(bool $discardoutput = false): void {
while ($task = \core\task\manager::get_next_adhoc_task(
time() + HOURSECS + 1,
false,
'\quiz_statistics\task\recalculate'
)) {
if ($discardoutput) {
ob_start();
}
$task->execute();
if ($discardoutput) {
ob_end_clean();
}
\core\task\manager::adhoc_task_complete($task);
}
}
}
@@ -0,0 +1,59 @@
<?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 quiz_statistics\tests;
use quiz_statistics\task\recalculate;
/**
* Test methods for statistics recalculations
*
* @package quiz_statistics
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
trait statistics_test_trait {
/**
* Return a user, and a quiz with 2 questions.
*
* @return array [$user, $quiz, $course]
*/
protected function create_test_data(): array {
$this->resetAfterTest(true);
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$course = $generator->create_course();
$quiz = $this->create_test_quiz($course);
$this->add_two_regular_questions($generator->get_plugin_generator('core_question'), $quiz);
return [$user, $quiz, $course];
}
/**
* Assert that a task is queued for a quiz.
*
* Check that the quizid stored in the task's custom data matches the provided quiz,
* and that the run time is in one hour from when the test is being run (within a small margin of error).
*
* @param recalculate $task
* @param \stdClass $quiz
* @return void
*/
protected function assert_task_is_queued_for_quiz(recalculate $task, \stdClass $quiz): void {
$data = $task->get_custom_data();
$this->assertEquals($quiz->id, $data->quizid);
$this->assertEqualsWithDelta(time() + HOURSECS, $task->get_next_run_time(), 1);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?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/>.
/**
* Capability definitions for the quiz statistics report.
*
* @package quiz_statistics
* @copyright 2008 Jamie Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$capabilities = [
'quiz/statistics:view' => [
'captype' => 'read',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'teacher' => CAP_ALLOW,
'editingteacher' => CAP_ALLOW,
'manager' => CAP_ALLOW
],
'clonepermissionsfrom' => 'mod/quiz:viewreports'
]
];
+37
View File
@@ -0,0 +1,37 @@
<?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/>.
/**
* Hook callback definitions for quiz_statistics
*
* @package quiz_statistics
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$callbacks = [
[
'hook' => mod_quiz\hook\structure_modified::class,
'callback' => quiz_statistics\hook_callbacks::class . '::quiz_structure_modified',
'priority' => 500,
],
[
'hook' => mod_quiz\hook\attempt_state_changed::class,
'callback' => quiz_statistics\hook_callbacks::class . '::quiz_attempt_submitted_or_deleted',
'priority' => 500,
],
];
+44
View File
@@ -0,0 +1,44 @@
<?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/>.
/**
* Post-install script for the quiz statistics report.
* @package quiz_statistics
* @copyright 2010 Petr Skoda (http://skodak.org)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Post-install script
*/
function xmldb_quiz_statistics_install() {
global $DB;
$dbman = $DB->get_manager();
$record = new stdClass();
$record->name = 'statistics';
$record->displayorder = 8000;
$record->capability = 'quiz/statistics:view';
if ($dbman->table_exists('quiz_reports')) {
$DB->insert_record('quiz_reports', $record);
} else {
$DB->insert_record('quiz_report', $record);
}
}
+34
View File
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="mod/quiz/report/statistics/db" VERSION="20130920" COMMENT="XMLDB file for Moodle mod/quiz/report/statistics"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<TABLE NAME="quiz_statistics" COMMENT="table to cache results from analysis done in statistics report for quizzes.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="hashcode" TYPE="char" LENGTH="40" NOTNULL="true" SEQUENCE="false" COMMENT="sha1 hash of serialized qubaids_condition class. Unique for every combination of class name and property."/>
<FIELD NAME="whichattempts" TYPE="int" LENGTH="4" NOTNULL="true" SEQUENCE="false" COMMENT="bool used to indicate whether these stats are for all attempts or just for the first."/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="firstattemptscount" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="highestattemptscount" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="lastattemptscount" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="allattemptscount" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="firstattemptsavg" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
<FIELD NAME="highestattemptsavg" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
<FIELD NAME="lastattemptsavg" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
<FIELD NAME="allattemptsavg" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
<FIELD NAME="median" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
<FIELD NAME="standarddeviation" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
<FIELD NAME="skewness" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="10"/>
<FIELD NAME="kurtosis" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
<FIELD NAME="cic" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="10"/>
<FIELD NAME="errorratio" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="10"/>
<FIELD NAME="standarderror" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="10"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>
+42
View File
@@ -0,0 +1,42 @@
<?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/>.
/**
* Post-install script for the quiz statistics report.
*
* @package quiz_statistics
* @copyright 2008 Jamie Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Quiz statistics report upgrade code.
*/
function xmldb_quiz_statistics_upgrade($oldversion) {
// Automatically generated Moodle v4.1.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.2.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.4.0 release upgrade line.
// Put any upgrade step following this.
return true;
}
@@ -0,0 +1,125 @@
<?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/>.
/**
* Strings for component 'quiz_statistics', language 'en', branch 'MOODLE_20_STABLE'
*
* @package quiz_statistics
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['actualresponse'] = 'Actual response';
$string['allattempts'] = 'all attempts';
$string['allattemptsavg'] = 'Average grade of all attempts';
$string['allattemptscount'] = 'Total number of complete graded attempts';
$string['analysisnameonly'] = '"{$a->name}"';
$string['analysisno'] = '({$a->number}) "{$a->name}"';
$string['analysisnovariant'] = '({$a->number}) "{$a->name}" variant {$a->variant}';
$string['analysisofresponses'] = 'Analysis of responses';
$string['analysisofresponsesfor'] = 'Analysis of responses for {$a}';
$string['analysisvariant'] = '"{$a->name}" variant {$a->variant}';
$string['attempts'] = 'Attempts';
$string['attemptsall'] = 'all attempts';
$string['attemptsfirst'] = 'first attempt';
$string['backtoquizreport'] = 'Back to main statistics report page.';
$string['calculatefrom'] = 'Calculate statistics from';
$string['calculatingallstats'] = 'Calculating statistics for quiz, questions and analysing response data';
$string['cic'] = 'Coefficient of internal consistency (for {$a})';
$string['completestatsfilename'] = 'completestats';
$string['count'] = 'Count';
$string['counttryno'] = 'Count Try {$a}';
$string['coursename'] = 'Course name';
$string['detailedanalysis'] = 'More detailed analysis of the responses to this question';
$string['detailedanalysisforvariant'] = 'More detailed analysis of the responses to variant {$a} of this question';
$string['discrimination_index'] = 'Discrimination index';
$string['discriminative_efficiency'] = 'Discriminative efficiency';
$string['downloadeverything'] = 'Download full report as';
$string['duration'] = 'Open for';
$string['effective_weight'] = 'Effective weight';
$string['errordeleting'] = 'Error deleting old {$a} records.';
$string['errormedian'] = 'Error fetching median';
$string['errorpowerquestions'] = 'Error fetching data to calculate variance for question grades';
$string['errorpowers'] = 'Error fetching data to calculate variance for quiz grades';
$string['errorrandom'] = 'Error getting sub item data';
$string['errorratio'] = 'Error ratio (for {$a})';
$string['errorstatisticsquestions'] = 'Error fetching data to calculate statistics for question grades';
$string['facility'] = 'Facility index';
$string['firstattempts'] = 'first attempts';
$string['firstattemptsavg'] = 'Average grade of first attempts';
$string['firstattemptscount'] = 'Number of complete graded first attempts';
$string['frequency'] = 'Frequency';
$string['getstatslockprogress'] = 'Waiting for task in progress. Please wait or try again later.';
$string['getstatslocktimeout'] = 'Statistics calculation lock timeout';
$string['getstatslocktimeoutdesc'] = 'How many seconds to wait for a lock when attempting to perform a statistics calculation for a quiz. This setting primarily exists for testing, do not modify it unless you know what you are doing.';
$string['highestattempts'] = 'highest graded attempt';
$string['highestattemptsavg'] = 'Average grade of highest graded attempts';
$string['intended_weight'] = 'Intended weight';
$string['kurtosis'] = 'Score distribution kurtosis (for {$a})';
$string['lastattempts'] = 'last attempt';
$string['lastattemptsavg'] = 'Average grade of last attempts';
$string['lastcalculated'] = 'Last calculated {$a->lastcalculated} ago there have been {$a->count} attempts since then.';
$string['maximumfacility'] = 'Maximum facility';
$string['median'] = 'Median grade (for {$a})';
$string['medianfacility'] = 'Median facility';
$string['minimumfacility'] = 'Minimum facility';
$string['modelresponse'] = 'Model response';
$string['nameforvariant'] = 'Variant {$a->variant} of {$a->name}';
$string['negcovar'] = 'Negative covariance of grade with total attempt grade';
$string['negcovar_help'] = 'This question\'s grade for this set of attempts on the quiz varies in an opposite way to the overall attempt grade. This means overall attempt grade tends to be below average when the grade for this question is above average and vice-versa.
Our equation for effective question weight cannot be calculated in this case. The calculations for effective question weight for other questions in this quiz are the effective question weight for these questions if the highlighted questions with a negative covariance are given a maximum grade of zero.
If you edit a quiz and give these question(s) with negative covariance a max grade of zero then the effective question weight of these questions will be zero and the real effective question weight of other questions will be as calculated now.';
$string['nogradedattempts'] = 'No attempts have been made at this quiz, or all attempts have questions that need manual grading.';
$string['nostudentsingroup'] = 'There are no students in this group yet';
$string['nostats'] = 'Could not complete the statistics calculation. There may be a long-running calculation in progress. Please try again later.';
$string['optiongrade'] = 'Partial credit';
$string['partofquestion'] = 'Part of question';
$string['pluginname'] = 'Statistics';
$string['privacy:metadata'] = 'Although the Quiz Statistics plugin has database tables, the data is aggregate data and does not describe a unique indidividual.';
$string['position'] = 'Position';
$string['positions'] = 'Position(s)';
$string['questioninformation'] = 'Question information';
$string['questionname'] = 'Question name';
$string['questionnumber'] = 'Q#';
$string['questionstatistics'] = 'Question statistics';
$string['questionstatsfilename'] = 'questionstats';
$string['questiontype'] = 'Question type';
$string['quizinformation'] = 'Quiz information';
$string['quizname'] = 'Quiz name';
$string['quizoverallstatistics'] = 'Quiz overall statistics';
$string['quizstructureanalysis'] = 'Quiz structure analysis';
$string['random_guess_score'] = 'Random guess score';
$string['rangeofvalues'] = 'Range of statistics for these questions';
$string['rangebetween'] = '{$a->min} {$a->max}';
$string['recalculatenow'] = 'Recalculate now';
$string['recalculatetask'] = 'Recalculate question statistics';
$string['reportsettings'] = 'Statistics calculation settings';
$string['response'] = 'Response';
$string['slotstructureanalysis'] = 'Structural analysis for question number {$a}';
$string['skewness'] = 'Score distribution skewness (for {$a})';
$string['standarddeviation'] = 'Standard deviation (for {$a})';
$string['standarddeviationq'] = 'Standard deviation';
$string['standarderror'] = 'Standard error (for {$a})';
$string['statistics'] = 'Statistics';
$string['statisticsreport'] = 'Statistics report';
$string['statisticsreportgraph'] = 'Statistics for question positions';
$string['statistics:view'] = 'View statistics report';
$string['statsfor'] = 'Quiz statistics (for {$a})';
$string['variant'] = 'Variant';
$string['viewanalysis'] = 'View details';
$string['whichtries'] = 'Analyze responses for';
+61
View File
@@ -0,0 +1,61 @@
<?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/>.
/**
* Standard plugin entry points of the quiz statistics report.
*
* @package quiz_statistics
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Serve questiontext files in the question text when they are displayed in this report.
*
* @package quiz_statistics
* @category files
* @param context $previewcontext the quiz context
* @param int $questionid the question id.
* @param context $filecontext the file (question) context
* @param string $filecomponent the component the file belongs to.
* @param string $filearea the file area.
* @param array $args remaining file args.
* @param bool $forcedownload.
* @param array $options additional options affecting the file serving.
*/
function quiz_statistics_question_preview_pluginfile($previewcontext, $questionid,
$filecontext, $filecomponent, $filearea, $args, $forcedownload, $options = []) {
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
list($context, $course, $cm) = get_context_info_array($previewcontext->id);
require_login($course, false, $cm);
// Assume only trusted people can see this report. There is no real way to
// validate questionid, becuase of the complexity of random quetsions.
require_capability('quiz/statistics:view', $context);
$fs = get_file_storage();
$relativepath = implode('/', $args);
$fullpath = "/{$filecontext->id}/{$filecomponent}/{$filearea}/{$relativepath}";
if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
send_file_not_found();
}
send_stored_file($file, 0, 0, $forcedownload, $options);
}
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
<?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/>.
/**
* Settings for the Statisics report
*
* @package quiz_statistics
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
if ($ADMIN->fulltree) {
$settings->add(new admin_setting_configtext(
'quiz_statistics/getstatslocktimeout',
get_string('getstatslocktimeout', 'quiz_statistics'),
get_string('getstatslocktimeoutdesc', 'quiz_statistics'),
MINSECS * 15
));
}
@@ -0,0 +1,62 @@
<?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/>.
/**
* Quiz statistics settings form definition.
*
* @package quiz_statistics
* @copyright 2014 Open University
* @author James Pratt <me@jamiep.org>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/formslib.php');
/**
* This is the settings form for the quiz statistics report.
*
* @package quiz_statistics
* @copyright 2014 Open University
* @author James Pratt <me@jamiep.org>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_statistics_settings_form extends moodleform {
protected function definition() {
$mform = $this->_form;
$mform->addElement('header', 'preferencespage', get_string('reportsettings', 'quiz_statistics'));
$options = [];
foreach (array_keys(quiz_get_grading_options()) as $which) {
$options[$which] = \quiz_statistics\calculator::using_attempts_lang_string($which);
}
$mform->addElement('select', 'whichattempts', get_string('calculatefrom', 'quiz_statistics'), $options);
if (quiz_allows_multiple_tries($this->_customdata['quiz'])) {
$mform->addElement('select', 'whichtries', get_string('whichtries', 'quiz_statistics'), [
question_attempt::FIRST_TRY => get_string('firsttry', 'question'),
question_attempt::LAST_TRY => get_string('lasttry', 'question'),
question_attempt::ALL_TRIES => get_string('alltries', 'question')]
);
$mform->setDefault('whichtries', question_attempt::LAST_TRY);
}
$mform->addElement('submit', 'submitbutton', get_string('preferencessave', 'quiz_overview'));
}
}
@@ -0,0 +1,186 @@
<?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/>.
/**
* Quiz statistics report, table for showing response analysis for a particular question (or sub question).
*
* @package quiz_statistics
* @copyright 2014 Open University
* @author James Pratt <me@jamiep.org>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/tablelib.php');
/**
* This table shows statistics about a particular question.
*
* Lists the responses that students made to this question, with frequency counts.
*
* The responses may be grouped, either by sub-part of the question, or by the
* answer they match.
*
* @copyright 2014 Open University
* @author James Pratt <me@jamiep.org>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_statistics_question_table extends flexible_table {
/** @var stdClass full question object for this question. */
protected $questiondata;
/** @var int no of attempts. */
protected $s;
/**
* Constructor.
*
* @param int $qid the id of the particular question whose statistics are being
* displayed.
*/
public function __construct($qid) {
parent::__construct('mod-quiz-report-statistics-question-table' . $qid);
}
/**
* Set up columns and column names and other table settings.
*
* @param moodle_url $reporturl
* @param stdClass $questiondata
* @param integer $s number of attempts on this question.
* @param \core_question\statistics\responses\analysis_for_question $responseanalysis
*/
public function question_setup($reporturl, $questiondata, $s, $responseanalysis) {
$this->questiondata = $questiondata;
$this->s = $s;
$this->define_baseurl($reporturl->out());
$this->collapsible(false);
$this->set_attribute('class', 'generaltable generalbox boxaligncenter quizresponseanalysis');
// Define the table columns.
$columns = [];
$headers = [];
if ($responseanalysis->has_subparts()) {
$columns[] = 'part';
$headers[] = get_string('partofquestion', 'quiz_statistics');
}
if ($responseanalysis->has_multiple_response_classes()) {
$columns[] = 'responseclass';
$headers[] = get_string('modelresponse', 'quiz_statistics');
if ($responseanalysis->has_actual_responses()) {
$columns[] = 'response';
$headers[] = get_string('actualresponse', 'quiz_statistics');
}
} else {
$columns[] = 'response';
$headers[] = get_string('response', 'quiz_statistics');
}
$columns[] = 'fraction';
$headers[] = get_string('optiongrade', 'quiz_statistics');
if (!$responseanalysis->has_multiple_tries_data()) {
$columns[] = 'totalcount';
$headers[] = get_string('count', 'quiz_statistics');
} else {
$countcolumns = range(1, $responseanalysis->get_maximum_tries());
foreach ($countcolumns as $countcolumn) {
$columns[] = 'trycount'.$countcolumn;
$headers[] = get_string('counttryno', 'quiz_statistics', $countcolumn);
}
}
$columns[] = 'frequency';
$headers[] = get_string('frequency', 'quiz_statistics');
$this->define_columns($columns);
$this->define_headers($headers);
$this->sortable(false);
$this->column_class('fraction', 'numcol');
$this->column_class('count', 'numcol');
$this->column_class('frequency', 'numcol');
$this->column_suppress('part');
$this->column_suppress('responseclass');
parent::setup();
}
/**
* Take a float where 1 represents 100% and return a string representing the percentage.
*
* @param float $fraction The fraction.
* @return string The fraction as a percentage.
*/
protected function format_percentage($fraction) {
return format_float($fraction * 100, 2) . '%';
}
/**
* The mark fraction that this response earns.
* @param stdClass $response containst the data to display.
* @return string contents of this table cell.
*/
protected function col_fraction($response) {
if (is_null($response->fraction)) {
return '';
}
return $this->format_percentage($response->fraction);
}
/**
* The frequency with which this response was given.
* @param stdClass $response contains the data to display.
* @return string contents of this table cell.
*/
protected function col_frequency($response) {
if (!$this->s) {
return '';
}
return $this->format_percentage($response->totalcount / $this->s);
}
/**
* If there is not a col_{column name} method then we call this method. If it returns null
* that means just output the property as in the table raw data. If this returns none null
* then this is the output for this cell of the table.
*
* @param string $colname The name of this column.
* @param stdClass $response The raw data for this row.
* @return string|null The value for this cell of the table or null means use raw data.
*/
public function other_cols($colname, $response) {
if (preg_match('/^trycount(\d+)$/', $colname, $matches)) {
if (isset($response->trycount[$matches[1]])) {
return $response->trycount[$matches[1]];
} else {
return 0;
}
} else if ($colname == 'part' || $colname == 'responseclass' || $colname == 'response') {
return s($response->$colname);
} else {
return null;
}
}
}
@@ -0,0 +1,567 @@
<?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/>.
/**
* Quiz statistics report, table for showing statistics of each question in the quiz.
*
* @package quiz_statistics
* @copyright 2008 Jamie Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/tablelib.php');
use \core_question\statistics\questions\calculated_question_summary;
/**
* This table has one row for each question in the quiz, with sub-rows when
* random questions and variants appear.
*
* There are columns for the various item and position statistics.
*
* @copyright 2008 Jamie Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_statistics_table extends flexible_table {
/** @var stdClass the quiz settings. */
protected $quiz;
/** @var integer the quiz course_module id. */
protected $cmid;
/**
* Constructor.
*/
public function __construct() {
parent::__construct('mod-quiz-report-statistics-report');
}
/**
* Set up the columns and headers and other properties of the table and then
* call flexible_table::setup() method.
*
* @param stdClass $quiz the quiz settings
* @param int $cmid the quiz course_module id
* @param moodle_url $reporturl the URL to redisplay this report.
* @param int $s number of attempts included in the statistics.
*/
public function statistics_setup($quiz, $cmid, $reporturl, $s) {
$this->quiz = $quiz;
$this->cmid = $cmid;
// Define the table columns.
$columns = [];
$headers = [];
$columns[] = 'number';
$headers[] = get_string('questionnumber', 'quiz_statistics');
if (!$this->is_downloading()) {
$columns[] = 'icon';
$headers[] = '';
$columns[] = 'actions';
$headers[] = '';
} else {
$columns[] = 'qtype';
$headers[] = get_string('questiontype', 'quiz_statistics');
}
$columns[] = 'name';
$headers[] = get_string('questionname', 'quiz');
$columns[] = 's';
$headers[] = get_string('attempts', 'quiz_statistics');
if ($s > 1) {
$columns[] = 'facility';
$headers[] = get_string('facility', 'quiz_statistics');
$columns[] = 'sd';
$headers[] = get_string('standarddeviationq', 'quiz_statistics');
}
$columns[] = 'random_guess_score';
$headers[] = get_string('random_guess_score', 'quiz_statistics');
$columns[] = 'intended_weight';
$headers[] = get_string('intended_weight', 'quiz_statistics');
$columns[] = 'effective_weight';
$headers[] = get_string('effective_weight', 'quiz_statistics');
$columns[] = 'discrimination_index';
$headers[] = get_string('discrimination_index', 'quiz_statistics');
$columns[] = 'discriminative_efficiency';
$headers[] = get_string('discriminative_efficiency', 'quiz_statistics');
$this->define_columns($columns);
$this->define_headers($headers);
$this->sortable(false);
$this->column_class('s', 'numcol');
$this->column_class('facility', 'numcol');
$this->column_class('sd', 'numcol');
$this->column_class('random_guess_score', 'numcol');
$this->column_class('intended_weight', 'numcol');
$this->column_class('effective_weight', 'numcol');
$this->column_class('discrimination_index', 'numcol');
$this->column_class('discriminative_efficiency', 'numcol');
// Set up the table.
$this->define_baseurl($reporturl->out());
$this->collapsible(true);
$this->set_attribute('id', 'questionstatistics');
$this->set_attribute('class', 'generaltable generalbox boxaligncenter');
parent::setup();
}
/**
* Open a div tag to wrap statistics table.
*/
public function wrap_html_start() {
// Horrible Moodle 2.0 wide-content work-around.
if (!$this->is_downloading()) {
echo html_writer::start_tag('div', ['id' => 'tablecontainer',
'class' => 'statistics-tablecontainer']);
}
}
/**
* Close a statistics table div.
*/
public function wrap_html_finish() {
if (!$this->is_downloading()) {
echo html_writer::end_tag('div');
}
}
/**
* The question number.
* @param \core_question\statistics\questions\calculated $questionstat stats for the question.
* @return string contents of this table cell.
*/
protected function col_number($questionstat) {
if ($this->is_calculated_question_summary($questionstat)) {
return '';
}
if (!isset($questionstat->question->number)) {
return '';
}
$number = $questionstat->question->number;
if (isset($questionstat->subqdisplayorder)) {
$number = $number . '.'.$questionstat->subqdisplayorder;
}
if ($questionstat->question->qtype != 'random' && !is_null($questionstat->variant)) {
$number = $number . '.'.$questionstat->variant;
}
return $number;
}
/**
* The question type icon.
* @param \core_question\statistics\questions\calculated $questionstat stats for the question.
* @return string contents of this table cell.
*/
protected function col_icon($questionstat) {
if ($this->is_calculated_question_summary($questionstat)) {
return '';
} else {
$questionobject = $questionstat->question;
return print_question_icon($questionobject);
}
}
/**
* Actions that can be performed on the question by this user (e.g. edit or preview).
* @param \core_question\statistics\questions\calculated $questionstat stats for the question.
* @return string contents of this table cell.
*/
protected function col_actions($questionstat) {
if ($this->is_calculated_question_summary($questionstat)) {
return '';
} else if ($questionstat->question->qtype === 'missingtype') {
return '';
} else {
return quiz_question_action_icons($this->quiz, $this->cmid,
$questionstat->question, $this->baseurl, $questionstat->variant);
}
}
/**
* The question type name.
*
* @param \core_question\statistics\questions\calculated $questionstat stats for the question.
* @return string contents of this table cell.
*/
protected function col_qtype($questionstat) {
return question_bank::get_qtype_name($questionstat->question->qtype);
}
/**
* The question name.
*
* @param \core_question\statistics\questions\calculated $questionstat stats for the question.
* @return string contents of this table cell.
*/
protected function col_name($questionstat) {
$name = $questionstat->question->name;
if (!is_null($questionstat->variant)) {
$a = new stdClass();
$a->name = $name;
$a->variant = $questionstat->variant;
$name = get_string('nameforvariant', 'quiz_statistics', $a);
}
if ($this->is_downloading()) {
return $name;
}
$baseurl = new moodle_url($this->baseurl);
if (!is_null($questionstat->variant)) {
if ($questionstat->subquestion) {
// Variant of a sub-question.
$url = new moodle_url($baseurl, ['qid' => $questionstat->questionid, 'variant' => $questionstat->variant]);
$name = html_writer::link($url, $name, ['title' => get_string('detailedanalysisforvariant',
'quiz_statistics',
$questionstat->variant)]);
} else if ($questionstat->slot) {
// Variant of a question in a slot.
$url = new moodle_url($baseurl, ['slot' => $questionstat->slot, 'variant' => $questionstat->variant]);
$name = html_writer::link($url, $name, ['title' => get_string('detailedanalysisforvariant',
'quiz_statistics',
$questionstat->variant)]);
}
} else {
if ($questionstat->subquestion && !$questionstat->get_variants()) {
// Sub question without variants.
$url = new moodle_url($baseurl, ['qid' => $questionstat->questionid]);
$name = html_writer::link($url, $name, ['title' => get_string('detailedanalysis', 'quiz_statistics')]);
} else if ($baseurl->param('slot') === null && $questionstat->slot) {
// Question in a slot, we are not on a page showing structural analysis of one slot,
// we don't want linking on those pages.
$number = $questionstat->question->number;
$israndomquestion = $questionstat->question->qtype == 'random';
$url = new moodle_url($baseurl, ['slot' => $questionstat->slot]);
if ($this->is_calculated_question_summary($questionstat)) {
// Only make the random question summary row name link to the slot structure
// analysis page with specific text to clearly indicate the link to the user.
// Random and variant question rows will render the name without a link to improve clarity
// in the UI.
$name = html_writer::div(get_string('rangeofvalues', 'quiz_statistics'));
} else if (!$israndomquestion && !$questionstat->get_variants() && !$questionstat->get_sub_question_ids()) {
// Question cannot be broken down into sub-questions or variants. Link will show response analysis page.
$name = html_writer::link($url,
$name,
['title' => get_string('detailedanalysis', 'quiz_statistics')]);
}
}
}
if ($this->is_dubious_question($questionstat)) {
$name = html_writer::tag('div', $name, ['class' => 'dubious']);
}
if ($this->is_calculated_question_summary($questionstat)) {
$name .= html_writer::link($url, get_string('viewanalysis', 'quiz_statistics'));
} else if (!empty($questionstat->minmedianmaxnotice)) {
$name = get_string($questionstat->minmedianmaxnotice, 'quiz_statistics') . '<br />' . $name;
}
return $name;
}
/**
* The number of attempts at this question.
*
* @param \core_question\statistics\questions\calculated $questionstat stats for the question.
* @return string contents of this table cell.
*/
protected function col_s($questionstat) {
if ($this->is_calculated_question_summary($questionstat)) {
list($min, $max) = $questionstat->get_min_max_of('s');
$min = $min ?: 0;
$max = $max ?: 0;
return $this->format_range($min, $max);
} else if (!isset($questionstat->s)) {
return 0;
} else {
return $questionstat->s;
}
}
/**
* The facility index (average fraction).
* @param \core_question\statistics\questions\calculated $questionstat stats for the question.
* @return string contents of this table cell.
*/
protected function col_facility($questionstat) {
if ($this->is_calculated_question_summary($questionstat)) {
list($min, $max) = $questionstat->get_min_max_of('facility');
return $this->format_percentage_range($min, $max);
} else if (is_null($questionstat->facility)) {
return '';
} else {
return $this->format_percentage($questionstat->facility);
}
}
/**
* The standard deviation of the fractions.
* @param \core_question\statistics\questions\calculated $questionstat stats for the question.
* @return string contents of this table cell.
*/
protected function col_sd($questionstat) {
if ($this->is_calculated_question_summary($questionstat)) {
list($min, $max) = $questionstat->get_min_max_of('sd');
return $this->format_percentage_range($min, $max);
} else if (is_null($questionstat->sd) || $questionstat->maxmark == 0) {
return '';
} else {
return $this->format_percentage($questionstat->sd / $questionstat->maxmark);
}
}
/**
* An estimate of the fraction a student would get by guessing randomly.
* @param \core_question\statistics\questions\calculated $questionstat stats for the question.
* @return string contents of this table cell.
*/
protected function col_random_guess_score($questionstat) {
if ($this->is_calculated_question_summary($questionstat)) {
list($min, $max) = $questionstat->get_min_max_of('randomguessscore');
return $this->format_percentage_range($min, $max);
} else if (is_null($questionstat->randomguessscore)) {
return '';
} else {
return $this->format_percentage($questionstat->randomguessscore);
}
}
/**
* The intended question weight. Maximum mark for the question as a percentage
* of maximum mark for the quiz. That is, the indended influence this question
* on the student's overall mark.
* @param \core_question\statistics\questions\calculated $questionstat stats for the question.
* @return string contents of this table cell.
*/
protected function col_intended_weight($questionstat) {
if ($this->is_calculated_question_summary($questionstat)) {
list($min, $max) = $questionstat->get_min_max_of('maxmark');
if (is_null($min) && is_null($max)) {
return '';
} else {
$min = quiz_report_scale_summarks_as_percentage($min, $this->quiz);
$max = quiz_report_scale_summarks_as_percentage($max, $this->quiz);
return $this->format_range($min, $max);
}
} else {
return quiz_report_scale_summarks_as_percentage($questionstat->maxmark, $this->quiz);
}
}
/**
* The effective question weight. That is, an estimate of the actual
* influence this question has on the student's overall mark.
* @param \core_question\statistics\questions\calculated $questionstat stats for the question.
* @return string contents of this table cell.
*/
protected function col_effective_weight($questionstat) {
global $OUTPUT;
if ($this->is_calculated_question_summary($questionstat)) {
list($min, $max) = $questionstat->get_min_max_of('effectiveweight');
if (is_null($min) && is_null($max)) {
return '';
} else {
list( , $negcovar) = $questionstat->get_min_max_of('negcovar');
if ($negcovar) {
$min = get_string('negcovar', 'quiz_statistics');
}
return $this->format_range($min, $max);
}
} else if (is_null($questionstat->effectiveweight)) {
return '';
} else if ($questionstat->negcovar) {
$negcovar = get_string('negcovar', 'quiz_statistics');
if (!$this->is_downloading()) {
$negcovar = html_writer::tag('div',
$negcovar . $OUTPUT->help_icon('negcovar', 'quiz_statistics'),
['class' => 'negcovar']);
}
return $negcovar;
} else {
return $this->format_percentage($questionstat->effectiveweight, false);
}
}
/**
* Discrimination index. This is the product moment correlation coefficient
* between the fraction for this question, and the average fraction for the
* other questions in this quiz.
* @param \core_question\statistics\questions\calculated $questionstat stats for the question.
* @return string contents of this table cell.
*/
protected function col_discrimination_index($questionstat) {
if ($this->is_calculated_question_summary($questionstat)) {
list($min, $max) = $questionstat->get_min_max_of('discriminationindex');
if (isset($max)) {
$min = $min ?: 0;
}
if (is_numeric($min)) {
$min = $this->format_percentage($min, false);
}
if (is_numeric($max)) {
$max = $this->format_percentage($max, false);
}
return $this->format_range($min, $max);
} else if (!is_numeric($questionstat->discriminationindex)) {
return $questionstat->discriminationindex;
} else {
return $this->format_percentage($questionstat->discriminationindex, false);
}
}
/**
* Discrimination efficiency, similar to, but different from, the Discrimination index.
*
* @param \core_question\statistics\questions\calculated $questionstat stats for the question.
* @return string contents of this table cell.
*/
protected function col_discriminative_efficiency($questionstat) {
if ($this->is_calculated_question_summary($questionstat)) {
list($min, $max) = $questionstat->get_min_max_of('discriminativeefficiency');
if (!is_numeric($min) && !is_numeric($max)) {
return '';
} else {
return $this->format_percentage_range($min, $max, false);
}
} else if (!is_numeric($questionstat->discriminativeefficiency)) {
return '';
} else {
return $this->format_percentage($questionstat->discriminativeefficiency, false);
}
}
/**
* This method encapsulates the test for wheter a question should be considered dubious.
* @param \core_question\statistics\questions\calculated $questionstat stats for the question.
* @return bool is this question possibly not pulling it's weight?
*/
protected function is_dubious_question($questionstat) {
if ($this->is_calculated_question_summary($questionstat)) {
// We only care about the minimum value here.
// If the minimum value is less than the threshold, then we know that there is at least one value below the threshold.
list($discriminativeefficiency) = $questionstat->get_min_max_of('discriminativeefficiency');
} else {
$discriminativeefficiency = $questionstat->discriminativeefficiency;
}
if (!is_numeric($discriminativeefficiency)) {
return false;
}
return $discriminativeefficiency < 15;
}
/**
* Check if the given stats object is an instance of calculated_question_summary.
*
* @param \core_question\statistics\questions\calculated $questionstat Stats object
* @return bool
*/
protected function is_calculated_question_summary($questionstat) {
return $questionstat instanceof calculated_question_summary;
}
/**
* Format inputs to represent a range between $min and $max.
* This function does not check if $min is less than $max or not.
* If both $min and $max are equal to null, this function returns an empty string.
*
* @param string|null $min The minimum value in the range
* @param string|null $max The maximum value in the range
* @return string
*/
protected function format_range(string $min = null, string $max = null) {
if (is_null($min) && is_null($max)) {
return '';
} else {
$a = new stdClass();
$a->min = $min;
$a->max = $max;
return get_string('rangebetween', 'quiz_statistics', $a);
}
}
/**
* Format a number to a localised percentage with specified decimal points.
*
* @param float $number The number being formatted
* @param bool $fraction An indicator for whether the number is a fraction or is already multiplied by 100
* @param int $decimals Sets the number of decimal points
* @return string
*/
protected function format_percentage(float $number, bool $fraction = true, int $decimals = 2) {
$coefficient = $fraction ? 100 : 1;
return get_string('percents', 'moodle', format_float($number * $coefficient, $decimals));
}
/**
* Format $min and $max to localised percentages and form a string that represents a range between them.
* This function does not check if $min is less than $max or not.
* If both $min and $max are equal to null, this function returns an empty string.
*
* @param float|null $min The minimum value of the range
* @param float|null $max The maximum value of the range
* @param bool $fraction An indicator for whether min and max are a fractions or are already multiplied by 100
* @param int $decimals Sets the number of decimal points
* @return string A formatted string that represents a range between $min to $max.
*/
protected function format_percentage_range(float $min = null, float $max = null, bool $fraction = true, int $decimals = 2) {
if (is_null($min) && is_null($max)) {
return '';
} else {
$min = $min ?: 0;
$max = $max ?: 0;
return $this->format_range(
$this->format_percentage($min, $fraction, $decimals),
$this->format_percentage($max, $fraction, $decimals)
);
}
}
}
@@ -0,0 +1,84 @@
<?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/>.
/**
* Common functions for the quiz statistics report.
*
* @package quiz_statistics
* @copyright 2013 The Open University
* @author James Pratt me@jamiep.org
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use mod_quiz\quiz_attempt;
defined('MOODLE_INTERNAL') || die;
/**
* SQL to fetch relevant 'quiz_attempts' records.
*
* @param int $quizid quiz id to get attempts for
* @param \core\dml\sql_join $groupstudentsjoins Contains joins, wheres, params, empty if not using groups
* @param string $whichattempts which attempts to use, represented internally as one of the constants as used in
* $quiz->grademethod ie.
* QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
* we calculate stats based on which attempts would affect the grade for each student.
* @param bool $includeungraded whether to fetch ungraded attempts too
* @return array FROM and WHERE sql fragments and sql params
*/
function quiz_statistics_attempts_sql($quizid, \core\dml\sql_join $groupstudentsjoins,
$whichattempts = QUIZ_GRADEAVERAGE, $includeungraded = false) {
$fromqa = "{quiz_attempts} quiza ";
$whereqa = 'quiza.quiz = :quizid AND quiza.preview = 0 AND quiza.state = :quizstatefinished';
$qaparams = ['quizid' => (int)$quizid, 'quizstatefinished' => quiz_attempt::FINISHED];
if (!empty($groupstudentsjoins->joins)) {
$fromqa .= "\nJOIN {user} u ON u.id = quiza.userid
{$groupstudentsjoins->joins} ";
$whereqa .= " AND {$groupstudentsjoins->wheres}";
$qaparams += $groupstudentsjoins->params;
}
$whichattemptsql = quiz_report_grade_method_sql($whichattempts);
if ($whichattemptsql) {
$whereqa .= ' AND ' . $whichattemptsql;
}
if (!$includeungraded) {
$whereqa .= ' AND quiza.sumgrades IS NOT NULL';
}
return [$fromqa, $whereqa, $qaparams];
}
/**
* Return a {@link qubaid_condition} from the values returned by {@link quiz_statistics_attempts_sql}.
*
* @param int $quizid
* @param \core\dml\sql_join $groupstudentsjoins Contains joins, wheres, params
* @param string $whichattempts which attempts to use, represented internally as one of the constants as used in
* $quiz->grademethod ie.
* QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
* we calculate stats based on which attempts would affect the grade for each student.
* @param bool $includeungraded
* @return \qubaid_join
*/
function quiz_statistics_qubaids_condition($quizid, \core\dml\sql_join $groupstudentsjoins,
$whichattempts = QUIZ_GRADEAVERAGE, $includeungraded = false) {
list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
$quizid, $groupstudentsjoins, $whichattempts, $includeungraded);
return new qubaid_join($fromqa, 'quiza.uniqueid', $whereqa, $qaparams);
}
@@ -0,0 +1,97 @@
@mod @mod_quiz @quiz @quiz_statistics
Feature: Basic use of the Statistics report
In order to see how my students are progressing
As a teacher
I need to see all their quiz responses
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
| student2 | Student | 2 | student2@example.com |
| student3 | Student | 3 | student3@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
| student3 | C1 | student |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | Question A | This is question 01 |
| Test questions | truefalse | Question B | This is question 02 |
| Test questions | truefalse | Question C | This is question 03 |
And the following "activities" exist:
| activity | name | course | idnumber |
| quiz | Quiz 1 | C1 | quiz1 |
And quiz "Quiz 1" contains the following questions:
| question | page | displaynumber |
| Question A | 1 | |
| Question B | 1 | |
| Question C | 2 | 3c |
@javascript
Scenario: Report works when there are no attempts
When I am on the "Quiz 1" "mod_quiz > Statistics report" page logged in as teacher1
Then I should see "No attempts have been made at this quiz, or all attempts have questions that need manual grading."
And I should not see "Statistics for question positions"
And "Show chart data" "link" should not exist
And user "student1" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | True |
| 2 | False |
| 3 | False |
And user "student2" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | True |
| 2 | True |
| 3 | True |
And user "student3" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | False |
| 2 | False |
| 3 | False |
And I am on the "Quiz 1" "mod_quiz > Statistics report" page logged in as teacher1
And I press "Show report"
And I should not see "No questions have been attempted yet"
And "Show chart data" "link" should exist
# Question A statistics breakdown.
And "1" row "Q#" column of "questionstatistics" table should contain "1"
And "1" row "Question name" column of "questionstatistics" table should contain "Question A"
And "1" row "Attempts" column of "questionstatistics" table should contain "3"
And "1" row "Facility index" column of "questionstatistics" table should contain "66.67%"
And "1" row "Standard deviation" column of "questionstatistics" table should contain "57.74%"
And "1" row "Random guess score" column of "questionstatistics" table should contain "50.00%"
And "1" row "Intended weight" column of "questionstatistics" table should contain "33.33%"
And "1" row "Effective weight" column of "questionstatistics" table should contain "30.90%"
And "1" row "Discrimination index" column of "questionstatistics" table should contain "50.00%"
# Question B statistics breakdown.
And "2" row "Q#" column of "questionstatistics" table should contain "2"
And "2" row "Question name" column of "questionstatistics" table should contain "Question B"
And "2" row "Attempts" column of "questionstatistics" table should contain "3"
And "2" row "Facility index" column of "questionstatistics" table should contain "33.33%"
And "2" row "Standard deviation" column of "questionstatistics" table should contain "57.74%"
And "2" row "Random guess score" column of "questionstatistics" table should contain "50.00%"
And "2" row "Intended weight" column of "questionstatistics" table should contain "33.33%"
And "2" row "Effective weight" column of "questionstatistics" table should contain "34.55%"
And "2" row "Discrimination index" column of "questionstatistics" table should contain "86.60%"
# Question C statistics breakdown.
And "3" row "Q#" column of "questionstatistics" table should contain "3c"
And "3" row "Question name" column of "questionstatistics" table should contain "Question C"
And "3" row "Attempts" column of "questionstatistics" table should contain "3"
And "3" row "Facility index" column of "questionstatistics" table should contain "33.33%"
And "3" row "Standard deviation" column of "questionstatistics" table should contain "57.74%"
And "3" row "Random guess score" column of "questionstatistics" table should contain "50.00%"
And "3" row "Intended weight" column of "questionstatistics" table should contain "33.33%"
And "3" row "Effective weight" column of "questionstatistics" table should contain "34.55%"
And "3" row "Discrimination index" column of "questionstatistics" table should contain "86.60%"
@@ -0,0 +1,48 @@
@mod @mod_quiz @quiz @quiz_statistics
Feature: Robustness of the statistics calculations with missing qusetions
In order to be able to install and uninstall plugins
As a teacher
I need the statistics to work even if a question type has been uninstalled
Scenario: Statistics can be calculated even after a question type has been uninstalled
Given the following "users" exist:
| username |
| teacher |
| student |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher | C1 | editingteacher |
| student | C1 | student |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name |
| Test questions | truefalse | Test question 1 |
| Test questions | truefalse | Test question 2 |
And the following "activities" exist:
| activity | name | course | idnumber |
| quiz | Quiz 1 | C1 | quiz1 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| Test question 1 | 1 |
| Test question 2 | 1 |
And user "student" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | True |
| 2 | True |
And question "Test question 1" is changed to simulate being of an uninstalled type
And question "Test question 2" no longer exists in the database
When I am on the "Quiz 1" "mod_quiz > Statistics report" page logged in as teacher
Then I should see "Quiz structure analysis"
And "1" row "Question name" column of "questionstatistics" table should contain "Missing question"
And "1" row "Attempts" column of "questionstatistics" table should contain "1"
And "1" row "Intended weight" column of "questionstatistics" table should contain "50.00%"
And "2" row "Question name" column of "questionstatistics" table should contain "Missing question"
And "2" row "Attempts" column of "questionstatistics" table should contain "1"
And "2" row "Intended weight" column of "questionstatistics" table should contain "50.00%"
@@ -0,0 +1,58 @@
@mod @mod_quiz @quiz @quiz_statistics
Feature: Statistics calculations with random questions
In order to verify my quizzes are performing well
As a teacher
I need the statistics to analyse any random questions they contain
Background:
Given the following "users" exist:
| username |
| teacher |
| student |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher | C1 | editingteacher |
| student | C1 | student |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
Scenario: Statistics can be calculated for random essays
Given the following "questions" exist:
| questioncategory | qtype | template | name | questiontext |
| Test questions | essay | plain | Test question 1 | |
| Test questions | essay | plain | Test question 2 | |
| Test questions | random | | Random (Test questions) | 0 |
And the following "activities" exist:
| activity | name | course | idnumber |
| quiz | Quiz 1 | C1 | quiz1 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| Random (Test questions) | 1 |
And user "student" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | Here is my wonderful essay |
When I am on the "Quiz 1" "mod_quiz > Statistics report" page logged in as teacher
Then I should see "No attempts have been made at this quiz, or all attempts have questions that need manual grading."
Scenario: View details works for random questions
Given the following "questions" exist:
| questioncategory | qtype | template | name | questiontext |
| Test questions | multichoice | one_of_four | Test question 1 | |
| Test questions | multichoice | one_of_four | Test question 2 | |
| Test questions | random | | Random (Test questions) | 0 |
And the following "activities" exist:
| activity | name | course | idnumber |
| quiz | Quiz 1 | C1 | quiz1 |
And quiz "Quiz 1" contains the following questions:
| question | page |
| Random (Test questions) | 1 |
And user "student" has attempted "Quiz 1" with responses:
| slot | response |
| 1 | One |
When I am on the "Quiz 1" "mod_quiz > Statistics report" page logged in as teacher
And I follow "View details"
Then I should see "Structural analysis for question number 1"
@@ -0,0 +1,21 @@
slot,id,number,category,parent,name,questiontext,questiontextformat,image,generalfeedback,defaultgrade,penalty,qtype,length,stamp,version,hidden,timecreated,timemodified,createdby,modifiedby,maxmark
1,1,1,5,0,questionessay-11,What is the purpose of life?,0,,General feedback,1,0,essay,1,localhost+080922073527+WeUaUK,localhost+080922073527+qxLjv1,0,1222068927,0,2,NULL,1
2,2,2,5,0,questionessay-17,What is the purpose of life?,0,,General feedback,1,0,essay,1,localhost+080922073527+pvCseX,localhost+080922073527+mzy6tY,0,1222068927,0,2,NULL,1
3,3,3,5,0,questionessay-6,What is the purpose of life?,0,,General feedback,1,0,essay,1,localhost+080922073527+Cr3gDO,localhost+080922073527+hnxfTy,0,1222068927,0,2,NULL,1
4,4,4,5,0,questionessay-8,What is the purpose of life?,0,,General feedback,1,0,essay,1,localhost+080922073527+sSq9ln,localhost+080922073527+PGyab3,0,1222068927,0,2,NULL,1
5,5,5,5,0,questionmatch-10,"test question, generated by script",0,,Well done,1,0.1,match,1,localhost+080922073527+oG1i2f,localhost+080922073527+S1UxZy,0,1222068927,0,2,NULL,0
6,6,6,5,0,questionmatch-16,"test question, generated by script",0,,Well done,1,0.1,match,1,localhost+080922073527+vMFHyY,localhost+080922073527+4GZIyQ,0,1222068927,0,2,NULL,0
7,7,7,5,0,questionmatch-18,"test question, generated by script",0,,Well done,1,0.1,match,1,localhost+080922073527+Xkxqn1,localhost+080922073527+xbU6U7,0,1222068927,0,2,NULL,0
8,8,8,5,0,questionmultianswer-12,This question consists of some text with an answer embedded right here {#1} and right after that you will have to deal with this short answer {#2} and finally we have a floating point number {#3}. Note that addresses like www.moodle.org and smileys :-) all work as normal: a) How good is this? {#4} b) What grade would you give it? {#5} Good luck!,0,,General feedback,8,0.1,multianswer,1,localhost+080922073527+0zKgpF,localhost+080922073527+r1gsde,0,1222068927,0,2,NULL,8
9,14,9,5,0,questionmultichoice-13,How old is the sun?,0,,General feedback,1,0.1,multichoice,1,localhost+080922073527+AjIjeV,localhost+080922073527+UhtTLR,0,1222068927,0,2,NULL,1
10,15,10,5,0,questionmultichoice-14,How old is the sun?,0,,General feedback,1,0.1,multichoice,1,localhost+080922073527+IrAqRl,localhost+080922073527+xRfta8,0,1222068927,0,2,NULL,1
11,16,11,5,0,questionmultichoice-3,How old is the sun?,0,,General feedback,1,0.1,multichoice,1,localhost+080922073527+DMGirU,localhost+080922073527+689V8k,0,1222068927,0,2,NULL,1
12,17,12,5,0,questionmultichoice-5,How old is the sun?,0,,General feedback,1,0.1,multichoice,1,localhost+080922073527+wileZw,localhost+080922073527+zGcaDa,0,1222068927,0,2,NULL,1
13,25,13,5,0,Random Short-Answer Matching,"<p>For each of the following questions, select the matching answer from the menu.</p>",1,,,1,0.1,randomsamatch,1,localhost+080922073724+qF803I,localhost+080922075820+zbZtaD,0,1222069044,1222070300,2,2,1
14,22,14,5,0,Is Thai difficult?,Is Thai difficult?,0,,,1,0.1,shortanswer,1,localhost+080922073655+2FLtCU,localhost+080922073655+fgUeOj,0,1222069015,0,2,NULL,1
15,23,15,5,0,Is Thai grammar difficult?,Is Thai grammar difficult?,0,,,1,0.1,shortanswer,1,localhost+080922073655+LYSD32,localhost+080922073655+WgRYk4,0,1222069015,0,2,NULL,1
16,24,16,5,0,Is Thai pronunciation difficult?,Is Thai pronunciation difficult?,0,,,1,0.1,shortanswer,1,localhost+080922073655+5p1w22,localhost+080922073655+g5jrXa,0,1222069015,0,2,NULL,1
17,20,17,5,0,Who's buried in Grant's tomb?,Who's buried in Grant's tomb?,0,,,1,0.1,shortanswer,1,localhost+080922073655+PTDcDZ,localhost+080922073655+aghyfu,0,1222069015,0,2,NULL,1
18,21,18,5,0,Who's buried in Jamie's tomb?,Who's buried in Jamie's tomb?,0,,,1,0.1,shortanswer,1,localhost+080922073655+Xvy1ns,localhost+080922073655+Mx0Izs,0,1222069015,0,2,NULL,1
19,18,19,5,0,questiontruefalse-7,This question is really stupid,0,,Well done,1,1,truefalse,1,localhost+080922073527+9bzTef,localhost+080922073527+hjcQR1,0,1222068927,0,2,NULL,1
20,19,20,5,0,questiontruefalse-9,This question is really stupid,0,,Well done,1,1,truefalse,1,localhost+080922073527+TI0yD4,localhost+080922073527+iXIulQ,0,1222068927,0,2,NULL,1
1 slot id number category parent name questiontext questiontextformat image generalfeedback defaultgrade penalty qtype length stamp version hidden timecreated timemodified createdby modifiedby maxmark
2 1 1 1 5 0 questionessay-11 What is the purpose of life? 0 General feedback 1 0 essay 1 localhost+080922073527+WeUaUK localhost+080922073527+qxLjv1 0 1222068927 0 2 NULL 1
3 2 2 2 5 0 questionessay-17 What is the purpose of life? 0 General feedback 1 0 essay 1 localhost+080922073527+pvCseX localhost+080922073527+mzy6tY 0 1222068927 0 2 NULL 1
4 3 3 3 5 0 questionessay-6 What is the purpose of life? 0 General feedback 1 0 essay 1 localhost+080922073527+Cr3gDO localhost+080922073527+hnxfTy 0 1222068927 0 2 NULL 1
5 4 4 4 5 0 questionessay-8 What is the purpose of life? 0 General feedback 1 0 essay 1 localhost+080922073527+sSq9ln localhost+080922073527+PGyab3 0 1222068927 0 2 NULL 1
6 5 5 5 5 0 questionmatch-10 test question, generated by script 0 Well done 1 0.1 match 1 localhost+080922073527+oG1i2f localhost+080922073527+S1UxZy 0 1222068927 0 2 NULL 0
7 6 6 6 5 0 questionmatch-16 test question, generated by script 0 Well done 1 0.1 match 1 localhost+080922073527+vMFHyY localhost+080922073527+4GZIyQ 0 1222068927 0 2 NULL 0
8 7 7 7 5 0 questionmatch-18 test question, generated by script 0 Well done 1 0.1 match 1 localhost+080922073527+Xkxqn1 localhost+080922073527+xbU6U7 0 1222068927 0 2 NULL 0
9 8 8 8 5 0 questionmultianswer-12 This question consists of some text with an answer embedded right here {#1} and right after that you will have to deal with this short answer {#2} and finally we have a floating point number {#3}. Note that addresses like www.moodle.org and smileys :-) all work as normal: a) How good is this? {#4} b) What grade would you give it? {#5} Good luck! 0 General feedback 8 0.1 multianswer 1 localhost+080922073527+0zKgpF localhost+080922073527+r1gsde 0 1222068927 0 2 NULL 8
10 9 14 9 5 0 questionmultichoice-13 How old is the sun? 0 General feedback 1 0.1 multichoice 1 localhost+080922073527+AjIjeV localhost+080922073527+UhtTLR 0 1222068927 0 2 NULL 1
11 10 15 10 5 0 questionmultichoice-14 How old is the sun? 0 General feedback 1 0.1 multichoice 1 localhost+080922073527+IrAqRl localhost+080922073527+xRfta8 0 1222068927 0 2 NULL 1
12 11 16 11 5 0 questionmultichoice-3 How old is the sun? 0 General feedback 1 0.1 multichoice 1 localhost+080922073527+DMGirU localhost+080922073527+689V8k 0 1222068927 0 2 NULL 1
13 12 17 12 5 0 questionmultichoice-5 How old is the sun? 0 General feedback 1 0.1 multichoice 1 localhost+080922073527+wileZw localhost+080922073527+zGcaDa 0 1222068927 0 2 NULL 1
14 13 25 13 5 0 Random Short-Answer Matching <p>For each of the following questions, select the matching answer from the menu.</p> 1 1 0.1 randomsamatch 1 localhost+080922073724+qF803I localhost+080922075820+zbZtaD 0 1222069044 1222070300 2 2 1
15 14 22 14 5 0 Is Thai difficult? Is Thai difficult? 0 1 0.1 shortanswer 1 localhost+080922073655+2FLtCU localhost+080922073655+fgUeOj 0 1222069015 0 2 NULL 1
16 15 23 15 5 0 Is Thai grammar difficult? Is Thai grammar difficult? 0 1 0.1 shortanswer 1 localhost+080922073655+LYSD32 localhost+080922073655+WgRYk4 0 1222069015 0 2 NULL 1
17 16 24 16 5 0 Is Thai pronunciation difficult? Is Thai pronunciation difficult? 0 1 0.1 shortanswer 1 localhost+080922073655+5p1w22 localhost+080922073655+g5jrXa 0 1222069015 0 2 NULL 1
18 17 20 17 5 0 Who's buried in Grant's tomb? Who's buried in Grant's tomb? 0 1 0.1 shortanswer 1 localhost+080922073655+PTDcDZ localhost+080922073655+aghyfu 0 1222069015 0 2 NULL 1
19 18 21 18 5 0 Who's buried in Jamie's tomb? Who's buried in Jamie's tomb? 0 1 0.1 shortanswer 1 localhost+080922073655+Xvy1ns localhost+080922073655+Mx0Izs 0 1222069015 0 2 NULL 1
20 19 18 19 5 0 questiontruefalse-7 This question is really stupid 0 Well done 1 1 truefalse 1 localhost+080922073527+9bzTef localhost+080922073527+hjcQR1 0 1222068927 0 2 NULL 1
21 20 19 20 5 0 questiontruefalse-9 This question is really stupid 0 Well done 1 1 truefalse 1 localhost+080922073527+TI0yD4 localhost+080922073527+iXIulQ 0 1222068927 0 2 NULL 1
@@ -0,0 +1,441 @@
id,sumgrades,questionid,slot,maxmark,mark,variant
39872,12,1,1,1,0,1
39873,12,2,2,1,0,1
39874,12,3,3,1,0,1
39875,12,4,4,1,0,1
39896,12,5,5,0,0,1
39897,12,6,6,0,0,1
39898,12,7,7,0,0,1
39899,12,8,8,8,5.5,1
39900,12,14,9,1,0.3,1
39901,12,15,10,1,0.9,1
39902,12,16,11,1,1,1
39903,12,17,12,1,0.3,1
39910,12,18,19,1,0,1
39911,12,19,20,1,1,1
39908,12,20,17,1,1,1
39909,12,21,18,1,0,1
39905,12,22,14,1,0,1
39906,12,23,15,1,0,1
39907,12,24,16,1,1,1
39904,12,25,13,1,1,1
39992,7.4,1,1,1,0,1
39993,7.4,2,2,1,0,1
39994,7.4,3,3,1,0,1
39995,7.4,4,4,1,0,1
40016,7.4,5,5,0,0,1
40017,7.4,6,6,0,0,1
40018,7.4,7,7,0,0,1
40019,7.4,8,8,8,1,1
40020,7.4,14,9,1,0.9,1
40021,7.4,15,10,1,0.9,1
40022,7.4,16,11,1,0.3,1
40023,7.4,17,12,1,0.3,1
40030,7.4,18,19,1,0,1
40031,7.4,19,20,1,1,1
40028,7.4,20,17,1,0,1
40029,7.4,21,18,1,1,1
40025,7.4,22,14,1,0,1
40026,7.4,23,15,1,0,1
40027,7.4,24,16,1,1,1
40024,7.4,25,13,1,1,1
40032,11.7,1,1,1,0,1
40033,11.7,2,2,1,0,1
40034,11.7,3,3,1,0,1
40035,11.7,4,4,1,0,1
40056,11.7,5,5,0,0,1
40057,11.7,6,6,0,0,1
40058,11.7,7,7,0,0,1
40059,11.7,8,8,8,2.5,1
40060,11.7,14,9,1,1,1
40061,11.7,15,10,1,0.9,1
40062,11.7,16,11,1,0.3,1
40063,11.7,17,12,1,1,1
40070,11.7,18,19,1,1,1
40071,11.7,19,20,1,1,1
40068,11.7,20,17,1,1,1
40069,11.7,21,18,1,1,1
40065,11.7,22,14,1,0,1
40066,11.7,23,15,1,1,1
40067,11.7,24,16,1,1,1
40064,11.7,25,13,1,0,1
39352,8.5,1,1,1,0,1
39353,8.5,2,2,1,0,1
39354,8.5,3,3,1,0,1
39355,8.5,4,4,1,0,1
39376,8.5,5,5,0,0,1
39377,8.5,6,6,0,0,1
39378,8.5,7,7,0,0,1
39379,8.5,8,8,8,4,1
39380,8.5,14,9,1,0.3,1
39381,8.5,15,10,1,0.9,1
39382,8.5,16,11,1,1,1
39383,8.5,17,12,1,0.3,1
39390,8.5,18,19,1,0,1
39391,8.5,19,20,1,1,1
39388,8.5,20,17,1,1,1
39389,8.5,21,18,1,0,1
39385,8.5,22,14,1,0,1
39386,8.5,23,15,1,0,1
39387,8.5,24,16,1,0,1
39384,8.5,25,13,1,0,1
37112,10.1,1,1,1,0,1
37113,10.1,2,2,1,0,1
37114,10.1,3,3,1,0,1
37115,10.1,4,4,1,0,1
37136,10.1,5,5,0,0,1
37137,10.1,6,6,0,0,1
37138,10.1,7,7,0,0,1
37139,10.1,8,8,8,3,1
37140,10.1,14,9,1,0.9,1
37141,10.1,15,10,1,0.3,1
37142,10.1,16,11,1,0.9,1
37143,10.1,17,12,1,1,1
37150,10.1,18,19,1,1,1
37151,10.1,19,20,1,0,1
37148,10.1,20,17,1,1,1
37149,10.1,21,18,1,1,1
37145,10.1,22,14,1,1,1
37146,10.1,23,15,1,0,1
37147,10.1,24,16,1,0,1
37144,10.1,25,13,1,0,1
37432,11.3,1,1,1,0,1
37433,11.3,2,2,1,0,1
37434,11.3,3,3,1,0,1
37435,11.3,4,4,1,0,1
37456,11.3,5,5,0,0,1
37457,11.3,6,6,0,0,1
37458,11.3,7,7,0,0,1
37459,11.3,8,8,8,4.5,1
37460,11.3,14,9,1,0.9,1
37461,11.3,15,10,1,1,1
37462,11.3,16,11,1,1,1
37463,11.3,17,12,1,0.9,1
37470,11.3,18,19,1,0,1
37471,11.3,19,20,1,0,1
37468,11.3,20,17,1,0,1
37469,11.3,21,18,1,0,1
37465,11.3,22,14,1,1,1
37466,11.3,23,15,1,1,1
37467,11.3,24,16,1,1,1
37464,11.3,25,13,1,0,1
37472,11.2,1,1,1,0,1
37473,11.2,2,2,1,0,1
37474,11.2,3,3,1,0,1
37475,11.2,4,4,1,0,1
37496,11.2,5,5,0,0,1
37497,11.2,6,6,0,0,1
37498,11.2,7,7,0,0,1
37499,11.2,8,8,8,3,1
37500,11.2,14,9,1,0.3,1
37501,11.2,15,10,1,0.3,1
37502,11.2,16,11,1,0.3,1
37503,11.2,17,12,1,0.3,1
37510,11.2,18,19,1,1,1
37511,11.2,19,20,1,1,1
37508,11.2,20,17,1,1,1
37509,11.2,21,18,1,1,1
37505,11.2,22,14,1,0,1
37506,11.2,23,15,1,1,1
37507,11.2,24,16,1,1,1
37504,11.2,25,13,1,1,1
37632,14.1,1,1,1,0,1
37633,14.1,2,2,1,0,1
37634,14.1,3,3,1,0,1
37635,14.1,4,4,1,0,1
37656,14.1,5,5,0,0,1
37657,14.1,6,6,0,0,1
37658,14.1,7,7,0,0,1
37659,14.1,8,8,8,6,1
37660,14.1,14,9,1,0.9,1
37661,14.1,15,10,1,0.9,1
37662,14.1,16,11,1,1,1
37663,14.1,17,12,1,0.3,1
37670,14.1,18,19,1,0,1
37671,14.1,19,20,1,1,1
37668,14.1,20,17,1,1,1
37669,14.1,21,18,1,0,1
37665,14.1,22,14,1,1,1
37666,14.1,23,15,1,1,1
37667,14.1,24,16,1,1,1
37664,14.1,25,13,1,0,1
37672,8.6,1,1,1,0,1
37673,8.6,2,2,1,0,1
37674,8.6,3,3,1,0,1
37675,8.6,4,4,1,0,1
37696,8.6,5,5,0,0,1
37697,8.6,6,6,0,0,1
37698,8.6,7,7,0,0,1
37699,8.6,8,8,8,0.5,1
37700,8.6,14,9,1,0.9,1
37701,8.6,15,10,1,1,1
37702,8.6,16,11,1,0.9,1
37703,8.6,17,12,1,0.3,1
37710,8.6,18,19,1,0,1
37711,8.6,19,20,1,1,1
37708,8.6,20,17,1,1,1
37709,8.6,21,18,1,1,1
37705,8.6,22,14,1,1,1
37706,8.6,23,15,1,1,1
37707,8.6,24,16,1,0,1
37704,8.6,25,13,1,0,1
37712,11.6,1,1,1,0,1
37713,11.6,2,2,1,0,1
37714,11.6,3,3,1,0,1
37715,11.6,4,4,1,0,1
37736,11.6,5,5,0,0,1
37737,11.6,6,6,0,0,1
37738,11.6,7,7,0,0,1
37739,11.6,8,8,8,5.5,1
37740,11.6,14,9,1,0.9,1
37741,11.6,15,10,1,1,1
37742,11.6,16,11,1,0.9,1
37743,11.6,17,12,1,0.3,1
37750,11.6,18,19,1,0,1
37751,11.6,19,20,1,0,1
37748,11.6,20,17,1,1,1
37749,11.6,21,18,1,0,1
37745,11.6,22,14,1,1,1
37746,11.6,23,15,1,0,1
37747,11.6,24,16,1,0,1
37744,11.6,25,13,1,1,1
38072,9.3,1,1,1,0,1
38073,9.3,2,2,1,0,1
38074,9.3,3,3,1,0,1
38075,9.3,4,4,1,0,1
38096,9.3,5,5,0,0,1
38097,9.3,6,6,0,0,1
38098,9.3,7,7,0,0,1
38099,9.3,8,8,8,2.5,1
38100,9.3,14,9,1,0.9,1
38101,9.3,15,10,1,1,1
38102,9.3,16,11,1,1,1
38103,9.3,17,12,1,0.9,1
38110,9.3,18,19,1,0,1
38111,9.3,19,20,1,1,1
38108,9.3,20,17,1,1,1
38109,9.3,21,18,1,0,1
38105,9.3,22,14,1,1,1
38106,9.3,23,15,1,0,1
38107,9.3,24,16,1,0,1
38104,9.3,25,13,1,0,1
38232,13.1,1,1,1,0,1
38233,13.1,2,2,1,0,1
38234,13.1,3,3,1,0,1
38235,13.1,4,4,1,0,1
38256,13.1,5,5,0,0,1
38257,13.1,6,6,0,0,1
38258,13.1,7,7,0,0,1
38259,13.1,8,8,8,4,1
38260,13.1,14,9,1,0.9,1
38261,13.1,15,10,1,0.3,1
38262,13.1,16,11,1,0.9,1
38263,13.1,17,12,1,1,1
38270,13.1,18,19,1,0,1
38271,13.1,19,20,1,1,1
38268,13.1,20,17,1,1,1
38269,13.1,21,18,1,1,1
38265,13.1,22,14,1,1,1
38266,13.1,23,15,1,1,1
38267,13.1,24,16,1,1,1
38264,13.1,25,13,1,0,1
38272,11.5,1,1,1,0,1
38273,11.5,2,2,1,0,1
38274,11.5,3,3,1,0,1
38275,11.5,4,4,1,0,1
38296,11.5,5,5,0,0,1
38297,11.5,6,6,0,0,1
38298,11.5,7,7,0,0,1
38299,11.5,8,8,8,4,1
38300,11.5,14,9,1,0.9,1
38301,11.5,15,10,1,0.3,1
38302,11.5,16,11,1,0.3,1
38303,11.5,17,12,1,1,1
38310,11.5,18,19,1,1,1
38311,11.5,19,20,1,0,1
38308,11.5,20,17,1,0,1
38309,11.5,21,18,1,1,1
38305,11.5,22,14,1,1,1
38306,11.5,23,15,1,1,1
38307,11.5,24,16,1,0,1
38304,11.5,25,13,1,1,1
38472,11.3,1,1,1,0,1
38473,11.3,2,2,1,0,1
38474,11.3,3,3,1,0,1
38475,11.3,4,4,1,0,1
38496,11.3,5,5,0,0,1
38497,11.3,6,6,0,0,1
38498,11.3,7,7,0,0,1
38499,11.3,8,8,8,3,1
38500,11.3,14,9,1,1,1
38501,11.3,15,10,1,1,1
38502,11.3,16,11,1,0.3,1
38503,11.3,17,12,1,1,1
38510,11.3,18,19,1,1,1
38511,11.3,19,20,1,0,1
38508,11.3,20,17,1,1,1
38509,11.3,21,18,1,1,1
38505,11.3,22,14,1,1,1
38506,11.3,23,15,1,0,1
38507,11.3,24,16,1,1,1
38504,11.3,25,13,1,0,1
38632,5.5,1,1,1,0,1
38633,5.5,2,2,1,0,1
38634,5.5,3,3,1,0,1
38635,5.5,4,4,1,0,1
38656,5.5,5,5,0,0,1
38657,5.5,6,6,0,0,1
38658,5.5,7,7,0,0,1
38659,5.5,8,8,8,1,1
38660,5.5,14,9,1,1,1
38661,5.5,15,10,1,0.9,1
38662,5.5,16,11,1,0.3,1
38663,5.5,17,12,1,0.3,1
38670,5.5,18,19,1,0,1
38671,5.5,19,20,1,0,1
38668,5.5,20,17,1,0,1
38669,5.5,21,18,1,0,1
38665,5.5,22,14,1,0,1
38666,5.5,23,15,1,1,1
38667,5.5,24,16,1,0,1
38664,5.5,25,13,1,1,1
38672,4.8,1,1,1,0,1
38673,4.8,2,2,1,0,1
38674,4.8,3,3,1,0,1
38675,4.8,4,4,1,0,1
38696,4.8,5,5,0,0,1
38697,4.8,6,6,0,0,1
38698,4.8,7,7,0,0,1
38699,4.8,8,8,8,1,1
38700,4.8,14,9,1,0.3,1
38701,4.8,15,10,1,0.3,1
38702,4.8,16,11,1,0.3,1
38703,4.8,17,12,1,0.9,1
38710,4.8,18,19,1,0,1
38711,4.8,19,20,1,1,1
38708,4.8,20,17,1,0,1
38709,4.8,21,18,1,0,1
38705,4.8,22,14,1,0,1
38706,4.8,23,15,1,0,1
38707,4.8,24,16,1,1,1
38704,4.8,25,13,1,0,1
38992,8.7,1,1,1,0,1
38993,8.7,2,2,1,0,1
38994,8.7,3,3,1,0,1
38995,8.7,4,4,1,0,1
39016,8.7,5,5,0,0,1
39017,8.7,6,6,0,0,1
39018,8.7,7,7,0,0,1
39019,8.7,8,8,8,1.5,1
39020,8.7,14,9,1,1,1
39021,8.7,15,10,1,0.3,1
39022,8.7,16,11,1,0.9,1
39023,8.7,17,12,1,1,1
39030,8.7,18,19,1,0,1
39031,8.7,19,20,1,0,1
39028,8.7,20,17,1,1,1
39029,8.7,21,18,1,0,1
39025,8.7,22,14,1,1,1
39026,8.7,23,15,1,0,1
39027,8.7,24,16,1,1,1
39024,8.7,25,13,1,1,1
39032,11.8,1,1,1,0,1
39033,11.8,2,2,1,0,1
39034,11.8,3,3,1,0,1
39035,11.8,4,4,1,0,1
39056,11.8,5,5,0,0,1
39057,11.8,6,6,0,0,1
39058,11.8,7,7,0,0,1
39059,11.8,8,8,8,4,1
39060,11.8,14,9,1,0.9,1
39061,11.8,15,10,1,1,1
39062,11.8,16,11,1,0.9,1
39063,11.8,17,12,1,1,1
39070,11.8,18,19,1,0,1
39071,11.8,19,20,1,0,1
39068,11.8,20,17,1,0,1
39069,11.8,21,18,1,1,1
39065,11.8,22,14,1,1,1
39066,11.8,23,15,1,1,1
39067,11.8,24,16,1,1,1
39064,11.8,25,13,1,0,1
39272,6,1,1,1,0,1
39273,6,2,2,1,0,1
39274,6,3,3,1,0,1
39275,6,4,4,1,0,1
39296,6,5,5,0,0,1
39297,6,6,6,0,0,1
39298,6,7,7,0,0,1
39299,6,8,8,8,2,1
39300,6,14,9,1,0.9,1
39301,6,15,10,1,0.9,1
39302,6,16,11,1,0.3,1
39303,6,17,12,1,0.9,1
39310,6,18,19,1,0,1
39311,6,19,20,1,0,1
39308,6,20,17,1,0,1
39309,6,21,18,1,0,1
39305,6,22,14,1,1,1
39306,6,23,15,1,0,1
39307,6,24,16,1,0,1
39304,6,25,13,1,0,1
36192,5.9,1,1,1,0,1
36193,5.9,2,2,1,0,1
36194,5.9,3,3,1,0,1
36195,5.9,4,4,1,0,1
36216,5.9,5,5,0,0,1
36217,5.9,6,6,0,0,1
36218,5.9,7,7,0,0,1
36219,5.9,8,8,8,1,1
36220,5.9,14,9,1,1,1
36221,5.9,15,10,1,0.3,1
36222,5.9,16,11,1,0.3,1
36223,5.9,17,12,1,0.3,1
36230,5.9,18,19,1,0,1
36231,5.9,19,20,1,0,1
36228,5.9,20,17,1,1,1
36229,5.9,21,18,1,0,1
36225,5.9,22,14,1,1,1
36226,5.9,23,15,1,1,1
36227,5.9,24,16,1,0,1
36224,5.9,25,13,1,0,1
36352,12.8,1,1,1,0,1
36353,12.8,2,2,1,0,1
36354,12.8,3,3,1,0,1
36355,12.8,4,4,1,0,1
36376,12.8,5,5,0,0,1
36377,12.8,6,6,0,0,1
36378,12.8,7,7,0,0,1
36379,12.8,8,8,8,6,1
36380,12.8,14,9,1,0.9,1
36381,12.8,15,10,1,1,1
36382,12.8,16,11,1,1,1
36383,12.8,17,12,1,0.9,1
36390,12.8,18,19,1,1,1
36391,12.8,19,20,1,1,1
36388,12.8,20,17,1,0,1
36389,12.8,21,18,1,0,1
36385,12.8,22,14,1,0,1
36386,12.8,23,15,1,0,1
36387,12.8,24,16,1,1,1
36384,12.8,25,13,1,0,1
36832,13.8,1,1,1,0,1
36833,13.8,2,2,1,0,1
36834,13.8,3,3,1,0,1
36835,13.8,4,4,1,0,1
36856,13.8,5,5,0,0,1
36857,13.8,6,6,0,0,1
36858,13.8,7,7,0,0,1
36859,13.8,8,8,8,7,1
36860,13.8,14,9,1,0.9,1
36861,13.8,15,10,1,0.3,1
36862,13.8,16,11,1,0.3,1
36863,13.8,17,12,1,0.3,1
36870,13.8,18,19,1,0,1
36871,13.8,19,20,1,0,1
36868,13.8,20,17,1,1,1
36869,13.8,21,18,1,1,1
36865,13.8,22,14,1,0,1
36866,13.8,23,15,1,1,1
36867,13.8,24,16,1,1,1
36864,13.8,25,13,1,1,1
1 id sumgrades questionid slot maxmark mark variant
2 39872 12 1 1 1 0 1
3 39873 12 2 2 1 0 1
4 39874 12 3 3 1 0 1
5 39875 12 4 4 1 0 1
6 39896 12 5 5 0 0 1
7 39897 12 6 6 0 0 1
8 39898 12 7 7 0 0 1
9 39899 12 8 8 8 5.5 1
10 39900 12 14 9 1 0.3 1
11 39901 12 15 10 1 0.9 1
12 39902 12 16 11 1 1 1
13 39903 12 17 12 1 0.3 1
14 39910 12 18 19 1 0 1
15 39911 12 19 20 1 1 1
16 39908 12 20 17 1 1 1
17 39909 12 21 18 1 0 1
18 39905 12 22 14 1 0 1
19 39906 12 23 15 1 0 1
20 39907 12 24 16 1 1 1
21 39904 12 25 13 1 1 1
22 39992 7.4 1 1 1 0 1
23 39993 7.4 2 2 1 0 1
24 39994 7.4 3 3 1 0 1
25 39995 7.4 4 4 1 0 1
26 40016 7.4 5 5 0 0 1
27 40017 7.4 6 6 0 0 1
28 40018 7.4 7 7 0 0 1
29 40019 7.4 8 8 8 1 1
30 40020 7.4 14 9 1 0.9 1
31 40021 7.4 15 10 1 0.9 1
32 40022 7.4 16 11 1 0.3 1
33 40023 7.4 17 12 1 0.3 1
34 40030 7.4 18 19 1 0 1
35 40031 7.4 19 20 1 1 1
36 40028 7.4 20 17 1 0 1
37 40029 7.4 21 18 1 1 1
38 40025 7.4 22 14 1 0 1
39 40026 7.4 23 15 1 0 1
40 40027 7.4 24 16 1 1 1
41 40024 7.4 25 13 1 1 1
42 40032 11.7 1 1 1 0 1
43 40033 11.7 2 2 1 0 1
44 40034 11.7 3 3 1 0 1
45 40035 11.7 4 4 1 0 1
46 40056 11.7 5 5 0 0 1
47 40057 11.7 6 6 0 0 1
48 40058 11.7 7 7 0 0 1
49 40059 11.7 8 8 8 2.5 1
50 40060 11.7 14 9 1 1 1
51 40061 11.7 15 10 1 0.9 1
52 40062 11.7 16 11 1 0.3 1
53 40063 11.7 17 12 1 1 1
54 40070 11.7 18 19 1 1 1
55 40071 11.7 19 20 1 1 1
56 40068 11.7 20 17 1 1 1
57 40069 11.7 21 18 1 1 1
58 40065 11.7 22 14 1 0 1
59 40066 11.7 23 15 1 1 1
60 40067 11.7 24 16 1 1 1
61 40064 11.7 25 13 1 0 1
62 39352 8.5 1 1 1 0 1
63 39353 8.5 2 2 1 0 1
64 39354 8.5 3 3 1 0 1
65 39355 8.5 4 4 1 0 1
66 39376 8.5 5 5 0 0 1
67 39377 8.5 6 6 0 0 1
68 39378 8.5 7 7 0 0 1
69 39379 8.5 8 8 8 4 1
70 39380 8.5 14 9 1 0.3 1
71 39381 8.5 15 10 1 0.9 1
72 39382 8.5 16 11 1 1 1
73 39383 8.5 17 12 1 0.3 1
74 39390 8.5 18 19 1 0 1
75 39391 8.5 19 20 1 1 1
76 39388 8.5 20 17 1 1 1
77 39389 8.5 21 18 1 0 1
78 39385 8.5 22 14 1 0 1
79 39386 8.5 23 15 1 0 1
80 39387 8.5 24 16 1 0 1
81 39384 8.5 25 13 1 0 1
82 37112 10.1 1 1 1 0 1
83 37113 10.1 2 2 1 0 1
84 37114 10.1 3 3 1 0 1
85 37115 10.1 4 4 1 0 1
86 37136 10.1 5 5 0 0 1
87 37137 10.1 6 6 0 0 1
88 37138 10.1 7 7 0 0 1
89 37139 10.1 8 8 8 3 1
90 37140 10.1 14 9 1 0.9 1
91 37141 10.1 15 10 1 0.3 1
92 37142 10.1 16 11 1 0.9 1
93 37143 10.1 17 12 1 1 1
94 37150 10.1 18 19 1 1 1
95 37151 10.1 19 20 1 0 1
96 37148 10.1 20 17 1 1 1
97 37149 10.1 21 18 1 1 1
98 37145 10.1 22 14 1 1 1
99 37146 10.1 23 15 1 0 1
100 37147 10.1 24 16 1 0 1
101 37144 10.1 25 13 1 0 1
102 37432 11.3 1 1 1 0 1
103 37433 11.3 2 2 1 0 1
104 37434 11.3 3 3 1 0 1
105 37435 11.3 4 4 1 0 1
106 37456 11.3 5 5 0 0 1
107 37457 11.3 6 6 0 0 1
108 37458 11.3 7 7 0 0 1
109 37459 11.3 8 8 8 4.5 1
110 37460 11.3 14 9 1 0.9 1
111 37461 11.3 15 10 1 1 1
112 37462 11.3 16 11 1 1 1
113 37463 11.3 17 12 1 0.9 1
114 37470 11.3 18 19 1 0 1
115 37471 11.3 19 20 1 0 1
116 37468 11.3 20 17 1 0 1
117 37469 11.3 21 18 1 0 1
118 37465 11.3 22 14 1 1 1
119 37466 11.3 23 15 1 1 1
120 37467 11.3 24 16 1 1 1
121 37464 11.3 25 13 1 0 1
122 37472 11.2 1 1 1 0 1
123 37473 11.2 2 2 1 0 1
124 37474 11.2 3 3 1 0 1
125 37475 11.2 4 4 1 0 1
126 37496 11.2 5 5 0 0 1
127 37497 11.2 6 6 0 0 1
128 37498 11.2 7 7 0 0 1
129 37499 11.2 8 8 8 3 1
130 37500 11.2 14 9 1 0.3 1
131 37501 11.2 15 10 1 0.3 1
132 37502 11.2 16 11 1 0.3 1
133 37503 11.2 17 12 1 0.3 1
134 37510 11.2 18 19 1 1 1
135 37511 11.2 19 20 1 1 1
136 37508 11.2 20 17 1 1 1
137 37509 11.2 21 18 1 1 1
138 37505 11.2 22 14 1 0 1
139 37506 11.2 23 15 1 1 1
140 37507 11.2 24 16 1 1 1
141 37504 11.2 25 13 1 1 1
142 37632 14.1 1 1 1 0 1
143 37633 14.1 2 2 1 0 1
144 37634 14.1 3 3 1 0 1
145 37635 14.1 4 4 1 0 1
146 37656 14.1 5 5 0 0 1
147 37657 14.1 6 6 0 0 1
148 37658 14.1 7 7 0 0 1
149 37659 14.1 8 8 8 6 1
150 37660 14.1 14 9 1 0.9 1
151 37661 14.1 15 10 1 0.9 1
152 37662 14.1 16 11 1 1 1
153 37663 14.1 17 12 1 0.3 1
154 37670 14.1 18 19 1 0 1
155 37671 14.1 19 20 1 1 1
156 37668 14.1 20 17 1 1 1
157 37669 14.1 21 18 1 0 1
158 37665 14.1 22 14 1 1 1
159 37666 14.1 23 15 1 1 1
160 37667 14.1 24 16 1 1 1
161 37664 14.1 25 13 1 0 1
162 37672 8.6 1 1 1 0 1
163 37673 8.6 2 2 1 0 1
164 37674 8.6 3 3 1 0 1
165 37675 8.6 4 4 1 0 1
166 37696 8.6 5 5 0 0 1
167 37697 8.6 6 6 0 0 1
168 37698 8.6 7 7 0 0 1
169 37699 8.6 8 8 8 0.5 1
170 37700 8.6 14 9 1 0.9 1
171 37701 8.6 15 10 1 1 1
172 37702 8.6 16 11 1 0.9 1
173 37703 8.6 17 12 1 0.3 1
174 37710 8.6 18 19 1 0 1
175 37711 8.6 19 20 1 1 1
176 37708 8.6 20 17 1 1 1
177 37709 8.6 21 18 1 1 1
178 37705 8.6 22 14 1 1 1
179 37706 8.6 23 15 1 1 1
180 37707 8.6 24 16 1 0 1
181 37704 8.6 25 13 1 0 1
182 37712 11.6 1 1 1 0 1
183 37713 11.6 2 2 1 0 1
184 37714 11.6 3 3 1 0 1
185 37715 11.6 4 4 1 0 1
186 37736 11.6 5 5 0 0 1
187 37737 11.6 6 6 0 0 1
188 37738 11.6 7 7 0 0 1
189 37739 11.6 8 8 8 5.5 1
190 37740 11.6 14 9 1 0.9 1
191 37741 11.6 15 10 1 1 1
192 37742 11.6 16 11 1 0.9 1
193 37743 11.6 17 12 1 0.3 1
194 37750 11.6 18 19 1 0 1
195 37751 11.6 19 20 1 0 1
196 37748 11.6 20 17 1 1 1
197 37749 11.6 21 18 1 0 1
198 37745 11.6 22 14 1 1 1
199 37746 11.6 23 15 1 0 1
200 37747 11.6 24 16 1 0 1
201 37744 11.6 25 13 1 1 1
202 38072 9.3 1 1 1 0 1
203 38073 9.3 2 2 1 0 1
204 38074 9.3 3 3 1 0 1
205 38075 9.3 4 4 1 0 1
206 38096 9.3 5 5 0 0 1
207 38097 9.3 6 6 0 0 1
208 38098 9.3 7 7 0 0 1
209 38099 9.3 8 8 8 2.5 1
210 38100 9.3 14 9 1 0.9 1
211 38101 9.3 15 10 1 1 1
212 38102 9.3 16 11 1 1 1
213 38103 9.3 17 12 1 0.9 1
214 38110 9.3 18 19 1 0 1
215 38111 9.3 19 20 1 1 1
216 38108 9.3 20 17 1 1 1
217 38109 9.3 21 18 1 0 1
218 38105 9.3 22 14 1 1 1
219 38106 9.3 23 15 1 0 1
220 38107 9.3 24 16 1 0 1
221 38104 9.3 25 13 1 0 1
222 38232 13.1 1 1 1 0 1
223 38233 13.1 2 2 1 0 1
224 38234 13.1 3 3 1 0 1
225 38235 13.1 4 4 1 0 1
226 38256 13.1 5 5 0 0 1
227 38257 13.1 6 6 0 0 1
228 38258 13.1 7 7 0 0 1
229 38259 13.1 8 8 8 4 1
230 38260 13.1 14 9 1 0.9 1
231 38261 13.1 15 10 1 0.3 1
232 38262 13.1 16 11 1 0.9 1
233 38263 13.1 17 12 1 1 1
234 38270 13.1 18 19 1 0 1
235 38271 13.1 19 20 1 1 1
236 38268 13.1 20 17 1 1 1
237 38269 13.1 21 18 1 1 1
238 38265 13.1 22 14 1 1 1
239 38266 13.1 23 15 1 1 1
240 38267 13.1 24 16 1 1 1
241 38264 13.1 25 13 1 0 1
242 38272 11.5 1 1 1 0 1
243 38273 11.5 2 2 1 0 1
244 38274 11.5 3 3 1 0 1
245 38275 11.5 4 4 1 0 1
246 38296 11.5 5 5 0 0 1
247 38297 11.5 6 6 0 0 1
248 38298 11.5 7 7 0 0 1
249 38299 11.5 8 8 8 4 1
250 38300 11.5 14 9 1 0.9 1
251 38301 11.5 15 10 1 0.3 1
252 38302 11.5 16 11 1 0.3 1
253 38303 11.5 17 12 1 1 1
254 38310 11.5 18 19 1 1 1
255 38311 11.5 19 20 1 0 1
256 38308 11.5 20 17 1 0 1
257 38309 11.5 21 18 1 1 1
258 38305 11.5 22 14 1 1 1
259 38306 11.5 23 15 1 1 1
260 38307 11.5 24 16 1 0 1
261 38304 11.5 25 13 1 1 1
262 38472 11.3 1 1 1 0 1
263 38473 11.3 2 2 1 0 1
264 38474 11.3 3 3 1 0 1
265 38475 11.3 4 4 1 0 1
266 38496 11.3 5 5 0 0 1
267 38497 11.3 6 6 0 0 1
268 38498 11.3 7 7 0 0 1
269 38499 11.3 8 8 8 3 1
270 38500 11.3 14 9 1 1 1
271 38501 11.3 15 10 1 1 1
272 38502 11.3 16 11 1 0.3 1
273 38503 11.3 17 12 1 1 1
274 38510 11.3 18 19 1 1 1
275 38511 11.3 19 20 1 0 1
276 38508 11.3 20 17 1 1 1
277 38509 11.3 21 18 1 1 1
278 38505 11.3 22 14 1 1 1
279 38506 11.3 23 15 1 0 1
280 38507 11.3 24 16 1 1 1
281 38504 11.3 25 13 1 0 1
282 38632 5.5 1 1 1 0 1
283 38633 5.5 2 2 1 0 1
284 38634 5.5 3 3 1 0 1
285 38635 5.5 4 4 1 0 1
286 38656 5.5 5 5 0 0 1
287 38657 5.5 6 6 0 0 1
288 38658 5.5 7 7 0 0 1
289 38659 5.5 8 8 8 1 1
290 38660 5.5 14 9 1 1 1
291 38661 5.5 15 10 1 0.9 1
292 38662 5.5 16 11 1 0.3 1
293 38663 5.5 17 12 1 0.3 1
294 38670 5.5 18 19 1 0 1
295 38671 5.5 19 20 1 0 1
296 38668 5.5 20 17 1 0 1
297 38669 5.5 21 18 1 0 1
298 38665 5.5 22 14 1 0 1
299 38666 5.5 23 15 1 1 1
300 38667 5.5 24 16 1 0 1
301 38664 5.5 25 13 1 1 1
302 38672 4.8 1 1 1 0 1
303 38673 4.8 2 2 1 0 1
304 38674 4.8 3 3 1 0 1
305 38675 4.8 4 4 1 0 1
306 38696 4.8 5 5 0 0 1
307 38697 4.8 6 6 0 0 1
308 38698 4.8 7 7 0 0 1
309 38699 4.8 8 8 8 1 1
310 38700 4.8 14 9 1 0.3 1
311 38701 4.8 15 10 1 0.3 1
312 38702 4.8 16 11 1 0.3 1
313 38703 4.8 17 12 1 0.9 1
314 38710 4.8 18 19 1 0 1
315 38711 4.8 19 20 1 1 1
316 38708 4.8 20 17 1 0 1
317 38709 4.8 21 18 1 0 1
318 38705 4.8 22 14 1 0 1
319 38706 4.8 23 15 1 0 1
320 38707 4.8 24 16 1 1 1
321 38704 4.8 25 13 1 0 1
322 38992 8.7 1 1 1 0 1
323 38993 8.7 2 2 1 0 1
324 38994 8.7 3 3 1 0 1
325 38995 8.7 4 4 1 0 1
326 39016 8.7 5 5 0 0 1
327 39017 8.7 6 6 0 0 1
328 39018 8.7 7 7 0 0 1
329 39019 8.7 8 8 8 1.5 1
330 39020 8.7 14 9 1 1 1
331 39021 8.7 15 10 1 0.3 1
332 39022 8.7 16 11 1 0.9 1
333 39023 8.7 17 12 1 1 1
334 39030 8.7 18 19 1 0 1
335 39031 8.7 19 20 1 0 1
336 39028 8.7 20 17 1 1 1
337 39029 8.7 21 18 1 0 1
338 39025 8.7 22 14 1 1 1
339 39026 8.7 23 15 1 0 1
340 39027 8.7 24 16 1 1 1
341 39024 8.7 25 13 1 1 1
342 39032 11.8 1 1 1 0 1
343 39033 11.8 2 2 1 0 1
344 39034 11.8 3 3 1 0 1
345 39035 11.8 4 4 1 0 1
346 39056 11.8 5 5 0 0 1
347 39057 11.8 6 6 0 0 1
348 39058 11.8 7 7 0 0 1
349 39059 11.8 8 8 8 4 1
350 39060 11.8 14 9 1 0.9 1
351 39061 11.8 15 10 1 1 1
352 39062 11.8 16 11 1 0.9 1
353 39063 11.8 17 12 1 1 1
354 39070 11.8 18 19 1 0 1
355 39071 11.8 19 20 1 0 1
356 39068 11.8 20 17 1 0 1
357 39069 11.8 21 18 1 1 1
358 39065 11.8 22 14 1 1 1
359 39066 11.8 23 15 1 1 1
360 39067 11.8 24 16 1 1 1
361 39064 11.8 25 13 1 0 1
362 39272 6 1 1 1 0 1
363 39273 6 2 2 1 0 1
364 39274 6 3 3 1 0 1
365 39275 6 4 4 1 0 1
366 39296 6 5 5 0 0 1
367 39297 6 6 6 0 0 1
368 39298 6 7 7 0 0 1
369 39299 6 8 8 8 2 1
370 39300 6 14 9 1 0.9 1
371 39301 6 15 10 1 0.9 1
372 39302 6 16 11 1 0.3 1
373 39303 6 17 12 1 0.9 1
374 39310 6 18 19 1 0 1
375 39311 6 19 20 1 0 1
376 39308 6 20 17 1 0 1
377 39309 6 21 18 1 0 1
378 39305 6 22 14 1 1 1
379 39306 6 23 15 1 0 1
380 39307 6 24 16 1 0 1
381 39304 6 25 13 1 0 1
382 36192 5.9 1 1 1 0 1
383 36193 5.9 2 2 1 0 1
384 36194 5.9 3 3 1 0 1
385 36195 5.9 4 4 1 0 1
386 36216 5.9 5 5 0 0 1
387 36217 5.9 6 6 0 0 1
388 36218 5.9 7 7 0 0 1
389 36219 5.9 8 8 8 1 1
390 36220 5.9 14 9 1 1 1
391 36221 5.9 15 10 1 0.3 1
392 36222 5.9 16 11 1 0.3 1
393 36223 5.9 17 12 1 0.3 1
394 36230 5.9 18 19 1 0 1
395 36231 5.9 19 20 1 0 1
396 36228 5.9 20 17 1 1 1
397 36229 5.9 21 18 1 0 1
398 36225 5.9 22 14 1 1 1
399 36226 5.9 23 15 1 1 1
400 36227 5.9 24 16 1 0 1
401 36224 5.9 25 13 1 0 1
402 36352 12.8 1 1 1 0 1
403 36353 12.8 2 2 1 0 1
404 36354 12.8 3 3 1 0 1
405 36355 12.8 4 4 1 0 1
406 36376 12.8 5 5 0 0 1
407 36377 12.8 6 6 0 0 1
408 36378 12.8 7 7 0 0 1
409 36379 12.8 8 8 8 6 1
410 36380 12.8 14 9 1 0.9 1
411 36381 12.8 15 10 1 1 1
412 36382 12.8 16 11 1 1 1
413 36383 12.8 17 12 1 0.9 1
414 36390 12.8 18 19 1 1 1
415 36391 12.8 19 20 1 1 1
416 36388 12.8 20 17 1 0 1
417 36389 12.8 21 18 1 0 1
418 36385 12.8 22 14 1 0 1
419 36386 12.8 23 15 1 0 1
420 36387 12.8 24 16 1 1 1
421 36384 12.8 25 13 1 0 1
422 36832 13.8 1 1 1 0 1
423 36833 13.8 2 2 1 0 1
424 36834 13.8 3 3 1 0 1
425 36835 13.8 4 4 1 0 1
426 36856 13.8 5 5 0 0 1
427 36857 13.8 6 6 0 0 1
428 36858 13.8 7 7 0 0 1
429 36859 13.8 8 8 8 7 1
430 36860 13.8 14 9 1 0.9 1
431 36861 13.8 15 10 1 0.3 1
432 36862 13.8 16 11 1 0.3 1
433 36863 13.8 17 12 1 0.3 1
434 36870 13.8 18 19 1 0 1
435 36871 13.8 19 20 1 0 1
436 36868 13.8 20 17 1 1 1
437 36869 13.8 21 18 1 1 1
438 36865 13.8 22 14 1 0 1
439 36866 13.8 23 15 1 1 1
440 36867 13.8 24 16 1 1 1
441 36864 13.8 25 13 1 1 1
+11
View File
@@ -0,0 +1,11 @@
slot,subqname,variant,s,facility,sd,effectiveweight,covariance,markvariance,othermarkvariance,discriminationindex,covariancemax,discriminativeefficiency,maxmark
1,,,25,0.704,0.4513682901,21.2922742344,-0.022555556,0.2037333333,0.5002777794,-7.0650767526,0.2385555565,-9.4550536967,1
1,numerical,,12,0.583333333,0.514928651,**NULL**,,,,35.803933,,39.39393939,1
2,,,25,0.48,0.5099019514,18.8979800309,-0.1172777785,0.26,0.6334555578,-28.8982125772,0.318833334,-36.7834118938,1
2,,1,6,0.50,0.5477225575,**NULL**,,,,-10.5999788,,-14.28571429,1
2,,8,5,0.40,0.547722558,**NULL**,,,,-57.77466679,,-71.05263241,1
3,,,25,0.973333332,0.13333334,4.443012573,-0.0098888894,0.0177777796,0.6609,-9.1230674268,0.045666669,-21.6545012165,1
4,,,25,0.68,0.4760952286,18.9347251357,-0.0833888893,0.2266666667,0.5990111128,-22.6306444113,0.2652222232,-31.4411395613,1
5,,,25,0.52,0.3055050463,11.1450138688,-0.0436944444,0.0933333333,0.6529555563,-17.6997047674,0.2063055556,-21.1794802584,1
6,,,25,0.64,0.4898979486,9.8081339177,-0.2015555547,0.24,0.8220111101,-45.3785178421,0.3539999995,-56.9365974439,1
7,,,25,0.62,0.331662479,15.4788602394,-0.0142499998,0.11,0.5774000005,-5.6543166602,0.2190833335,-6.5043742058,1
1 slot subqname variant s facility sd effectiveweight covariance markvariance othermarkvariance discriminationindex covariancemax discriminativeefficiency maxmark
2 1 25 0.704 0.4513682901 21.2922742344 -0.022555556 0.2037333333 0.5002777794 -7.0650767526 0.2385555565 -9.4550536967 1
3 1 numerical 12 0.583333333 0.514928651 **NULL** 35.803933 39.39393939 1
4 2 25 0.48 0.5099019514 18.8979800309 -0.1172777785 0.26 0.6334555578 -28.8982125772 0.318833334 -36.7834118938 1
5 2 1 6 0.50 0.5477225575 **NULL** -10.5999788 -14.28571429 1
6 2 8 5 0.40 0.547722558 **NULL** -57.77466679 -71.05263241 1
7 3 25 0.973333332 0.13333334 4.443012573 -0.0098888894 0.0177777796 0.6609 -9.1230674268 0.045666669 -21.6545012165 1
8 4 25 0.68 0.4760952286 18.9347251357 -0.0833888893 0.2266666667 0.5990111128 -22.6306444113 0.2652222232 -31.4411395613 1
9 5 25 0.52 0.3055050463 11.1450138688 -0.0436944444 0.0933333333 0.6529555563 -17.6997047674 0.2063055556 -21.1794802584 1
10 6 25 0.64 0.4898979486 9.8081339177 -0.2015555547 0.24 0.8220111101 -45.3785178421 0.3539999995 -56.9365974439 1
11 7 25 0.62 0.331662479 15.4788602394 -0.0142499998 0.11 0.5774000005 -5.6543166602 0.2190833335 -6.5043742058 1
@@ -0,0 +1,10 @@
slot,type,which,cat,mark
1,random,,rand,1
,shortanswer,,rand,1
,numerical,,rand,1
2,calculatedsimple,sumwithvariants,maincat,1
3,match,,maincat,1
4,truefalse,,maincat,1
5,multichoice,two_of_four,maincat,1
6,multichoice,one_of_four,maincat,1
7,multianswer,,maincat,1
1 slot type which cat mark
2 1 random rand 1
3 shortanswer rand 1
4 numerical rand 1
5 2 calculatedsimple sumwithvariants maincat 1
6 3 match maincat 1
7 4 truefalse maincat 1
8 5 multichoice two_of_four maincat 1
9 6 multichoice one_of_four maincat 1
10 7 multianswer maincat 1
@@ -0,0 +1,7 @@
slot,type,which,cat,mark,overrides.hint.0.text,overrides.hint.0.format,overrides.hint.1.text,overrides.hint.1.format,overrides.hint.2.text,overrides.hint.2.format,overrides.hint.3.text,overrides.hint.3.format,overrides.shuffleanswers
1,random,,rand,1,"Hint 1",0,"Hint 2",0,"Hint 3",0,"Hint 4",0,0
,shortanswer,,rand,1,"Hint 1",0,"Hint 2",0,"Hint 3",0,"Hint 4",0,0
,numerical,,rand,1,"Hint 1",0,"Hint 2",0,"Hint 3",0,"Hint 4",0,0
2,calculatedsimple,sumwithvariants,maincat,1,"Hint 1",0,"Hint 2",0,"Hint 3",0,"Hint 4",0,0
3,match,,maincat,1,"Hint 1",0,"Hint 2",0,"Hint 3",0,"Hint 4",0,0
4,truefalse,,maincat,1,"Hint 1",0,"Hint 2",0,"Hint 3",0,"Hint 4",0,0
1 slot type which cat mark overrides.hint.0.text overrides.hint.0.format overrides.hint.1.text overrides.hint.1.format overrides.hint.2.text overrides.hint.2.format overrides.hint.3.text overrides.hint.3.format overrides.shuffleanswers
2 1 random rand 1 Hint 1 0 Hint 2 0 Hint 3 0 Hint 4 0 0
3 shortanswer rand 1 Hint 1 0 Hint 2 0 Hint 3 0 Hint 4 0 0
4 numerical rand 1 Hint 1 0 Hint 2 0 Hint 3 0 Hint 4 0 0
5 2 calculatedsimple sumwithvariants maincat 1 Hint 1 0 Hint 2 0 Hint 3 0 Hint 4 0 0
6 3 match maincat 1 Hint 1 0 Hint 2 0 Hint 3 0 Hint 4 0 0
7 4 truefalse maincat 1 Hint 1 0 Hint 2 0 Hint 3 0 Hint 4 0 0
@@ -0,0 +1,2 @@
slot,type,which,cat,mark,overrides.hint.0.text,overrides.hint.0.format,overrides.hint.1.text,overrides.hint.1.format,overrides.hint.2.text,overrides.hint.2.format,overrides.hint.3.text,overrides.hint.3.format,overrides.hint.4.text,overrides.hint.4.format,overrides.shuffleanswers
1,match,,maincat,1,"Hint 1",0,"Hint 2",0,"Hint 3",0,"Hint 4",0,"Hint 5",0,0
1 slot type which cat mark overrides.hint.0.text overrides.hint.0.format overrides.hint.1.text overrides.hint.1.format overrides.hint.2.text overrides.hint.2.format overrides.hint.3.text overrides.hint.3.format overrides.hint.4.text overrides.hint.4.format overrides.shuffleanswers
2 1 match maincat 1 Hint 1 0 Hint 2 0 Hint 3 0 Hint 4 0 Hint 5 0 0
@@ -0,0 +1,2 @@
slot,type,which,cat,mark
1,calculatedsimple,sumwithvariants,maincat,1
1 slot type which cat mark
2 1 calculatedsimple sumwithvariants maincat 1
+5
View File
@@ -0,0 +1,5 @@
testnumber,preferredbehaviour
00,deferredfeedback
01,interactive
02,interactive
03,deferredfeedback
1 testnumber preferredbehaviour
2 00 deferredfeedback
3 01 interactive
4 02 interactive
5 03 deferredfeedback
@@ -0,0 +1,19 @@
slot,randq,variant,subpart,modelresponse,actualresponse,totalcount
1,numerical,1,1,"3.142 (3.142..3.142)",3.142,1
1,numerical,1,1,"3.14 (3.14..3.14)",3.14,7
1,numerical,1,1,"3.1 (3.1..3.1)",3.1,4
1,shortanswer,1,1,frog,frog,9
1,shortanswer,1,1,*,butterfly,2
1,shortanswer,1,1,toad,toad,2
2,,1,1,{a} + {b} (±0.01 Relative),9.9,3
2,,1,1,[NO MATCH],-0.7,1
2,,1,1,[NO MATCH],-0.2,1
2,,1,1,[NO MATCH],-1,1
2,,4,1,{a} + {b} (±0.01 Relative),19.4,2
2,,4,1,[NO MATCH],0,1
2,,4,1,[NO MATCH],-0.4,1
3,,1,1,frog: amphibian,amphibian,25
3,,1,2,cat: mammal,mammal,24
3,,1,2,cat: amphibian,amphibian,1
3,,1,3,newt: amphibian,amphibian,24
3,,1,3,newt: mammal,mammal,1
1 slot randq variant subpart modelresponse actualresponse totalcount
2 1 numerical 1 1 3.142 (3.142..3.142) 3.142 1
3 1 numerical 1 1 3.14 (3.14..3.14) 3.14 7
4 1 numerical 1 1 3.1 (3.1..3.1) 3.1 4
5 1 shortanswer 1 1 frog frog 9
6 1 shortanswer 1 1 * butterfly 2
7 1 shortanswer 1 1 toad toad 2
8 2 1 1 {a} + {b} (±0.01 Relative) 9.9 3
9 2 1 1 [NO MATCH] -0.7 1
10 2 1 1 [NO MATCH] -0.2 1
11 2 1 1 [NO MATCH] -1 1
12 2 4 1 {a} + {b} (±0.01 Relative) 19.4 2
13 2 4 1 [NO MATCH] 0 1
14 2 4 1 [NO MATCH] -0.4 1
15 3 1 1 frog: amphibian amphibian 25
16 3 1 2 cat: mammal mammal 24
17 3 1 2 cat: amphibian amphibian 1
18 3 1 3 newt: amphibian amphibian 24
19 3 1 3 newt: mammal mammal 1
@@ -0,0 +1,73 @@
slot,randq,variant,subpart,modelresponse,actualresponse,count1,count2,count3,count4,count5,totalcount
1,shortanswer,1,1,frog,,0,0,0,0,0,0
1,shortanswer,1,1,toad,toad,2,1,1,0,0,4
1,shortanswer,1,1,*,butterfly,1,0,0,0,0,1
1,shortanswer,1,1,*,dog,1,1,0,0,0,2
1,shortanswer,1,1,*,chicken,0,0,1,0,0,1
1,shortanswer,1,1,*,Tod,1,0,0,0,0,1
1,shortanswer,1,1,*,Tony,0,1,0,0,0,1
1,shortanswer,1,1,*,Sharon,0,0,1,0,0,1
1,shortanswer,1,1,*,snake,1,1,0,0,0,2
1,shortanswer,1,1,*,snakes,0,0,1,0,0,1
1,shortanswer,1,1,*,Snakes,0,0,0,1,0,1
1,shortanswer,1,1,*,SnakeS,0,0,0,0,1,1
1,shortanswer,1,1,*,goat,1,0,0,0,0,1
1,shortanswer,1,1,*,"Mexican burrowing caecilian",0,1,1,0,0,2
1,shortanswer,1,1,*,newt,0,0,0,1,0,1
1,shortanswer,1,1,*,human,0,0,0,0,1,1
1,shortanswer,1,1,*,eggs,1,0,0,0,0,1
1,shortanswer,1,1,"[No response]",,0,0,0,0,0,0
1,numerical,1,1,3.14 (3.14..3.14),3.14,2,0,0,0,0,2
1,numerical,1,1,3.142 (3.142..3.142),,0,0,0,0,0,0
1,numerical,1,1,3.1 (3.1..3.1),3.1,1,0,0,0,0,1
1,numerical,1,1,3 (3..3),,0,0,0,0,0,0
1,numerical,1,1,*,2,1,0,0,0,0,1
1,numerical,1,1,*,20,0,1,0,0,0,1
1,numerical,1,1,*,34,0,0,1,0,0,1
1,numerical,1,1,[No response],,0,0,0,0,0,0
2,,1,1,{a} + {b} (±0.01 Relative),9.9,0,0,0,0,1,1
2,,1,1,[Did not match any answer],23,1,0,0,0,0,1
2,,1,1,[Did not match any answer],22,0,1,0,0,0,1
2,,1,1,[Did not match any answer],21,0,0,1,0,0,1
2,,1,1,[Did not match any answer],9,0,0,0,1,0,1
2,,1,1,[No response],,0,0,0,0,0,0
2,,2,1,{a} + {b} (±0.01 Relative),8.5,1,0,0,0,1,2
2,,2,1,"[Did not match any answer]",19.4,1,1,0,0,0,2
2,,2,1,[Did not match any answer],4.5,1,0,0,0,0,1
2,,2,1,"[Did not match any answer]",8,1,1,1,1,0,4
2,,2,1,[No response],,0,0,0,0,0,0
2,,3,1,{a} + {b} (±0.01 Relative),3.3,0,1,0,0,0,1
2,,3,1,[Did not match any answer],19.4,1,0,0,0,0,1
2,,3,1,[No response],,0,0,0,0,0,0
2,,4,1,{a} + {b} (±0.01 Relative),19.4,2,0,0,0,0,2
2,,4,1,{a} + {b} (±0.01 Relative),19.3,1,0,0,0,0,1
2,,4,1,[Did not match any answer],,0,0,0,0,0,0
2,,4,1,[No response],,0,0,0,0,0,0
2,,6,1,"{a} + {b} (±0.01 Relative)",9.4,1,0,0,0,0,1
2,,6,1,"[Did not match any answer]",,0,0,0,0,0,0
2,,6,1,"[No response]",,0,0,0,0,0,0
2,,9,1,"{a} + {b} (±0.01 Relative)",,0,0,0,0,0,0
2,,9,1,"[Did not match any answer]",7,1,0,0,0,0,1
2,,9,1,"[No response]",,0,0,0,0,0,0
2,,10,1,"{a} + {b} (±0.01 Relative)",,0,0,0,0,0,0
2,,10,1,"[Did not match any answer]",555,1,0,0,0,0,1
2,,10,1,"[Did not match any answer]",44,0,1,0,0,0,1
2,,10,1,"[Did not match any answer]",22,0,0,1,0,0,1
2,,10,1,"[Did not match any answer]",11,0,0,0,1,0,1
2,,10,1,"[Did not match any answer]",12,0,0,0,0,1,1
2,,10,1,"[No response]",,0,0,0,0,0,0
3,,1,1,"frog: amphibian",amphibian,8,2,3,3,0,16
3,,1,1,frog: mammal,mammal,0,0,0,1,1,2
3,,1,1,"frog: insect",insect,4,4,2,1,0,11
3,,1,1,[No response],,0,0,0,0,0,0
3,,1,2,"cat: amphibian",amphibian,8,1,2,1,1,13
3,,1,2,cat: mammal,mammal,0,1,1,2,0,4
3,,1,2,"cat: insect",insect,4,4,2,2,0,12
3,,1,2,[No response],,0,0,0,0,0,0
3,,1,3,"newt: amphibian",amphibian,6,4,4,3,1,18
3,,1,3,"newt: mammal",mammal,3,0,1,2,0,6
3,,1,3,newt: insect,insect,3,2,0,0,0,5
3,,1,3,[No response],,0,0,0,0,0,0
4,,1,1,False,,3,0,0,0,0,3
4,,1,1,True,,9,0,0,0,0,9
4,,1,1,[No response],,0,0,0,0,0,0
1 slot randq variant subpart modelresponse actualresponse count1 count2 count3 count4 count5 totalcount
2 1 shortanswer 1 1 frog 0 0 0 0 0 0
3 1 shortanswer 1 1 toad toad 2 1 1 0 0 4
4 1 shortanswer 1 1 * butterfly 1 0 0 0 0 1
5 1 shortanswer 1 1 * dog 1 1 0 0 0 2
6 1 shortanswer 1 1 * chicken 0 0 1 0 0 1
7 1 shortanswer 1 1 * Tod 1 0 0 0 0 1
8 1 shortanswer 1 1 * Tony 0 1 0 0 0 1
9 1 shortanswer 1 1 * Sharon 0 0 1 0 0 1
10 1 shortanswer 1 1 * snake 1 1 0 0 0 2
11 1 shortanswer 1 1 * snakes 0 0 1 0 0 1
12 1 shortanswer 1 1 * Snakes 0 0 0 1 0 1
13 1 shortanswer 1 1 * SnakeS 0 0 0 0 1 1
14 1 shortanswer 1 1 * goat 1 0 0 0 0 1
15 1 shortanswer 1 1 * Mexican burrowing caecilian 0 1 1 0 0 2
16 1 shortanswer 1 1 * newt 0 0 0 1 0 1
17 1 shortanswer 1 1 * human 0 0 0 0 1 1
18 1 shortanswer 1 1 * eggs 1 0 0 0 0 1
19 1 shortanswer 1 1 [No response] 0 0 0 0 0 0
20 1 numerical 1 1 3.14 (3.14..3.14) 3.14 2 0 0 0 0 2
21 1 numerical 1 1 3.142 (3.142..3.142) 0 0 0 0 0 0
22 1 numerical 1 1 3.1 (3.1..3.1) 3.1 1 0 0 0 0 1
23 1 numerical 1 1 3 (3..3) 0 0 0 0 0 0
24 1 numerical 1 1 * 2 1 0 0 0 0 1
25 1 numerical 1 1 * 20 0 1 0 0 0 1
26 1 numerical 1 1 * 34 0 0 1 0 0 1
27 1 numerical 1 1 [No response] 0 0 0 0 0 0
28 2 1 1 {a} + {b} (±0.01 Relative) 9.9 0 0 0 0 1 1
29 2 1 1 [Did not match any answer] 23 1 0 0 0 0 1
30 2 1 1 [Did not match any answer] 22 0 1 0 0 0 1
31 2 1 1 [Did not match any answer] 21 0 0 1 0 0 1
32 2 1 1 [Did not match any answer] 9 0 0 0 1 0 1
33 2 1 1 [No response] 0 0 0 0 0 0
34 2 2 1 {a} + {b} (±0.01 Relative) 8.5 1 0 0 0 1 2
35 2 2 1 [Did not match any answer] 19.4 1 1 0 0 0 2
36 2 2 1 [Did not match any answer] 4.5 1 0 0 0 0 1
37 2 2 1 [Did not match any answer] 8 1 1 1 1 0 4
38 2 2 1 [No response] 0 0 0 0 0 0
39 2 3 1 {a} + {b} (±0.01 Relative) 3.3 0 1 0 0 0 1
40 2 3 1 [Did not match any answer] 19.4 1 0 0 0 0 1
41 2 3 1 [No response] 0 0 0 0 0 0
42 2 4 1 {a} + {b} (±0.01 Relative) 19.4 2 0 0 0 0 2
43 2 4 1 {a} + {b} (±0.01 Relative) 19.3 1 0 0 0 0 1
44 2 4 1 [Did not match any answer] 0 0 0 0 0 0
45 2 4 1 [No response] 0 0 0 0 0 0
46 2 6 1 {a} + {b} (±0.01 Relative) 9.4 1 0 0 0 0 1
47 2 6 1 [Did not match any answer] 0 0 0 0 0 0
48 2 6 1 [No response] 0 0 0 0 0 0
49 2 9 1 {a} + {b} (±0.01 Relative) 0 0 0 0 0 0
50 2 9 1 [Did not match any answer] 7 1 0 0 0 0 1
51 2 9 1 [No response] 0 0 0 0 0 0
52 2 10 1 {a} + {b} (±0.01 Relative) 0 0 0 0 0 0
53 2 10 1 [Did not match any answer] 555 1 0 0 0 0 1
54 2 10 1 [Did not match any answer] 44 0 1 0 0 0 1
55 2 10 1 [Did not match any answer] 22 0 0 1 0 0 1
56 2 10 1 [Did not match any answer] 11 0 0 0 1 0 1
57 2 10 1 [Did not match any answer] 12 0 0 0 0 1 1
58 2 10 1 [No response] 0 0 0 0 0 0
59 3 1 1 frog: amphibian amphibian 8 2 3 3 0 16
60 3 1 1 frog: mammal mammal 0 0 0 1 1 2
61 3 1 1 frog: insect insect 4 4 2 1 0 11
62 3 1 1 [No response] 0 0 0 0 0 0
63 3 1 2 cat: amphibian amphibian 8 1 2 1 1 13
64 3 1 2 cat: mammal mammal 0 1 1 2 0 4
65 3 1 2 cat: insect insect 4 4 2 2 0 12
66 3 1 2 [No response] 0 0 0 0 0 0
67 3 1 3 newt: amphibian amphibian 6 4 4 3 1 18
68 3 1 3 newt: mammal mammal 3 0 1 2 0 6
69 3 1 3 newt: insect insect 3 2 0 0 0 5
70 3 1 3 [No response] 0 0 0 0 0 0
71 4 1 1 False 3 0 0 0 0 3
72 4 1 1 True 9 0 0 0 0 9
73 4 1 1 [No response] 0 0 0 0 0 0
@@ -0,0 +1,9 @@
slot,subpart,modelresponse,actualresponse,totalcount,count1,count2,count3,count4,count5
1,1,"frog: insect",insect,12,3,3,2,2,2
1,1,"frog: mammal",mammal,4,0,0,1,1,2
1,1,"frog: amphibian",amphibian,1,0,0,0,0,1
1,2,"cat: insect",insect,13,2,3,3,3,2
1,2,cat: amphibian,amphibian,1,1,0,0,0,0
1,2,cat: mammal,mammal,3,0,0,0,0,3
1,3,newt: insect,insect,15,3,3,3,3,3
1,3,newt: amphibian,amphibian,2,0,0,0,0,2
1 slot subpart modelresponse actualresponse totalcount count1 count2 count3 count4 count5
2 1 1 frog: insect insect 12 3 3 2 2 2
3 1 1 frog: mammal mammal 4 0 0 1 1 2
4 1 1 frog: amphibian amphibian 1 0 0 0 0 1
5 1 2 cat: insect insect 13 2 3 3 3 2
6 1 2 cat: amphibian amphibian 1 1 0 0 0 0
7 1 2 cat: mammal mammal 3 0 0 0 0 3
8 1 3 newt: insect insect 15 3 3 3 3 3
9 1 3 newt: amphibian amphibian 2 0 0 0 0 2
@@ -0,0 +1,2 @@
slot,variant,modelresponse,actualresponse,totalcount
1,4,"{a} + {b} (±0.01 Relative)",19.4,1
1 slot variant modelresponse actualresponse totalcount
2 1 4 {a} + {b} (±0.01 Relative) 19.4 1
+26
View File
@@ -0,0 +1,26 @@
quizattempt,slots.1.mark,slots.2.mark,slots.3.mark,slots.4.mark,slots.5.mark,slots.6.mark,slots.7.mark,summarks
1,1,1,1,1,1,0,1,6
2,1,1,1,1,0.5,1,0,5.5
3,1,1,1,1,0.5,0,1,5.5
4,0,0,1,1,1,0,0,3
5,1,0,1,0,0,1,0.5,3.5
6,1,1,1,1,0.5,0,0.5,5
7,0,1,1,1,0.5,1,0.5,5
8,1,1,1,0,0.5,1,0.5,5
9,1,0,1,1,1,1,1,6
10,0,1,1,0,0.5,1,0.5,4
11,0.8,0,1,1,0.5,1,0.5,4.8
12,1,1,1,0,0.5,1,1,5.5
13,1,0,1,1,0.5,1,0.5,5
14,1,0,1,1,1,0,0.5,4.5
15,0,0,1,1,0,1,1,4
16,1,0,1,0,0.5,1,1,4.5
17,0.8,0,1,1,0.5,0,1,4.3
18,1,0,1,1,0,1,0.5,4.5
19,1,0,1,1,0.5,0,0.5,4
20,0,1,1,0,0.5,1,0,3.5
21,1,1,0.3333333,1,0.5,0,0.5,4.33333
22,1,1,1,0,0.5,0,0.5,4
23,1,1,1,1,0,1,0.5,5.5
24,0,0,1,0,0.5,1,1,3.5
25,0,0,1,1,1,1,1,5
1 quizattempt slots.1.mark slots.2.mark slots.3.mark slots.4.mark slots.5.mark slots.6.mark slots.7.mark summarks
2 1 1 1 1 1 1 0 1 6
3 2 1 1 1 1 0.5 1 0 5.5
4 3 1 1 1 1 0.5 0 1 5.5
5 4 0 0 1 1 1 0 0 3
6 5 1 0 1 0 0 1 0.5 3.5
7 6 1 1 1 1 0.5 0 0.5 5
8 7 0 1 1 1 0.5 1 0.5 5
9 8 1 1 1 0 0.5 1 0.5 5
10 9 1 0 1 1 1 1 1 6
11 10 0 1 1 0 0.5 1 0.5 4
12 11 0.8 0 1 1 0.5 1 0.5 4.8
13 12 1 1 1 0 0.5 1 1 5.5
14 13 1 0 1 1 0.5 1 0.5 5
15 14 1 0 1 1 1 0 0.5 4.5
16 15 0 0 1 1 0 1 1 4
17 16 1 0 1 0 0.5 1 1 4.5
18 17 0.8 0 1 1 0.5 0 1 4.3
19 18 1 0 1 1 0 1 0.5 4.5
20 19 1 0 1 1 0.5 0 0.5 4
21 20 0 1 1 0 0.5 1 0 3.5
22 21 1 1 0.3333333 1 0.5 0 0.5 4.33333
23 22 1 1 1 0 0.5 0 0.5 4
24 23 1 1 1 1 0 1 0.5 5.5
25 24 0 0 1 0 0.5 1 1 3.5
26 25 0 0 1 1 1 1 1 5
+26
View File
@@ -0,0 +1,26 @@
quizattempt,firstname,lastname,randqs.1,responses.1.answer,variants.2,responses.2.answer,responses.3.cat,responses.3.frog,responses.3.newt,responses.4.answer,responses.5.Four,responses.5.One,responses.5.Three,responses.5.Two,responses.6.answer,responses.7.1.answer,responses.7.2.answer
1,John,Jones,numerical,3.14,1,9.9,mammal,amphibian,amphibian,1,,1,1,,Two,Owl,Pussy-cat
2,John,Smith,shortanswer,frog,1,9.9,mammal,amphibian,amphibian,1,,,1,1,One,Dog,Bow-wow
3,John,Vicars,numerical,3.14,6,9.4,mammal,amphibian,amphibian,1,1,,1,,Two,Owl,Pussy-cat
4,John,Pacino,shortanswer,butterfly,6,-0.1,mammal,amphibian,amphibian,1,,1,1,,Four,Dog,Bow-wow
5,John,Deniro,numerical,3.14,4,0,mammal,amphibian,amphibian,0,1,,,1,One,Dog,Pussy-cat
6,John,Banks,numerical,3.14,1,9.9,mammal,amphibian,amphibian,1,1,1,,,Two,Owl,"Wiggly worm"
7,John,Asimov,numerical,3.142,7,9.1,mammal,amphibian,amphibian,1,1,,1,,One,Owl,"Wiggly worm"
8,John,Chomsky,numerical,3.14,4,19.4,mammal,amphibian,amphibian,0,1,,1,,One,Owl,"Wiggly worm"
9,John,Yamaguchi,shortanswer,frog,1,-0.7,mammal,amphibian,amphibian,1,,1,1,,One,Owl,Pussy-cat
10,John,Robbins,numerical,3.1,5,14.2,mammal,amphibian,amphibian,0,,,1,1,One,Owl,Bow-wow
11,Joe,Jones,shortanswer,toad,6,-0.2,mammal,amphibian,amphibian,1,,1,,1,One,Owl,Bow-wow
12,Joe,Smith,shortanswer,frog,8,5.7,mammal,amphibian,amphibian,0,1,,1,,One,Owl,Pussy-cat
13,Joe,Vicars,numerical,3.14,8,-0.2,mammal,amphibian,amphibian,1,1,1,,,One,wfz9p,Pussy-cat
14,Joe,Pacino,shortanswer,frog,1,-0.2,mammal,amphibian,amphibian,1,,1,1,,Two,Owl,Bow-wow
15,Joe,Deniro,numerical,3.1,7,-0.9,mammal,amphibian,amphibian,1,1,,,1,One,Owl,Pussy-cat
16,Joe,Banks,shortanswer,frog,10,-0.7,mammal,amphibian,amphibian,0,1,1,,,One,Owl,Pussy-cat
17,Joe,Asimov,shortanswer,toad,4,-0.4,mammal,amphibian,amphibian,1,,,1,1,Three,Owl,Pussy-cat
18,Joe,Chomsky,shortanswer,frog,6,-1,mammal,amphibian,amphibian,1,1,,,1,One,Pussy-cat,Pussy-cat
19,Joe,Yamaguchi,numerical,3.14,8,-0.5,mammal,amphibian,amphibian,1,,1,,1,Two,Owl,Bow-wow
20,Joe,Robbins,shortanswer,butterfly,4,19.4,mammal,amphibian,amphibian,0,,,1,1,One,"Wiggly worm",Bow-wow
21,Roberto,Jones,shortanswer,frog,8,5.7,amphibian,amphibian,mammal,1,,,1,1,Three,RjUpn,Pussy-cat
22,Roberto,Smith,shortanswer,frog,5,14.2,mammal,amphibian,amphibian,0,1,1,,,Two,Dog,Pussy-cat
23,Roberto,Vicars,shortanswer,frog,5,14.2,mammal,amphibian,amphibian,1,1,,,1,One,"Wiggly worm",Pussy-cat
24,Roberto,Pacino,numerical,3.1,1,-1,mammal,amphibian,amphibian,0,,1,,1,One,Owl,Pussy-cat
25,Roberto,Deniro,numerical,3.1,8,-0.1,mammal,amphibian,amphibian,1,,1,1,,One,Owl,Pussy-cat
1 quizattempt firstname lastname randqs.1 responses.1.answer variants.2 responses.2.answer responses.3.cat responses.3.frog responses.3.newt responses.4.answer responses.5.Four responses.5.One responses.5.Three responses.5.Two responses.6.answer responses.7.1.answer responses.7.2.answer
2 1 John Jones numerical 3.14 1 9.9 mammal amphibian amphibian 1 1 1 Two Owl Pussy-cat
3 2 John Smith shortanswer frog 1 9.9 mammal amphibian amphibian 1 1 1 One Dog Bow-wow
4 3 John Vicars numerical 3.14 6 9.4 mammal amphibian amphibian 1 1 1 Two Owl Pussy-cat
5 4 John Pacino shortanswer butterfly 6 -0.1 mammal amphibian amphibian 1 1 1 Four Dog Bow-wow
6 5 John Deniro numerical 3.14 4 0 mammal amphibian amphibian 0 1 1 One Dog Pussy-cat
7 6 John Banks numerical 3.14 1 9.9 mammal amphibian amphibian 1 1 1 Two Owl Wiggly worm
8 7 John Asimov numerical 3.142 7 9.1 mammal amphibian amphibian 1 1 1 One Owl Wiggly worm
9 8 John Chomsky numerical 3.14 4 19.4 mammal amphibian amphibian 0 1 1 One Owl Wiggly worm
10 9 John Yamaguchi shortanswer frog 1 -0.7 mammal amphibian amphibian 1 1 1 One Owl Pussy-cat
11 10 John Robbins numerical 3.1 5 14.2 mammal amphibian amphibian 0 1 1 One Owl Bow-wow
12 11 Joe Jones shortanswer toad 6 -0.2 mammal amphibian amphibian 1 1 1 One Owl Bow-wow
13 12 Joe Smith shortanswer frog 8 5.7 mammal amphibian amphibian 0 1 1 One Owl Pussy-cat
14 13 Joe Vicars numerical 3.14 8 -0.2 mammal amphibian amphibian 1 1 1 One wfz9p Pussy-cat
15 14 Joe Pacino shortanswer frog 1 -0.2 mammal amphibian amphibian 1 1 1 Two Owl Bow-wow
16 15 Joe Deniro numerical 3.1 7 -0.9 mammal amphibian amphibian 1 1 1 One Owl Pussy-cat
17 16 Joe Banks shortanswer frog 10 -0.7 mammal amphibian amphibian 0 1 1 One Owl Pussy-cat
18 17 Joe Asimov shortanswer toad 4 -0.4 mammal amphibian amphibian 1 1 1 Three Owl Pussy-cat
19 18 Joe Chomsky shortanswer frog 6 -1 mammal amphibian amphibian 1 1 1 One Pussy-cat Pussy-cat
20 19 Joe Yamaguchi numerical 3.14 8 -0.5 mammal amphibian amphibian 1 1 1 Two Owl Bow-wow
21 20 Joe Robbins shortanswer butterfly 4 19.4 mammal amphibian amphibian 0 1 1 One Wiggly worm Bow-wow
22 21 Roberto Jones shortanswer frog 8 5.7 amphibian amphibian mammal 1 1 1 Three RjUpn Pussy-cat
23 22 Roberto Smith shortanswer frog 5 14.2 mammal amphibian amphibian 0 1 1 Two Dog Pussy-cat
24 23 Roberto Vicars shortanswer frog 5 14.2 mammal amphibian amphibian 1 1 1 One Wiggly worm Pussy-cat
25 24 Roberto Pacino numerical 3.1 1 -1 mammal amphibian amphibian 0 1 1 One Owl Pussy-cat
26 25 Roberto Deniro numerical 3.1 8 -0.1 mammal amphibian amphibian 1 1 1 One Owl Pussy-cat
+79
View File
@@ -0,0 +1,79 @@
quizattempt,firstname,lastname,randqs.1,responses.1.-submit,responses.1.-tryagain,responses.1.answer,variants.2,responses.2.-submit,responses.2.-tryagain,responses.2.answer,responses.3.-submit,responses.3.-tryagain,responses.3.cat,responses.3.frog,responses.3.newt,responses.4.-submit,responses.4.answer,finished
1,John,Jones,shortanswer,1,,butterfly,4,,,19.4,,,amphibian,,,,1,0
1,,,,,1,butterfly,,,,19.4,1,,insect,insect,insect,,1,0
1,,,,1,,dog,,,,19.4,,1,insect,insect,insect,,1,0
1,,,,,1,dog,,,,19.4,1,,insect,insect,amphibian,,1,0
1,,,,1,,chicken,,,,19.4,,1,insect,insect,amphibian,,1,0
1,,,,,,chicken,,,,19.4,1,,mammal,insect,amphibian,,1,0
1,,,,,,chicken,,,,19.4,,1,mammal,insect,amphibian,,1,0
1,,,,,,chicken,,,,19.4,1,,amphibian,insect,amphibian,,1,0
1,,,,,,chicken,,,,19.4,,1,amphibian,insect,amphibian,,1,0
1,,,,,,chicken,,,,19.4,1,,amphibian,mammal,amphibian,,1,1
2,Han,Solo,shortanswer,1,,Tod,2,1,,19.4,1,,amphibian,amphibian,amphibian,1,0,0
2,,,,,1,Tod,,,1,19.4,,1,amphibian,amphibian,amphibian,,0,0
2,,,,1,,Tony,,,,19.4,1,,mammal,insect,insect,,0,0
2,,,,,1,Tony,,,,19.4,,1,mammal,insect,insect,,0,0
2,,,,1,,Sharon,,,,19.4,1,,insect,amphibian,mammal,,0,0
2,,,,,,Sharon,,,,19.4,,1,insect,amphibian,mammal,,0,0
2,,,,,,Sharon,,,,19.4,1,,insect,amphibian,mammal,,0,1
3,Yoda,"Wise He Is",shortanswer,1,,snake,9,1,,7,1,,insect,amphibian,amphibian,,1,0
3,,,,,1,snake,,,,7,,1,insect,amphibian,amphibian,,1,0
3,,,,1,,snake,,,,7,1,,insect,amphibian,amphibian,,1,0
3,,,,,1,snake,,,,7,,1,insect,amphibian,amphibian,,1,0
3,,,,1,,snakes,,,,7,1,,insect,amphibian,amphibian,,1,0
3,,,,,1,snakes,,,,7,,1,insect,amphibian,amphibian,,1,0
3,,,,1,,Snakes,,,,7,1,,mammal,amphibian,amphibian,,1,0
3,,,,,1,Snakes,,,,7,,,mammal,amphibian,amphibian,,1,0
3,,,,1,,SnakeS,,,,7,,,mammal,amphibian,amphibian,,1,1
4,Herbert,Garrison,shortanswer,,,dog,6,,,9.4,1,,amphibian,amphibian,amphibian,,1,0
4,,,,,,dog,,,,9.4,,1,amphibian,amphibian,amphibian,,1,0
4,,,,,,dog,,,,9.4,1,,insect,insect,insect,,1,0
4,,,,,,dog,,,,9.4,,1,insect,insect,insect,,1,0
4,,,,,,dog,,,,9.4,1,,amphibian,insect,amphibian,,1,0
4,,,,,,dog,,,,9.4,,1,amphibian,insect,amphibian,,1,0
4,,,,,,dog,,,,9.4,1,,insect,mammal,mammal,,1,1
5,Agent,Smith,numerical,1,,3.1,2,1,,x+y,,,amphibian,amphibian,mammal,,1,0
5,,,,,,3.1,,1,,4.5,,,amphibian,amphibian,mammal,,1,1
6,Agent,Smith,numerical,,,3.142,3,,,19.4,,,insect,amphibian,mammal,1,0,0
6,,,,1,,3.14,,1,,19.4,1,,insect,amphibian,mammal,,0,0
6,,,,,,3.14,,,1,19.4,,1,insect,amphibian,mammal,,0,0
6,,,,,,3.14,,1,,3.3,1,,insect,insect,amphibian,,0,1
7,Agent,Smith,numerical,1,,3.14,4,1,,19.3,,,amphibian,insect,insect,,1,1
8,Bebe,Stevens,shortanswer,1,,goat,2,1,,8.5,,,amphibian,amphibian,insect,,1,0
8,,,,,1,goat,,,,8.5,,,amphibian,amphibian,insect,,1,0
8,,,,1,,"Mexican burrowing caecilian",,,,8.5,,,amphibian,amphibian,insect,,1,0
8,,,,,1,"Mexican burrowing caecilian",,,,8.5,,,amphibian,amphibian,insect,,1,0
8,,,,1,,"Mexican burrowing caecilian",,,,8.5,,,amphibian,amphibian,insect,,1,0
8,,,,,1,"Mexican burrowing caecilian",,,,8.5,,,amphibian,amphibian,insect,,1,0
8,,,,1,,newt,,,,8.5,,,amphibian,amphibian,insect,,1,0
8,,,,,1,newt,,,,8.5,,,amphibian,amphibian,insect,,1,0
8,,,,1,,human,,,,8.5,,,amphibian,amphibian,insect,,1,1
9,Luke,Skywalker,numerical,1,,2,10,1,,555,1,,amphibian,amphibian,amphibian,1,0,0
9,,,,,1,2,,,1,555,,1,amphibian,amphibian,amphibian,,0,0
9,,,,1,,20,,1,,44,1,,amphibian,amphibian,amphibian,,0,0
9,,,,,1,20,,,1,44,,1,amphibian,amphibian,amphibian,,0,0
9,,,,1,,34,,1,,22,1,,amphibian,amphibian,amphibian,,0,0
9,,,,,,34,,,1,22,,1,amphibian,amphibian,amphibian,,0,0
9,,,,,,34,,1,,11,1,,mammal,amphibian,amphibian,,0,0
9,,,,,,34,,,1,11,,,mammal,amphibian,amphibian,,0,0
9,,,,,,34,,1,,12,,,mammal,amphibian,amphibian,,0,1
10,Luke,Skywalker,shortanswer,1,,toad,1,1,,23,1,,amphibian,insect,amphibian,,1,0
10,,,,,,toad,,,1,23,,,amphibian,insect,amphibian,,1,0
10,,,,,,toad,,1,,22,,,amphibian,insect,amphibian,,1,0
10,,,,,,toad,,,1,22,,,amphibian,insect,amphibian,,1,0
10,,,,,,toad,,1,,21,,,amphibian,insect,amphibian,,1,0
10,,,,,,toad,,,1,21,,,amphibian,insect,amphibian,,1,0
10,,,,,,toad,,1,,9,,,amphibian,insect,amphibian,,1,0
10,,,,,,toad,,,1,9,,,amphibian,insect,amphibian,,1,0
10,,,,,,toad,,1,,9.9,,,amphibian,insect,amphibian,,1,1
11,Luke,Skywalker,shortanswer,1,,toad,2,1,,8,1,,insect,insect,amphibian,,1,0
11,,,,,1,toad,,,1,8,,,insect,insect,amphibian,,1,0
11,,,,1,,toad,,1,,8,,,insect,insect,amphibian,,1,0
11,,,,,1,toad,,,1,8,,,insect,insect,amphibian,,1,0
11,,,,1,,toad,,1,,8,,,insect,insect,amphibian,,1,0
11,,,,,,toad,,,1,8,,,insect,insect,amphibian,,1,0
11,,,,,,toad,,1,,8,,,insect,insect,amphibian,,1,0
11,,,,,,toad,,,1,8,,,insect,insect,amphibian,,1,0
11,,,,,,toad,,1,,8.5,,,insect,insect,amphibian,,1,1
12,Leia,"The Princess",shortanswer,,,eggs,4,,,19.4,1,,amphibian,amphibian,,1,1,0
12,,,,,,eggs,,,,19.4,1,,amphibian,amphibian,mammal,,1,1
1 quizattempt firstname lastname randqs.1 responses.1.-submit responses.1.-tryagain responses.1.answer variants.2 responses.2.-submit responses.2.-tryagain responses.2.answer responses.3.-submit responses.3.-tryagain responses.3.cat responses.3.frog responses.3.newt responses.4.-submit responses.4.answer finished
2 1 John Jones shortanswer 1 butterfly 4 19.4 amphibian 1 0
3 1 1 butterfly 19.4 1 insect insect insect 1 0
4 1 1 dog 19.4 1 insect insect insect 1 0
5 1 1 dog 19.4 1 insect insect amphibian 1 0
6 1 1 chicken 19.4 1 insect insect amphibian 1 0
7 1 chicken 19.4 1 mammal insect amphibian 1 0
8 1 chicken 19.4 1 mammal insect amphibian 1 0
9 1 chicken 19.4 1 amphibian insect amphibian 1 0
10 1 chicken 19.4 1 amphibian insect amphibian 1 0
11 1 chicken 19.4 1 amphibian mammal amphibian 1 1
12 2 Han Solo shortanswer 1 Tod 2 1 19.4 1 amphibian amphibian amphibian 1 0 0
13 2 1 Tod 1 19.4 1 amphibian amphibian amphibian 0 0
14 2 1 Tony 19.4 1 mammal insect insect 0 0
15 2 1 Tony 19.4 1 mammal insect insect 0 0
16 2 1 Sharon 19.4 1 insect amphibian mammal 0 0
17 2 Sharon 19.4 1 insect amphibian mammal 0 0
18 2 Sharon 19.4 1 insect amphibian mammal 0 1
19 3 Yoda Wise He Is shortanswer 1 snake 9 1 7 1 insect amphibian amphibian 1 0
20 3 1 snake 7 1 insect amphibian amphibian 1 0
21 3 1 snake 7 1 insect amphibian amphibian 1 0
22 3 1 snake 7 1 insect amphibian amphibian 1 0
23 3 1 snakes 7 1 insect amphibian amphibian 1 0
24 3 1 snakes 7 1 insect amphibian amphibian 1 0
25 3 1 Snakes 7 1 mammal amphibian amphibian 1 0
26 3 1 Snakes 7 mammal amphibian amphibian 1 0
27 3 1 SnakeS 7 mammal amphibian amphibian 1 1
28 4 Herbert Garrison shortanswer dog 6 9.4 1 amphibian amphibian amphibian 1 0
29 4 dog 9.4 1 amphibian amphibian amphibian 1 0
30 4 dog 9.4 1 insect insect insect 1 0
31 4 dog 9.4 1 insect insect insect 1 0
32 4 dog 9.4 1 amphibian insect amphibian 1 0
33 4 dog 9.4 1 amphibian insect amphibian 1 0
34 4 dog 9.4 1 insect mammal mammal 1 1
35 5 Agent Smith numerical 1 3.1 2 1 x+y amphibian amphibian mammal 1 0
36 5 3.1 1 4.5 amphibian amphibian mammal 1 1
37 6 Agent Smith numerical 3.142 3 19.4 insect amphibian mammal 1 0 0
38 6 1 3.14 1 19.4 1 insect amphibian mammal 0 0
39 6 3.14 1 19.4 1 insect amphibian mammal 0 0
40 6 3.14 1 3.3 1 insect insect amphibian 0 1
41 7 Agent Smith numerical 1 3.14 4 1 19.3 amphibian insect insect 1 1
42 8 Bebe Stevens shortanswer 1 goat 2 1 8.5 amphibian amphibian insect 1 0
43 8 1 goat 8.5 amphibian amphibian insect 1 0
44 8 1 Mexican burrowing caecilian 8.5 amphibian amphibian insect 1 0
45 8 1 Mexican burrowing caecilian 8.5 amphibian amphibian insect 1 0
46 8 1 Mexican burrowing caecilian 8.5 amphibian amphibian insect 1 0
47 8 1 Mexican burrowing caecilian 8.5 amphibian amphibian insect 1 0
48 8 1 newt 8.5 amphibian amphibian insect 1 0
49 8 1 newt 8.5 amphibian amphibian insect 1 0
50 8 1 human 8.5 amphibian amphibian insect 1 1
51 9 Luke Skywalker numerical 1 2 10 1 555 1 amphibian amphibian amphibian 1 0 0
52 9 1 2 1 555 1 amphibian amphibian amphibian 0 0
53 9 1 20 1 44 1 amphibian amphibian amphibian 0 0
54 9 1 20 1 44 1 amphibian amphibian amphibian 0 0
55 9 1 34 1 22 1 amphibian amphibian amphibian 0 0
56 9 34 1 22 1 amphibian amphibian amphibian 0 0
57 9 34 1 11 1 mammal amphibian amphibian 0 0
58 9 34 1 11 mammal amphibian amphibian 0 0
59 9 34 1 12 mammal amphibian amphibian 0 1
60 10 Luke Skywalker shortanswer 1 toad 1 1 23 1 amphibian insect amphibian 1 0
61 10 toad 1 23 amphibian insect amphibian 1 0
62 10 toad 1 22 amphibian insect amphibian 1 0
63 10 toad 1 22 amphibian insect amphibian 1 0
64 10 toad 1 21 amphibian insect amphibian 1 0
65 10 toad 1 21 amphibian insect amphibian 1 0
66 10 toad 1 9 amphibian insect amphibian 1 0
67 10 toad 1 9 amphibian insect amphibian 1 0
68 10 toad 1 9.9 amphibian insect amphibian 1 1
69 11 Luke Skywalker shortanswer 1 toad 2 1 8 1 insect insect amphibian 1 0
70 11 1 toad 1 8 insect insect amphibian 1 0
71 11 1 toad 1 8 insect insect amphibian 1 0
72 11 1 toad 1 8 insect insect amphibian 1 0
73 11 1 toad 1 8 insect insect amphibian 1 0
74 11 toad 1 8 insect insect amphibian 1 0
75 11 toad 1 8 insect insect amphibian 1 0
76 11 toad 1 8 insect insect amphibian 1 0
77 11 toad 1 8.5 insect insect amphibian 1 1
78 12 Leia The Princess shortanswer eggs 4 19.4 1 amphibian amphibian 1 1 0
79 12 eggs 19.4 1 amphibian amphibian mammal 1 1
+32
View File
@@ -0,0 +1,32 @@
quizattempt,firstname,lastname,responses.1.-submit,responses.1.-tryagain,responses.1.cat,responses.1.frog,responses.1.newt,finished
1,John,Jones,1,,insect,insect,insect,0
1,,,,1,insect,insect,insect,0
1,,,1,,insect,insect,insect,0
1,,,,1,insect,insect,insect,0
1,,,1,,insect,insect,insect,0
1,,,,1,insect,insect,insect,0
1,,,1,,insect,insect,insect,0
1,,,,1,insect,insect,insect,0
1,,,1,,insect,insect,insect,1
2,Mark,Jones,1,,amphibian,insect,insect,0
2,,,,1,amphibian,insect,insect,0
2,,,1,,insect,insect,insect,0
2,,,,1,insect,insect,insect,0
2,,,1,,insect,mammal,insect,0
2,,,,1,insect,mammal,insect,0
2,,,1,,insect,mammal,insect,0
2,,,,1,insect,mammal,insect,0
2,,,1,,mammal,mammal,insect,0
2,,,,1,mammal,mammal,insect,0
2,,,1,,mammal,mammal,amphibian,1
3,Michael,Jackson,1,,insect,insect,insect,0
3,,,,1,insect,insect,insect,0
3,,,1,,insect,insect,insect,0
3,,,,1,insect,insect,insect,0
3,,,1,,insect,insect,insect,0
3,,,,1,insect,insect,insect,0
3,,,1,,insect,insect,insect,0
3,,,,1,insect,insect,insect,0
3,,,1,,insect,insect,insect,0
3,,,,1,insect,insect,insect,0
3,,,1,,mammal,amphibian,amphibian,1
1 quizattempt firstname lastname responses.1.-submit responses.1.-tryagain responses.1.cat responses.1.frog responses.1.newt finished
2 1 John Jones 1 insect insect insect 0
3 1 1 insect insect insect 0
4 1 1 insect insect insect 0
5 1 1 insect insect insect 0
6 1 1 insect insect insect 0
7 1 1 insect insect insect 0
8 1 1 insect insect insect 0
9 1 1 insect insect insect 0
10 1 1 insect insect insect 1
11 2 Mark Jones 1 amphibian insect insect 0
12 2 1 amphibian insect insect 0
13 2 1 insect insect insect 0
14 2 1 insect insect insect 0
15 2 1 insect mammal insect 0
16 2 1 insect mammal insect 0
17 2 1 insect mammal insect 0
18 2 1 insect mammal insect 0
19 2 1 mammal mammal insect 0
20 2 1 mammal mammal insect 0
21 2 1 mammal mammal amphibian 1
22 3 Michael Jackson 1 insect insect insect 0
23 3 1 insect insect insect 0
24 3 1 insect insect insect 0
25 3 1 insect insect insect 0
26 3 1 insect insect insect 0
27 3 1 insect insect insect 0
28 3 1 insect insect insect 0
29 3 1 insect insect insect 0
30 3 1 insect insect insect 0
31 3 1 insect insect insect 0
32 3 1 mammal amphibian amphibian 1
+2
View File
@@ -0,0 +1,2 @@
quizattempt,firstname,lastname,responses.1.answer,variants.1
1,John,Jones,19.4,4
1 quizattempt firstname lastname responses.1.answer variants.1
2 1 John Jones 19.4 4
@@ -0,0 +1,154 @@
<?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 quiz_statistics;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
use core\task\manager;
use quiz_statistics\task\recalculate;
use quiz_statistics\tests\statistics_helper;
use quiz_statistics\tests\statistics_test_trait;
/**
* Unit tests for attempt_deleted observer
*
* @package quiz_statistics
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \quiz_statistics\hook_callbacks::quiz_attempt_submitted_or_deleted
*/
class quiz_attempt_deleted_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
use statistics_test_trait;
/**
* Deleting an attempt should queue the recalculation task for that quiz in 1 hour's time.
*
* @return void
*/
public function test_queue_task_on_deletion(): void {
[$user, $quiz] = $this->create_test_data();
$this->attempt_quiz($quiz, $user);
[, , $attempt] = $this->attempt_quiz($quiz, $user, 2);
statistics_helper::run_pending_recalculation_tasks(true);
$tasks = manager::get_adhoc_tasks(recalculate::class);
$this->assertEmpty($tasks);
quiz_delete_attempt($attempt->get_attemptid(), $quiz);
$tasks = manager::get_adhoc_tasks(recalculate::class);
$this->assertCount(1, $tasks);
$task = reset($tasks);
$this->assert_task_is_queued_for_quiz($task, $quiz);
}
/**
* Deleting multiple attempts of the same quiz should only queue one instance of the task.
*
* @return void
*/
public function test_queue_single_task_for_multiple_deletions(): void {
[$user1, $quiz] = $this->create_test_data();
$user2 = $this->getDataGenerator()->create_user();
$this->attempt_quiz($quiz, $user1);
[, , $attempt1] = $this->attempt_quiz($quiz, $user1, 2);
$this->attempt_quiz($quiz, $user2);
[, , $attempt2] = $this->attempt_quiz($quiz, $user2, 2);
statistics_helper::run_pending_recalculation_tasks(true);
$tasks = manager::get_adhoc_tasks(recalculate::class);
$this->assertEmpty($tasks);
quiz_delete_attempt($attempt1->get_attemptid(), $quiz);
quiz_delete_attempt($attempt2->get_attemptid(), $quiz);
$tasks = manager::get_adhoc_tasks(recalculate::class);
$this->assertCount(1, $tasks);
$task = reset($tasks);
$this->assert_task_is_queued_for_quiz($task, $quiz);
}
/**
* Deleting another attempt after processing the task should queue a new task.
*
* @return void
*/
public function test_queue_new_task_after_processing(): void {
[$user1, $quiz, $course] = $this->create_test_data();
$user2 = $this->getDataGenerator()->create_user();
$this->attempt_quiz($quiz, $user1);
[, , $attempt1] = $this->attempt_quiz($quiz, $user1, 2);
$this->attempt_quiz($quiz, $user2);
[, , $attempt2] = $this->attempt_quiz($quiz, $user2, 2);
statistics_helper::run_pending_recalculation_tasks(true);
$tasks = manager::get_adhoc_tasks(recalculate::class);
$this->assertEmpty($tasks);
quiz_delete_attempt($attempt1->get_attemptid(), $quiz);
$tasks = manager::get_adhoc_tasks(recalculate::class);
$this->assertCount(1, $tasks);
$this->expectOutputRegex("~Re-calculating statistics for quiz {$quiz->name} \({$quiz->id}\) " .
"from course {$course->shortname} \({$course->id}\) with 3 attempts~");
statistics_helper::run_pending_recalculation_tasks();
$tasks = manager::get_adhoc_tasks(recalculate::class);
$this->assertEmpty($tasks);
quiz_delete_attempt($attempt2->get_attemptid(), $quiz);
$tasks = manager::get_adhoc_tasks(recalculate::class);
$this->assertCount(1, $tasks);
$task = reset($tasks);
$this->assert_task_is_queued_for_quiz($task, $quiz);
}
/**
* Deleting attempts from different quizzes will queue a task for each.
*
* @return void
*/
public function test_queue_separate_tasks_for_multiple_quizzes(): void {
[$user1, $quiz1] = $this->create_test_data();
[$user2, $quiz2] = $this->create_test_data();
$this->attempt_quiz($quiz1, $user1);
[, , $attempt1] = $this->attempt_quiz($quiz1, $user1, 2);
$this->attempt_quiz($quiz2, $user2);
[, , $attempt2] = $this->attempt_quiz($quiz2, $user2, 2);
statistics_helper::run_pending_recalculation_tasks(true);
$tasks = manager::get_adhoc_tasks(recalculate::class);
$this->assertEmpty($tasks);
quiz_delete_attempt($attempt1->get_attemptid(), $quiz1);
quiz_delete_attempt($attempt2->get_attemptid(), $quiz2);
$tasks = manager::get_adhoc_tasks(recalculate::class);
$this->assertCount(2, $tasks);
$task1 = array_shift($tasks);
$this->assert_task_is_queued_for_quiz($task1, $quiz1);
$task2 = array_shift($tasks);
$this->assert_task_is_queued_for_quiz($task2, $quiz2);
}
}
@@ -0,0 +1,136 @@
<?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 quiz_statistics;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
use core\task\manager;
use quiz_statistics\task\recalculate;
use quiz_statistics\tests\statistics_helper;
use quiz_statistics\tests\statistics_test_trait;
/**
* Unit tests for attempt_submitted observer
*
* @package quiz_statistics
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \quiz_statistics\hook_callbacks::quiz_attempt_submitted_or_deleted
*/
class quiz_attempt_submitted_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
use statistics_test_trait;
/**
* Attempting a quiz should queue the recalculation task for that quiz in 1 hour's time.
*
* @return void
*/
public function test_queue_task_on_submission(): void {
[$user, $quiz] = $this->create_test_data();
$tasks = manager::get_adhoc_tasks(recalculate::class);
$this->assertEmpty($tasks);
$this->attempt_quiz($quiz, $user);
$tasks = manager::get_adhoc_tasks(recalculate::class);
$this->assertCount(1, $tasks);
$task = reset($tasks);
$this->assert_task_is_queued_for_quiz($task, $quiz);
}
/**
* Attempting a quiz multiple times should only queue one instance of the task.
*
* @return void
*/
public function test_queue_single_task_for_multiple_submissions(): void {
[$user1, $quiz] = $this->create_test_data();
$user2 = $this->getDataGenerator()->create_user();
$tasks = manager::get_adhoc_tasks(recalculate::class);
$this->assertEmpty($tasks);
$this->attempt_quiz($quiz, $user1);
$this->attempt_quiz($quiz, $user2);
$tasks = manager::get_adhoc_tasks(recalculate::class);
$this->assertCount(1, $tasks);
$task = reset($tasks);
$this->assert_task_is_queued_for_quiz($task, $quiz);
}
/**
* Attempting the quiz again after processing the task should queue a new task.
*
* @return void
*/
public function test_queue_new_task_after_processing(): void {
[$user1, $quiz, $course] = $this->create_test_data();
$user2 = $this->getDataGenerator()->create_user();
$tasks = manager::get_adhoc_tasks(recalculate::class);
$this->assertEmpty($tasks);
$this->attempt_quiz($quiz, $user1);
$tasks = manager::get_adhoc_tasks(recalculate::class);
$this->assertCount(1, $tasks);
$this->expectOutputRegex("~Re-calculating statistics for quiz {$quiz->name} \({$quiz->id}\) " .
"from course {$course->shortname} \({$course->id}\) with 1 attempts~");
statistics_helper::run_pending_recalculation_tasks();
$tasks = manager::get_adhoc_tasks(recalculate::class);
$this->assertEmpty($tasks);
$this->attempt_quiz($quiz, $user2);
$tasks = manager::get_adhoc_tasks(recalculate::class);
$this->assertCount(1, $tasks);
$task = reset($tasks);
$this->assert_task_is_queued_for_quiz($task, $quiz);
}
/**
* Attempting different quizzes will queue a task for each.
*
* @return void
*/
public function test_queue_separate_tasks_for_multiple_quizzes(): void {
[$user1, $quiz1] = $this->create_test_data();
[$user2, $quiz2] = $this->create_test_data();
$tasks = manager::get_adhoc_tasks(recalculate::class);
$this->assertEmpty($tasks);
$this->attempt_quiz($quiz1, $user1);
$this->attempt_quiz($quiz2, $user2);
$tasks = manager::get_adhoc_tasks(recalculate::class);
$this->assertCount(2, $tasks);
$task1 = array_shift($tasks);
$this->assert_task_is_queued_for_quiz($task1, $quiz1);
$task2 = array_shift($tasks);
$this->assert_task_is_queued_for_quiz($task2, $quiz2);
}
}
@@ -0,0 +1,223 @@
<?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 quiz_statistics;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/report/statistics/report.php');
require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');
require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
/**
* Tests for statistics report
*
* @package quiz_statistics
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \quiz_statistics_report
*/
class quiz_statistics_report_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
/**
* Secondary database connection for creating locks.
*
* @var \moodle_database|null
*/
protected static ?\moodle_database $lockdb;
/**
* Lock factory using the secondary database connection.
*
* @var \moodle_database|null
*/
protected static ?\core\lock\lock_factory $lockfactory;
/**
* Create a lock factory with a second database session.
*
* This allows us to create a lock in our test code that will block a lock request
* on the same key in code under test.
*/
public function setUp(): void {
global $CFG;
self::$lockdb = \moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
self::$lockdb->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->prefix, $CFG->dboptions);
$lockfactoryclass = \core\lock\lock_config::get_lock_factory_class();
$lockfactory = new $lockfactoryclass('quiz_statistics_get_stats');
// Iterate lock factory hierarchy to see if it contains a 'db' property we can use.
$reflectionclass = new \ReflectionClass($lockfactory);
while ($reflectionclass) {
if ($reflectionhasdb = $reflectionclass->hasProperty('db')) {
break;
}
$reflectionclass = $reflectionclass->getParentClass();
}
if (!$reflectionhasdb) {
$this->markTestSkipped('Test lock factory should be a db type');
}
$reflectiondb = new \ReflectionProperty($lockfactory, 'db');
$reflectiondb->setValue($lockfactory, self::$lockdb);
self::$lockfactory = $lockfactory;
}
/**
* Dispose of the extra DB connection and lock factory.
*/
public function tearDown(): void {
self::$lockdb->dispose();
self::$lockdb = null;
self::$lockfactory = null;
}
/**
* Return a generated quiz
*
* @return \stdClass
*/
protected function create_and_attempt_quiz(): \stdClass {
$course = $this->getDataGenerator()->create_course();
$user = $this->getDataGenerator()->create_user();
$quiz = $this->create_test_quiz($course);
$quizcontext = \context_module::instance($quiz->cmid);
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$this->add_two_regular_questions($questiongenerator, $quiz, ['contextid' => $quizcontext->id]);
$this->attempt_quiz($quiz, $user);
return $quiz;
}
/**
* Test locking the calculation process.
*
* When there is a lock on the hash code, test_get_all_stats_and_analysis() should wait until the lock timeout, then throw an
* exception.
*
* When there is no lock (or the lock has been released), it should return a result.
*
* @return void
*/
public function test_get_all_stats_and_analysis_locking(): void {
$this->resetAfterTest(true);
$quiz = $this->create_and_attempt_quiz();
$whichattempts = QUIZ_GRADEAVERAGE; // All attempts.
$whichtries = \question_attempt::ALL_TRIES;
$groupstudentsjoins = new \core\dml\sql_join();
$qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudentsjoins, $whichattempts);
$report = new \quiz_statistics_report();
$questions = $report->load_and_initialise_questions_for_calculations($quiz);
$timeoutseconds = 20;
set_config('getstatslocktimeout', $timeoutseconds, 'quiz_statistics');
$lock = self::$lockfactory->get_lock($qubaids->get_hash_code(), 0);
$progress = new \core\progress\none();
$this->resetDebugging();
$timebefore = microtime(true);
try {
$result = $report->get_all_stats_and_analysis(
$quiz,
$whichattempts,
$whichtries,
$groupstudentsjoins,
$questions,
$progress
);
$timeafter = microtime(true);
// Verify that we waited as long as the timeout.
$this->assertEqualsWithDelta($timeoutseconds, $timeafter - $timebefore, 1);
$this->assertDebuggingCalled('Could not get lock on ' .
$qubaids->get_hash_code() . ' (Quiz ID ' . $quiz->id . ') after ' .
$timeoutseconds . ' seconds');
$this->assertEquals([null, null], $result);
} finally {
$lock->release();
}
$this->resetDebugging();
$result = $report->get_all_stats_and_analysis(
$quiz,
$whichattempts,
$whichtries,
$groupstudentsjoins,
$questions
);
$this->assertDebuggingNotCalled();
$this->assertNotEquals([null, null], $result);
}
/**
* Test locking when the current page does not require calculations.
*
* When there is a lock on the hash code, test_get_all_stats_and_analysis() should return a null result immediately,
* with no exception thrown.
*
* @return void
*/
public function test_get_all_stats_and_analysis_locking_no_calculation(): void {
$this->resetAfterTest(true);
$quiz = $this->create_and_attempt_quiz();
$whichattempts = QUIZ_GRADEAVERAGE; // All attempts.
$whichtries = \question_attempt::ALL_TRIES;
$groupstudentsjoins = new \core\dml\sql_join();
$qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudentsjoins, $whichattempts);
$report = new \quiz_statistics_report();
$questions = $report->load_and_initialise_questions_for_calculations($quiz);
$timeoutseconds = 20;
set_config('getstatslocktimeout', $timeoutseconds, 'quiz_statistics');
$lock = self::$lockfactory->get_lock($qubaids->get_hash_code(), 0);
$this->resetDebugging();
try {
$progress = new \core\progress\none();
$timebefore = microtime(true);
$result = $report->get_all_stats_and_analysis(
$quiz,
$whichattempts,
$whichtries,
$groupstudentsjoins,
$questions,
$progress,
false
);
$timeafter = microtime(true);
// Verify that we did not wait for the timeout before returning.
$this->assertLessThan($timeoutseconds, $timeafter - $timebefore);
$this->assertEquals([null, null], $result);
$this->assertDebuggingNotCalled();
} finally {
$lock->release();
}
}
}
@@ -0,0 +1,88 @@
<?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 quiz_statistics;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
use core\progress\none;
use mod_quiz\grade_calculator;
use mod_quiz\quiz_settings;
/**
* Unit tests for quiz_statistics\event\observer\slots_updated
*
* @package quiz_statistics
* @copyright 2023 onwards Catalyst IT EU {@link https://catalyst-eu.net}
* @author Mark Johnson <mark.johnson@catalyst-eu.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \quiz_statistics\quiz_structure_modified
*/
class quiz_structure_modified_test extends \advanced_testcase {
use \quiz_question_helper_test_trait;
/**
* Clear the statistics cache for a quiz when it structure is modified.
*
* When recompute_quiz_sumgrades() is called, it should trigger this plugin's quiz_structure_modified callback
* which clears the statistics cache for the quiz.
*
* @return void
*/
public function test_clear_cache_on_structure_modified(): void {
global $DB;
$this->resetAfterTest(true);
// Create and attempt a quiz.
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$course = $generator->create_course();
$quiz = $this->create_test_quiz($course);
$questiongenerator = $generator->get_plugin_generator('core_question');
$category = $questiongenerator->create_question_category();
$question = $questiongenerator->create_question('match', null, ['category' => $category->id]);
$questiongenerator->update_question($question);
quiz_add_quiz_question($question->id, $quiz);
[, , $attempt] = $this->attempt_quiz($quiz, $user);
// Run the statistics calculation to prime the cache.
$report = new \quiz_statistics_report();
$questions = $report->load_and_initialise_questions_for_calculations($quiz);
$report->get_all_stats_and_analysis(
$quiz,
$quiz->grademethod,
\question_attempt::ALL_TRIES,
new \core\dml\sql_join(),
$questions,
new none(),
);
$hashcode = quiz_statistics_qubaids_condition($quiz->id, new \core\dml\sql_join(), $quiz->grademethod)->get_hash_code();
$this->assertTrue($DB->record_exists('quiz_statistics', ['hashcode' => $hashcode]));
$this->assertTrue($DB->record_exists('question_statistics', ['hashcode' => $hashcode]));
$this->assertTrue($DB->record_exists('question_response_analysis', ['hashcode' => $hashcode]));
// Recompute sumgrades, which triggers the quiz_structure_modified callback.
grade_calculator::create($attempt->get_quizobj())->recompute_quiz_sumgrades();
$this->assertFalse($DB->record_exists('quiz_statistics', ['hashcode' => $hashcode]));
$this->assertFalse($DB->record_exists('question_statistics', ['hashcode' => $hashcode]));
$this->assertFalse($DB->record_exists('question_response_analysis', ['hashcode' => $hashcode]));
}
}
@@ -0,0 +1,89 @@
<?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 quiz_statistics;
use quiz_statistics_table;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_table.php');
/**
* Class quiz_statistics_statistics_table_testcase
*
* @package quiz_statistics
* @category test
* @copyright 2018 Shamim Rezaie <shamim@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class statistics_table_test extends \advanced_testcase {
public function test_format_percentage(): void {
$table = new quiz_statistics_table();
// The format_percentage method is protected. Use Reflection to call the method.
$reflector = new \ReflectionClass('quiz_statistics_table');
$method = $reflector->getMethod('format_percentage');
$this->assertEquals(
'84.758%',
$method->invokeArgs($table, [0.847576, true, 3])
);
$this->assertEquals(
'84.758%',
$method->invokeArgs($table, [84.7576, false, 3])
);
}
public function test_format_percentage_range(): void {
$table = new quiz_statistics_table();
// The format_percentage_range method is protected. Use Reflection to call the method.
$reflector = new \ReflectionClass('quiz_statistics_table');
$method = $reflector->getMethod('format_percentage_range');
$this->assertEquals(
'54.400% 84.758%',
$method->invokeArgs($table, [0.544, 0.847576, true, 3])
);
$this->assertEquals(
'54.400% 84.758%',
$method->invokeArgs($table, [54.4, 84.7576, false, 3])
);
}
public function test_format_range(): void {
$table = new quiz_statistics_table();
// The format_range method is protected. Use Reflection to call the method.
$reflector = new \ReflectionClass('quiz_statistics_table');
$method = $reflector->getMethod('format_range');
$this->assertEquals(
'5 10',
$method->invokeArgs($table, [5, 10])
);
$this->assertEquals(
'Some Text 10',
$method->invokeArgs($table, ['Some Text', 10])
);
}
}
@@ -0,0 +1,199 @@
<?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/>.
/**
* Unit tests for (some of) /question/engine/statistics.php
*
* @package quiz_statistics
* @category test
* @copyright 2008 Jamie Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace quiz_statistics;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir . '/questionlib.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
class testable_all_calculated_for_qubaid_condition extends \core_question\statistics\questions\all_calculated_for_qubaid_condition {
/**
* Disabling caching in tests so we are always sure to force the calculation of stats right then and there.
*
* @param qubaid_condition $qubaids
*/
public function cache($qubaids) {
}
}
/**
* Test helper subclass of question_statistics
*
* @copyright 2010 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class testable_question_statistics extends \core_question\statistics\questions\calculator {
/**
* @var stdClass[]
*/
protected $lateststeps;
protected $statscollectionclassname = '\quiz_statistics\testable_all_calculated_for_qubaid_condition';
public function set_step_data($states) {
$this->lateststeps = $states;
}
protected function get_random_guess_score($questiondata) {
return 0;
}
/**
* @param $qubaids qubaid_condition is ignored in this test
* @return array with two items
* - $lateststeps array of latest step data for the question usages
* - $summarks array of total marks for each usage, indexed by usage id
*/
protected function get_latest_steps($qubaids) {
$summarks = [];
$fakeusageid = 0;
foreach ($this->lateststeps as $step) {
// The same 'sumgrades' field is available in step data for every slot, we will ignore all slots but slot 1.
// The step for slot 1 is always the first one in the csv file for each usage, we will use that to separate steps from
// each usage.
if ($step->slot == 1) {
$fakeusageid++;
$summarks[$fakeusageid] = $step->sumgrades;
}
unset($step->sumgrades);
$step->questionusageid = $fakeusageid;
}
return [$this->lateststeps, $summarks];
}
protected function cache_stats($qubaids) {
// No caching wanted for tests.
}
}
/**
* Unit tests for (some of) question_statistics.
*
* @copyright 2008 Jamie Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class statistics_test extends \basic_testcase {
/** @var testable_all_calculated_for_qubaid_condition object created to test class. */
protected $qstats;
public function test_qstats(): void {
global $CFG;
// Data is taken from randomly generated attempts data generated by
// contrib/tools/generators/qagenerator/.
$steps = $this->get_records_from_csv(__DIR__.'/fixtures/mdl_question_states.csv');
// Data is taken from questions mostly generated by
// contrib/tools/generators/generator.php.
$questions = $this->get_records_from_csv(__DIR__.'/fixtures/mdl_question.csv');
$calculator = new testable_question_statistics($questions);
$calculator->set_step_data($steps);
$this->qstats = $calculator->calculate(null);
// Values expected are taken from contrib/tools/quiz_tools/stats.xls.
$facility = [0, 0, 0, 0, null, null, null, 41.19318182, 81.36363636,
71.36363636, 65.45454545, 65.90909091, 36.36363636, 59.09090909, 50,
59.09090909, 63.63636364, 45.45454545, 27.27272727, 50];
$this->qstats_q_fields('facility', $facility, 100);
$sd = [0, 0, 0, 0, null, null, null, 1912.733589, 251.2738111,
322.6312277, 333.4199022, 337.5811591, 492.3659639, 503.2362797,
511.7663157, 503.2362797, 492.3659639, 509.6471914, 455.8423058, 511.7663157];
$this->qstats_q_fields('sd', $sd, 1000);
$effectiveweight = [0, 0, 0, 0, 0, 0, 0, 26.58464457, 3.368456046,
3.253955259, 7.584083694, 3.79658376, 3.183278505, 4.532356904,
7.78856243, 10.08351572, 8.381139345, 8.727645713, 7.946277111, 4.769500946];
$this->qstats_q_fields('effectiveweight', $effectiveweight);
$discriminationindex = [null, null, null, null, null, null, null,
25.88327077, 1.170256965, -4.207816809, 28.16930644, -2.513606859,
-12.99017581, -8.900638238, 8.670004606, 29.63337745, 15.18945843,
16.21079629, 15.52451404, -8.396734802];
$this->qstats_q_fields('discriminationindex', $discriminationindex);
$discriminativeefficiency = [null, null, null, null, null, null, null,
27.23492723, 1.382386552, -4.691171307, 31.12404354, -2.877487579,
-17.5074184, -10.27568922, 10.86956522, 34.58997279, 17.4790556,
20.14359793, 22.06477733, -10];
$this->qstats_q_fields('discriminativeefficiency', $discriminativeefficiency);
}
public function qstats_q_fields($fieldname, $values, $multiplier=1) {
foreach ($this->qstats->get_all_slots() as $slot) {
$value = array_shift($values);
if ($value !== null) {
$this->assertEqualsWithDelta($value, $this->qstats->for_slot($slot)->{$fieldname} * $multiplier, 1E-6);
} else {
$this->assertEquals($value, $this->qstats->for_slot($slot)->{$fieldname} * $multiplier);
}
}
}
public function get_fields_from_csv($line) {
$line = trim($line);
$items = preg_split('!,!', $line);
$cnt = count($items);
for ($key = 0; $key < $cnt; $key++) {
if ($items[$key]!='') {
if ($start = ($items[$key][0]=='"')) {
$items[$key] = substr($items[$key], 1);
while (!$end = ($items[$key][strlen($items[$key])-1]=='"')) {
$item = $items[$key];
unset($items[$key]);
$key++;
$items[$key] = $item . ',' . $items[$key];
}
$items[$key] = substr($items[$key], 0, strlen($items[$key])-1);
}
}
}
return $items;
}
public function get_records_from_csv($filename) {
$filecontents = file($filename, FILE_IGNORE_NEW_LINES);
$records = [];
// Skip the first line containing field names.
$keys = $this->get_fields_from_csv(array_shift($filecontents));
while (null !== ($line = array_shift($filecontents))) {
$data = $this->get_fields_from_csv($line);
$arraykey = reset($data);
$object = new \stdClass();
foreach ($keys as $key) {
$value = array_shift($data);
if ($value !== null) {
$object->{$key} = $value;
} else {
$object->{$key} = '';
}
}
$records[$arraykey] = $object;
}
return $records;
}
}

Some files were not shown because too many files have changed in this diff Show More