See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 401 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * This file contains the code to analyse all the responses to a particular question. 19 * 20 * @package core_question 21 * @copyright 2014 Open University 22 * @author Jamie Pratt <me@jamiep.org> 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 namespace core_question\statistics\responses; 27 defined('MOODLE_INTERNAL') || die(); 28 29 /** 30 * This class can compute, store and cache the analysis of the responses to a particular question. 31 * 32 * @package core_question 33 * @copyright 2014 The Open University 34 * @author James Pratt me@jamiep.org 35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 36 */ 37 class analyser { 38 /** 39 * @var int When analysing responses and breaking down the count of responses per try, how many columns should we break down 40 * tries into? This is set to 5 columns, any response in a try more than try 5 will be counted in the fifth column. 41 */ 42 const MAX_TRY_COUNTED = 5; 43 44 /** @var int No longer used. Previously the time after which statistics are automatically recomputed. */ 45 const TIME_TO_CACHE = 900; // 15 minutes. 46 47 /** @var object full question data from db. */ 48 protected $questiondata; 49 50 /** 51 * @var analysis_for_question|analysis_for_question_all_tries 52 */ 53 public $analysis; 54 55 /** 56 * @var int used during calculations, so all results are stored with the same timestamp. 57 */ 58 protected $calculationtime; 59 60 /** 61 * @var array Two index array first index is unique string for each sub question part, the second string index is the 'class' 62 * that sub-question part can be classified into. 63 * 64 * This is the return value from {@link \question_type::get_possible_responses()} see that method for fuller documentation. 65 */ 66 public $responseclasses = array(); 67 68 /** 69 * @var bool whether to break down response analysis by variant. This only applies to questions that have variants and is 70 * used to suppress the break down of analysis by variant when there are going to be very many variants. 71 */ 72 protected $breakdownbyvariant; 73 74 /** 75 * Create a new instance of this class for holding/computing the statistics 76 * for a particular question. 77 * 78 * @param object $questiondata the full question data from the database defining this question. 79 * @param string $whichtries which tries to analyse. 80 */ 81 public function __construct($questiondata, $whichtries = \question_attempt::LAST_TRY) { 82 $this->questiondata = $questiondata; 83 $qtypeobj = \question_bank::get_qtype($this->questiondata->qtype); 84 if ($whichtries != \question_attempt::ALL_TRIES) { 85 $this->analysis = new analysis_for_question($qtypeobj->get_possible_responses($this->questiondata)); 86 } else { 87 $this->analysis = new analysis_for_question_all_tries($qtypeobj->get_possible_responses($this->questiondata)); 88 } 89 90 $this->breakdownbyvariant = $qtypeobj->break_down_stats_and_response_analysis_by_variant($this->questiondata); 91 } 92 93 /** 94 * Does the computed analysis have sub parts? 95 * 96 * @return bool whether this analysis has more than one subpart. 97 */ 98 public function has_subparts() { 99 return count($this->responseclasses) > 1; 100 } 101 102 /** 103 * Does the computed analysis's sub parts have classes? 104 * 105 * @return bool whether this analysis has (a subpart with) more than one response class. 106 */ 107 public function has_response_classes() { 108 foreach ($this->responseclasses as $partclasses) { 109 if (count($partclasses) > 1) { 110 return true; 111 } 112 } 113 return false; 114 } 115 116 /** 117 * Analyse all the response data for all the specified attempts at this question. 118 * 119 * @param \qubaid_condition $qubaids which attempts to consider. 120 * @param string $whichtries which tries to analyse. Will be one of 121 * \question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES. 122 * @return analysis_for_question 123 */ 124 public function calculate($qubaids, $whichtries = \question_attempt::LAST_TRY) { 125 $this->calculationtime = time(); 126 // Load data. 127 $dm = new \question_engine_data_mapper(); 128 $questionattempts = $dm->load_attempts_at_question($this->questiondata->id, $qubaids); 129 130 // Analyse it. 131 foreach ($questionattempts as $qa) { 132 $responseparts = $qa->classify_response($whichtries); 133 if ($this->breakdownbyvariant) { 134 $this->analysis->count_response_parts($qa->get_variant(), $responseparts); 135 } else { 136 $this->analysis->count_response_parts(1, $responseparts); 137 } 138 139 } 140 $this->analysis->cache($qubaids, $whichtries, $this->questiondata->id, $this->calculationtime); 141 return $this->analysis; 142 } 143 144 /** 145 * Retrieve the computed response analysis from the question_response_analysis table. 146 * 147 * @param \qubaid_condition $qubaids load the analysis of which question usages? 148 * @param string $whichtries load the analysis of which tries? 149 * @return analysis_for_question|boolean analysis or false if no cached analysis found. 150 */ 151 public function load_cached($qubaids, $whichtries) { 152 global $DB; 153 154 $timemodified = self::get_last_analysed_time($qubaids, $whichtries); 155 // Variable name 'analyses' is the plural of 'analysis'. 156 $responseanalyses = $DB->get_records('question_response_analysis', 157 ['hashcode' => $qubaids->get_hash_code(), 'whichtries' => $whichtries, 158 'questionid' => $this->questiondata->id, 'timemodified' => $timemodified]); 159 if (!$responseanalyses) { 160 return false; 161 } 162 163 $analysisids = []; 164 foreach ($responseanalyses as $responseanalysis) { 165 $analysisforsubpart = $this->analysis->get_analysis_for_subpart($responseanalysis->variant, $responseanalysis->subqid); 166 $class = $analysisforsubpart->get_response_class($responseanalysis->aid); 167 $class->add_response($responseanalysis->response, $responseanalysis->credit); 168 $analysisids[] = $responseanalysis->id; 169 } 170 [$sql, $params] = $DB->get_in_or_equal($analysisids); 171 $counts = $DB->get_records_select('question_response_count', "analysisid {$sql}", $params); 172 foreach ($counts as $count) { 173 $responseanalysis = $responseanalyses[$count->analysisid]; 174 $analysisforsubpart = $this->analysis->get_analysis_for_subpart($responseanalysis->variant, $responseanalysis->subqid); 175 $class = $analysisforsubpart->get_response_class($responseanalysis->aid); 176 $class->set_response_count($responseanalysis->response, $count->try, $count->rcount); 177 178 } 179 return $this->analysis; 180 } 181 182 183 /** 184 * Find time of non-expired analysis in the database. 185 * 186 * @param \qubaid_condition $qubaids check for the analysis of which question usages? 187 * @param string $whichtries check for the analysis of which tries? 188 * @return integer|boolean Time of cached record that matches this qubaid_condition or false if none found. 189 */ 190 public function get_last_analysed_time($qubaids, $whichtries) { 191 global $DB; 192 return $DB->get_field('question_response_analysis', 'MAX(timemodified)', 193 ['hashcode' => $qubaids->get_hash_code(), 'whichtries' => $whichtries, 194 'questionid' => $this->questiondata->id]); 195 } 196 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body