From 6dd9362e84e044a6578b120edd04897cc7fbc1f1 Mon Sep 17 00:00:00 2001 From: Jamie Pratt Date: Mon, 30 Sep 2013 18:05:46 +0700 Subject: [PATCH] MDL-41761 statistics should be calculable for last or highest attempts as well as for first or all attempts. This commit drops a database table and then recreates it. This is in order to be able to have some new columns in the table be NOT NULL with no default value, which would not be possible if I added fields to an existing table. The quiz_statistics table is used for caching calculated values only and thus we can safely drop it and recreate it. --- mod/quiz/report/attemptsreport.php | 5 +- mod/quiz/report/reportlib.php | 66 +++++---- .../report/statistics/classes/calculated.php | 79 +++++++---- .../report/statistics/classes/calculator.php | 131 +++++++++++------- mod/quiz/report/statistics/db/install.xml | 6 +- mod/quiz/report/statistics/db/upgrade.php | 45 ++++++ .../statistics/lang/en/quiz_statistics.php | 5 + mod/quiz/report/statistics/report.php | 70 +++++----- .../report/statistics/statistics_form.php | 16 +-- .../report/statistics/statistics_graph.php | 12 +- mod/quiz/report/statistics/statisticslib.php | 35 +++-- .../stats_from_steps_walkthrough_test.php | 7 +- mod/quiz/report/statistics/version.php | 4 +- 13 files changed, 309 insertions(+), 172 deletions(-) diff --git a/mod/quiz/report/attemptsreport.php b/mod/quiz/report/attemptsreport.php index fe223b78394..4ea24a9926c 100644 --- a/mod/quiz/report/attemptsreport.php +++ b/mod/quiz/report/attemptsreport.php @@ -84,8 +84,7 @@ abstract class quiz_attempts_report extends quiz_default_report { $this->qmsubselect = quiz_report_qm_filter_select($quiz); $this->form = new $formclass($this->get_base_url(), - array('qmsubselect' => $this->qmsubselect, 'quiz' => $quiz, - 'currentgroup' => $currentgroup, 'context' => $this->context)); + array('quiz' => $quiz, 'currentgroup' => $currentgroup, 'context' => $this->context)); return array($currentgroup, $students, $groupstudents, $allowed); } @@ -103,7 +102,7 @@ abstract class quiz_attempts_report extends quiz_default_report { * Get information about which students to show in the report. * @param object $cm the coures module. * @param object $course the course settings. - * @return an array with four elements: + * @return array with four elements: * 0 => integer the current group id (0 for none). * 1 => array ids of all the students in this course. * 2 => array ids of all the students in the current group. diff --git a/mod/quiz/report/reportlib.php b/mod/quiz/report/reportlib.php index bc88dc84f8c..a25a76834c6 100644 --- a/mod/quiz/report/reportlib.php +++ b/mod/quiz/report/reportlib.php @@ -137,52 +137,64 @@ function quiz_report_can_filter_only_graded($quiz) { } /** - * Given the quiz grading method return sub select sql to find the id of the - * one attempt that will be graded for each user. Or return - * empty string if all attempts contribute to final grade. + * This is a wrapper for {@link quiz_report_grade_method_sql} that takes the whole quiz object instead of just the grading method + * as a param. See definition for {@link quiz_report_grade_method_sql} below. + * + * @param object $quiz + * @param string $quizattemptsalias sql alias for 'quiz_attempts' table + * @return string sql to test if this is an attempt that will contribute towards the grade of the user */ function quiz_report_qm_filter_select($quiz, $quizattemptsalias = 'quiza') { if ($quiz->attempts == 1) { // This quiz only allows one attempt. return ''; } + return quiz_report_grade_method_sql($quiz->grademethod, $quizattemptsalias); +} - switch ($quiz->grademethod) { +/** + * Given a quiz grading method return sql to test if this is an + * attempt that will be contribute towards the grade of the user. Or return an + * empty string if the grading method is QUIZ_GRADEAVERAGE and thus all attempts + * contribute to final grade. + * + * @param string $grademethod quiz grading method. + * @param string $quizattemptsalias sql alias for 'quiz_attempts' table + * @return string sql to test if this is an attempt that will contribute towards the graded of the user + */ +function quiz_report_grade_method_sql($grademethod, $quizattemptsalias = 'quiza') { + switch ($grademethod) { case QUIZ_GRADEHIGHEST : return "$quizattemptsalias.id = ( - SELECT MIN(qa2.id) - FROM {quiz_attempts} qa2 - WHERE qa2.quiz = $quizattemptsalias.quiz AND - qa2.userid = $quizattemptsalias.userid AND - COALESCE(qa2.sumgrades, 0) = ( - SELECT MAX(COALESCE(qa3.sumgrades, 0)) - FROM {quiz_attempts} qa3 - WHERE qa3.quiz = $quizattemptsalias.quiz AND - qa3.userid = $quizattemptsalias.userid - ) - )"; + SELECT MIN(qa2.id) + FROM {quiz_attempts} qa2 + WHERE qa2.quiz = $quizattemptsalias.quiz AND + qa2.userid = $quizattemptsalias.userid AND + COALESCE(qa2.sumgrades, 0) = ( + SELECT MAX(COALESCE(qa3.sumgrades, 0)) + FROM {quiz_attempts} qa3 + WHERE qa3.quiz = $quizattemptsalias.quiz AND + qa3.userid = $quizattemptsalias.userid + ) + )"; case QUIZ_GRADEAVERAGE : return ''; case QUIZ_ATTEMPTFIRST : - return "$quizattemptsalias.id = ( - SELECT MIN(qa2.id) - FROM {quiz_attempts} qa2 - WHERE qa2.quiz = $quizattemptsalias.quiz AND - qa2.userid = $quizattemptsalias.userid)"; + return "$quizattemptsalias.attempt = 1"; case QUIZ_ATTEMPTLAST : return "$quizattemptsalias.id = ( - SELECT MAX(qa2.id) - FROM {quiz_attempts} qa2 - WHERE qa2.quiz = $quizattemptsalias.quiz AND - qa2.userid = $quizattemptsalias.userid)"; + SELECT MAX(qa2.id) + FROM {quiz_attempts} qa2 + WHERE qa2.quiz = $quizattemptsalias.quiz AND + qa2.userid = $quizattemptsalias.userid)"; } } /** - * Get the nuber of students whose score was in a particular band for this quiz. + * Get the number of students whose score was in a particular band for this quiz. * @param number $bandwidth the width of each band. * @param int $bands the number of bands * @param int $quizid the quiz id. @@ -225,10 +237,10 @@ ORDER BY $data = $DB->get_records_sql_menu($sql, $params); // We need to create array elements with values 0 at indexes where there is no element. - $data = $data + array_fill(0, $bands + 1, 0); + $data = $data + array_fill(0, $bands + 1, 0); ksort($data); - // Place the maximum (prefect grade) into the last band i.e. make last + // Place the maximum (perfect grade) into the last band i.e. make last // band for example 9 <= g <=10 (where 10 is the perfect grade) rather than // just 9 <= g <10. $data[$bands - 1] += $data[$bands]; diff --git a/mod/quiz/report/statistics/classes/calculated.php b/mod/quiz/report/statistics/classes/calculated.php index 8103b7c752d..3f5b6e64fb6 100644 --- a/mod/quiz/report/statistics/classes/calculated.php +++ b/mod/quiz/report/statistics/classes/calculated.php @@ -28,16 +28,24 @@ */ class quiz_statistics_calculated { - public function __construct($allattempts = null) { - if ($allattempts !== null) { - $this->allattempts = $allattempts; + /** + * @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 bool whether we are calculating calculate stats from all attempts. + * @var int which attempts we are calculating calculate stats from. */ - public $allattempts; + public $whichattempts; /* Following stats all described here : http://docs.moodle.org/dev/Quiz_statistics_calculations#Test_statistics */ @@ -45,10 +53,18 @@ class quiz_statistics_calculated { public $allattemptscount = 0; + public $lastattemptscount = 0; + + public $highestattemptscount = 0; + public $firstattemptsavg; public $allattemptsavg; + public $lastattemptsavg; + + public $highestattemptsavg; + public $median; public $standarddeviation; @@ -68,20 +84,34 @@ class quiz_statistics_calculated { */ public $timemodified; + /** + * Count of attempts selected by $this->whichattempts + * + * @return int + */ public function s() { - if ($this->allattempts) { - return $this->allattemptscount; - } else { - return $this->firstattemptscount; - } + return $this->get_field('count'); } + /** + * Average grade for the attempts selected by $this->whichattempts + * + * @return float + */ public function avg() { - if ($this->allattempts) { - return $this->allattemptsavg; - } else { - return $this->firstattemptsavg; - } + 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 = quiz_statistics_calculator::using_attempts_string_id($this->whichattempts).$field; + return $this->{$fieldname}; } /** @@ -97,6 +127,8 @@ class quiz_statistics_calculated { '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', @@ -149,27 +181,18 @@ class quiz_statistics_calculated { $formattedvalue = $value; } - $quizinfo[get_string($property, 'quiz_statistics', $this->using_attempts_string())] = $formattedvalue; + $quizinfo[get_string($property, 'quiz_statistics', + quiz_statistics_calculator::using_attempts_lang_string($this->whichattempts))] = $formattedvalue; } return $quizinfo; } - /** - * @return string the appropriate lang string to describe this option. - */ - protected function using_attempts_string() { - if ($this->allattempts) { - return get_string('allattempts', 'quiz_statistics'); - } else { - return get_string('firstattempts', 'quiz_statistics'); - } - } - /** * @var array of names of properties of this class that are cached in db record. */ - protected $fieldsindb = array('allattempts', 'firstattemptscount', 'allattemptscount', 'firstattemptsavg', 'allattemptsavg', + protected $fieldsindb = array('whichattempts', 'firstattemptscount', 'allattemptscount', 'firstattemptsavg', 'allattemptsavg', + 'lastattemptscount', 'highestattemptscount', 'lastattemptsavg', 'highestattemptsavg', 'median', 'standarddeviation', 'skewness', 'kurtosis', 'cic', 'errorratio', 'standarderror'); diff --git a/mod/quiz/report/statistics/classes/calculator.php b/mod/quiz/report/statistics/classes/calculator.php index 85c8cb78f56..dc775079e41 100644 --- a/mod/quiz/report/statistics/classes/calculator.php +++ b/mod/quiz/report/statistics/classes/calculator.php @@ -32,16 +32,26 @@ class quiz_statistics_calculator { * Compute the quiz statistics. * * @param int $quizid the quiz id. - * @param int $currentgroup the current group. 0 for none. - * @param bool $useallattempts use all attempts, or just first attempts. + * @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 array $groupstudents 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 quiz_statistics_calculated $quizstats The statistics for overall attempt scores. */ - public function calculate($quizid, $currentgroup, $useallattempts, $groupstudents, $p, $sumofmarkvariance) { + public function calculate($quizid, $whichattempts, $groupstudents, $p, $sumofmarkvariance) { - $quizstats = $this->attempt_counts_and_averages($quizid, $currentgroup, $useallattempts, $groupstudents); + + + $quizstats = new quiz_statistics_calculated($whichattempts); + + $countsandaverages = $this->attempt_counts_and_averages($quizid, $groupstudents); + + foreach ($countsandaverages as $propertyname => $value) { + $quizstats->{$propertyname} = $value; + } $s = $quizstats->s(); @@ -51,7 +61,7 @@ class quiz_statistics_calculator { // Recalculate sql again this time possibly including test for first attempt. list($fromqa, $whereqa, $qaparams) = - quiz_statistics_attempts_sql($quizid, $currentgroup, $groupstudents, $useallattempts); + quiz_statistics_attempts_sql($quizid, $groupstudents, $whichattempts); $quizstats->median = $this->median($s, $fromqa, $whereqa, $qaparams); @@ -80,7 +90,7 @@ class quiz_statistics_calculator { } if ($p > 1) { - $quizstats->cic = (100 * $p / ($p -1)) * (1 - ($sumofmarkvariance / $k2)); + $quizstats->cic = (100 * $p / ($p - 1)) * (1 - ($sumofmarkvariance / $k2)); $quizstats->errorratio = 100 * sqrt(1 - ($quizstats->cic / 100)); $quizstats->standarderror = $quizstats->errorratio * $quizstats->standarddeviation / 100; @@ -90,7 +100,7 @@ class quiz_statistics_calculator { } } - $quizstats->cache(quiz_statistics_qubaids_condition($quizid, $currentgroup, $groupstudents, $useallattempts)); + $quizstats->cache(quiz_statistics_qubaids_condition($quizid, $groupstudents, $whichattempts)); return $quizstats; } @@ -129,61 +139,88 @@ class quiz_statistics_calculator { array($qubaids->get_hash_code(), $timemodified)); } + /** + * 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 int $currentgroup - * @param bool $useallattempts * @param array $groupstudents - * @return quiz_statistics_calculated containing calculated counts, totals and averages. + * @return stdClass with properties with count and avg with prefixes firstattempts, highestattempts, etc. */ - protected function attempt_counts_and_averages($quizid, $currentgroup, $useallattempts, $groupstudents) { + protected function attempt_counts_and_averages($quizid, $groupstudents) { global $DB; - $quizstats = new quiz_statistics_calculated($useallattempts); + list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $groupstudents); - list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $currentgroup, $groupstudents, true); + $selects = array(); + foreach (array_keys(quiz_get_grading_options()) as $which) { + $fieldprefix = static::using_attempts_string_id($which); + $condition = quiz_report_grade_method_sql($which); + if ($condition == '') { + $case = '1'; + } else { + $case = "CASE WHEN ($condition) THEN 1 ELSE 0 END"; + } + $selects[] = " + SUM($case) AS {$fieldprefix}count, + SUM(sumgrades * $case) AS {$fieldprefix}total"; + } + $select = join(',', $selects); - $attempttotals = $DB->get_records_sql(" - SELECT - CASE WHEN attempt = 1 THEN 1 ELSE 0 END AS isfirst, - COUNT(1) AS countrecs, - SUM(sumgrades) AS total + $attempttotals = $DB->get_record_sql(" + SELECT {$select} FROM $fromqa - WHERE $whereqa - GROUP BY CASE WHEN attempt = 1 THEN 1 ELSE 0 END", $qaparams); + WHERE $whereqa", $qaparams); - // Above query that returns sums and counts for first attempt and other non first attempts. - // We want to work out stats for first attempt or ALL attempts. - - if (isset($attempttotals[1])) { - $quizstats->firstattemptscount = $attempttotals[1]->countrecs; - $firstattemptstotal = $attempttotals[1]->total; - } else { - $quizstats->firstattemptscount = 0; - $firstattemptstotal = 0; + foreach (array_keys(quiz_get_grading_options()) as $which) { + $fieldprefix = static::using_attempts_string_id($which); + $attempttotals->{$fieldprefix.'avg'} = $attempttotals->{$fieldprefix.'total'} / $attempttotals->{$fieldprefix.'count'}; + unset($attempttotals->{$fieldprefix.'total'}); } - - if (isset($attempttotals[0])) { - $quizstats->allattemptscount = $quizstats->firstattemptscount + $attempttotals[0]->countrecs; - $allattemptstotal = $firstattemptstotal + $attempttotals[0]->total; - } else { - $quizstats->allattemptscount = $quizstats->firstattemptscount; - $allattemptstotal = $firstattemptstotal; - } - - if ($quizstats->allattemptscount !== 0) { - $quizstats->allattemptsavg = $allattemptstotal / $quizstats->allattemptscount; - } - - if ($quizstats->firstattemptscount !== 0) { - $quizstats->firstattemptsavg = $firstattemptstotal / $quizstats->firstattemptscount; - } - - return $quizstats; + return $attempttotals; } /** diff --git a/mod/quiz/report/statistics/db/install.xml b/mod/quiz/report/statistics/db/install.xml index 853270d3fb8..b53f0e1e369 100644 --- a/mod/quiz/report/statistics/db/install.xml +++ b/mod/quiz/report/statistics/db/install.xml @@ -8,11 +8,15 @@ - + + + + + diff --git a/mod/quiz/report/statistics/db/upgrade.php b/mod/quiz/report/statistics/db/upgrade.php index 3581dfe1837..9f98c14e964 100644 --- a/mod/quiz/report/statistics/db/upgrade.php +++ b/mod/quiz/report/statistics/db/upgrade.php @@ -85,6 +85,51 @@ function xmldb_quiz_statistics_upgrade($oldversion) { upgrade_plugin_savepoint(true, 2013092000, 'quiz', 'statistics'); } + if ($oldversion < 2013093000) { + // Define table quiz_statistics to be dropped. + $table = new xmldb_table('quiz_statistics'); + + // Conditionally launch drop table for quiz_statistics. + if ($dbman->table_exists($table)) { + $dbman->drop_table($table); + } + + // Define table quiz_statistics to be created. + $table = new xmldb_table('quiz_statistics'); + + // Adding fields to table quiz_statistics. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('hashcode', XMLDB_TYPE_CHAR, '40', null, XMLDB_NOTNULL, null, null); + $table->add_field('whichattempts', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('firstattemptscount', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('highestattemptscount', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('lastattemptscount', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('allattemptscount', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('firstattemptsavg', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null); + $table->add_field('highestattemptsavg', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null); + $table->add_field('lastattemptsavg', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null); + $table->add_field('allattemptsavg', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null); + $table->add_field('median', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null); + $table->add_field('standarddeviation', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null); + $table->add_field('skewness', XMLDB_TYPE_NUMBER, '15, 10', null, null, null, null); + $table->add_field('kurtosis', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null); + $table->add_field('cic', XMLDB_TYPE_NUMBER, '15, 10', null, null, null, null); + $table->add_field('errorratio', XMLDB_TYPE_NUMBER, '15, 10', null, null, null, null); + $table->add_field('standarderror', XMLDB_TYPE_NUMBER, '15, 10', null, null, null, null); + + // Adding keys to table quiz_statistics. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Conditionally launch create table for quiz_statistics. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Statistics savepoint reached. + upgrade_plugin_savepoint(true, 2013093000, 'quiz', 'statistics'); + } + return true; } diff --git a/mod/quiz/report/statistics/lang/en/quiz_statistics.php b/mod/quiz/report/statistics/lang/en/quiz_statistics.php index a83cfa3db0e..11ea4ee9b59 100644 --- a/mod/quiz/report/statistics/lang/en/quiz_statistics.php +++ b/mod/quiz/report/statistics/lang/en/quiz_statistics.php @@ -56,8 +56,12 @@ $string['firstattempts'] = 'first attempts'; $string['firstattemptsavg'] = 'Average grade of first attempts'; $string['firstattemptscount'] = 'Number of complete graded first attempts'; $string['frequency'] = 'Frequency'; +$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['median'] = 'Median grade (for {$a})'; $string['modelresponse'] = 'Model response'; @@ -85,6 +89,7 @@ $string['quizoverallstatistics'] = 'Quiz overall statistics'; $string['quizstructureanalysis'] = 'Quiz structure analysis'; $string['random_guess_score'] = 'Random guess score'; $string['recalculatenow'] = 'Recalculate now'; +$string['reportsettings'] = 'Statistics calculation settings'; $string['response'] = 'Response'; $string['skewness'] = 'Score distribution skewness (for {$a})'; $string['standarddeviation'] = 'Standard deviation (for {$a})'; diff --git a/mod/quiz/report/statistics/report.php b/mod/quiz/report/statistics/report.php index 19fa679f795..3cc1e2a7d9a 100644 --- a/mod/quiz/report/statistics/report.php +++ b/mod/quiz/report/statistics/report.php @@ -61,6 +61,7 @@ class quiz_statistics_report extends quiz_default_report { // A qid paramter indicates we should display the detailed analysis of a sub question. $qid = optional_param('qid', 0, PARAM_INT); $slot = optional_param('slot', 0, PARAM_INT); + $whichattempts = optional_param('whichattempts', $quiz->grademethod, PARAM_INT); $pageoptions = array(); $pageoptions['id'] = $cm->id; @@ -69,17 +70,15 @@ class quiz_statistics_report extends quiz_default_report { $reporturl = new moodle_url('/mod/quiz/report.php', $pageoptions); $mform = new quiz_statistics_settings_form($reporturl); - if ($fromform = $mform->get_data()) { - $useallattempts = $fromform->useallattempts; - if ($fromform->useallattempts) { - set_user_preference('quiz_report_statistics_useallattempts', - $fromform->useallattempts); - } else { - unset_user_preference('quiz_report_statistics_useallattempts'); - } - } else { - $useallattempts = get_user_preferences('quiz_report_statistics_useallattempts', 0); + $mform->set_data(array('whichattempts' => $whichattempts)); + + if ($fromform = $mform->get_data()) { + $whichattempts = $fromform->whichattempts; + } + + if ($whichattempts != $quiz->grademethod) { + $reporturl->param('whichattempts', $whichattempts); } // Find out current groups mode. @@ -103,7 +102,7 @@ class quiz_statistics_report extends quiz_default_report { } } - $qubaids = quiz_statistics_qubaids_condition($quiz->id, $currentgroup, $groupstudents, $useallattempts); + $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudents, $whichattempts); // If recalculate was requested, handle that. if ($recalculate && confirm_sesskey()) { @@ -128,10 +127,10 @@ class quiz_statistics_report extends quiz_default_report { if (!$nostudentsingroup) { // Get the data to be displayed. list($quizstats, $questionstats, $subquestionstats) = - $this->get_quiz_and_questions_stats($quiz, $currentgroup, $useallattempts, $groupstudents, $questions); + $this->get_quiz_and_questions_stats($quiz, $whichattempts, $groupstudents, $questions); } else { // Or create empty stats containers. - $quizstats = new quiz_statistics_calculated($useallattempts); + $quizstats = new quiz_statistics_calculated($whichattempts); $questionstats = array(); $subquestionstats = array(); } @@ -159,7 +158,6 @@ class quiz_statistics_report extends quiz_default_report { } // Print display options form. - $mform->set_data(array('useallattempts' => $useallattempts)); $mform->display(); } @@ -172,7 +170,7 @@ class quiz_statistics_report extends quiz_default_report { $this->output_quiz_structure_analysis_table($quizstats->s(), $questionstats, $subquestionstats); if ($this->table->is_downloading() == 'xhtml' && $quizstats->s() != 0) { - $this->output_statistics_graph($quiz->id, $currentgroup, $useallattempts); + $this->output_statistics_graph($quiz->id, $currentgroup, $whichattempts); } foreach ($questions as $slot => $question) { @@ -232,15 +230,14 @@ class quiz_statistics_report extends quiz_default_report { } else { // On-screen display of overview report. echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics')); - echo $this->output_caching_info($quizstats, $quiz->id, $currentgroup, - $groupstudents, $useallattempts, $reporturl); + echo $this->output_caching_info($quizstats, $quiz->id, $groupstudents, $whichattempts, $reporturl); echo $this->everything_download_options(); $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz); echo $this->output_quiz_info_table($quizinfo); if ($quizstats->s()) { echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics')); $this->output_quiz_structure_analysis_table($quizstats->s(), $questionstats, $subquestionstats); - $this->output_statistics_graph($quiz->id, $currentgroup, $useallattempts); + $this->output_statistics_graph($quiz->id, $currentgroup, $whichattempts); } } @@ -471,14 +468,14 @@ class quiz_statistics_report extends quiz_default_report { * Output the HTML needed to show the statistics graph. * @param $quizid * @param $currentgroup - * @param $useallattempts + * @param $whichattempts */ - protected function output_statistics_graph($quizid, $currentgroup, $useallattempts) { + protected function output_statistics_graph($quizid, $currentgroup, $whichattempts) { global $PAGE; $output = $PAGE->get_renderer('mod_quiz'); $imageurl = new moodle_url('/mod/quiz/report/statistics/statistics_graph.php', - compact('quizid', 'currentgroup', 'useallattempts')); + compact('quizid', 'currentgroup', 'whichattempts')); $graphname = get_string('statisticsreportgraph', 'quiz_statistics'); echo $output->graph($imageurl, $graphname); } @@ -488,18 +485,20 @@ class quiz_statistics_report extends quiz_default_report { * or by recomputing them. * * @param object $quiz the quiz settings. - * @param int $currentgroup the current group. 0 for none. - * @param bool $useallattempts use all attempts, or just first attempts. + * @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 array $groupstudents students in this group. - * @param array $questions question definitions. + * @param array $questions full question data. * @return array with 4 elements: * - $quizstats The statistics for overall attempt scores. * - $questionstats array of \core_question\statistics\questions\calculated objects keyed by slot. * - $subquestionstats array of \core_question\statistics\questions\calculated_for_subquestion objects keyed by question id. */ - protected function get_quiz_and_questions_stats($quiz, $currentgroup, $useallattempts, $groupstudents, $questions) { + protected function get_quiz_and_questions_stats($quiz, $whichattempts, $groupstudents, $questions) { - $qubaids = quiz_statistics_qubaids_condition($quiz->id, $currentgroup, $groupstudents, $useallattempts); + $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudents, $whichattempts); $qcalc = new \core_question\statistics\questions\calculator($questions); @@ -509,8 +508,8 @@ class quiz_statistics_report extends quiz_default_report { // Recalculate now. list($questionstats, $subquestionstats) = $qcalc->calculate($qubaids); - $quizstats = $quizcalc->calculate($quiz->id, $currentgroup, $useallattempts, - $groupstudents, count($questions), $qcalc->get_sum_of_mark_variance()); + $quizstats = $quizcalc->calculate($quiz->id, $whichattempts, $groupstudents, count($questions), + $qcalc->get_sum_of_mark_variance()); if ($quizstats->s()) { $this->analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats); @@ -574,16 +573,16 @@ class quiz_statistics_report extends quiz_default_report { * with a recalcuate now button. * @param object $quizstats the overall quiz statistics. * @param int $quizid the quiz id. - * @param int $currentgroup the id of the currently selected group, or 0. - * @param array $groupstudents ids of students in the group. - * @param bool $useallattempts whether to use all attempts, instead of just - * first attempts. + * @param array $groupstudents ids of students in the group or empty array if groups not used. + * @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 moodle_url $reporturl url for this report * @return string a HTML snipped saying when the stats were last computed, * or blank if that is not appropriate. */ - protected function output_caching_info($quizstats, $quizid, $currentgroup, - $groupstudents, $useallattempts, $reporturl) { + protected function output_caching_info($quizstats, $quizid, $groupstudents, $whichattempts, $reporturl) { global $DB, $OUTPUT; if (empty($quizstats->timemodified)) { @@ -591,8 +590,7 @@ class quiz_statistics_report extends quiz_default_report { } // Find the number of attempts since the cached statistics were computed. - list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql( - $quizid, $currentgroup, $groupstudents, $useallattempts, true); + list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $groupstudents, $whichattempts, true); $count = $DB->count_records_sql(" SELECT COUNT(1) FROM $fromqa diff --git a/mod/quiz/report/statistics/statistics_form.php b/mod/quiz/report/statistics/statistics_form.php index 02fb2f44baa..18071eb09a5 100644 --- a/mod/quiz/report/statistics/statistics_form.php +++ b/mod/quiz/report/statistics/statistics_form.php @@ -36,17 +36,15 @@ class quiz_statistics_settings_form extends moodleform { protected function definition() { $mform = $this->_form; - $mform->addElement('header', 'preferencespage', - get_string('preferencespage', 'quiz_overview')); + $mform->addElement('header', 'preferencespage', get_string('reportsettings', 'quiz_statistics')); $options = array(); - $options[0] = get_string('attemptsfirst', 'quiz_statistics'); - $options[1] = get_string('attemptsall', 'quiz_statistics'); - $mform->addElement('select', 'useallattempts', - get_string('calculatefrom', 'quiz_statistics'), $options); - $mform->setDefault('useallattempts', 0); + foreach (array_keys(quiz_get_grading_options()) as $which) { + $options[$which] = \quiz_statistics_calculator::using_attempts_lang_string($which); + } - $mform->addElement('submit', 'submitbutton', - get_string('preferencessave', 'quiz_overview')); + $mform->addElement('select', 'whichattempts', get_string('calculatefrom', 'quiz_statistics'), $options); + + $mform->addElement('submit', 'submitbutton', get_string('preferencessave', 'quiz_overview')); } } diff --git a/mod/quiz/report/statistics/statistics_graph.php b/mod/quiz/report/statistics/statistics_graph.php index bb21b0f70c1..de6a3f0b00f 100644 --- a/mod/quiz/report/statistics/statistics_graph.php +++ b/mod/quiz/report/statistics/statistics_graph.php @@ -37,7 +37,7 @@ require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php'); // Get the parameters. $quizid = required_param('quizid', PARAM_INT); $currentgroup = required_param('currentgroup', PARAM_INT); -$useallattempts = required_param('useallattempts', PARAM_INT); +$whichattempts = required_param('whichattempts', PARAM_INT); $quiz = $DB->get_record('quiz', array('id' => $quizid), '*', MUST_EXIST); $cm = get_coursemodule_from_instance('quiz', $quiz->id); @@ -55,10 +55,14 @@ if (groups_get_activity_groupmode($cm)) { if ($currentgroup && !in_array($currentgroup, array_keys($groups))) { print_error('groupnotamember', 'group'); } -$groupstudents = get_users_by_capability($modcontext, array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), - '', '', '', '', $currentgroup, '', false); -$qubaids = quiz_statistics_qubaids_condition($quizid, $currentgroup, $groupstudents, $useallattempts); +if (empty($currentgroup)) { + $groupstudents = array(); +} else { + $groupstudents = get_users_by_capability($modcontext, array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), + '', '', '', '', $currentgroup, '', false); +} +$qubaids = quiz_statistics_qubaids_condition($quizid, $groupstudents, $whichattempts); // Load the rest of the required data. $questions = quiz_report_get_significant_questions($quiz); diff --git a/mod/quiz/report/statistics/statisticslib.php b/mod/quiz/report/statistics/statisticslib.php index c947dc852d1..b66b83db7bc 100644 --- a/mod/quiz/report/statistics/statisticslib.php +++ b/mod/quiz/report/statistics/statisticslib.php @@ -23,8 +23,19 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -function quiz_statistics_attempts_sql($quizid, $currentgroup, $groupstudents, - $allattempts = true, $includeungraded = false) { +/** + * SQL to fetch relevant 'quiz_attempts' records. + * + * @param int $quizid quiz id to get attempts for + * @param array $groupstudents empty array if not using groups or array of students in current group. + * @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, $groupstudents, $whichattempts = QUIZ_GRADEAVERAGE, $includeungraded = false) { global $DB; $fromqa = '{quiz_attempts} quiza '; @@ -32,15 +43,17 @@ function quiz_statistics_attempts_sql($quizid, $currentgroup, $groupstudents, $whereqa = 'quiza.quiz = :quizid AND quiza.preview = 0 AND quiza.state = :quizstatefinished'; $qaparams = array('quizid' => (int)$quizid, 'quizstatefinished' => quiz_attempt::FINISHED); - if (!empty($currentgroup) && $groupstudents) { + if ($groupstudents) { + ksort($groupstudents); list($grpsql, $grpparams) = $DB->get_in_or_equal(array_keys($groupstudents), SQL_PARAMS_NAMED, 'u'); $whereqa .= " AND quiza.userid $grpsql"; $qaparams += $grpparams; } - if (!$allattempts) { - $whereqa .= ' AND quiza.attempt = 1'; + $whichattemptsql = quiz_report_grade_method_sql($whichattempts); + if ($whichattemptsql) { + $whereqa .= ' AND '.$whichattemptsql; } if (!$includeungraded) { @@ -54,16 +67,16 @@ function quiz_statistics_attempts_sql($quizid, $currentgroup, $groupstudents, * Return a {@link qubaid_condition} from the values returned by {@link quiz_statistics_attempts_sql}. * * @param int $quizid - * @param int $currentgroup * @param array $groupstudents - * @param bool $allattempts + * @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, $currentgroup, $groupstudents, - $allattempts = true, $includeungraded = false) { - list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $currentgroup, - $groupstudents, $allattempts, $includeungraded); +function quiz_statistics_qubaids_condition($quizid, $groupstudents, $whichattempts = QUIZ_GRADEAVERAGE, $includeungraded = false) { + list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $groupstudents, $whichattempts, $includeungraded); return new qubaid_join($fromqa, 'quiza.uniqueid', $whereqa, $qaparams); } diff --git a/mod/quiz/report/statistics/tests/stats_from_steps_walkthrough_test.php b/mod/quiz/report/statistics/tests/stats_from_steps_walkthrough_test.php index 94c3a8b7d1d..831ed2840d6 100644 --- a/mod/quiz/report/statistics/tests/stats_from_steps_walkthrough_test.php +++ b/mod/quiz/report/statistics/tests/stats_from_steps_walkthrough_test.php @@ -40,12 +40,11 @@ require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php'); */ class testable_quiz_statistics_report extends quiz_statistics_report { - public function get_stats($quiz, $useallattempts = true, - $currentgroup = 0, $groupstudents = array(), $nostudentsingroup = false) { - $qubaids = quiz_statistics_qubaids_condition($quiz->id, $currentgroup, $groupstudents, $useallattempts); + public function get_stats($quiz, $whichattempts = QUIZ_GRADEAVERAGE, $groupstudents = array()) { + $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudents, $whichattempts); $this->clear_cached_data($qubaids); $questions = $this->load_and_initialise_questions_for_calculations($quiz); - return $this->get_quiz_and_questions_stats($quiz, $currentgroup, $useallattempts, $groupstudents, $questions); + return $this->get_quiz_and_questions_stats($quiz, $whichattempts, $groupstudents, $questions); } } diff --git a/mod/quiz/report/statistics/version.php b/mod/quiz/report/statistics/version.php index 81516932665..f2f9a9ca2ec 100644 --- a/mod/quiz/report/statistics/version.php +++ b/mod/quiz/report/statistics/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2013092000; -$plugin->requires = 2013092000; +$plugin->version = 2013093000; +$plugin->requires = 2013092700; $plugin->cron = 18000; $plugin->component = 'quiz_statistics';