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';