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
@@ -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;
}
}
@@ -0,0 +1,392 @@
<?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 question_attempt;
use question_bank;
use question_finder;
use quiz_statistics_report;
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.
*
* The quiz stats below and the question stats found in qstats00.csv were calculated independently in a spreadsheet which is
* available in open document or excel format here :
* https://github.com/jamiepratt/moodle-quiz-tools/tree/master/statsspreadsheet
*
* Similarly the question variant's stats in qstats00.csv are calculated in stats_for_variant_1.xls and stats_for_variant_8.xls
* The calculations in the spreadsheets are the same as for the other question stats but applied just to the attempts where the
* variants appeared.
*
* @package quiz_statistics
* @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 stats_from_steps_walkthrough_test extends \mod_quiz\attempt_walkthrough_from_csv_test {
/**
* @var quiz_statistics_report object to do stats calculations.
*/
protected $report;
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', 'results', 'qstats', 'responsecounts'];
/**
* Create a quiz add questions to it, walk through quiz attempts and then check results.
*
* @param array $csvdata data read from csv file "questionsXX.csv", "stepsXX.csv" and "resultsXX.csv".
* @dataProvider get_data_for_walkthrough
*/
public function test_walkthrough_from_csv($quizsettings, $csvdata): void {
$this->create_quiz_simulate_attempts_and_check_results($quizsettings, $csvdata);
$whichattempts = QUIZ_GRADEAVERAGE; // All attempts.
$whichtries = question_attempt::ALL_TRIES;
$groupstudentsjoins = new \core\dml\sql_join();
list($questions, $quizstats, $questionstats, $qubaids) =
$this->check_stats_calculations_and_response_analysis($csvdata,
$whichattempts, $whichtries, $groupstudentsjoins);
if ($quizsettings['testnumber'] === '00') {
$this->check_variants_count_for_quiz_00($questions, $questionstats, $whichtries, $qubaids);
$this->check_quiz_stats_for_quiz_00($quizstats);
}
}
/**
* Check actual question stats are the same as that found in csv file.
*
* @param $qstats array data from csv file.
* @param $questionstats \core_question\statistics\questions\all_calculated_for_qubaid_condition Calculated stats.
*/
protected function check_question_stats($qstats, $questionstats) {
foreach ($qstats as $slotqstats) {
foreach ($slotqstats as $statname => $slotqstat) {
if (!in_array($statname, ['slot', 'subqname']) && $slotqstat !== '') {
$this->assert_stat_equals($slotqstat,
$questionstats,
$slotqstats['slot'],
$slotqstats['subqname'],
$slotqstats['variant'],
$statname);
}
}
// Check that sub-question boolean field is correctly set.
$this->assert_stat_equals(!empty($slotqstats['subqname']),
$questionstats,
$slotqstats['slot'],
$slotqstats['subqname'],
$slotqstats['variant'],
'subquestion');
}
}
/**
* Check that the stat is as expected within a reasonable tolerance.
*
* @param float|string|bool $expected expected value of stat.
* @param \core_question\statistics\questions\all_calculated_for_qubaid_condition $questionstats
* @param int $slot
* @param string $subqname if empty string then not an item stat.
* @param int|string $variant if empty string then not a variantstat.
* @param string $statname
*/
protected function assert_stat_equals($expected, $questionstats, $slot, $subqname, $variant, $statname) {
if ($variant === '' && $subqname === '') {
$actual = $questionstats->for_slot($slot)->{$statname};
} else if ($subqname !== '') {
$actual = $questionstats->for_subq($this->randqids[$slot][$subqname])->{$statname};
} else {
$actual = $questionstats->for_slot($slot, $variant)->{$statname};
}
$message = "$statname for slot $slot";
if ($expected === '**NULL**') {
$this->assertEquals(null, $actual, $message);
} else if (is_bool($expected)) {
$this->assertEquals($expected, $actual, $message);
} else if (is_numeric($expected)) {
switch ($statname) {
case 'covariance' :
case 'discriminationindex' :
case 'discriminativeefficiency' :
case 'effectiveweight' :
$precision = 1e-5;
break;
default :
$precision = 1e-6;
}
$delta = abs($expected) * $precision;
$this->assertEqualsWithDelta((float)$expected, $actual, $delta, $message);
} else {
$this->assertEquals($expected, $actual, $message);
}
}
protected function assert_response_count_equals($question, $qubaids, $expected, $whichtries) {
$responesstats = new \core_question\statistics\responses\analyser($question);
$analysis = $responesstats->load_cached($qubaids, $whichtries);
if (!isset($expected['subpart'])) {
$subpart = 1;
} else {
$subpart = $expected['subpart'];
}
list($subpartid, $responseclassid) = $this->get_response_subpart_and_class_id($question,
$subpart,
$expected['modelresponse']);
$subpartanalysis = $analysis->get_analysis_for_subpart($expected['variant'], $subpartid);
$responseclassanalysis = $subpartanalysis->get_response_class($responseclassid);
$actualresponsecounts = $responseclassanalysis->data_for_question_response_table('', '');
foreach ($actualresponsecounts as $actualresponsecount) {
if ($actualresponsecount->response == $expected['actualresponse'] || count($actualresponsecounts) == 1) {
$i = 1;
$partofanalysis = " slot {$expected['slot']}, rand q '{$expected['randq']}', variant {$expected['variant']}, ".
"for expected model response {$expected['modelresponse']}, ".
"actual response {$expected['actualresponse']}";
while (isset($expected['count'.$i])) {
if ($expected['count'.$i] != 0) {
$this->assertTrue(isset($actualresponsecount->trycount[$i]),
"There is no count at all for try $i on ".$partofanalysis);
$this->assertEquals($expected['count'.$i], $actualresponsecount->trycount[$i],
"Count for try $i on ".$partofanalysis);
}
$i++;
}
if (isset($expected['totalcount'])) {
$this->assertEquals($expected['totalcount'], $actualresponsecount->totalcount,
"Total count on ".$partofanalysis);
}
return;
}
}
throw new \coding_exception("Expected response '{$expected['actualresponse']}' not found.");
}
protected function get_response_subpart_and_class_id($question, $subpart, $modelresponse) {
$qtypeobj = question_bank::get_qtype($question->qtype, false);
$possibleresponses = $qtypeobj->get_possible_responses($question);
$possibleresponsesubpartids = array_keys($possibleresponses);
if (!isset($possibleresponsesubpartids[$subpart - 1])) {
throw new \coding_exception("Subpart '{$subpart}' not found.");
}
$subpartid = $possibleresponsesubpartids[$subpart - 1];
if ($modelresponse == '[NO RESPONSE]') {
return [$subpartid, null];
} else if ($modelresponse == '[NO MATCH]') {
return [$subpartid, 0];
}
$modelresponses = [];
foreach ($possibleresponses[$subpartid] as $responseclassid => $subpartpossibleresponse) {
$modelresponses[$responseclassid] = $subpartpossibleresponse->responseclass;
}
$this->assertContains($modelresponse, $modelresponses);
$responseclassid = array_search($modelresponse, $modelresponses);
return [$subpartid, $responseclassid];
}
/**
* @param $responsecounts
* @param $qubaids
* @param $questions
* @param $whichtries
*/
protected function check_response_counts($responsecounts, $qubaids, $questions, $whichtries) {
foreach ($responsecounts as $expected) {
$defaultsforexpected = ['randq' => '', 'variant' => '1', 'subpart' => '1'];
foreach ($defaultsforexpected as $key => $expecteddefault) {
if (!isset($expected[$key])) {
$expected[$key] = $expecteddefault;
}
}
if ($expected['randq'] == '') {
$question = $questions[$expected['slot']];
} else {
$qid = $this->randqids[$expected['slot']][$expected['randq']];
$question = question_finder::get_instance()->load_question_data($qid);
}
$this->assert_response_count_equals($question, $qubaids, $expected, $whichtries);
}
}
/**
* @param $questions
* @param $questionstats
* @param $whichtries
* @param $qubaids
*/
protected function check_variants_count_for_quiz_00($questions, $questionstats, $whichtries, $qubaids) {
$expectedvariantcounts = [2 => [1 => 6,
4 => 4,
5 => 3,
6 => 4,
7 => 2,
8 => 5,
10 => 1]];
foreach ($questions as $slot => $question) {
if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
continue;
}
$responesstats = new \core_question\statistics\responses\analyser($question);
$this->assertTimeCurrent($responesstats->get_last_analysed_time($qubaids, $whichtries));
$analysis = $responesstats->load_cached($qubaids, $whichtries);
$variantsnos = $analysis->get_variant_nos();
if (isset($expectedvariantcounts[$slot])) {
// Compare contents, ignore ordering of array, using canonicalize parameter of assertEquals.
$this->assertEqualsCanonicalizing(array_keys($expectedvariantcounts[$slot]), $variantsnos);
} else {
$this->assertEquals([1], $variantsnos);
}
$totalspervariantno = [];
foreach ($variantsnos as $variantno) {
$subpartids = $analysis->get_subpart_ids($variantno);
foreach ($subpartids as $subpartid) {
if (!isset($totalspervariantno[$subpartid])) {
$totalspervariantno[$subpartid] = [];
}
$totalspervariantno[$subpartid][$variantno] = 0;
$subpartanalysis = $analysis->get_analysis_for_subpart($variantno, $subpartid);
$classids = $subpartanalysis->get_response_class_ids();
foreach ($classids as $classid) {
$classanalysis = $subpartanalysis->get_response_class($classid);
$actualresponsecounts = $classanalysis->data_for_question_response_table('', '');
foreach ($actualresponsecounts as $actualresponsecount) {
$totalspervariantno[$subpartid][$variantno] += $actualresponsecount->totalcount;
}
}
}
}
// Count all counted responses for each part of question and confirm that counted responses, for most question types
// are the number of attempts at the question for each question part.
if ($slot != 5) {
// Slot 5 holds a multi-choice multiple question.
// Multi-choice multiple is slightly strange. Actual answer counts given for each sub part do not add up to the
// total attempt count.
// This is because each option is counted as a sub part and each option can be off or on in each attempt. Off is
// not counted in response analysis for this question type.
foreach ($totalspervariantno as $totalpervariantno) {
if (isset($expectedvariantcounts[$slot])) {
// If we know how many attempts there are at each variant we can check
// that we have counted the correct amount of responses for each variant.
$this->assertEqualsCanonicalizing($expectedvariantcounts[$slot],
$totalpervariantno,
"Totals responses do not add up in response analysis for slot {$slot}.");
} else {
$this->assertEquals(25,
array_sum($totalpervariantno),
"Totals responses do not add up in response analysis for slot {$slot}.");
}
}
}
}
foreach ($expectedvariantcounts as $slot => $expectedvariantcount) {
foreach ($expectedvariantcount as $variantno => $s) {
$this->assertEquals($s, $questionstats->for_slot($slot, $variantno)->s);
}
}
}
/**
* @param $quizstats
*/
protected function check_quiz_stats_for_quiz_00($quizstats) {
$quizstatsexpected = [
'median' => 4.5,
'firstattemptsavg' => 4.617333332,
'allattemptsavg' => 4.617333332,
'firstattemptscount' => 25,
'allattemptscount' => 25,
'standarddeviation' => 0.8117265554,
'skewness' => -0.092502502,
'kurtosis' => -0.7073968557,
'cic' => -87.2230935542,
'errorratio' => 136.8294900795,
'standarderror' => 1.1106813066
];
foreach ($quizstatsexpected as $statname => $statvalue) {
$this->assertEqualsWithDelta($statvalue, $quizstats->$statname, abs($statvalue) * 1.5e-5, $quizstats->$statname);
}
}
/**
* Check the question stats and the response counts used in the statistics report. If the appropriate files exist in fixtures/.
*
* @param array $csvdata Data loaded from csv files for this test.
* @param string $whichattempts
* @param string $whichtries
* @param \core\dml\sql_join $groupstudentsjoins
* @return array with contents 0 => $questions, 1 => $quizstats, 2 => $questionstats, 3 => $qubaids Might be needed for further
* testing.
*/
protected function check_stats_calculations_and_response_analysis($csvdata, $whichattempts, $whichtries,
\core\dml\sql_join $groupstudentsjoins) {
$this->report = new quiz_statistics_report();
$questions = $this->report->load_and_initialise_questions_for_calculations($this->quiz);
list($quizstats, $questionstats) = $this->report->get_all_stats_and_analysis($this->quiz,
$whichattempts,
$whichtries,
$groupstudentsjoins,
$questions);
$qubaids = quiz_statistics_qubaids_condition($this->quiz->id, $groupstudentsjoins, $whichattempts);
// We will create some quiz and question stat calculator instances and some response analyser instances, just in order
// to check the last analysed time then returned.
$quizcalc = new calculator();
// Should not be a delay of more than one second between the calculation of stats above and here.
$this->assertTimeCurrent($quizcalc->get_last_calculated_time($qubaids));
$qcalc = new \core_question\statistics\questions\calculator($questions);
$this->assertTimeCurrent($qcalc->get_last_calculated_time($qubaids));
if (isset($csvdata['responsecounts'])) {
$this->check_response_counts($csvdata['responsecounts'], $qubaids, $questions, $whichtries);
}
if (isset($csvdata['qstats'])) {
$this->check_question_stats($csvdata['qstats'], $questionstats);
return [$questions, $quizstats, $questionstats, $qubaids];
}
return [$questions, $quizstats, $questionstats, $qubaids];
}
}
+21
View File
@@ -0,0 +1,21 @@
This files describes API changes in /mod/quiz/report/statistics/*,
information provided here is intended especially for developers.
=== 4.3 ===
* The methods quiz_statistics_report::calculate_questions_stats_for_question_bank and get_all_stats_and_analysis
(which are really private to the quiz, and not part of any API you should be using) now have a new
optional argument $calculateifrequired.
* In the past, the methods \quiz_statistics\calculator::get_last_calculated_time() and calculator::get_cached()
only returned the pre-computed statistics if they were computed less than 15 minutes ago. Now, they will
always return any computed statistics that exist. Therefore, the constant calculator::TIME_TO_CACHE has been
deprecated.
=== 3.2 ===
* The function quiz_statistics_graph_get_new_colour() is deprecated in favour of the
funtionality present in the new charting library.
* The function quiz_statistics_renumber_placeholders() is removed as the changes
in MDL-31243 and MDL-27072 make this redundant.
+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 statistics report version information.
*
* @package quiz_statistics
* @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_statistics';