first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,200 @@
<?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_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;
defined('MOODLE_INTERNAL') || die();
/**
* This class can compute, store and cache the analysis of the responses to a particular question.
*
* @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 When analysing responses and breaking down the count of responses per try, how many columns should we break down
* tries into? This is set to 5 columns, any response in a try more than try 5 will be counted in the fifth column.
*/
const MAX_TRY_COUNTED = 5;
/**
* @var int previously, the time after which statistics are automatically recomputed.
* @deprecated since Moodle 4.3. Use of pre-computed stats is no longer time-limited.
* @todo MDL-78090 Final deprecation in Moodle 4.7
*/
const TIME_TO_CACHE = 900; // 15 minutes.
/** @var object full question data from db. */
protected $questiondata;
/**
* @var analysis_for_question|analysis_for_question_all_tries
*/
public $analysis;
/**
* @var int used during calculations, so all results are stored with the same timestamp.
*/
protected $calculationtime;
/**
* @var array Two index array first index is unique string for each sub question part, the second string index is the 'class'
* that sub-question part can be classified into.
*
* This is the return value from {@link \question_type::get_possible_responses()} see that method for fuller documentation.
*/
public $responseclasses = array();
/**
* @var bool whether to break down response analysis by variant. This only applies to questions that have variants and is
* used to suppress the break down of analysis by variant when there are going to be very many variants.
*/
protected $breakdownbyvariant;
/**
* Create a new instance of this class for holding/computing the statistics
* 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, $whichtries = \question_attempt::LAST_TRY) {
$this->questiondata = $questiondata;
$qtypeobj = \question_bank::get_qtype($this->questiondata->qtype);
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() {
return count($this->responseclasses) > 1;
}
/**
* 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() {
foreach ($this->responseclasses as $partclasses) {
if (count($partclasses) > 1) {
return true;
}
}
return false;
}
/**
* Analyse all the response data 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, $whichtries = \question_attempt::LAST_TRY) {
$this->calculationtime = time();
// 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($whichtries);
if ($this->breakdownbyvariant) {
$this->analysis->count_response_parts($qa->get_variant(), $responseparts);
} else {
$this->analysis->count_response_parts(1, $responseparts);
}
}
$this->analysis->cache($qubaids, $whichtries, $this->questiondata->id, $this->calculationtime);
return $this->analysis;
}
/**
* Retrieve the computed response analysis from the question_response_analysis table.
*
* @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, $whichtries) {
global $DB;
$timemodified = self::get_last_analysed_time($qubaids, $whichtries);
// Variable name 'analyses' is the plural of 'analysis'.
$responseanalyses = $DB->get_records('question_response_analysis',
['hashcode' => $qubaids->get_hash_code(), 'whichtries' => $whichtries,
'questionid' => $this->questiondata->id, 'timemodified' => $timemodified]);
if (!$responseanalyses) {
return false;
}
$analysisids = [];
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;
}
[$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;
}
/**
* Find time of non-expired analysis in the database.
*
* @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, $whichtries) {
global $DB;
return $DB->get_field('question_response_analysis', 'MAX(timemodified)',
['hashcode' => $qubaids->get_hash_code(), 'whichtries' => $whichtries,
'questionid' => $this->questiondata->id]);
}
}
@@ -0,0 +1,176 @@
<?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/>.
/**
* @package core_question
* @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;
/**
* 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 per try for this response.
*/
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.
*/
protected $fraction;
/**
* @var string the response as it will be displayed in report.
*/
protected $response;
/**
* @param string $response
* @param float $fraction
*/
public function __construct($response, $fraction) {
$this->response = $response;
$this->fraction = $fraction;
}
/**
* 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($try = 0) {
$this->totalcount++;
if ($try != 0) {
if ($try > analyser::MAX_TRY_COUNTED) {
$try = analyser::MAX_TRY_COUNTED;
}
if (!isset($this->trycount[$try])) {
$this->trycount[$try] = 0;
}
$this->trycount[$try]++;
}
}
/**
* 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 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?
* @param int|null $calculationtime time when the analysis was done. (Defaults to time()).
*/
public function cache($qubaids, $whichtries, $questionid, $variantno, $subpartid, $responseclassid, $calculationtime = null) {
global $DB;
$row = new \stdClass();
$row->hashcode = $qubaids->get_hash_code();
$row->whichtries = $whichtries;
$row->questionid = $questionid;
$row->variant = $variantno;
$row->subqid = $subpartid;
if ($responseclassid === '') {
$row->aid = null;
} else {
$row->aid = $responseclassid;
}
$row->response = $this->response;
$row->credit = $this->fraction;
$row->timemodified = $calculationtime ?? time();
$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
*/
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->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));
}
}
@@ -0,0 +1,217 @@
<?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/>.
/**
* @package core_question
* @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;
/**
* Counts a class of responses for this sub part of the question.
*
* No response is one possible class of response to a 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_class {
/**
* @var string must be unique for each response class within this sub part.
*/
protected $responseclassid;
/**
* @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.
*/
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;
}
/**
* 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, $try) {
if (!isset($this->actualresponses[$actualresponse])) {
if ($fraction === null) {
$fraction = $this->fraction;
}
$this->add_response($actualresponse, $fraction);
}
$this->get_response($actualresponse)->increment_count($try);
}
/**
* 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.
* @param int|null $calculationtime time when the analysis was done. (Defaults to time()).
*/
public function cache($qubaids, $whichtries, $questionid, $variantno, $subpartid, $calculationtime = null) {
foreach ($this->get_responses() as $response) {
$analysisforactualresponse = $this->get_response($response);
$analysisforactualresponse->cache($qubaids, $whichtries, $questionid, $variantno, $subpartid,
$this->responseclassid, $calculationtime);
}
}
/**
* 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() {
$actualresponses = $this->get_responses();
if (count($actualresponses) > 1) {
return true;
} else if (count($actualresponses) === 1) {
$singleactualresponse = reset($actualresponses);
return (string)$singleactualresponse !== (string)$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 (count($this->get_responses()) == 0) {
$rowdata = new \stdClass();
$rowdata->part = $partid;
$rowdata->responseclass = $this->modelresponse;
if (!$responseclasscolumn) {
$rowdata->response = $this->modelresponse;
} else {
$rowdata->response = '';
}
$rowdata->fraction = $this->fraction;
$rowdata->totalcount = 0;
$rowdata->trycount = array();
$return[] = $rowdata;
} else {
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];
}
}
@@ -0,0 +1,252 @@
<?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_question
* @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. 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.
*
* - 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 {
/**
* Constructor method.
*
* @param array[] Two index array, first index is unique string for each sub question part,
* the second string index is the 'class' that sub-question part can be classified into.
* Value in array is instance of {@link \question_possible_response}
* This is the return value from {@link \question_type::get_possible_responses()}
* see that method for fuller documentation.
*/
public function __construct(array $possiblereponses = null) {
if ($possiblereponses !== null) {
$this->possibleresponses = $possiblereponses;
}
}
/**
* @var array[] See description above in constructor method.
*/
protected $possibleresponses = array();
/**
* A multidimensional array whose first index is variant no and second index is subpart id, array contents are of type
* {@link analysis_for_subpart}.
*
* @var array[]
*/
protected $subparts = array();
/**
* Initialise data structure for response analysis of one variant.
*
* @param int $variantno
*/
protected function initialise_stats_for_variant($variantno) {
$this->subparts[$variantno] = array();
foreach ($this->possibleresponses as $subpartid => $classes) {
$this->subparts[$variantno][$subpartid] = new analysis_for_subpart($classes);
}
}
/**
* Variant nos found in this question's attempt data.
*
* @return int[]
*/
public function get_variant_nos() {
return array_keys($this->subparts);
}
/**
* Unique ids for sub parts.
*
* @param int $variantno
* @return string[]
*/
public function get_subpart_ids($variantno) {
return array_keys($this->subparts[$variantno]);
}
/**
* Get the response counts etc. for variant $variantno, question sub part $subpartid.
*
* Or if there is no recorded analysis yet then initialise the data structure for that part of the analysis and return the
* initialised analysis objects.
*
* @param int $variantno
* @param string $subpartid id for sub part.
* @return analysis_for_subpart
*/
public function get_analysis_for_subpart($variantno, $subpartid) {
if (!isset($this->subparts[$variantno])) {
$this->initialise_stats_for_variant($variantno);
}
if (!isset($this->subparts[$variantno][$subpartid])) {
debugging('Unexpected sub-part id ' . $subpartid .
' encountered.');
$this->subparts[$variantno][$subpartid] = new analysis_for_subpart();
}
return $this->subparts[$variantno][$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->get_variant_nos() as $variantno) {
foreach ($this->get_subpart_ids($variantno) as $subpartid) {
if ($this->get_analysis_for_subpart($variantno, $subpartid)->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() {
foreach ($this->get_variant_nos() as $variantno) {
if (count($this->get_subpart_ids($variantno)) > 1) {
return true;
}
}
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.
*
* @param int $variantno
* @param \question_classified_response[] $responseparts keys are sub-part id.
*/
public function count_response_parts($variantno, $responseparts) {
foreach ($responseparts as $subpartid => $responsepart) {
$this->get_analysis_for_subpart($variantno, $subpartid)->count_response($responsepart);
}
}
/**
* Save the analysis to the DB, first cleaning up any old ones.
*
* @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|null $calculationtime time when the analysis was done. (Defaults to time()).
*/
public function cache($qubaids, $whichtries, $questionid, $calculationtime = null) {
global $DB;
$transaction = $DB->start_delegated_transaction();
$analysisids = $DB->get_fieldset(
'question_response_analysis',
'id',
[
'hashcode' => $qubaids->get_hash_code(),
'whichtries' => $whichtries,
'questionid' => $questionid,
]
);
if (!empty($analysisids)) {
[$insql, $params] = $DB->get_in_or_equal($analysisids);
$DB->delete_records_select('question_response_count', 'analysisid ' . $insql, $params);
$DB->delete_records_select('question_response_analysis', 'id ' . $insql, $params);
}
foreach ($this->get_variant_nos() as $variantno) {
foreach ($this->get_subpart_ids($variantno) as $subpartid) {
$analysisforsubpart = $this->get_analysis_for_subpart($variantno, $subpartid);
$analysisforsubpart->cache($qubaids, $whichtries, $questionid, $variantno, $subpartid, $calculationtime);
}
}
$transaction->allow_commit();
}
/**
* @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->get_variant_nos() as $variantno) {
foreach ($this->get_subpart_ids($variantno) as $subpartid) {
if ($this->get_analysis_for_subpart($variantno, $subpartid)->has_actual_responses()) {
return true;
}
}
}
return false;
}
}
@@ -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;
}
}
@@ -0,0 +1,159 @@
<?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/>.
/**
*
* Data structure to count responses for each of the sub parts of a question.
*
* @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
*/
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 {
/**
* @var analysis_for_class[]
*/
protected $responseclasses;
/**
* Takes an array of possible_responses as returned from {@link \question_type::get_possible_responses()}.
*
* @param \question_possible_response[] $responseclasses as returned from {@link \question_type::get_possible_responses()}.
*/
public function __construct(array $responseclasses = null) {
if (is_array($responseclasses)) {
foreach ($responseclasses as $responseclassid => $responseclass) {
$this->responseclasses[$responseclassid] = new analysis_for_class($responseclass, $responseclassid);
}
} else {
$this->responseclasses = [];
}
}
/**
* Unique ids for response classes.
*
* @return string[]
*/
public function get_response_class_ids() {
return array_keys($this->responseclasses);
}
/**
* 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
*/
public function get_response_class($classid) {
if (!isset($this->responseclasses[$classid])) {
debugging('Unexpected class id ' . $classid . ' encountered.');
$this->responseclasses[$classid] = new analysis_for_class('[Unknown]', $classid);
}
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->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, $try = 0) {
$responseanalysisforclass = $this->get_response_class($subpart->responseclassid);
$responseanalysisforclass->count_response($subpart->response, $subpart->fraction, $try);
}
/**
* 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.
* @param int|null $calculationtime time when the analysis was done. (Defaults to time()).
*/
public function cache($qubaids, $whichtries, $questionid, $variantno, $subpartid, $calculationtime = null) {
foreach ($this->get_response_class_ids() as $responseclassid) {
$analysisforclass = $this->get_response_class($responseclassid);
$analysisforclass->cache($qubaids, $whichtries, $questionid, $variantno, $subpartid, $calculationtime);
}
}
/**
* 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->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;
}
}