mirror of
https://github.com/moodle/moodle.git
synced 2025-05-12 11:15:49 +02:00
Merge branch 'MDL-41751-master' of git://github.com/jamiepratt/moodle
This commit is contained in:
commit
6579a7b791
mod/quiz/report/statistics
question
behaviour
classes/statistics/responses
analyser.phpanalysis_for_actual_response.phpanalysis_for_class.phpanalysis_for_question.phpanalysis_for_subpart.php
engine
@ -58,7 +58,7 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
$download = optional_param('download', '', PARAM_ALPHA);
|
||||
$everything = optional_param('everything', 0, PARAM_BOOL);
|
||||
$recalculate = optional_param('recalculate', 0, PARAM_BOOL);
|
||||
// A qid paramter indicates we should display the detailed analysis of a question.
|
||||
// A qid paramter indicates we should display the detailed analysis of a sub question.
|
||||
$qid = optional_param('qid', 0, PARAM_INT);
|
||||
$slot = optional_param('slot', 0, PARAM_INT);
|
||||
|
||||
@ -135,7 +135,6 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
$questionstats = array();
|
||||
$subquestionstats = array();
|
||||
}
|
||||
$quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
|
||||
|
||||
// Set up the table, if there is data.
|
||||
if ($quizstats->s()) {
|
||||
@ -166,6 +165,7 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
|
||||
if ($everything) { // Implies is downloading.
|
||||
// Overall report, then the analysis of each question.
|
||||
$quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
|
||||
$this->download_quiz_info_table($quizinfo);
|
||||
|
||||
if ($quizstats->s()) {
|
||||
@ -209,7 +209,7 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
|
||||
} else if ($qid) {
|
||||
// Report on an individual sub-question indexed questionid.
|
||||
if (!isset($subquestions[$qid])) {
|
||||
if (!isset($subquestionstats[$qid])) {
|
||||
print_error('questiondoesnotexist', 'question');
|
||||
}
|
||||
|
||||
@ -224,6 +224,7 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
|
||||
} else if ($this->table->is_downloading()) {
|
||||
// Downloading overview report.
|
||||
$quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
|
||||
$this->download_quiz_info_table($quizinfo);
|
||||
$this->output_quiz_structure_analysis_table($quizstats->s(), $questionstats, $subquestionstats);
|
||||
$this->table->finish_output();
|
||||
@ -234,6 +235,7 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
echo $this->output_caching_info($quizstats, $quiz->id, $currentgroup,
|
||||
$groupstudents, $useallattempts, $reporturl);
|
||||
echo $this->everything_download_options();
|
||||
$quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
|
||||
echo $this->output_quiz_info_table($quizinfo);
|
||||
if ($quizstats->s()) {
|
||||
echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics'));
|
||||
@ -248,10 +250,8 @@ 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 object $quiz the quiz settings.
|
||||
* @param \core_question\statistics\questions\calculated $questionstat the question to report on.
|
||||
* @param moodle_url $reporturl the URL to resisplay this report.
|
||||
* @param object $quizstats Holds the quiz statistics.
|
||||
*/
|
||||
protected function output_individual_question_data($quiz, $questionstat) {
|
||||
global $OUTPUT;
|
||||
@ -329,8 +329,9 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
|
||||
/**
|
||||
* Display the response analysis for a question.
|
||||
* @param object $question the question to report on.
|
||||
* @param moodle_url $reporturl the URL to resisplay this report.
|
||||
* @param object $question the question to report on.
|
||||
* @param int $s
|
||||
* @param moodle_url $reporturl the URL to redisplay this report.
|
||||
* @param qubaid_condition $qubaids
|
||||
*/
|
||||
protected function output_individual_question_response_analysis($question, $s, $reporturl, $qubaids) {
|
||||
@ -354,8 +355,7 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
$questiontabletitle = '(' . $question->number . ') ' . $questiontabletitle;
|
||||
}
|
||||
if ($this->table->is_downloading() == 'xhtml') {
|
||||
$questiontabletitle = get_string('analysisofresponsesfor',
|
||||
'quiz_statistics', $questiontabletitle);
|
||||
$questiontabletitle = get_string('analysisofresponsesfor', 'quiz_statistics', $questiontabletitle);
|
||||
}
|
||||
|
||||
// Set up the table.
|
||||
@ -366,38 +366,20 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
}
|
||||
}
|
||||
|
||||
$responesstats = new \core_question\statistics\responses\analyser($question);
|
||||
$responesstats->load_cached($qubaids);
|
||||
$responesanalyser = new \core_question\statistics\responses\analyser($question);
|
||||
$responseanalysis = $responesanalyser->load_cached($qubaids);
|
||||
|
||||
$qtable->question_setup($reporturl, $question, $s, $responesstats);
|
||||
$qtable->question_setup($reporturl, $question, $s, $responseanalysis);
|
||||
if ($this->table->is_downloading()) {
|
||||
$exportclass->output_headers($qtable->headers);
|
||||
}
|
||||
|
||||
foreach ($responesstats->responseclasses as $partid => $partclasses) {
|
||||
$rowdata = new stdClass();
|
||||
$rowdata->part = $partid;
|
||||
foreach ($partclasses as $responseclassid => $responseclass) {
|
||||
$rowdata->responseclass = $responseclass->responseclass;
|
||||
|
||||
$responsesdata = $responesstats->responses[$partid][$responseclassid];
|
||||
if (empty($responsesdata)) {
|
||||
if (!array_key_exists('responseclass', $qtable->columns)) {
|
||||
$rowdata->response = $responseclass->responseclass;
|
||||
} else {
|
||||
$rowdata->response = '';
|
||||
}
|
||||
$rowdata->fraction = $responseclass->fraction;
|
||||
$rowdata->count = 0;
|
||||
$qtable->add_data_keyed($qtable->format_row($rowdata));
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($responsesdata as $response => $data) {
|
||||
$rowdata->response = $response;
|
||||
$rowdata->fraction = $data->fraction;
|
||||
$rowdata->count = $data->count;
|
||||
$qtable->add_data_keyed($qtable->format_row($rowdata));
|
||||
foreach ($responseanalysis->get_subpart_ids() as $partid) {
|
||||
$subpart = $responseanalysis->get_subpart($partid);
|
||||
foreach ($subpart->get_response_class_ids() as $responseclassid) {
|
||||
$responseclass = $subpart->get_response_class($responseclassid);
|
||||
$tabledata = $responseclass->data_for_question_response_table($subpart->has_multiple_response_classes(), $partid);
|
||||
foreach ($tabledata as $row) {
|
||||
$qtable->add_data_keyed($qtable->format_row($row));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -531,7 +513,7 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
$groupstudents, count($questions), $qcalc->get_sum_of_mark_variance());
|
||||
|
||||
if ($quizstats->s()) {
|
||||
$this->calculate_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats);
|
||||
$this->analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats);
|
||||
}
|
||||
} else {
|
||||
$quizstats = $quizcalc->get_cached($qubaids);
|
||||
@ -541,7 +523,7 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
return array($quizstats, $questionstats, $subquestionstats);
|
||||
}
|
||||
|
||||
protected function calculate_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats) {
|
||||
protected function analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats) {
|
||||
|
||||
$done = array();
|
||||
foreach ($questions as $question) {
|
||||
|
@ -38,9 +38,12 @@ require_once($CFG->libdir . '/tablelib.php');
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class quiz_statistics_question_table extends flexible_table {
|
||||
/** @var object this question. */
|
||||
/** @var object full question object for this question. */
|
||||
protected $questiondata;
|
||||
|
||||
/** @var int no of attempts. */
|
||||
protected $s;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param int $qid the id of the particular question whose statistics are being
|
||||
@ -51,12 +54,12 @@ class quiz_statistics_question_table extends flexible_table {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param moodle_url $reporturl
|
||||
* @param object $questiondata
|
||||
* @param integer $s number of attempts on this question.
|
||||
* @param \core_question\statistics\responses\analyser $responesstats
|
||||
* @param moodle_url $reporturl
|
||||
* @param object $questiondata
|
||||
* @param integer $s number of attempts on this question.
|
||||
* @param \core_question\statistics\responses\analysis_for_question $responseanalysis
|
||||
*/
|
||||
public function question_setup($reporturl, $questiondata, $s, \core_question\statistics\responses\analyser $responesstats) {
|
||||
public function question_setup($reporturl, $questiondata, $s, $responseanalysis) {
|
||||
$this->questiondata = $questiondata;
|
||||
$this->s = $s;
|
||||
|
||||
@ -68,16 +71,16 @@ class quiz_statistics_question_table extends flexible_table {
|
||||
$columns = array();
|
||||
$headers = array();
|
||||
|
||||
if ($responesstats->has_subparts()) {
|
||||
if ($responseanalysis->has_subparts()) {
|
||||
$columns[] = 'part';
|
||||
$headers[] = get_string('partofquestion', 'quiz_statistics');
|
||||
}
|
||||
|
||||
if ($responesstats->has_response_classes()) {
|
||||
if ($responseanalysis->has_multiple_response_classes()) {
|
||||
$columns[] = 'responseclass';
|
||||
$headers[] = get_string('modelresponse', 'quiz_statistics');
|
||||
|
||||
if ($responesstats->has_actual_responses()) {
|
||||
if ($responseanalysis->has_actual_responses()) {
|
||||
$columns[] = 'response';
|
||||
$headers[] = get_string('actualresponse', 'quiz_statistics');
|
||||
}
|
||||
@ -129,7 +132,7 @@ class quiz_statistics_question_table extends flexible_table {
|
||||
|
||||
/**
|
||||
* The frequency with which this response was given.
|
||||
* @param object $response containst the data to display.
|
||||
* @param object $response contains the data to display.
|
||||
* @return string contents of this table cell.
|
||||
*/
|
||||
protected function col_frequency($response) {
|
||||
|
@ -301,12 +301,7 @@ abstract class question_behaviour {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array subpartid => object with fields
|
||||
* ->responseclassid matches one of the values returned from
|
||||
* quetion_type::get_possible_responses.
|
||||
* ->response the actual response the student gave to this part, as a string.
|
||||
* ->fraction the credit awarded for this subpart, may be null.
|
||||
* returns an empty array if no analysis is possible.
|
||||
* @return question_possible_response[] where keys are subpartid or an empty array if no classification is possible.
|
||||
*/
|
||||
public function classify_response() {
|
||||
return $this->question->classify_response($this->qa->get_last_qt_data());
|
||||
|
@ -37,41 +37,31 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class analyser {
|
||||
/** @var object the data from the database that defines the question. */
|
||||
/** @var object full question data from db. */
|
||||
protected $questiondata;
|
||||
|
||||
/**
|
||||
* @var array This is a multi-dimensional array that stores the results of
|
||||
* the analysis.
|
||||
*
|
||||
* The description of {@link question_type::get_possible_responses()} should
|
||||
* help understand this description.
|
||||
*
|
||||
* $this->responses[$subpartid][$responseclassid][$response] is an
|
||||
* object with two fields, ->count and ->fraction.
|
||||
* @var analysis_for_question
|
||||
*/
|
||||
public $responses = array();
|
||||
public $analysis;
|
||||
|
||||
/**
|
||||
* @var array $this->fractions[$subpartid][$responseclassid] is an object
|
||||
* with two fields, ->responseclass and ->fraction.
|
||||
* @var array Two index array first index is unique for each sub question part, the second index is the 'class' that this sub
|
||||
* question part can be classified into. This is the return value from {@link \question_type::get_possible_responses()}
|
||||
*/
|
||||
public $responseclasses = array();
|
||||
|
||||
/**
|
||||
* Create a new instance of this class for holding/computing the statistics
|
||||
* for a particular question.
|
||||
* @param object $questiondata the data from the database defining this question.
|
||||
*
|
||||
* @param object $questiondata the full question data from the database defining this question.
|
||||
*/
|
||||
public function __construct($questiondata) {
|
||||
$this->questiondata = $questiondata;
|
||||
$qtypeobj = \question_bank::get_qtype($this->questiondata->qtype);
|
||||
$this->analysis = new analysis_for_question($qtypeobj->get_possible_responses($this->questiondata));
|
||||
|
||||
$this->responseclasses = \question_bank::get_qtype($questiondata->qtype)->get_possible_responses($questiondata);
|
||||
foreach ($this->responseclasses as $subpartid => $responseclasses) {
|
||||
foreach ($responseclasses as $responseclassid => $notused) {
|
||||
$this->responses[$subpartid][$responseclassid] = array();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -82,8 +72,7 @@ class analyser {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool whether this analysis has (a subpart with) more than one
|
||||
* response class.
|
||||
* @return bool whether this analysis has (a subpart with) more than one response class.
|
||||
*/
|
||||
public function has_response_classes() {
|
||||
foreach ($this->responseclasses as $partclasses) {
|
||||
@ -119,6 +108,7 @@ class analyser {
|
||||
* Analyse all the response data for for all the specified attempts at
|
||||
* this question.
|
||||
* @param \qubaid_condition $qubaids which attempts to consider.
|
||||
* @return analysis_for_question
|
||||
*/
|
||||
public function calculate($qubaids) {
|
||||
// Load data.
|
||||
@ -127,91 +117,53 @@ class analyser {
|
||||
|
||||
// Analyse it.
|
||||
foreach ($questionattempts as $qa) {
|
||||
$this->add_data_from_one_attempt($qa);
|
||||
$responseparts = $qa->classify_response();
|
||||
$this->analysis->count_response_parts($responseparts);
|
||||
}
|
||||
$this->store_cached($qubaids);
|
||||
|
||||
$this->analysis->cache($qubaids, $this->questiondata->id);
|
||||
return $this->analysis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyse the data from one question attempt.
|
||||
* @param \question_attempt $qa the data to analyse.
|
||||
*/
|
||||
protected function add_data_from_one_attempt(\question_attempt $qa) {
|
||||
$blankresponse = \question_classified_response::no_response();
|
||||
/** @var integer Time after which responses are automatically reanalysed. */
|
||||
const TIME_TO_CACHE = 900; // 15 minutes.
|
||||
|
||||
$partresponses = $qa->classify_response();
|
||||
foreach ($partresponses as $subpartid => $partresponse) {
|
||||
if (!isset($this->responses[$subpartid][$partresponse->responseclassid]
|
||||
[$partresponse->response])) {
|
||||
$resp = new \stdClass();
|
||||
$resp->count = 0;
|
||||
if (!is_null($partresponse->fraction)) {
|
||||
$resp->fraction = $partresponse->fraction;
|
||||
} else {
|
||||
$resp->fraction = $this->responseclasses[$subpartid]
|
||||
[$partresponse->responseclassid]->fraction;
|
||||
}
|
||||
|
||||
$this->responses[$subpartid][$partresponse->responseclassid]
|
||||
[$partresponse->response] = $resp;
|
||||
}
|
||||
|
||||
$this->responses[$subpartid][$partresponse->responseclassid]
|
||||
[$partresponse->response]->count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the computed response analysis in the question_response_analysis table.
|
||||
* @param \qubaid_condition $qubaids
|
||||
* data corresponding to.
|
||||
* @return bool true if cached data was found in the database and loaded, otherwise false, to mean no data was loaded.
|
||||
* Retrieve the computed response analysis from the question_response_analysis table.
|
||||
*
|
||||
* @param \qubaid_condition $qubaids which attempts to get cached response analysis for.
|
||||
* @return analysis_for_question|boolean analysis or false if no cached analysis found.
|
||||
*/
|
||||
public function load_cached($qubaids) {
|
||||
global $DB;
|
||||
|
||||
$rows = $DB->get_records('question_response_analysis',
|
||||
array('hashcode' => $qubaids->get_hash_code(), 'questionid' => $this->questiondata->id));
|
||||
$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) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$this->responses[$row->subqid][$row->aid][$row->response] = new \stdClass();
|
||||
$this->responses[$row->subqid][$row->aid][$row->response]->count = $row->rcount;
|
||||
$this->responses[$row->subqid][$row->aid][$row->response]->fraction = $row->credit;
|
||||
$class = $this->analysis->get_subpart($row->subqid)->get_response_class($row->aid);
|
||||
$class->add_response_and_count($row->response, $row->credit, $row->rcount);
|
||||
}
|
||||
return true;
|
||||
return $this->analysis;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Store the computed response analysis in the question_response_analysis table.
|
||||
* @param \qubaid_condition $qubaids
|
||||
* Find time of non-expired analysis in the database.
|
||||
*
|
||||
* @param $qubaids \qubaid_condition
|
||||
* @return integer|boolean Time of cached record that matches this qubaid_condition or false if none found.
|
||||
*/
|
||||
public function store_cached($qubaids) {
|
||||
public function get_last_analysed_time($qubaids) {
|
||||
global $DB;
|
||||
|
||||
$cachetime = time();
|
||||
foreach ($this->responses as $subpartid => $partdata) {
|
||||
foreach ($partdata as $responseclassid => $classdata) {
|
||||
foreach ($classdata as $response => $data) {
|
||||
$row = new \stdClass();
|
||||
$row->hashcode = $qubaids->get_hash_code();
|
||||
$row->questionid = $this->questiondata->id;
|
||||
$row->subqid = $subpartid;
|
||||
if ($responseclassid === '') {
|
||||
$row->aid = null;
|
||||
} else {
|
||||
$row->aid = $responseclassid;
|
||||
}
|
||||
$row->response = $response;
|
||||
$row->rcount = $data->count;
|
||||
$row->credit = $data->fraction;
|
||||
$row->timemodified = $cachetime;
|
||||
$DB->insert_record('question_response_analysis', $row, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
$timemodified = time() - self::TIME_TO_CACHE;
|
||||
return $DB->get_field_select('question_response_analysis', 'timemodified', 'hashcode = ? AND timemodified > ? '.
|
||||
'ORDER BY timemodified DESC LIMIT 1',
|
||||
array($qubaids->get_hash_code(), $timemodified));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,106 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* ${filedescription}
|
||||
*
|
||||
* @package ${package}_{subpackage}
|
||||
* @copyright 2013 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;
|
||||
|
||||
|
||||
class analysis_for_actual_response {
|
||||
/**
|
||||
* @var int count of this response
|
||||
*/
|
||||
protected $count;
|
||||
|
||||
/**
|
||||
* @var float grade for this response, normally between 0 and 1.
|
||||
*/
|
||||
protected $fraction;
|
||||
|
||||
/**
|
||||
* @var string the response as it will be displayed in report.
|
||||
*/
|
||||
protected $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) {
|
||||
$this->response = $response;
|
||||
$this->fraction = $fraction;
|
||||
$this->count = $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to count the occurrences of response sub parts.
|
||||
*/
|
||||
public function increment_count() {
|
||||
$this->count++;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param \qubaid_condition $qubaids
|
||||
* @param int $questionid the question id
|
||||
* @param string $subpartid
|
||||
* @param string $responseclassid
|
||||
*/
|
||||
public function cache($qubaids, $questionid, $subpartid, $responseclassid) {
|
||||
global $DB;
|
||||
$row = new \stdClass();
|
||||
$row->hashcode = $qubaids->get_hash_code();
|
||||
$row->questionid = $questionid;
|
||||
$row->subqid = $subpartid;
|
||||
if ($responseclassid === '') {
|
||||
$row->aid = null;
|
||||
} else {
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $partid
|
||||
* @param string $modelresponse
|
||||
* @return object
|
||||
*/
|
||||
public function data_for_question_response_table($partid, $modelresponse) {
|
||||
$rowdata = new \stdClass();
|
||||
$rowdata->part = $partid;
|
||||
$rowdata->responseclass = $modelresponse;
|
||||
$rowdata->response = $this->response;
|
||||
$rowdata->fraction = $this->fraction;
|
||||
$rowdata->count = $this->count;
|
||||
return $rowdata;
|
||||
}
|
||||
}
|
141
question/classes/statistics/responses/analysis_for_class.php
Normal file
141
question/classes/statistics/responses/analysis_for_class.php
Normal file
@ -0,0 +1,141 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* ${filedescription}
|
||||
*
|
||||
* @package ${package}_{subpackage}
|
||||
* @copyright 2013 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;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* No response is one possible class of response to a question.
|
||||
*
|
||||
* @copyright 2010 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class analysis_for_class {
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $responseclassid;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
protected $actualresponses = array();
|
||||
|
||||
/**
|
||||
* Constructor, just an easy way to set the fields.
|
||||
* @param \question_possible_response $possibleresponse
|
||||
* @param string $responseclassid
|
||||
*/
|
||||
public function __construct($possibleresponse, $responseclassid) {
|
||||
$this->modelresponse = $possibleresponse->responseclass;
|
||||
$this->fraction = $possibleresponse->fraction;
|
||||
$this->responseclassid = $responseclassid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $actualresponse
|
||||
* @param float|null $fraction
|
||||
*/
|
||||
public function count_response($actualresponse, $fraction) {
|
||||
if (!isset($this->actualresponses[$actualresponse])) {
|
||||
if ($fraction === null) {
|
||||
$fraction = $this->fraction;
|
||||
}
|
||||
$this->actualresponses[$actualresponse] = new analysis_for_actual_response($actualresponse, $fraction);
|
||||
}
|
||||
$this->actualresponses[$actualresponse]->increment_count();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \qubaid_condition $qubaids
|
||||
* @param int $questionid the question id
|
||||
* @param string $subpartid
|
||||
*/
|
||||
public function cache($qubaids, $questionid, $subpartid) {
|
||||
foreach ($this->actualresponses as $response => $actualresponse) {
|
||||
$actualresponse->cache($qubaids, $questionid, $subpartid, $this->responseclassid, $response);
|
||||
}
|
||||
}
|
||||
|
||||
public function add_response_and_count($response, $fraction, $count) {
|
||||
$this->actualresponses[$response] = new analysis_for_actual_response($response, $fraction, $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
return true;
|
||||
} else if (count($this->actualresponses) == 1) {
|
||||
$onlyactualresponse = reset($this->actualresponses);
|
||||
return !$onlyactualresponse->response_matches($this->modelresponse);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return object[]
|
||||
*/
|
||||
public function data_for_question_response_table($responseclasscolumn, $partid) {
|
||||
$return = array();
|
||||
if (empty($this->actualresponses)) {
|
||||
$rowdata = new \stdClass();
|
||||
$rowdata->part = $partid;
|
||||
$rowdata->responseclass = $this->modelresponse;
|
||||
if (!$responseclasscolumn) {
|
||||
$rowdata->response = $this->modelresponse;
|
||||
} else {
|
||||
$rowdata->response = '';
|
||||
}
|
||||
$rowdata->fraction = $this->fraction;
|
||||
$rowdata->count = 0;
|
||||
$return[] = $rowdata;
|
||||
} else {
|
||||
foreach ($this->actualresponses as $actualresponse) {
|
||||
$return[] = $actualresponse->data_for_question_response_table($partid, $this->modelresponse);
|
||||
}
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
}
|
141
question/classes/statistics/responses/analysis_for_question.php
Normal file
141
question/classes/statistics/responses/analysis_for_question.php
Normal file
@ -0,0 +1,141 @@
|
||||
<?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 the code to analyse all the responses to a particular
|
||||
* question.
|
||||
*
|
||||
* @package core
|
||||
* @subpackage questionbank
|
||||
* @copyright 2013 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;
|
||||
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
|
||||
* 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
|
||||
*
|
||||
* @copyright 2013 Open University
|
||||
* @author Jamie Pratt <me@jamiep.org>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class analysis_for_question {
|
||||
|
||||
/**
|
||||
* Takes either a two index array as a parameter with keys subpartid and classid and values possible_response.
|
||||
* Or takes an array of {@link responses_for_classes} objects.
|
||||
*
|
||||
* @param $subparts[string]array[]\question_possible_response $array
|
||||
*/
|
||||
public function __construct(array $subparts = null) {
|
||||
if (!is_null($subparts)) {
|
||||
foreach ($subparts as $subpartid => $classes) {
|
||||
$this->subparts[$subpartid] = new analysis_for_subpart($classes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @var analysis_for_subpart[]
|
||||
*/
|
||||
protected $subparts;
|
||||
|
||||
/**
|
||||
* Unique ids for sub parts.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function get_subpart_ids() {
|
||||
return array_keys($this->subparts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $subpartid id for sub part.
|
||||
* @return analysis_for_subpart
|
||||
*/
|
||||
public function get_subpart($subpartid) {
|
||||
return $this->subparts[$subpartid];
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to work out what kind of table is needed to display stats.
|
||||
*
|
||||
* @return bool whether this question has (a subpart with) more than one response class.
|
||||
*/
|
||||
public function has_multiple_response_classes() {
|
||||
foreach ($this->subparts as $subpart) {
|
||||
if ($subpart->has_multiple_response_classes()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to work out what kind of table is needed to display stats.
|
||||
*
|
||||
* @return bool whether this analysis has more than one subpart.
|
||||
*/
|
||||
public function has_subparts() {
|
||||
return count($this->subparts) > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an array of {@link \question_classified_response} and adds counts of the responses to the sub parts and classes.
|
||||
*
|
||||
* @var \question_classified_response[] $responseparts keys are sub-part id.
|
||||
*/
|
||||
public function count_response_parts($responseparts) {
|
||||
foreach ($responseparts as $subpartid => $responsepart) {
|
||||
$this->get_subpart($subpartid)->count_response($responsepart);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \qubaid_condition $qubaids
|
||||
* @param int $questionid the question id
|
||||
*/
|
||||
public function cache($qubaids, $questionid) {
|
||||
foreach ($this->subparts as $subpartid => $subpart) {
|
||||
$subpart->cache($qubaids, $questionid, $subpartid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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->subparts as $subpartid => $subpart) {
|
||||
if ($subpart->has_actual_responses()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
105
question/classes/statistics/responses/analysis_for_subpart.php
Normal file
105
question/classes/statistics/responses/analysis_for_subpart.php
Normal file
@ -0,0 +1,105 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
*
|
||||
* 'Classes' to classify the sub parts of a question response into.
|
||||
*
|
||||
* @package core
|
||||
* @subpackage questionbank
|
||||
* @copyright 2013 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;
|
||||
|
||||
|
||||
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.
|
||||
*
|
||||
* @param \question_possible_response[] $responseclasses
|
||||
*/
|
||||
public function __construct(array $responseclasses = null) {
|
||||
if (is_array($responseclasses)) {
|
||||
foreach ($responseclasses as $responseclassid => $reponseclass) {
|
||||
$this->responseclasses[$responseclassid] = new analysis_for_class($reponseclass, $responseclassid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @var analysis_for_class[]
|
||||
*/
|
||||
protected $responseclasses;
|
||||
|
||||
/**
|
||||
* Unique ids for response classes.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function get_response_class_ids() {
|
||||
return array_keys($this->responseclasses);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $classid id for response class.
|
||||
* @return analysis_for_class
|
||||
*/
|
||||
public function get_response_class($classid) {
|
||||
return $this->responseclasses[$classid];
|
||||
}
|
||||
|
||||
public function has_multiple_response_classes() {
|
||||
return count($this->responseclasses) > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \question_classified_response $subpart
|
||||
*/
|
||||
public function count_response($subpart) {
|
||||
$this->responseclasses[$subpart->responseclassid]->count_response($subpart->response, $subpart->fraction);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \qubaid_condition $qubaids
|
||||
* @param int $questionid the question id
|
||||
* @param string $subpartid
|
||||
*/
|
||||
public function cache($qubaids, $questionid, $subpartid) {
|
||||
foreach ($this->responseclasses as $responseclassid => $responseclass) {
|
||||
$responseclass->cache($qubaids, $questionid, $subpartid, $responseclassid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@ -1242,11 +1242,11 @@ class question_attempt {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array subpartid => object with fields
|
||||
* ->responseclassid matches one of the values returned from quetion_type::get_possible_responses.
|
||||
* ->response the actual response the student gave to this part, as a string.
|
||||
* ->fraction the credit awarded for this subpart, may be null.
|
||||
* returns an empty array if no analysis is possible.
|
||||
* Break down a student response by sub part and classification.
|
||||
* See also {@link question_type::get_possible_responses()}
|
||||
* Used for response analysis.
|
||||
*
|
||||
* @return question_possible_response[] where keys are subpartid.
|
||||
*/
|
||||
public function classify_response() {
|
||||
return $this->behaviour->classify_response();
|
||||
|
Loading…
x
Reference in New Issue
Block a user