mirror of
https://github.com/moodle/moodle.git
synced 2025-04-13 12:32:08 +02:00
Merge branch 'MDL-41760-master-v8' of https://github.com/jamiepratt/moodle
Conflicts: version.php
This commit is contained in:
commit
1c774322ba
@ -1455,19 +1455,31 @@
|
||||
<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="whichtries" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="variant" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="subqid" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="aid" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="response" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="rcount" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="credit" TYPE="number" LENGTH="15" NOTNULL="true" SEQUENCE="false" DECIMALS="5"/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
<TABLE NAME="question_response_count" COMMENT="Count for each responses for each try at a question.">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="analysisid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="try" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="rcount" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
<KEY NAME="analysisid" TYPE="foreign" FIELDS="analysisid" REFTABLE="question_response_analysis" REFFIELDS="id"/>
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
<TABLE NAME="mnet_application" COMMENT="Information about applications on remote hosts">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
|
@ -3124,5 +3124,64 @@ function xmldb_main_upgrade($oldversion) {
|
||||
upgrade_main_savepoint(true, 2014022600.00);
|
||||
}
|
||||
|
||||
if ($oldversion < 2014031400.01) {
|
||||
// Delete any cached stats to force recalculation later, then we can be sure that cached records will have the correct
|
||||
// field.
|
||||
$DB->delete_records('question_response_analysis');
|
||||
$DB->delete_records('question_statistics');
|
||||
$DB->delete_records('quiz_statistics');
|
||||
|
||||
// Define field response to be dropped from question_response_analysis.
|
||||
$table = new xmldb_table('question_response_analysis');
|
||||
$field = new xmldb_field('rcount');
|
||||
|
||||
// Conditionally launch drop field response.
|
||||
if ($dbman->field_exists($table, $field)) {
|
||||
$dbman->drop_field($table, $field);
|
||||
}
|
||||
|
||||
// Main savepoint reached.
|
||||
upgrade_main_savepoint(true, 2014031400.01);
|
||||
}
|
||||
|
||||
if ($oldversion < 2014031400.02) {
|
||||
|
||||
// Define table question_response_count to be created.
|
||||
$table = new xmldb_table('question_response_count');
|
||||
|
||||
// Adding fields to table question_response_count.
|
||||
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
|
||||
$table->add_field('analysisid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('try', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('rcount', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
|
||||
// Adding keys to table question_response_count.
|
||||
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
|
||||
$table->add_key('analysisid', XMLDB_KEY_FOREIGN, array('analysisid'), 'question_response_analysis', array('id'));
|
||||
|
||||
// Conditionally launch create table for question_response_count.
|
||||
if (!$dbman->table_exists($table)) {
|
||||
$dbman->create_table($table);
|
||||
}
|
||||
|
||||
// Main savepoint reached.
|
||||
upgrade_main_savepoint(true, 2014031400.02);
|
||||
}
|
||||
|
||||
if ($oldversion < 2014031400.03) {
|
||||
|
||||
// Define field whichtries to be added to question_response_analysis.
|
||||
$table = new xmldb_table('question_response_analysis');
|
||||
$field = new xmldb_field('whichtries', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null, 'hashcode');
|
||||
|
||||
// Conditionally launch add field whichtries.
|
||||
if (!$dbman->field_exists($table, $field)) {
|
||||
$dbman->add_field($table, $field);
|
||||
}
|
||||
|
||||
// Main savepoint reached.
|
||||
upgrade_main_savepoint(true, 2014031400.03);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ $string['calculatingallstats'] = 'Calculating statistics for quiz, questions and
|
||||
$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';
|
||||
@ -110,4 +111,5 @@ $string['statistics:view'] = 'View statistics report';
|
||||
$string['statsfor'] = 'Quiz statistics (for {$a})';
|
||||
$string['variant'] = 'Variant';
|
||||
$string['variantno'] = 'Variant {$a}';
|
||||
$string['whichtries'] = 'Analyze responses for';
|
||||
|
||||
|
@ -18,7 +18,8 @@
|
||||
* Quiz statistics report class.
|
||||
*
|
||||
* @package quiz_statistics
|
||||
* @copyright 2008 Jamie Pratt
|
||||
* @copyright 2014 Open University
|
||||
* @author James Pratt <me@jamiep.org>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
@ -38,9 +39,7 @@ require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');
|
||||
*/
|
||||
class quiz_statistics_report extends quiz_default_report {
|
||||
|
||||
/**
|
||||
* @var context_module
|
||||
*/
|
||||
/** @var context_module context of this quiz.*/
|
||||
protected $context;
|
||||
|
||||
/** @var quiz_statistics_table instance of table class used for main questions stats table. */
|
||||
@ -74,6 +73,7 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
$slot = optional_param('slot', 0, PARAM_INT);
|
||||
$variantno = optional_param('variant', null, PARAM_INT);
|
||||
$whichattempts = optional_param('whichattempts', $quiz->grademethod, PARAM_INT);
|
||||
$whichtries = optional_param('whichtries', question_attempt::LAST_TRY, PARAM_ALPHA);
|
||||
|
||||
$pageoptions = array();
|
||||
$pageoptions['id'] = $cm->id;
|
||||
@ -81,18 +81,18 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
|
||||
$reporturl = new moodle_url('/mod/quiz/report.php', $pageoptions);
|
||||
|
||||
$mform = new quiz_statistics_settings_form($reporturl);
|
||||
$mform = new quiz_statistics_settings_form($reporturl, compact('quiz'));
|
||||
|
||||
$mform->set_data(array('whichattempts' => $whichattempts));
|
||||
|
||||
if ($fromform = $mform->get_data()) {
|
||||
$whichattempts = $fromform->whichattempts;
|
||||
}
|
||||
$mform->set_data(array('whichattempts' => $whichattempts, 'whichtries' => $whichtries));
|
||||
|
||||
if ($whichattempts != $quiz->grademethod) {
|
||||
$reporturl->param('whichattempts', $whichattempts);
|
||||
}
|
||||
|
||||
if ($whichtries != question_attempt::LAST_TRY) {
|
||||
$reporturl->param('whichtries', $whichtries);
|
||||
}
|
||||
|
||||
// Find out current groups mode.
|
||||
$currentgroup = $this->get_current_group($cm, $course, $this->context);
|
||||
$nostudentsingroup = false; // True if a group is selected and there is no one in it.
|
||||
@ -145,7 +145,7 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
// Get the data to be displayed.
|
||||
$progress = $this->get_progress_trace_instance();
|
||||
list($quizstats, $questionstats) =
|
||||
$this->get_all_stats_and_analysis($quiz, $whichattempts, $groupstudents, $questions, $progress);
|
||||
$this->get_all_stats_and_analysis($quiz, $whichattempts, $whichtries, $groupstudents, $questions, $progress);
|
||||
} else {
|
||||
// Or create empty stats containers.
|
||||
$quizstats = new \quiz_statistics\calculated($whichattempts);
|
||||
@ -191,7 +191,7 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
$this->output_statistics_graph($quiz->id, $currentgroup, $whichattempts);
|
||||
}
|
||||
|
||||
$this->output_all_question_response_analysis($qubaids, $questions, $questionstats, $reporturl);
|
||||
$this->output_all_question_response_analysis($qubaids, $questions, $questionstats, $reporturl, $whichtries);
|
||||
}
|
||||
|
||||
$this->table->export_class_instance()->finish_document();
|
||||
@ -207,7 +207,8 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
$variantno,
|
||||
$questionstats->for_subq($qid, $variantno)->s,
|
||||
$reporturl,
|
||||
$qubaids);
|
||||
$qubaids,
|
||||
$whichtries);
|
||||
// Back to overview link.
|
||||
echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
|
||||
get_string('backtoquizreport', 'quiz_statistics') . '</a>',
|
||||
@ -233,7 +234,8 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
$variantno,
|
||||
$questionstats->for_slot($slot, $variantno)->s,
|
||||
$reporturl,
|
||||
$qubaids);
|
||||
$qubaids,
|
||||
$whichtries);
|
||||
}
|
||||
if (!$this->table->is_downloading()) {
|
||||
// Back to overview link.
|
||||
@ -256,7 +258,7 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
} else {
|
||||
// On-screen display of overview report.
|
||||
echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'), 3);
|
||||
echo $this->output_caching_info($quizstats, $quiz->id, $groupstudents, $whichattempts, $reporturl);
|
||||
echo $this->output_caching_info($quizstats->timemodified, $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);
|
||||
@ -273,6 +275,7 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
/**
|
||||
* Display the statistical and introductory information about a question.
|
||||
* Only called when not downloading.
|
||||
*
|
||||
* @param object $quiz the quiz settings.
|
||||
* @param \core_question\statistics\questions\calculated $questionstat the question to report on.
|
||||
*/
|
||||
@ -340,6 +343,8 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
}
|
||||
|
||||
/**
|
||||
* Output question text in a box with urls appropriate for a preview of the question.
|
||||
*
|
||||
* @param object $question question data.
|
||||
* @return string HTML of question text, ready for display.
|
||||
*/
|
||||
@ -363,8 +368,10 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
* @param int $s
|
||||
* @param moodle_url $reporturl the URL to redisplay this report.
|
||||
* @param qubaid_condition $qubaids
|
||||
* @param string $whichtries
|
||||
*/
|
||||
protected function output_individual_question_response_analysis($question, $variantno, $s, $reporturl, $qubaids) {
|
||||
protected function output_individual_question_response_analysis($question, $variantno, $s, $reporturl, $qubaids,
|
||||
$whichtries = question_attempt::LAST_TRY) {
|
||||
global $OUTPUT;
|
||||
|
||||
if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
|
||||
@ -399,8 +406,8 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
}
|
||||
}
|
||||
|
||||
$responesanalyser = new \core_question\statistics\responses\analyser($question);
|
||||
$responseanalysis = $responesanalyser->load_cached($qubaids);
|
||||
$responesanalyser = new \core_question\statistics\responses\analyser($question, $whichtries);
|
||||
$responseanalysis = $responesanalyser->load_cached($qubaids, $whichtries);
|
||||
|
||||
$qtable->question_setup($reporturl, $question, $s, $responseanalysis);
|
||||
if ($this->table->is_downloading()) {
|
||||
@ -427,6 +434,7 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
|
||||
/**
|
||||
* Output the table that lists all the questions in the quiz with their statistics.
|
||||
*
|
||||
* @param \core_question\statistics\questions\all_calculated_for_qubaid_condition $questionstats the stats for all questions in
|
||||
* the quiz including subqs and
|
||||
* variants.
|
||||
@ -442,7 +450,8 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the table of overall quiz statistics.
|
||||
* Return HTML for table of overall quiz statistics.
|
||||
*
|
||||
* @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
|
||||
* @return string the HTML.
|
||||
*/
|
||||
@ -463,6 +472,7 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
|
||||
/**
|
||||
* Download the table of overall quiz statistics.
|
||||
*
|
||||
* @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
|
||||
*/
|
||||
protected function download_quiz_info_table($quizinfo) {
|
||||
@ -493,6 +503,7 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
|
||||
/**
|
||||
* Output the HTML needed to show the statistics graph.
|
||||
*
|
||||
* @param $quizid
|
||||
* @param $currentgroup
|
||||
* @param $whichattempts
|
||||
@ -516,13 +527,15 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
* $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 string $whichtries which tries to analyse for response analysis. Will be one of
|
||||
* question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
|
||||
* @param array $groupstudents students in this group.
|
||||
* @param array $questions full question data.
|
||||
* @param \core\progress\base|null $progress
|
||||
* @return array with 2 elements: - $quizstats The statistics for overall attempt scores.
|
||||
* - $questionstats \core_question\statistics\questions\all_calculated_for_qubaid_condition
|
||||
*/
|
||||
public function get_all_stats_and_analysis($quiz, $whichattempts, $groupstudents, $questions, $progress = null) {
|
||||
public function get_all_stats_and_analysis($quiz, $whichattempts, $whichtries, $groupstudents, $questions, $progress = null) {
|
||||
|
||||
if ($progress === null) {
|
||||
$progress = new \core\progress\null();
|
||||
@ -534,10 +547,9 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
|
||||
$quizcalc = new \quiz_statistics\calculator($progress);
|
||||
|
||||
$progress->start_progress('', 3);
|
||||
if ($quizcalc->get_last_calculated_time($qubaids) === false) {
|
||||
|
||||
$progress->start_progress('', 3);
|
||||
|
||||
// Recalculate now.
|
||||
$questionstats = $qcalc->calculate($qubaids);
|
||||
$progress->progress(1);
|
||||
@ -545,17 +557,24 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
$quizstats = $quizcalc->calculate($quiz->id, $whichattempts, $groupstudents, count($questions),
|
||||
$qcalc->get_sum_of_mark_variance());
|
||||
$progress->progress(2);
|
||||
if ($quizstats->s()) {
|
||||
$subquestions = $questionstats->get_sub_questions();
|
||||
$this->analyse_responses_for_all_questions_and_subquestions($questions, $subquestions, $qubaids, $progress);
|
||||
}
|
||||
$progress->progress(3);
|
||||
$progress->end_progress();
|
||||
} else {
|
||||
$quizstats = $quizcalc->get_cached($qubaids);
|
||||
$progress->progress(1);
|
||||
$questionstats = $qcalc->get_cached($qubaids);
|
||||
$progress->progress(2);
|
||||
}
|
||||
|
||||
if ($quizstats->s()) {
|
||||
$subquestions = $questionstats->get_sub_questions();
|
||||
$this->analyse_responses_for_all_questions_and_subquestions($questions,
|
||||
$subquestions,
|
||||
$qubaids,
|
||||
$whichtries,
|
||||
$progress);
|
||||
}
|
||||
$progress->progress(3);
|
||||
$progress->end_progress();
|
||||
|
||||
return array($quizstats, $questionstats);
|
||||
}
|
||||
|
||||
@ -576,7 +595,17 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
return $this->progress;
|
||||
}
|
||||
|
||||
protected function analyse_responses_for_all_questions_and_subquestions($questions, $subquestions, $qubaids, $progress = null) {
|
||||
/**
|
||||
* Analyse responses for all questions and sub questions in this quiz.
|
||||
*
|
||||
* @param object[] $questions as returned by self::load_and_initialise_questions_for_calculations
|
||||
* @param object[] $subquestions full question objects.
|
||||
* @param qubaid_condition $qubaids the question usages whose responses to analyse.
|
||||
* @param string $whichtries which tries to analyse \question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
|
||||
* @param null|\core\progress\base $progress Used to indicate progress of task.
|
||||
*/
|
||||
protected function analyse_responses_for_all_questions_and_subquestions($questions, $subquestions, $qubaids,
|
||||
$whichtries, $progress = null) {
|
||||
if ($progress === null) {
|
||||
$progress = new \core\progress\null();
|
||||
}
|
||||
@ -584,15 +613,25 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
// Starting response analysis tasks.
|
||||
$progress->start_progress('', count($questions) + count($subquestions));
|
||||
|
||||
$done = $this->analyse_responses_for_questions($questions, $qubaids, $progress);
|
||||
$done = $this->analyse_responses_for_questions($questions, $qubaids, $whichtries, $progress);
|
||||
|
||||
$this->analyse_responses_for_questions($subquestions, $qubaids, $progress, $done);
|
||||
$this->analyse_responses_for_questions($subquestions, $qubaids, $whichtries, $progress, $done);
|
||||
|
||||
// Finished all response analysis tasks.
|
||||
$progress->end_progress();
|
||||
}
|
||||
|
||||
protected function analyse_responses_for_questions($questions, $qubaids, $progress = null, $done = array()) {
|
||||
/**
|
||||
* Analyse responses for an array of questions or sub questions.
|
||||
*
|
||||
* @param object[] $questions as returned by self::load_and_initialise_questions_for_calculations.
|
||||
* @param qubaid_condition $qubaids the question usages whose responses to analyse.
|
||||
* @param string $whichtries which tries to analyse \question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
|
||||
* @param null|\core\progress\base $progress Used to indicate progress of task.
|
||||
* @param int[] $done array keys are ids of questions that have been analysed before calling method.
|
||||
* @return array array keys are ids of questions that were analysed after this method call.
|
||||
*/
|
||||
protected function analyse_responses_for_questions($questions, $qubaids, $whichtries, $progress = null, $done = array()) {
|
||||
$countquestions = count($questions);
|
||||
if (!$countquestions) {
|
||||
return array();
|
||||
@ -604,8 +643,10 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
foreach ($questions as $question) {
|
||||
$progress->increment_progress();
|
||||
if (question_bank::get_qtype($question->qtype, false)->can_analyse_responses() && !isset($done[$question->id])) {
|
||||
$responesstats = new \core_question\statistics\responses\analyser($question);
|
||||
$responesstats->calculate($qubaids);
|
||||
$responesstats = new \core_question\statistics\responses\analyser($question, $whichtries);
|
||||
if ($responesstats->get_last_analysed_time($qubaids, $whichtries) === false) {
|
||||
$responesstats->calculate($qubaids, $whichtries);
|
||||
}
|
||||
}
|
||||
$done[$question->id] = 1;
|
||||
}
|
||||
@ -614,7 +655,10 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string HTML snipped for the Download full report as UI.
|
||||
* Return a little form for the user to request to download the full report, including quiz stats and response analysis for
|
||||
* all questions and sub-questions.
|
||||
*
|
||||
* @return string HTML.
|
||||
*/
|
||||
protected function everything_download_options() {
|
||||
$downloadoptions = $this->table->get_download_menu();
|
||||
@ -635,9 +679,9 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the snipped of HTML that says when the stats were last caculated,
|
||||
* with a recalcuate now button.
|
||||
* @param object $quizstats the overall quiz statistics.
|
||||
* Return HTML for a message that says when the stats were last calculated and a 'recalculate now' button.
|
||||
*
|
||||
* @param int $lastcachetime the time the stats were last cached.
|
||||
* @param int $quizid the quiz id.
|
||||
* @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
|
||||
@ -645,13 +689,12 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
* 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.
|
||||
* @return string HTML.
|
||||
*/
|
||||
protected function output_caching_info($quizstats, $quizid, $groupstudents, $whichattempts, $reporturl) {
|
||||
protected function output_caching_info($lastcachetime, $quizid, $groupstudents, $whichattempts, $reporturl) {
|
||||
global $DB, $OUTPUT;
|
||||
|
||||
if (empty($quizstats->timemodified)) {
|
||||
if (empty($lastcachetime)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@ -661,7 +704,7 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
SELECT COUNT(1)
|
||||
FROM $fromqa
|
||||
WHERE $whereqa
|
||||
AND quiza.timefinish > {$quizstats->timemodified}", $qaparams);
|
||||
AND quiza.timefinish > {$lastcachetime}", $qaparams);
|
||||
|
||||
if (!$count) {
|
||||
$count = 0;
|
||||
@ -669,7 +712,7 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
|
||||
// Generate the output.
|
||||
$a = new stdClass();
|
||||
$a->lastcalculated = format_time(time() - $quizstats->timemodified);
|
||||
$a->lastcalculated = format_time(time() - $lastcachetime);
|
||||
$a->count = $count;
|
||||
|
||||
$recalcualteurl = new moodle_url($reporturl,
|
||||
@ -686,8 +729,9 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cached data for a particular report configuration. This will
|
||||
* trigger a re-computation the next time the report is displayed.
|
||||
* Clear the cached data for a particular report configuration. This will trigger a re-computation the next time the report
|
||||
* is displayed.
|
||||
*
|
||||
* @param $qubaids qubaid_condition
|
||||
*/
|
||||
protected function clear_cached_data($qubaids) {
|
||||
@ -698,6 +742,8 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the questions in this quiz and add some properties to the objects needed in the reports.
|
||||
*
|
||||
* @param object $quiz the quiz.
|
||||
* @return array of questions for this quiz.
|
||||
*/
|
||||
@ -726,8 +772,13 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
* @param $questions
|
||||
* @param $questionstats
|
||||
* @param $reporturl
|
||||
* @param $whichtries string
|
||||
*/
|
||||
protected function output_all_question_response_analysis($qubaids, $questions, $questionstats, $reporturl) {
|
||||
protected function output_all_question_response_analysis($qubaids,
|
||||
$questions,
|
||||
$questionstats,
|
||||
$reporturl,
|
||||
$whichtries = question_attempt::LAST_TRY) {
|
||||
foreach ($questions as $slot => $question) {
|
||||
if (question_bank::get_qtype(
|
||||
$question->qtype, false)->can_analyse_responses()
|
||||
@ -738,14 +789,16 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
$variantno,
|
||||
$questionstats->for_slot($slot, $variantno)->s,
|
||||
$reporturl,
|
||||
$qubaids);
|
||||
$qubaids,
|
||||
$whichtries);
|
||||
}
|
||||
} else {
|
||||
$this->output_individual_question_response_analysis($question,
|
||||
null,
|
||||
$questionstats->for_slot($slot)->s,
|
||||
$reporturl,
|
||||
$qubaids);
|
||||
$qubaids,
|
||||
$whichtries);
|
||||
}
|
||||
} else if ($subqids = $questionstats->for_slot($slot)->get_sub_question_ids()) {
|
||||
foreach ($subqids as $subqid) {
|
||||
@ -756,7 +809,8 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
$variantno,
|
||||
$questionstats->for_subq($subqid, $variantno)->s,
|
||||
$reporturl,
|
||||
$qubaids);
|
||||
$qubaids,
|
||||
$whichtries);
|
||||
}
|
||||
} else {
|
||||
$this->output_individual_question_response_analysis(
|
||||
@ -764,7 +818,8 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
null,
|
||||
$questionstats->for_subq($subqid)->s,
|
||||
$reporturl,
|
||||
$qubaids);
|
||||
$qubaids,
|
||||
$whichtries);
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,8 @@
|
||||
* Quiz statistics settings form definition.
|
||||
*
|
||||
* @package quiz_statistics
|
||||
* @copyright 2008 Jamie Pratt
|
||||
* @copyright 2014 Open University
|
||||
* @author James Pratt <me@jamiep.org>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
@ -29,7 +30,9 @@ require_once($CFG->libdir . '/formslib.php');
|
||||
/**
|
||||
* This is the settings form for the quiz statistics report.
|
||||
*
|
||||
* @copyright 2008 Jamie Pratt
|
||||
* @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 {
|
||||
@ -45,6 +48,15 @@ class quiz_statistics_settings_form extends moodleform {
|
||||
|
||||
$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'), array(
|
||||
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'));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -15,10 +15,11 @@
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Quiz statistics report, table for showing statistics about a particular question.
|
||||
* Quiz statistics report, table for showing response analysis for a particular question (or sub question).
|
||||
*
|
||||
* @package quiz_statistics
|
||||
* @copyright 2008 Jamie Pratt
|
||||
* @copyright 2014 Open University
|
||||
* @author James Pratt <me@jamiep.org>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
@ -31,10 +32,11 @@ require_once($CFG->libdir . '/tablelib.php');
|
||||
*
|
||||
* Lists the responses that students made to this question, with frequency counts.
|
||||
*
|
||||
* The responses may be grouped, either by subpart of the question, or by the
|
||||
* The responses may be grouped, either by sub-part of the question, or by the
|
||||
* answer they match.
|
||||
*
|
||||
* @copyright 2008 Jamie Pratt
|
||||
* @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 {
|
||||
@ -46,6 +48,7 @@ class quiz_statistics_question_table extends flexible_table {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param int $qid the id of the particular question whose statistics are being
|
||||
* displayed.
|
||||
*/
|
||||
@ -54,6 +57,8 @@ class quiz_statistics_question_table extends flexible_table {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up columns and column names and other table settings.
|
||||
*
|
||||
* @param moodle_url $reporturl
|
||||
* @param object $questiondata
|
||||
* @param integer $s number of attempts on this question.
|
||||
@ -93,8 +98,16 @@ class quiz_statistics_question_table extends flexible_table {
|
||||
$columns[] = 'fraction';
|
||||
$headers[] = get_string('optiongrade', 'quiz_statistics');
|
||||
|
||||
$columns[] = 'count';
|
||||
$headers[] = get_string('count', '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');
|
||||
@ -113,6 +126,12 @@ class quiz_statistics_question_table extends flexible_table {
|
||||
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) . '%';
|
||||
}
|
||||
@ -139,7 +158,27 @@ class quiz_statistics_question_table extends flexible_table {
|
||||
if (!$this->s) {
|
||||
return '';
|
||||
}
|
||||
return $this->format_percentage($response->totalcount / $this->s);
|
||||
}
|
||||
|
||||
return $this->format_percentage($response->count / $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 object $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 {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
slot,facility,sd,effectiveweight,covariance,markvariance,othermarkvariance,discriminationindex,covariancemax,discriminativeefficiency
|
||||
1,0.704,0.4513682901,21.2922742344,-0.022555556,0.2037333333,0.5002777794,-7.0650767526,0.2385555565,-9.4550536967
|
||||
2,0.48,0.5099019514,18.8979800309,-0.1172777785,0.26,0.6334555578,-28.8982125772,0.318833334,-36.7834118938
|
||||
3,0.973333332,0.13333334,4.443012573,-0.0098888894,0.0177777796,0.6609,-9.1230674268,0.045666669,-21.6545012165
|
||||
4,0.68,0.4760952286,18.9347251357,-0.0833888893,0.2266666667,0.5990111128,-22.6306444113,0.2652222232,-31.4411395613
|
||||
5,0.52,0.3055050463,11.1450138688,-0.0436944444,0.0933333333,0.6529555563,-17.6997047674,0.2063055556,-21.1794802584
|
||||
6,0.64,0.4898979486,9.8081339177,-0.2015555547,0.24,0.8220111101,-45.3785178421,0.3539999995,-56.9365974439
|
||||
7,0.62,0.331662479,15.4788602394,-0.0142499998,0.11,0.5774000005,-5.6543166602,0.2190833335,-6.5043742058
|
||||
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
|
||||
|
|
7
mod/quiz/report/statistics/tests/fixtures/questions01.csv
vendored
Normal file
7
mod/quiz/report/statistics/tests/fixtures/questions01.csv
vendored
Normal file
@ -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
|
|
2
mod/quiz/report/statistics/tests/fixtures/questions02.csv
vendored
Normal file
2
mod/quiz/report/statistics/tests/fixtures/questions02.csv
vendored
Normal file
@ -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,2 +1,4 @@
|
||||
testnumber,preferredbehaviour
|
||||
00,deferredfeedback
|
||||
01,interactive
|
||||
02,interactive
|
||||
|
|
@ -1,4 +1,4 @@
|
||||
slot,randq,variant,subpart,modelresponse,actualresponse,count
|
||||
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
|
||||
@ -10,7 +10,7 @@ slot,randq,variant,subpart,modelresponse,actualresponse,count
|
||||
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 RESPONSE],,1
|
||||
2,,4,1,"[NO RESPONSE]",,1
|
||||
2,,4,1,[NO MATCH],-0.4,1
|
||||
3,,1,1,frog: amphibian,amphibian,25
|
||||
3,,1,2,cat: mammal,mammal,24
|
||||
|
|
73
mod/quiz/report/statistics/tests/fixtures/responsecounts01.csv
vendored
Normal file
73
mod/quiz/report/statistics/tests/fixtures/responsecounts01.csv
vendored
Normal file
@ -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,1,0,1,0,0,2
|
||||
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,0,1,0,0,0,1
|
||||
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,0,1,0,0,1
|
||||
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,0,1,0,0,0,1
|
||||
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,0,0,0,1,0,1
|
||||
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,0,1,0,0,9
|
||||
3,,1,1,frog: mammal,mammal,0,0,0,1,1,2
|
||||
3,,1,1,frog: insect,insect,4,3,0,0,0,7
|
||||
3,,1,1,[No response],,0,0,0,0,0,0
|
||||
3,,1,2,"cat: amphibian",amphibian,8,0,1,1,0,10
|
||||
3,,1,2,cat: mammal,mammal,0,1,1,2,0,4
|
||||
3,,1,2,"cat: insect",insect,4,1,1,1,0,7
|
||||
3,,1,2,[No response],,0,0,0,0,0,0
|
||||
3,,1,3,"newt: amphibian",amphibian,6,2,1,0,0,9
|
||||
3,,1,3,"newt: mammal",mammal,3,0,0,1,0,5
|
||||
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
|
|
6
mod/quiz/report/statistics/tests/fixtures/responsecounts02.csv
vendored
Normal file
6
mod/quiz/report/statistics/tests/fixtures/responsecounts02.csv
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
slot,subpart,modelresponse,actualresponse,totalcount,count1,count2,count3,count4,count5
|
||||
1,1,frog: insect,insect,2,2,0,0,0,0
|
||||
1,1,"frog: mammal",mammal,1,0,0,1,0,0
|
||||
1,2,"cat: insect",insect,2,1,1,0,0,0
|
||||
1,2,cat: amphibian,amphibian,1,1,0,0,0,0
|
||||
1,3,newt: insect,insect,2,2,0,0,0,0
|
|
83
mod/quiz/report/statistics/tests/fixtures/steps01.csv
vendored
Normal file
83
mod/quiz/report/statistics/tests/fixtures/steps01.csv
vendored
Normal file
@ -0,0 +1,83 @@
|
||||
quizattempt,firstname,lastname,finished,randqs.1,responses.1.-submit,responses.1.-tryagain,responses.1.answer,responses.2.-submit,responses.2.-tryagain,responses.2.answer,variants.2,responses.3.-submit,responses.3.-tryagain,responses.3.0,responses.3.1,responses.3.2,responses.4.-submit,responses.4.answer
|
||||
1,John,Jones,0,shortanswer,1,0,butterfly,0,0,19.4,4,0,0,,amphibian,,0,1
|
||||
1,John,Jones,0,shortanswer,0,1,butterfly,0,0,19.4,4,1,0,insect,insect,insect,0,1
|
||||
1,John,Jones,0,shortanswer,1,0,dog,0,0,19.4,4,0,1,insect,insect,insect,0,1
|
||||
1,John,Jones,0,shortanswer,0,1,dog,0,0,19.4,4,1,0,insect,insect,amphibian,0,1
|
||||
1,John,Jones,0,shortanswer,1,0,chicken,0,0,19.4,4,0,1,insect,insect,amphibian,0,1
|
||||
1,John,Jones,0,shortanswer,0,0,chicken,0,0,19.4,4,1,0,insect,mammal,amphibian,0,1
|
||||
1,John,Jones,0,shortanswer,0,0,chicken,0,0,19.4,4,0,1,insect,mammal,amphibian,0,1
|
||||
1,John,Jones,0,shortanswer,0,0,chicken,0,0,19.4,4,1,0,insect,amphibian,amphibian,0,1
|
||||
1,John,Jones,0,shortanswer,0,0,chicken,0,0,19.4,4,0,1,insect,amphibian,amphibian,0,1
|
||||
1,John,Jones,1,shortanswer,0,0,chicken,0,0,19.4,4,1,0,mammal,amphibian,amphibian,0,1
|
||||
2,Han,Solo,0,shortanswer,1,0,Tod,1,0,19.4,2,1,0,amphibian,amphibian,amphibian,1,0
|
||||
2,Han,Solo,0,shortanswer,0,1,Tod,0,1,19.4,2,0,1,amphibian,amphibian,amphibian,0,0
|
||||
2,Han,Solo,0,shortanswer,1,0,Tony,0,0,19.4,2,1,0,insect,mammal,insect,0,0
|
||||
2,Han,Solo,0,shortanswer,0,1,Tony,0,0,19.4,2,0,1,insect,mammal,insect,0,0
|
||||
2,Han,Solo,0,shortanswer,1,0,Sharon,0,0,19.4,2,1,0,amphibian,insect,mammal,0,0
|
||||
2,Han,Solo,0,shortanswer,0,0,Sharon,0,0,19.4,2,0,1,amphibian,insect,mammal,0,0
|
||||
2,Han,Solo,0,shortanswer,0,0,Sharon,0,0,19.4,2,1,0,amphibian,insect,mammal,0,0
|
||||
2,Han,Solo,1,shortanswer,0,0,Sharon,0,0,19.4,2,0,0,amphibian,insect,mammal,0,0
|
||||
3,Yoda,Wise He Is,0,shortanswer,1,0,snake,1,0,7,9,1,0,amphibian,insect,amphibian,0,1
|
||||
3,Yoda,Wise He Is,0,shortanswer,0,1,snake,0,0,7,9,0,1,amphibian,insect,amphibian,0,1
|
||||
3,Yoda,Wise He Is,0,shortanswer,1,0,snake,0,0,7,9,1,0,amphibian,insect,amphibian,0,1
|
||||
3,Yoda,Wise He Is,0,shortanswer,0,1,snake,0,0,7,9,0,1,amphibian,insect,amphibian,0,1
|
||||
3,Yoda,Wise He Is,0,shortanswer,1,0,snakes,0,0,7,9,1,0,amphibian,insect,amphibian,0,1
|
||||
3,Yoda,Wise He Is,0,shortanswer,0,1,snakes,0,0,7,9,0,1,amphibian,insect,amphibian,0,1
|
||||
3,Yoda,Wise He Is,0,shortanswer,1,0,Snakes,0,0,7,9,1,0,amphibian,mammal,amphibian,0,1
|
||||
3,Yoda,Wise He Is,0,shortanswer,0,1,Snakes,0,0,7,9,0,0,amphibian,mammal,amphibian,0,1
|
||||
3,Yoda,Wise He Is,1,shortanswer,1,0,SnakeS,0,0,7,9,0,0,amphibian,mammal,amphibian,0,1
|
||||
4,Herbert,Garrison,0,shortanswer,0,0,dog,0,0,9.4,6,1,0,amphibian,amphibian,amphibian,0,1
|
||||
4,Herbert,Garrison,0,shortanswer,0,0,dog,0,0,9.4,6,0,1,amphibian,amphibian,amphibian,0,1
|
||||
4,Herbert,Garrison,0,shortanswer,0,0,dog,0,0,9.4,6,1,0,insect,insect,insect,0,1
|
||||
4,Herbert,Garrison,0,shortanswer,0,0,dog,0,0,9.4,6,0,1,insect,insect,insect,0,1
|
||||
4,Herbert,Garrison,0,shortanswer,0,0,dog,0,0,9.4,6,1,0,insect,amphibian,amphibian,0,1
|
||||
4,Herbert,Garrison,0,shortanswer,0,0,dog,0,0,9.4,6,0,1,insect,amphibian,amphibian,0,1
|
||||
4,Herbert,Garrison,1,shortanswer,0,0,dog,0,0,9.4,6,1,0,mammal,insect,mammal,0,1
|
||||
5,Agent,Smith,0,numerical,1,0,3.1,1,0,x+y,2,0,0,amphibian,amphibian,mammal,0,1
|
||||
5,Agent,Smith,0,numerical,0,0,3.1,1,0,4.5,2,0,0,amphibian,amphibian,mammal,0,1
|
||||
5,Agent,Smith,1,numerical,0,0,3.1,0,0,4.5,2,0,0,amphibian,amphibian,mammal,0,1
|
||||
6,Agent,Smith,0,numerical,0,0,3.142,0,0,19.4,3,0,0,amphibian,insect,mammal,1,0
|
||||
6,Agent,Smith,0,numerical,1,0,3.14,1,0,19.4,3,1,0,amphibian,insect,mammal,0,0
|
||||
6,Agent,Smith,0,numerical,0,0,3.14,0,1,19.4,3,0,1,amphibian,insect,mammal,0,0
|
||||
6,Agent,Smith,1,numerical,0,0,3.14,1,0,3.3,3,1,0,insect,insect,amphibian,0,0
|
||||
7,Agent,Smith,0,numerical,1,0,3.14,1,0,19.3,4,0,0,insect,amphibian,insect,0,1
|
||||
7,Agent,Smith,1,numerical,0,0,3.14,0,0,19.3,4,0,0,insect,amphibian,insect,0,1
|
||||
9,Bebe,Stevens,0,shortanswer,1,0,goat,1,0,8.5,2,0,0,amphibian,amphibian,insect,0,1
|
||||
9,Bebe,Stevens,0,shortanswer,0,1,goat,0,0,8.5,2,0,0,amphibian,amphibian,insect,0,1
|
||||
9,Bebe,Stevens,0,shortanswer,1,0,Mexican burrowing caecilian,0,0,8.5,2,0,0,amphibian,amphibian,insect,0,1
|
||||
9,Bebe,Stevens,0,shortanswer,0,1,Mexican burrowing caecilian,0,0,8.5,2,0,0,amphibian,amphibian,insect,0,1
|
||||
9,Bebe,Stevens,0,shortanswer,1,0,Mexican burrowing caecilian,0,0,8.5,2,0,0,amphibian,amphibian,insect,0,1
|
||||
9,Bebe,Stevens,0,shortanswer,0,1,Mexican burrowing caecilian,0,0,8.5,2,0,0,amphibian,amphibian,insect,0,1
|
||||
9,Bebe,Stevens,0,shortanswer,1,0,newt,0,0,8.5,2,0,0,amphibian,amphibian,insect,0,1
|
||||
9,Bebe,Stevens,0,shortanswer,0,1,newt,0,0,8.5,2,0,0,amphibian,amphibian,insect,0,1
|
||||
9,Bebe,Stevens,1,shortanswer,1,0,human,0,0,8.5,2,0,0,amphibian,amphibian,insect,0,1
|
||||
10,Luke,Skywalker,0,numerical,1,0,2,1,0,555,10,1,0,amphibian,amphibian,amphibian,1,0
|
||||
10,Luke,Skywalker,0,numerical,0,1,2,0,1,555,10,0,1,amphibian,amphibian,amphibian,0,0
|
||||
10,Luke,Skywalker,0,numerical,1,0,20,1,0,44,10,1,0,amphibian,amphibian,amphibian,0,0
|
||||
10,Luke,Skywalker,0,numerical,0,1,20,0,1,44,10,0,1,amphibian,amphibian,amphibian,0,0
|
||||
10,Luke,Skywalker,0,numerical,1,0,34,1,0,22,10,1,0,amphibian,amphibian,amphibian,0,0
|
||||
10,Luke,Skywalker,0,numerical,0,0,34,0,1,22,10,0,1,amphibian,amphibian,amphibian,0,0
|
||||
10,Luke,Skywalker,0,numerical,0,0,34,1,0,11,10,1,0,amphibian,mammal,amphibian,0,0
|
||||
10,Luke,Skywalker,0,numerical,0,0,34,0,1,11,10,0,0,amphibian,mammal,amphibian,0,0
|
||||
10,Luke,Skywalker,1,numerical,0,0,34,1,0,12,10,0,0,amphibian,mammal,amphibian,0,0
|
||||
11,Luke,Skywalker,0,shortanswer,1,0,toad,1,0,23,1,1,0,insect,amphibian,amphibian,0,1
|
||||
11,Luke,Skywalker,0,shortanswer,0,0,toad,0,1,23,1,0,0,insect,amphibian,amphibian,0,1
|
||||
11,Luke,Skywalker,0,shortanswer,0,0,toad,1,0,22,1,0,0,insect,amphibian,amphibian,0,1
|
||||
11,Luke,Skywalker,0,shortanswer,0,0,toad,0,1,22,1,0,0,insect,amphibian,amphibian,0,1
|
||||
11,Luke,Skywalker,0,shortanswer,0,0,toad,1,0,21,1,0,0,insect,amphibian,amphibian,0,1
|
||||
11,Luke,Skywalker,0,shortanswer,0,0,toad,0,1,21,1,0,0,insect,amphibian,amphibian,0,1
|
||||
11,Luke,Skywalker,0,shortanswer,0,0,toad,1,0,9,1,0,0,insect,amphibian,amphibian,0,1
|
||||
11,Luke,Skywalker,0,shortanswer,0,0,toad,0,1,9,1,0,0,insect,amphibian,amphibian,0,1
|
||||
11,Luke,Skywalker,1,shortanswer,0,0,toad,1,0,9.9,1,0,0,insect,amphibian,amphibian,0,1
|
||||
12,Luke,Skywalker,0,shortanswer,1,0,toad,1,0,8,2,1,0,insect,insect,amphibian,0,1
|
||||
12,Luke,Skywalker,0,shortanswer,0,1,toad,0,1,8,2,0,0,insect,insect,amphibian,0,1
|
||||
12,Luke,Skywalker,0,shortanswer,1,0,toad,1,0,8,2,0,0,insect,insect,amphibian,0,1
|
||||
12,Luke,Skywalker,0,shortanswer,0,1,toad,0,1,8,2,0,0,insect,insect,amphibian,0,1
|
||||
12,Luke,Skywalker,0,shortanswer,1,0,toad,1,0,8,2,0,0,insect,insect,amphibian,0,1
|
||||
12,Luke,Skywalker,0,shortanswer,0,0,toad,0,1,8,2,0,0,insect,insect,amphibian,0,1
|
||||
12,Luke,Skywalker,0,shortanswer,0,0,toad,1,0,8,2,0,0,insect,insect,amphibian,0,1
|
||||
12,Luke,Skywalker,0,shortanswer,0,0,toad,0,1,8,2,0,0,insect,insect,amphibian,0,1
|
||||
12,Luke,Skywalker,1,shortanswer,0,0,toad,1,0,8.5,2,0,0,insect,insect,amphibian,0,1
|
||||
13,Leia,The Princess,0,shortanswer,0,0,eggs,0,0,19.4,4,1,0,amphibian,amphibian,,1,1
|
||||
13,Leia,The Princess,0,shortanswer,0,0,eggs,0,0,19.4,4,1,0,amphibian,amphibian,mammal,0,1
|
||||
13,Leia,The Princess,1,shortanswer,0,0,eggs,0,0,19.4,4,0,0,amphibian,amphibian,mammal,0,1
|
|
19
mod/quiz/report/statistics/tests/fixtures/steps02.csv
vendored
Normal file
19
mod/quiz/report/statistics/tests/fixtures/steps02.csv
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
quizattempt,firstname,lastname,responses.1.0,responses.1.1,responses.1.2,responses.1.-submit,responses.1.-tryagain,finished
|
||||
1,John,Jones,insect,insect,insect,1,0,0
|
||||
1,John,Jones,insect,insect,insect,0,1,0
|
||||
1,John,Jones,insect,insect,insect,1,0,0
|
||||
1,John,Jones,insect,insect,insect,0,1,0
|
||||
1,John,Jones,insect,insect,insect,1,0,0
|
||||
1,John,Jones,insect,insect,insect,0,1,0
|
||||
1,John,Jones,insect,insect,insect,1,0,0
|
||||
1,John,Jones,insect,insect,insect,0,1,0
|
||||
1,John,Jones,insect,insect,insect,1,0,1
|
||||
2,Mark,Jones,insect,amphibian,insect,1,0,0
|
||||
2,Mark,Jones,insect,amphibian,insect,0,1,0
|
||||
2,Mark,Jones,insect,insect,insect,1,0,0
|
||||
2,Mark,Jones,insect,insect,insect,0,1,0
|
||||
2,Mark,Jones,mammal,insect,insect,1,0,0
|
||||
2,Mark,Jones,mammal,insect,insect,0,1,0
|
||||
2,Mark,Jones,mammal,insect,insect,1,0,0
|
||||
2,Mark,Jones,mammal,insect,insect,0,1,0
|
||||
2,Mark,Jones,mammal,insect,insect,1,0,1
|
|
@ -17,6 +17,14 @@
|
||||
/**
|
||||
* 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 phpunit
|
||||
* @copyright 2013 The Open University
|
||||
@ -74,14 +82,20 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth
|
||||
|
||||
$attemptids = $this->walkthrough_attempts($csvdata['steps']);
|
||||
|
||||
$this->check_attempts_results($csvdata['results'], $attemptids);
|
||||
if (isset($csvdata['results'])) {
|
||||
$this->check_attempts_results($csvdata['results'], $attemptids);
|
||||
}
|
||||
|
||||
$this->report = new quiz_statistics_report();
|
||||
$whichattempts = QUIZ_GRADEAVERAGE;
|
||||
$whichattempts = QUIZ_GRADEAVERAGE; // All attempts.
|
||||
$whichtries = question_attempt::ALL_TRIES;
|
||||
$groupstudents = array();
|
||||
$questions = $this->report->load_and_initialise_questions_for_calculations($this->quiz);
|
||||
list($quizstats, $questionstats) =
|
||||
$this->report->get_all_stats_and_analysis($this->quiz, $whichattempts, $groupstudents, $questions);
|
||||
list($quizstats, $questionstats) = $this->report->get_all_stats_and_analysis($this->quiz,
|
||||
$whichattempts,
|
||||
$whichtries,
|
||||
$groupstudents,
|
||||
$questions);
|
||||
|
||||
$qubaids = quiz_statistics_qubaids_condition($this->quiz->id, $groupstudents, $whichattempts);
|
||||
|
||||
@ -94,21 +108,203 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth
|
||||
$qcalc = new \core_question\statistics\questions\calculator($questions);
|
||||
$this->assertTimeCurrent($qcalc->get_last_calculated_time($qubaids));
|
||||
|
||||
$expectedvariantcounts = array(2 => array(1 => 6,
|
||||
4 => 4,
|
||||
5 => 3,
|
||||
6 => 4,
|
||||
7 => 2,
|
||||
8 => 5,
|
||||
10 => 1));
|
||||
if (isset($csvdata['responsecounts'])) {
|
||||
$this->check_response_counts($csvdata['responsecounts'], $qubaids, $questions, $whichtries);
|
||||
}
|
||||
if (isset($csvdata['qstats'])) {
|
||||
$this->check_question_stats($csvdata['qstats'], $questionstats);
|
||||
}
|
||||
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 PHPUnit_Extensions_Database_DataSet_ITable data from csv file.
|
||||
* @param $questionstats \core_question\statistics\questions\all_calculated_for_qubaid_condition Calculated stats.
|
||||
*/
|
||||
protected function check_question_stats($qstats, $questionstats) {
|
||||
for ($rowno = 0; $rowno < $qstats->getRowCount(); $rowno++) {
|
||||
$slotqstats = $qstats->getRow($rowno);
|
||||
foreach ($slotqstats as $statname => $slotqstat) {
|
||||
if (!in_array($statname, array('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->assertEquals((float)$expected, $actual, $message, $delta);
|
||||
} 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 array($subpartid, null);
|
||||
|
||||
} else if ($modelresponse == '[NO MATCH]') {
|
||||
return array($subpartid, 0);
|
||||
}
|
||||
|
||||
$modelresponses = array();
|
||||
foreach ($possibleresponses[$subpartid] as $responseclassid => $subpartpossibleresponse) {
|
||||
$modelresponses[$responseclassid] = $subpartpossibleresponse->responseclass;
|
||||
}
|
||||
$this->assertContains($modelresponse, $modelresponses);
|
||||
$responseclassid = array_search($modelresponse, $modelresponses);
|
||||
return array($subpartid, $responseclassid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $responsecounts
|
||||
* @param $qubaids
|
||||
* @param $questions
|
||||
* @param $whichtries
|
||||
*/
|
||||
protected function check_response_counts($responsecounts, $qubaids, $questions, $whichtries) {
|
||||
for ($rowno = 0; $rowno < $responsecounts->getRowCount(); $rowno++) {
|
||||
$expected = $responsecounts->getRow($rowno);
|
||||
$defaultsforexpected = array('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 = array(2 => array(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));
|
||||
$analysis = $responesstats->load_cached($qubaids);
|
||||
$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.
|
||||
@ -132,7 +328,7 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth
|
||||
$classanalysis = $subpartanalysis->get_response_class($classid);
|
||||
$actualresponsecounts = $classanalysis->data_for_question_response_table('', '');
|
||||
foreach ($actualresponsecounts as $actualresponsecount) {
|
||||
$totalspervariantno[$subpartid][$variantno] += $actualresponsecount->count;
|
||||
$totalspervariantno[$subpartid][$variantno] += $actualresponsecount->totalcount;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -163,89 +359,7 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth
|
||||
}
|
||||
}
|
||||
}
|
||||
for ($rowno = 0; $rowno < $csvdata['responsecounts']->getRowCount(); $rowno++) {
|
||||
$responsecount = $csvdata['responsecounts']->getRow($rowno);
|
||||
if ($responsecount['randq'] == '') {
|
||||
$question = $questions[$responsecount['slot']];
|
||||
} else {
|
||||
$qid = $this->randqids[$responsecount['slot']][$responsecount['randq']];
|
||||
$question = question_finder::get_instance()->load_question_data($qid);
|
||||
}
|
||||
$this->assert_response_count_equals($question, $qubaids, $responsecount);
|
||||
}
|
||||
|
||||
// These quiz stats and the question stats found in qstats00.csv were calculated independently in spreadsheet which is
|
||||
// available in open document or excel format here :
|
||||
// https://github.com/jamiepratt/moodle-quiz-tools/tree/master/statsspreadsheet
|
||||
$quizstatsexpected = array(
|
||||
'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->assertEquals($statvalue, $quizstats->$statname, $quizstats->$statname, abs($statvalue) * 1.5e-5);
|
||||
}
|
||||
|
||||
for ($rowno = 0; $rowno < $csvdata['qstats']->getRowCount(); $rowno++) {
|
||||
$slotqstats = $csvdata['qstats']->getRow($rowno);
|
||||
foreach ($slotqstats as $statname => $slotqstat) {
|
||||
if ($statname !== 'slot') {
|
||||
$this->assert_stat_equals($questionstats, $slotqstats['slot'], null, null, $statname, (float)$slotqstat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$itemstats = array('s' => 12,
|
||||
'effectiveweight' => null,
|
||||
'discriminationindex' => 35.803933,
|
||||
'discriminativeefficiency' => 39.39393939,
|
||||
'sd' => 0.514928651,
|
||||
'facility' => 0.583333333,
|
||||
'maxmark' => 1,
|
||||
'positions' => '1',
|
||||
'slot' => null,
|
||||
'subquestion' => true);
|
||||
foreach ($itemstats as $statname => $expected) {
|
||||
$this->assert_stat_equals($questionstats, 1, null, 'numerical', $statname, $expected);
|
||||
}
|
||||
|
||||
// These variant's stats are calculated in stats_for_variant_1.xls and stats_for_variant_8.xls
|
||||
// The calculations in the spreadsheets are the same but applied just to the attempts where the variants appeared.
|
||||
|
||||
$statsforslot2variants = array(1 => array('s' => 6,
|
||||
'effectiveweight' => null,
|
||||
'discriminationindex' => -10.5999788,
|
||||
'discriminativeefficiency' => -14.28571429,
|
||||
'sd' => 0.5477225575,
|
||||
'facility' => 0.50,
|
||||
'maxmark' => 1,
|
||||
'variant' => 1,
|
||||
'slot' => 2,
|
||||
'subquestion' => false),
|
||||
8 => array('s' => 5,
|
||||
'effectiveweight' => null,
|
||||
'discriminationindex' => -57.77466679,
|
||||
'discriminativeefficiency' => -71.05263241,
|
||||
'sd' => 0.547722558,
|
||||
'facility' => 0.40,
|
||||
'maxmark' => 1,
|
||||
'variant' => 8,
|
||||
'slot' => 2,
|
||||
'subquestion' => false));
|
||||
foreach ($statsforslot2variants as $variant => $stats) {
|
||||
foreach ($stats as $statname => $expected) {
|
||||
$this->assert_stat_equals($questionstats, 2, $variant, null, $statname, $expected);
|
||||
}
|
||||
}
|
||||
foreach ($expectedvariantcounts as $slot => $expectedvariantcount) {
|
||||
foreach ($expectedvariantcount as $variantno => $s) {
|
||||
$this->assertEquals($s, $questionstats->for_slot($slot, $variantno)->s);
|
||||
@ -254,94 +368,26 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the stat is as expected within a reasonable tolerance.
|
||||
*
|
||||
* @param \core_question\statistics\questions\all_calculated_for_qubaid_condition $questionstats
|
||||
* @param int $slot
|
||||
* @param int|null $variant if null then not a variant stat.
|
||||
* @param string|null $subqname if null then not an item stat.
|
||||
* @param string $statname
|
||||
* @param float $expected
|
||||
* @param $quizstats
|
||||
*/
|
||||
protected function assert_stat_equals($questionstats, $slot, $variant, $subqname, $statname, $expected) {
|
||||
protected function check_quiz_stats_for_quiz_00($quizstats) {
|
||||
$quizstatsexpected = array(
|
||||
'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
|
||||
);
|
||||
|
||||
if ($variant === null && $subqname === null) {
|
||||
$actual = $questionstats->for_slot($slot)->{$statname};
|
||||
} else if ($subqname !== null) {
|
||||
$actual = $questionstats->for_subq($this->randqids[$slot][$subqname])->{$statname};
|
||||
} else {
|
||||
$actual = $questionstats->for_slot($slot, $variant)->{$statname};
|
||||
foreach ($quizstatsexpected as $statname => $statvalue) {
|
||||
$this->assertEquals($statvalue, $quizstats->$statname, $quizstats->$statname, abs($statvalue) * 1.5e-5);
|
||||
}
|
||||
if (is_bool($expected) || is_string($expected)) {
|
||||
$this->assertEquals($expected, $actual, "$statname for slot $slot");
|
||||
} else {
|
||||
switch ($statname) {
|
||||
case 'covariance' :
|
||||
case 'discriminationindex' :
|
||||
case 'discriminativeefficiency' :
|
||||
case 'effectiveweight' :
|
||||
$precision = 1e-5;
|
||||
break;
|
||||
default :
|
||||
$precision = 1e-6;
|
||||
}
|
||||
$delta = abs($expected) * $precision;
|
||||
$this->assertEquals(floatval($expected), $actual, "$statname for slot $slot", $delta);
|
||||
}
|
||||
}
|
||||
|
||||
protected function assert_response_count_equals($question, $qubaids, $responsecount) {
|
||||
$responesstats = new \core_question\statistics\responses\analyser($question);
|
||||
$analysis = $responesstats->load_cached($qubaids);
|
||||
if (!isset($responsecount['subpart'])) {
|
||||
$subpart = 1;
|
||||
} else {
|
||||
$subpart = $responsecount['subpart'];
|
||||
}
|
||||
list($subpartid, $responseclassid) = $this->get_response_subpart_and_class_id($question,
|
||||
$subpart,
|
||||
$responsecount['modelresponse']);
|
||||
|
||||
$subpartanalysis = $analysis->get_analysis_for_subpart($responsecount['variant'], $subpartid);
|
||||
$responseclassanalysis = $subpartanalysis->get_response_class($responseclassid);
|
||||
$actualresponsecounts = $responseclassanalysis->data_for_question_response_table('', '');
|
||||
if ($responsecount['modelresponse'] !== '[NO RESPONSE]') {
|
||||
foreach ($actualresponsecounts as $actualresponsecount) {
|
||||
if ($actualresponsecount->response == $responsecount['actualresponse']) {
|
||||
$this->assertEquals($responsecount['count'], $actualresponsecount->count);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new coding_exception("Actual response '{$responsecount['actualresponse']}' not found.");
|
||||
} else {
|
||||
$actualresponsecount = array_pop($actualresponsecounts);
|
||||
$this->assertEquals($responsecount['count'], $actualresponsecount->count);
|
||||
}
|
||||
}
|
||||
|
||||
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 array($subpartid, null);
|
||||
|
||||
} else if ($modelresponse == '[NO MATCH]') {
|
||||
return array($subpartid, 0);
|
||||
}
|
||||
|
||||
$modelresponses = array();
|
||||
foreach ($possibleresponses[$subpartid] as $responseclassid => $subpartpossibleresponse) {
|
||||
$modelresponses[$responseclassid] = $subpartpossibleresponse->responseclass;
|
||||
}
|
||||
$this->assertContains($modelresponse, $modelresponses);
|
||||
$responseclassid = array_search($modelresponse, $modelresponses);
|
||||
return array($subpartid, $responseclassid);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -183,7 +183,9 @@ class mod_quiz_attempt_walkthrough_from_csv_testcase extends advanced_testcase {
|
||||
$quizsettings = $quizzes->getRow($rowno);
|
||||
$dataset = array();
|
||||
foreach ($this->files as $file) {
|
||||
$dataset[$file] = $this->load_csv_data_file($file, $quizsettings['testnumber']);
|
||||
if (file_exists($this->get_full_path_of_csv_file($file, $quizsettings['testnumber']))) {
|
||||
$dataset[$file] = $this->load_csv_data_file($file, $quizsettings['testnumber']);
|
||||
}
|
||||
}
|
||||
$datasets[] = array($quizsettings, $dataset);
|
||||
}
|
||||
|
@ -281,10 +281,70 @@ abstract class question_behaviour {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return question_possible_response[] where keys are subpartid or an empty array if no classification is possible.
|
||||
* Classify responses for this question into a number of sub parts and response classes as defined by
|
||||
* {@link \question_type::get_possible_responses} for this question type.
|
||||
*
|
||||
* @param string $whichtries which tries to analyse for response analysis. Will be one of
|
||||
* question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
|
||||
* Defaults to question_attempt::LAST_TRY.
|
||||
* @return (question_classified_response|array)[] If $whichtries is question_attempt::FIRST_TRY or LAST_TRY index is subpartid
|
||||
* and values are question_classified_response instances.
|
||||
* If $whichtries is question_attempt::ALL_TRIES then first key is submitted response no
|
||||
* and the second key is subpartid.
|
||||
*/
|
||||
public function classify_response() {
|
||||
return $this->question->classify_response($this->qa->get_last_qt_data());
|
||||
public function classify_response($whichtries = question_attempt::LAST_TRY) {
|
||||
if ($whichtries == question_attempt::LAST_TRY) {
|
||||
return $this->question->classify_response($this->qa->get_last_qt_data());
|
||||
} else {
|
||||
$stepswithsubmit = $this->qa->get_steps_with_submitted_response_iterator();
|
||||
if ($whichtries == question_attempt::FIRST_TRY) {
|
||||
return $this->question->classify_response($stepswithsubmit[1]->get_qt_data());
|
||||
} else {
|
||||
$classifiedresponses = array();
|
||||
foreach ($stepswithsubmit as $submittedresponseno => $step) {
|
||||
$classifiedresponses[$submittedresponseno] = $this->question->classify_response($step->get_qt_data());
|
||||
}
|
||||
return $this->remove_repeated_submitted_responses($classifiedresponses);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter classified responses for multiple tries to remove identical responses that are not significant.
|
||||
*
|
||||
* In base class we compare last response to preceding responses and remove all identical responses until a different one is
|
||||
* found then for that different response compare to preceding and remove all identical until ...
|
||||
*
|
||||
* @param array[] $classifiedresponses first index is submitted response no and second is sub-part id. Value is of type
|
||||
* question_classified_response. Return value from self::classify_response for ALL_TRIES.
|
||||
* @return array[] return non repeated responses.
|
||||
*/
|
||||
protected function remove_repeated_submitted_responses($classifiedresponses) {
|
||||
$submittedresponsenos = array_keys($classifiedresponses);
|
||||
$submittedresponsenos = array_reverse($submittedresponsenos);
|
||||
$lastsubmittedresponseno = array_shift($submittedresponsenos);
|
||||
$nooflastnewresponse = $lastsubmittedresponseno;
|
||||
while (count($submittedresponsenos)) {
|
||||
$precedingresponseno = array_shift($submittedresponsenos);
|
||||
$responsesrepeated = true;
|
||||
if (count($classifiedresponses[$precedingresponseno]) !== count($classifiedresponses[$nooflastnewresponse])) {
|
||||
$responsesrepeated = false;
|
||||
} else {
|
||||
foreach (array_keys($classifiedresponses[$precedingresponseno]) as $subpartid) {
|
||||
if ($classifiedresponses[$precedingresponseno][$subpartid] !=
|
||||
$classifiedresponses[$nooflastnewresponse][$subpartid]) {
|
||||
$responsesrepeated = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($responsesrepeated) {
|
||||
unset($classifiedresponses[$precedingresponseno]);
|
||||
} else {
|
||||
$nooflastnewresponse = $precedingresponseno;
|
||||
}
|
||||
}
|
||||
return $classifiedresponses;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -90,4 +90,31 @@ class qbehaviour_interactivecountback extends qbehaviour_interactive {
|
||||
|
||||
return $this->question->compute_final_grade($responses, $totaltries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter classified responses for multiple tries to remove identical responses that are not significant.
|
||||
*
|
||||
* For this behaviour the significant repeated response part are the first of any repeated responses, for any part of the
|
||||
* question. These are the responses that are graded.
|
||||
*
|
||||
* @param array[] $classifiedresponses first index is submitted response no and second is sub-part id. Value is of type
|
||||
* question_classified_response. Return value from self::classify_response for ALL_TRIES.
|
||||
* @return array[] return non repeated responses.
|
||||
*/
|
||||
protected function remove_repeated_submitted_responses($classifiedresponses) {
|
||||
$subpartids = array_keys($classifiedresponses[1]);
|
||||
foreach ($subpartids as $subpartid) {
|
||||
$lastdifferentresponsepart = 1;
|
||||
$tryno = 2;
|
||||
while (isset($classifiedresponses[$tryno])) {
|
||||
if ($classifiedresponses[$tryno][$subpartid] != $classifiedresponses[$lastdifferentresponsepart][$subpartid]) {
|
||||
$lastdifferentresponsepart = $tryno;
|
||||
} else {
|
||||
unset($classifiedresponses[$tryno][$subpartid]);
|
||||
}
|
||||
$tryno++;
|
||||
}
|
||||
}
|
||||
return $classifiedresponses;
|
||||
}
|
||||
}
|
||||
|
@ -15,11 +15,10 @@
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* This file contains the code to analyse all the responses to a particular
|
||||
* question.
|
||||
* This file contains the code to analyse all the responses to a particular question.
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2013 Open University
|
||||
* @copyright 2014 Open University
|
||||
* @author Jamie Pratt <me@jamiep.org>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
@ -28,19 +27,22 @@ namespace core_question\statistics\responses;
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* This class can store and compute the analysis of the responses to a particular
|
||||
* question.
|
||||
* This class can compute, store and cache the analysis of the responses to a particular question.
|
||||
*
|
||||
* @copyright 2013 Open University
|
||||
* @author Jamie Pratt <me@jamiep.org>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @package core_question
|
||||
* @copyright 2014 The Open University
|
||||
* @author James Pratt me@jamiep.org
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class analyser {
|
||||
/** @var int Time after which responses are automatically reanalysed. */
|
||||
const TIME_TO_CACHE = 900; // 15 minutes.
|
||||
|
||||
/** @var object full question data from db. */
|
||||
protected $questiondata;
|
||||
|
||||
/**
|
||||
* @var analysis_for_question
|
||||
* @var analysis_for_question|analysis_for_question_all_tries
|
||||
*/
|
||||
public $analysis;
|
||||
|
||||
@ -63,15 +65,23 @@ class analyser {
|
||||
* for a particular question.
|
||||
*
|
||||
* @param object $questiondata the full question data from the database defining this question.
|
||||
* @param string $whichtries which tries to analyse.
|
||||
*/
|
||||
public function __construct($questiondata) {
|
||||
public function __construct($questiondata, $whichtries = \question_attempt::LAST_TRY) {
|
||||
$this->questiondata = $questiondata;
|
||||
$qtypeobj = \question_bank::get_qtype($this->questiondata->qtype);
|
||||
$this->analysis = new analysis_for_question($qtypeobj->get_possible_responses($this->questiondata));
|
||||
if ($whichtries != \question_attempt::ALL_TRIES) {
|
||||
$this->analysis = new analysis_for_question($qtypeobj->get_possible_responses($this->questiondata));
|
||||
} else {
|
||||
$this->analysis = new analysis_for_question_all_tries($qtypeobj->get_possible_responses($this->questiondata));
|
||||
}
|
||||
|
||||
$this->breakdownbyvariant = $qtypeobj->break_down_stats_and_response_analysis_by_variant($this->questiondata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the computed analysis have sub parts?
|
||||
*
|
||||
* @return bool whether this analysis has more than one subpart.
|
||||
*/
|
||||
public function has_subparts() {
|
||||
@ -79,6 +89,8 @@ class analyser {
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the computed analysis's sub parts have classes?
|
||||
*
|
||||
* @return bool whether this analysis has (a subpart with) more than one response class.
|
||||
*/
|
||||
public function has_response_classes() {
|
||||
@ -91,40 +103,21 @@ class analyser {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool whether this analysis has a response class more than one
|
||||
* different acutal response, or if the actual response is different from
|
||||
* the model response.
|
||||
*/
|
||||
public function has_actual_responses() {
|
||||
foreach ($this->responseclasses as $subpartid => $partclasses) {
|
||||
foreach ($partclasses as $responseclassid => $modelresponse) {
|
||||
$numresponses = count($this->responses[$subpartid][$responseclassid]);
|
||||
if ($numresponses > 1) {
|
||||
return true;
|
||||
}
|
||||
$actualresponse = key($this->responses[$subpartid][$responseclassid]);
|
||||
if ($numresponses == 1 && $actualresponse != $modelresponse->responseclass) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyse all the response data for for all the specified attempts at
|
||||
* this question.
|
||||
* Analyse all the response data for for all the specified attempts at this question.
|
||||
*
|
||||
* @param \qubaid_condition $qubaids which attempts to consider.
|
||||
* @param string $whichtries which tries to analyse. Will be one of
|
||||
* \question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
|
||||
* @return analysis_for_question
|
||||
*/
|
||||
public function calculate($qubaids) {
|
||||
public function calculate($qubaids, $whichtries = \question_attempt::LAST_TRY) {
|
||||
// Load data.
|
||||
$dm = new \question_engine_data_mapper();
|
||||
$questionattempts = $dm->load_attempts_at_question($this->questiondata->id, $qubaids);
|
||||
|
||||
// Analyse it.
|
||||
foreach ($questionattempts as $qa) {
|
||||
$responseparts = $qa->classify_response();
|
||||
$responseparts = $qa->classify_response($whichtries);
|
||||
if ($this->breakdownbyvariant) {
|
||||
$this->analysis->count_response_parts($qa->get_variant(), $responseparts);
|
||||
} else {
|
||||
@ -132,33 +125,44 @@ class analyser {
|
||||
}
|
||||
|
||||
}
|
||||
$this->analysis->cache($qubaids, $this->questiondata->id);
|
||||
$this->analysis->cache($qubaids, $whichtries, $this->questiondata->id);
|
||||
return $this->analysis;
|
||||
}
|
||||
|
||||
/** @var integer Time after which responses are automatically reanalysed. */
|
||||
const TIME_TO_CACHE = 900; // 15 minutes.
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the computed response analysis from the question_response_analysis table.
|
||||
*
|
||||
* @param \qubaid_condition $qubaids which attempts to get cached response analysis for.
|
||||
* @param \qubaid_condition $qubaids load the analysis of which question usages?
|
||||
* @param string $whichtries load the analysis of which tries?
|
||||
* @return analysis_for_question|boolean analysis or false if no cached analysis found.
|
||||
*/
|
||||
public function load_cached($qubaids) {
|
||||
public function load_cached($qubaids, $whichtries) {
|
||||
global $DB;
|
||||
|
||||
$timemodified = time() - self::TIME_TO_CACHE;
|
||||
$rows = $DB->get_records_select('question_response_analysis', 'hashcode = ? AND questionid = ? AND timemodified > ?',
|
||||
array($qubaids->get_hash_code(), $this->questiondata->id, $timemodified));
|
||||
if (!$rows) {
|
||||
// Variable name 'analyses' is the plural of 'analysis'.
|
||||
$responseanalyses = $DB->get_records_select('question_response_analysis',
|
||||
'hashcode = ? AND whichtries = ? AND questionid = ? AND timemodified > ?',
|
||||
array($qubaids->get_hash_code(), $whichtries, $this->questiondata->id, $timemodified));
|
||||
if (!$responseanalyses) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$class = $this->analysis->get_analysis_for_subpart($row->variant, $row->subqid)->get_response_class($row->aid);
|
||||
$class->add_response_and_count($row->response, $row->credit, $row->rcount);
|
||||
$analysisids = array();
|
||||
foreach ($responseanalyses as $responseanalysis) {
|
||||
$analysisforsubpart = $this->analysis->get_analysis_for_subpart($responseanalysis->variant, $responseanalysis->subqid);
|
||||
$class = $analysisforsubpart->get_response_class($responseanalysis->aid);
|
||||
$class->add_response($responseanalysis->response, $responseanalysis->credit);
|
||||
$analysisids[] = $responseanalysis->id;
|
||||
}
|
||||
list($sql, $params) = $DB->get_in_or_equal($analysisids);
|
||||
$counts = $DB->get_records_select('question_response_count', "analysisid {$sql}", $params);
|
||||
foreach ($counts as $count) {
|
||||
$responseanalysis = $responseanalyses[$count->analysisid];
|
||||
$analysisforsubpart = $this->analysis->get_analysis_for_subpart($responseanalysis->variant, $responseanalysis->subqid);
|
||||
$class = $analysisforsubpart->get_response_class($responseanalysis->aid);
|
||||
$class->set_response_count($responseanalysis->response, $count->try, $count->rcount);
|
||||
|
||||
}
|
||||
return $this->analysis;
|
||||
}
|
||||
@ -167,15 +171,17 @@ class analyser {
|
||||
/**
|
||||
* Find time of non-expired analysis in the database.
|
||||
*
|
||||
* @param $qubaids \qubaid_condition
|
||||
* @param \qubaid_condition $qubaids check for the analysis of which question usages?
|
||||
* @param string $whichtries check for the analysis of which tries?
|
||||
* @return integer|boolean Time of cached record that matches this qubaid_condition or false if none found.
|
||||
*/
|
||||
public function get_last_analysed_time($qubaids) {
|
||||
public function get_last_analysed_time($qubaids, $whichtries) {
|
||||
global $DB;
|
||||
|
||||
$timemodified = time() - self::TIME_TO_CACHE;
|
||||
return $DB->get_field_select('question_response_analysis', 'timemodified',
|
||||
'hashcode = ? AND questionid = ? AND timemodified > ?',
|
||||
array($qubaids->get_hash_code(), $this->questiondata->id, $timemodified), IGNORE_MULTIPLE);
|
||||
'hashcode = ? AND whichtries = ? AND questionid = ? AND timemodified > ?',
|
||||
array($qubaids->get_hash_code(), $whichtries, $this->questiondata->id, $timemodified),
|
||||
IGNORE_MULTIPLE);
|
||||
}
|
||||
}
|
||||
|
@ -23,12 +23,35 @@
|
||||
|
||||
namespace core_question\statistics\responses;
|
||||
|
||||
|
||||
/**
|
||||
* The leafs of the analysis data structure.
|
||||
*
|
||||
* - There is a separate data structure for each question or sub question's analysis
|
||||
* {@link \core_question\statistics\responses\analysis_for_question}
|
||||
* or {@link \core_question\statistics\responses\analysis_for_question_all_tries}.
|
||||
* - There are separate analysis for each variant in this top level instance.
|
||||
* - Then there are class instances representing the analysis of each of the sub parts of each variant of the question.
|
||||
* {@link \core_question\statistics\responses\analysis_for_subpart}.
|
||||
* - Then within the sub part analysis there are response class analysis
|
||||
* {@link \core_question\statistics\responses\analysis_for_class}.
|
||||
* - Then within each class analysis there are analysis for each actual response
|
||||
* {@link \core_question\statistics\responses\analysis_for_actual_response}.
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2014 The Open University
|
||||
* @author James Pratt me@jamiep.org
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class analysis_for_actual_response {
|
||||
/**
|
||||
* @var int count of this response
|
||||
* @var int[] count per try for this response.
|
||||
*/
|
||||
protected $count;
|
||||
protected $trycount = array();
|
||||
|
||||
/**
|
||||
* @var int total count of tries with this response.
|
||||
*/
|
||||
protected $totalcount = 0;
|
||||
|
||||
/**
|
||||
* @var float grade for this response, normally between 0 and 1.
|
||||
@ -43,32 +66,54 @@ class analysis_for_actual_response {
|
||||
/**
|
||||
* @param string $response
|
||||
* @param float $fraction
|
||||
* @param int $count defaults to zero, this param used when loading from db.
|
||||
*/
|
||||
public function __construct($response, $fraction, $count = 0) {
|
||||
public function __construct($response, $fraction) {
|
||||
$this->response = $response;
|
||||
$this->fraction = $fraction;
|
||||
$this->count = $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to count the occurrences of response sub parts.
|
||||
*
|
||||
* @param int $try the try number, or 0 if only keeping one count, not a count for each try.
|
||||
*/
|
||||
public function increment_count() {
|
||||
$this->count++;
|
||||
public function increment_count($try = 0) {
|
||||
$this->totalcount++;
|
||||
if ($try != 0) {
|
||||
if (!isset($this->trycount[$try])) {
|
||||
$this->trycount[$try] = 0;
|
||||
}
|
||||
$this->trycount[$try]++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \qubaid_condition $qubaids
|
||||
* @param int $questionid the question id
|
||||
* @param int $variantno
|
||||
* @param string $subpartid
|
||||
* @param string $responseclassid
|
||||
* Used to set the count of occurrences of response sub parts, when loading count from cache.
|
||||
*
|
||||
* @param int $try the try number, or 0 if only keeping one count, not a count for each try.
|
||||
* @param int $count
|
||||
*/
|
||||
public function cache($qubaids, $questionid, $variantno, $subpartid, $responseclassid) {
|
||||
public function set_count($try, $count) {
|
||||
$this->totalcount = $this->totalcount + $count;
|
||||
$this->trycount[$try] = $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache analysis for class.
|
||||
*
|
||||
* @param \qubaid_condition $qubaids which question usages have been analysed.
|
||||
* @param string $whichtries which tries have been analysed?
|
||||
* @param int $questionid which question.
|
||||
* @param int $variantno which variant.
|
||||
* @param string $subpartid which sub part is this actual response in?
|
||||
* @param string $responseclassid which response class is this actual response in?
|
||||
*/
|
||||
public function cache($qubaids, $whichtries, $questionid, $variantno, $subpartid, $responseclassid) {
|
||||
global $DB;
|
||||
$row = new \stdClass();
|
||||
$row->hashcode = $qubaids->get_hash_code();
|
||||
$row->whichtries = $whichtries;
|
||||
$row->questionid = $questionid;
|
||||
$row->variant = $variantno;
|
||||
$row->subqid = $subpartid;
|
||||
@ -78,17 +123,29 @@ class analysis_for_actual_response {
|
||||
$row->aid = $responseclassid;
|
||||
}
|
||||
$row->response = $this->response;
|
||||
$row->rcount = $this->count;
|
||||
$row->credit = $this->fraction;
|
||||
$row->timemodified = time();
|
||||
$DB->insert_record('question_response_analysis', $row, false);
|
||||
}
|
||||
|
||||
public function response_matches($response) {
|
||||
return $response == $this->response;
|
||||
$analysisid = $DB->insert_record('question_response_analysis', $row);
|
||||
if ($whichtries === \question_attempt::ALL_TRIES) {
|
||||
foreach ($this->trycount as $try => $count) {
|
||||
$countrow = new \stdClass();
|
||||
$countrow->try = $try;
|
||||
$countrow->rcount = $count;
|
||||
$countrow->analysisid = $analysisid;
|
||||
$DB->insert_record('question_response_count', $countrow, false);
|
||||
}
|
||||
} else {
|
||||
$countrow = new \stdClass();
|
||||
$countrow->try = 0;
|
||||
$countrow->rcount = $this->totalcount;
|
||||
$countrow->analysisid = $analysisid;
|
||||
$DB->insert_record('question_response_count', $countrow, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object with a property for each column of the question response analysis table.
|
||||
*
|
||||
* @param string $partid
|
||||
* @param string $modelresponse
|
||||
* @return object
|
||||
@ -99,7 +156,17 @@ class analysis_for_actual_response {
|
||||
$rowdata->responseclass = $modelresponse;
|
||||
$rowdata->response = $this->response;
|
||||
$rowdata->fraction = $this->fraction;
|
||||
$rowdata->count = $this->count;
|
||||
$rowdata->totalcount = $this->totalcount;
|
||||
$rowdata->trycount = $this->trycount;
|
||||
return $rowdata;
|
||||
}
|
||||
|
||||
/**
|
||||
* What is the highest try number that this response has been seen?
|
||||
*
|
||||
* @return int try number
|
||||
*/
|
||||
public function get_maximum_tries() {
|
||||
return max(array_keys($this->trycount));
|
||||
}
|
||||
}
|
||||
|
@ -26,39 +26,48 @@ namespace core_question\statistics\responses;
|
||||
|
||||
|
||||
/**
|
||||
* Represents an actual part of the response that has been classified in a class of responses for this sub part of the question.
|
||||
*
|
||||
* A question and it's response is represented as having one or more sub parts where the response to each sub-part might fall
|
||||
* into one of one or more classes.
|
||||
* Counts a class of responses for this sub part of the question.
|
||||
*
|
||||
* No response is one possible class of response to a question.
|
||||
*
|
||||
* @copyright 2010 The Open University
|
||||
* - There is a separate data structure for each question or sub question's analysis
|
||||
* {@link \core_question\statistics\responses\analysis_for_question}
|
||||
* or {@link \core_question\statistics\responses\analysis_for_question_all_tries}.
|
||||
* - There are separate analysis for each variant in this top level instance.
|
||||
* - Then there are class instances representing the analysis of each of the sub parts of each variant of the question.
|
||||
* {@link \core_question\statistics\responses\analysis_for_subpart}.
|
||||
* - Then within the sub part analysis there are response class analysis
|
||||
* {@link \core_question\statistics\responses\analysis_for_class}.
|
||||
* - Then within each class analysis there are analysis for each actual response
|
||||
* {@link \core_question\statistics\responses\analysis_for_actual_response}.
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2014 The Open University
|
||||
* @author James Pratt me@jamiep.org
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class analysis_for_class {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @var string must be unique for each response class within this sub part.
|
||||
*/
|
||||
protected $responseclassid;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @var string represent this class in the response analysis table.
|
||||
*/
|
||||
protected $modelresponse;
|
||||
|
||||
/** @var string the (partial) credit awarded for this responses. */
|
||||
protected $fraction;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var analysis_for_actual_response[] key is the actual response represented as a string as it will be displayed in report.
|
||||
/** @var analysis_for_actual_response[] key is the actual response represented as a string as it will be displayed in report.
|
||||
*/
|
||||
protected $actualresponses = array();
|
||||
|
||||
/**
|
||||
* Constructor, just an easy way to set the fields.
|
||||
*
|
||||
* @param \question_possible_response $possibleresponse
|
||||
* @param string $responseclassid
|
||||
*/
|
||||
@ -69,56 +78,88 @@ class analysis_for_class {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $actualresponse
|
||||
* Keep a count of a response to this question sub part that falls within this class.
|
||||
*
|
||||
* @param string $actualresponse
|
||||
* @param float|null $fraction
|
||||
* @param int $try
|
||||
* @return \core_question\statistics\responses\analysis_for_actual_response
|
||||
*/
|
||||
public function count_response($actualresponse, $fraction) {
|
||||
public function count_response($actualresponse, $fraction, $try) {
|
||||
if (!isset($this->actualresponses[$actualresponse])) {
|
||||
if ($fraction === null) {
|
||||
$fraction = $this->fraction;
|
||||
}
|
||||
$this->actualresponses[$actualresponse] = new analysis_for_actual_response($actualresponse, $fraction);
|
||||
$this->add_response($actualresponse, $fraction);
|
||||
}
|
||||
$this->actualresponses[$actualresponse]->increment_count();
|
||||
$this->get_response($actualresponse)->increment_count($try);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \qubaid_condition $qubaids
|
||||
* @param int $questionid the question id
|
||||
* @param int $variantno
|
||||
* @param string $subpartid
|
||||
* Cache analysis for class.
|
||||
*
|
||||
* @param \qubaid_condition $qubaids which question usages have been analysed.
|
||||
* @param string $whichtries which tries have been analysed?
|
||||
* @param int $questionid which question.
|
||||
* @param int $variantno which variant.
|
||||
* @param string $subpartid which sub part.
|
||||
*/
|
||||
public function cache($qubaids, $questionid, $variantno, $subpartid) {
|
||||
foreach ($this->actualresponses as $response => $actualresponse) {
|
||||
$actualresponse->cache($qubaids, $questionid, $variantno, $subpartid, $this->responseclassid, $response);
|
||||
public function cache($qubaids, $whichtries, $questionid, $variantno, $subpartid) {
|
||||
foreach ($this->get_responses() as $response) {
|
||||
$analysisforactualresponse = $this->get_response($response);
|
||||
$analysisforactualresponse->cache($qubaids, $whichtries, $questionid, $variantno, $subpartid, $this->responseclassid);
|
||||
}
|
||||
}
|
||||
|
||||
public function add_response_and_count($response, $fraction, $count) {
|
||||
$this->actualresponses[$response] = new analysis_for_actual_response($response, $fraction, $count);
|
||||
/**
|
||||
* Add an actual response to the data structure.
|
||||
*
|
||||
* @param string $response A string representing the actual response.
|
||||
* @param float $fraction The fraction of grade awarded for this response.
|
||||
*/
|
||||
public function add_response($response, $fraction) {
|
||||
$this->actualresponses[$response] = new analysis_for_actual_response($response, $fraction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used when loading cached counts.
|
||||
*
|
||||
* @param string $response
|
||||
* @param int $try the try number, will be zero if not keeping track of try.
|
||||
* @param int $count the count
|
||||
*/
|
||||
public function set_response_count($response, $try, $count) {
|
||||
$this->actualresponses[$response]->set_count($try, $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Are there actual responses to sub parts that where classified into this class?
|
||||
*
|
||||
* @return bool whether this analysis has a response class with more than one
|
||||
* different actual response, or if the actual response is different from
|
||||
* the model response.
|
||||
*/
|
||||
public function has_actual_responses() {
|
||||
if (count($this->actualresponses) > 1) {
|
||||
$actualresponses = $this->get_responses();
|
||||
if (count($actualresponses) > 1) {
|
||||
return true;
|
||||
} else if (count($this->actualresponses) == 1) {
|
||||
$onlyactualresponse = reset($this->actualresponses);
|
||||
return !$onlyactualresponse->response_matches($this->modelresponse);
|
||||
} else if (count($actualresponses) === 1) {
|
||||
$singleactualresponse = reset($actualresponses);
|
||||
return $singleactualresponse != $this->modelresponse;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the data to display in the response analysis table.
|
||||
*
|
||||
* @param bool $responseclasscolumn
|
||||
* @param string $partid
|
||||
* @return object[]
|
||||
*/
|
||||
public function data_for_question_response_table($responseclasscolumn, $partid) {
|
||||
$return = array();
|
||||
if (empty($this->actualresponses)) {
|
||||
if (count($this->get_responses()) == 0) {
|
||||
$rowdata = new \stdClass();
|
||||
$rowdata->part = $partid;
|
||||
$rowdata->responseclass = $this->modelresponse;
|
||||
@ -128,13 +169,47 @@ class analysis_for_class {
|
||||
$rowdata->response = '';
|
||||
}
|
||||
$rowdata->fraction = $this->fraction;
|
||||
$rowdata->count = 0;
|
||||
$rowdata->totalcount = 0;
|
||||
$rowdata->trycount = array();
|
||||
$return[] = $rowdata;
|
||||
} else {
|
||||
foreach ($this->actualresponses as $actualresponse) {
|
||||
$return[] = $actualresponse->data_for_question_response_table($partid, $this->modelresponse);
|
||||
foreach ($this->get_responses() as $actualresponse) {
|
||||
$response = $this->get_response($actualresponse);
|
||||
$return[] = $response->data_for_question_response_table($partid, $this->modelresponse);
|
||||
}
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* What is the highest try number that an actual response of this response class has been seen?
|
||||
*
|
||||
* @return int try number
|
||||
*/
|
||||
public function get_maximum_tries() {
|
||||
$max = 1;
|
||||
foreach ($this->get_responses() as $actualresponse) {
|
||||
$max = max($max, $this->get_response($actualresponse)->get_maximum_tries());
|
||||
}
|
||||
return $max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return array of the actual responses to this sub part that were classified into this class.
|
||||
*
|
||||
* @return string[] the actual responses we are counting tries at.
|
||||
*/
|
||||
protected function get_responses() {
|
||||
return array_keys($this->actualresponses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data structure used to count the responses that match an actual response within this class of responses.
|
||||
*
|
||||
* @param string $response
|
||||
* @return analysis_for_actual_response the instance for keeping count of tries for $response.
|
||||
*/
|
||||
protected function get_response($response) {
|
||||
return $this->actualresponses[$response];
|
||||
}
|
||||
}
|
||||
|
@ -29,17 +29,29 @@ defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Analysis for possible responses for parts of a question. It is up to a question type designer to decide on how many parts their
|
||||
* question has. A sub part might represent a sub question embedded in the question for example in a matching question there are
|
||||
* question has. See {@link \question_type::get_possible_responses()} and sub classes where the sub parts and response classes are
|
||||
* defined.
|
||||
*
|
||||
* A sub part might represent a sub question embedded in the question for example in a matching question there are
|
||||
* several sub parts. A numeric question with a unit might be divided into two sub parts for the purposes of response analysis
|
||||
* or the question type designer might decide to treat the answer, both the numeric and unit part,
|
||||
* as a whole for the purposes of response analysis.
|
||||
*
|
||||
* Responses can be further divided into 'classes' in which they are classified. One or more of these 'classes' are contained in
|
||||
* the responses
|
||||
* - There is a separate data structure for each question or sub question's analysis
|
||||
* {@link \core_question\statistics\responses\analysis_for_question}
|
||||
* or {@link \core_question\statistics\responses\analysis_for_question_all_tries}.
|
||||
* - There are separate analysis for each variant in this top level instance.
|
||||
* - Then there are class instances representing the analysis of each of the sub parts of each variant of the question.
|
||||
* {@link \core_question\statistics\responses\analysis_for_subpart}.
|
||||
* - Then within the sub part analysis there are response class analysis
|
||||
* {@link \core_question\statistics\responses\analysis_for_class}.
|
||||
* - Then within each class analysis there are analysis for each actual response
|
||||
* {@link \core_question\statistics\responses\analysis_for_actual_response}.
|
||||
*
|
||||
* @copyright 2013 Open University
|
||||
* @author Jamie Pratt <me@jamiep.org>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @package core_question
|
||||
* @copyright 2014 The Open University
|
||||
* @author James Pratt me@jamiep.org
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class analysis_for_question {
|
||||
|
||||
@ -149,6 +161,23 @@ class analysis_for_question {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Does this response analysis include counts for responses for multiple tries of the question?
|
||||
*/
|
||||
public function has_multiple_tries_data() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* What is the highest number of tries at this question?
|
||||
*
|
||||
* @return int always 1 as this class is for analysing only one try.
|
||||
*/
|
||||
public function get_maximum_tries() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Takes an array of {@link \question_classified_response} and adds counts of the responses to the sub parts and classes.
|
||||
*
|
||||
@ -162,13 +191,15 @@ class analysis_for_question {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \qubaid_condition $qubaids
|
||||
* @param int $questionid the question id
|
||||
* @param \qubaid_condition $qubaids which question usages have been analysed.
|
||||
* @param string $whichtries which tries have been analysed?
|
||||
* @param int $questionid which question.
|
||||
*/
|
||||
public function cache($qubaids, $questionid) {
|
||||
public function cache($qubaids, $whichtries, $questionid) {
|
||||
foreach ($this->get_variant_nos() as $variantno) {
|
||||
foreach ($this->get_subpart_ids($variantno) as $subpartid) {
|
||||
$this->get_analysis_for_subpart($variantno, $subpartid)->cache($qubaids, $questionid, $variantno, $subpartid);
|
||||
$analysisforsubpart = $this->get_analysis_for_subpart($variantno, $subpartid);
|
||||
$analysisforsubpart->cache($qubaids, $whichtries, $questionid, $variantno, $subpartid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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/>.
|
||||
|
||||
/**
|
||||
* This file contains a class to analyse all the responses for multiple tries at a particular question.
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2014 Open University
|
||||
* @author Jamie Pratt <me@jamiep.org>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core_question\statistics\responses;
|
||||
|
||||
/**
|
||||
* Analysis for possible responses for parts of a question with multiple submitted responses.
|
||||
*
|
||||
* If the analysis was for a single try it would be handled by {@link \core_question\statistics\responses\analysis_for_question}.
|
||||
*
|
||||
* - There is a separate data structure for each question or sub question's analysis
|
||||
* {@link \core_question\statistics\responses\analysis_for_question}
|
||||
* or {@link \core_question\statistics\responses\analysis_for_question_all_tries}.
|
||||
* - There are separate analysis for each variant in this top level instance.
|
||||
* - Then there are class instances representing the analysis of each of the sub parts of each variant of the question.
|
||||
* {@link \core_question\statistics\responses\analysis_for_subpart}.
|
||||
* - Then within the sub part analysis there are response class analysis
|
||||
* {@link \core_question\statistics\responses\analysis_for_class}.
|
||||
* - Then within each class analysis there are analysis for each actual response
|
||||
* {@link \core_question\statistics\responses\analysis_for_actual_response}.
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2014 The Open University
|
||||
* @author James Pratt me@jamiep.org
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class analysis_for_question_all_tries extends analysis_for_question{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param int $variantno variant number
|
||||
* @param \array[] $responsepartsforeachtry for question with multiple tries we expect an array with first index being try no
|
||||
* then second index is subpartid and values are \question_classified_response
|
||||
*/
|
||||
public function count_response_parts($variantno, $responsepartsforeachtry) {
|
||||
foreach ($responsepartsforeachtry as $try => $responseparts) {
|
||||
foreach ($responseparts as $subpartid => $responsepart) {
|
||||
$this->get_analysis_for_subpart($variantno, $subpartid)->count_response($responsepart, $try);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function has_multiple_tries_data() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* What is the highest number of tries at this question?
|
||||
*
|
||||
* @return int try number
|
||||
*/
|
||||
public function get_maximum_tries() {
|
||||
$max = 1;
|
||||
foreach ($this->get_variant_nos() as $variantno) {
|
||||
foreach ($this->get_subpart_ids($variantno) as $subpartid) {
|
||||
$max = max($max, $this->get_analysis_for_subpart($variantno, $subpartid)->get_maximum_tries());
|
||||
}
|
||||
}
|
||||
return $max;
|
||||
}
|
||||
|
||||
}
|
@ -16,24 +16,41 @@
|
||||
|
||||
/**
|
||||
*
|
||||
* 'Classes' to classify the sub parts of a question response into.
|
||||
* Data structure to count responses for each of the sub parts of a question.
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2013 The Open University
|
||||
* @copyright 2014 The Open University
|
||||
* @author James Pratt me@jamiep.org
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core_question\statistics\responses;
|
||||
|
||||
|
||||
/**
|
||||
* Representing the analysis of each of the sub parts of each variant of the question.
|
||||
*
|
||||
* - There is a separate data structure for each question or sub question's analysis
|
||||
* {@link \core_question\statistics\responses\analysis_for_question}
|
||||
* or {@link \core_question\statistics\responses\analysis_for_question_all_tries}.
|
||||
* - There are separate analysis for each variant in this top level instance.
|
||||
* - Then there are class instances representing the analysis of each of the sub parts of each variant of the question.
|
||||
* {@link \core_question\statistics\responses\analysis_for_subpart}.
|
||||
* - Then within the sub part analysis there are response class analysis
|
||||
* {@link \core_question\statistics\responses\analysis_for_class}.
|
||||
* - Then within each class analysis there are analysis for each actual response
|
||||
* {@link \core_question\statistics\responses\analysis_for_actual_response}.
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2014 The Open University
|
||||
* @author James Pratt me@jamiep.org
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class analysis_for_subpart {
|
||||
|
||||
/**
|
||||
* Takes an array of possible_responses - ({@link \question_possible_response} objects).
|
||||
* Or takes an array of {@link \question_possible_response} objects.
|
||||
* Takes an array of possible_responses as returned from {@link \question_type::get_possible_responses()}.
|
||||
*
|
||||
* @param \question_possible_response[] $responseclasses
|
||||
* @param \question_possible_response[] $responseclasses as returned from {@link \question_type::get_possible_responses()}.
|
||||
*/
|
||||
public function __construct(array $responseclasses = null) {
|
||||
if (is_array($responseclasses)) {
|
||||
@ -44,7 +61,6 @@ class analysis_for_subpart {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @var analysis_for_class[]
|
||||
*/
|
||||
protected $responseclasses;
|
||||
@ -59,6 +75,8 @@ class analysis_for_subpart {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the instance of the class handling the analysis of $classid for this sub part.
|
||||
*
|
||||
* @param string $classid id for response class.
|
||||
* @return analysis_for_class
|
||||
*/
|
||||
@ -66,40 +84,68 @@ class analysis_for_subpart {
|
||||
return $this->responseclasses[$classid];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether there is more than one response class for responses in this question sub part?
|
||||
*
|
||||
* @return bool Are there?
|
||||
*/
|
||||
public function has_multiple_response_classes() {
|
||||
return count($this->responseclasses) > 1;
|
||||
return count($this->get_response_class_ids()) > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count a part of a response.
|
||||
*
|
||||
* @param \question_classified_response $subpart
|
||||
* @param int $try the try number or zero if not keeping track of try number
|
||||
*/
|
||||
public function count_response($subpart) {
|
||||
$this->responseclasses[$subpart->responseclassid]->count_response($subpart->response, $subpart->fraction);
|
||||
public function count_response($subpart, $try = 0) {
|
||||
$responseanalysisforclass = $this->get_response_class($subpart->responseclassid);
|
||||
$responseanalysisforclass->count_response($subpart->response, $subpart->fraction, $try);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \qubaid_condition $qubaids
|
||||
* @param int $questionid the question id
|
||||
* @param int $variantno
|
||||
* @param string $subpartid
|
||||
* Cache analysis for sub part.
|
||||
*
|
||||
* @param \qubaid_condition $qubaids which question usages have been analysed.
|
||||
* @param string $whichtries which tries have been analysed?
|
||||
* @param int $questionid which question.
|
||||
* @param int $variantno which variant.
|
||||
* @param string $subpartid which sub part.
|
||||
*/
|
||||
public function cache($qubaids, $questionid, $variantno, $subpartid) {
|
||||
foreach ($this->responseclasses as $responseclassid => $responseclass) {
|
||||
$responseclass->cache($qubaids, $questionid, $variantno, $subpartid, $responseclassid);
|
||||
public function cache($qubaids, $whichtries, $questionid, $variantno, $subpartid) {
|
||||
foreach ($this->get_response_class_ids() as $responseclassid) {
|
||||
$analysisforclass = $this->get_response_class($responseclassid);
|
||||
$analysisforclass->cache($qubaids, $whichtries, $questionid, $variantno, $subpartid, $responseclassid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Has actual responses different to the model response for this class?
|
||||
*
|
||||
* @return bool whether this analysis has a response class with more than one
|
||||
* different actual response, or if the actual response is different from
|
||||
* the model response.
|
||||
*/
|
||||
public function has_actual_responses() {
|
||||
foreach ($this->responseclasses as $responseclass) {
|
||||
if ($responseclass->has_actual_responses()) {
|
||||
foreach ($this->get_response_class_ids() as $responseclassid) {
|
||||
if ($this->get_response_class($responseclassid)->has_actual_responses()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* What is the highest try number for this sub part?
|
||||
*
|
||||
* @return int max tries
|
||||
*/
|
||||
public function get_maximum_tries() {
|
||||
$max = 1;
|
||||
foreach ($this->get_response_class_ids() as $responseclassid) {
|
||||
$max = max($max, $this->get_response_class($responseclassid)->get_maximum_tries());
|
||||
}
|
||||
return $max;
|
||||
}
|
||||
}
|
||||
|
@ -691,10 +691,11 @@ ORDER BY qa.slot
|
||||
/**
|
||||
* Load a {@link question_attempt} from the database, including all its
|
||||
* steps.
|
||||
*
|
||||
* @param int $questionid the question to load all the attempts fors.
|
||||
* @param qubaid_condition $qubaids used to restrict which usages are included
|
||||
* in the query. See {@link qubaid_condition}.
|
||||
* @return array of question_attempts.
|
||||
* @return question_attempt[] array of question_attempts that were loaded.
|
||||
*/
|
||||
public function load_attempts_at_question($questionid, qubaid_condition $qubaids) {
|
||||
$sql = "
|
||||
|
@ -1321,14 +1321,19 @@ class question_attempt {
|
||||
}
|
||||
|
||||
/**
|
||||
* Break down a student response by sub part and classification.
|
||||
* See also {@link question_type::get_possible_responses()}
|
||||
* Break down a student response by sub part and classification. See also {@link question::classify_response}.
|
||||
* Used for response analysis.
|
||||
*
|
||||
* @return question_possible_response[] where keys are subpartid.
|
||||
* @param string $whichtries which tries to analyse for response analysis. Will be one of
|
||||
* question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
|
||||
* Defaults to question_attempt::LAST_TRY.
|
||||
* @return (question_classified_response|array)[] If $whichtries is question_attempt::FIRST_TRY or LAST_TRY index is subpartid
|
||||
* and values are question_classified_response instances.
|
||||
* If $whichtries is question_attempt::ALL_TRIES then first key is submitted response no
|
||||
* and the second key is subpartid.
|
||||
*/
|
||||
public function classify_response() {
|
||||
return $this->behaviour->classify_response();
|
||||
public function classify_response($whichtries = self::LAST_TRY) {
|
||||
return $this->behaviour->classify_response($whichtries);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1686,7 +1691,7 @@ class question_attempt_steps_with_submitted_response_iterator extends question_a
|
||||
}
|
||||
/** @return bool */
|
||||
public function valid() {
|
||||
return $this->offsetExists($this->submittedresponseno);
|
||||
return $this->submittedresponseno >= 1 && $this->submittedresponseno <= count($this->stepswithsubmittedresponses);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -32,12 +32,19 @@ defined('MOODLE_INTERNAL') || die();
|
||||
function question_usage_statistics_cron() {
|
||||
global $DB;
|
||||
|
||||
$expiretime = time() - 5*HOURSECS;
|
||||
$expiretime = time() - 5 * HOURSECS;
|
||||
|
||||
mtrace("\n Cleaning up old question statistics cache records...", '');
|
||||
|
||||
$DB->delete_records_select('question_statistics', 'timemodified < ?', array($expiretime));
|
||||
$DB->delete_records_select('question_response_analysis', 'timemodified < ?', array($expiretime));
|
||||
$responseanlysisids = $DB->get_records_select_menu('question_response_analysis',
|
||||
'timemodified < ?',
|
||||
array($expiretime),
|
||||
'id',
|
||||
'id, id AS id2');
|
||||
|
||||
$DB->delete_records_list('question_response_analysis', 'id', $responseanlysisids);
|
||||
$DB->delete_records_list('question_response_count', 'analysisid', $responseanlysisids);
|
||||
|
||||
mtrace('done.');
|
||||
return true;
|
||||
|
@ -29,7 +29,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$version = 2014031400.01; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
$version = 2014031400.04; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
// RR = release increments - 00 in DEV branches.
|
||||
// .XX = incremental changes.
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user