Differences Between: [Versions 400 and 403] [Versions 401 and 403] [Versions 402 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 namespace qbank_statistics; 18 19 use core_question\statistics\questions\all_calculated_for_qubaid_condition; 20 use core_component; 21 22 /** 23 * Helper for statistics 24 * 25 * @package qbank_statistics 26 * @copyright 2021 Catalyst IT Australia Pty Ltd 27 * @author Nathan Nguyen <nathannguyen@catalyst-au.net> 28 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 29 */ 30 class helper { 31 32 /** 33 * @var float Threshold to determine 'Needs checking?' 34 */ 35 private const NEED_FOR_REVISION_LOWER_THRESHOLD = 30; 36 37 /** 38 * @var float Threshold to determine 'Needs checking?' 39 */ 40 private const NEED_FOR_REVISION_UPPER_THRESHOLD = 50; 41 42 /** 43 * For a list of questions find all the places, defined by (component, contextid) where there are attempts. 44 * 45 * @param int[] $questionids array of question ids that we are interested in. 46 * @return \stdClass[] list of objects with fields ->component and ->contextid. 47 * @deprecated since Moodle 4.3 please use the method from statistics_bulk_loader. 48 * @todo MDL-78090 Final deprecation in Moodle 4.7 49 */ 50 private static function get_all_places_where_questions_were_attempted(array $questionids): array { 51 global $DB; 52 53 [$questionidcondition, $params] = $DB->get_in_or_equal($questionids); 54 // The MIN(qu.id) is just to ensure that the rows have a unique key. 55 $places = $DB->get_records_sql(" 56 SELECT MIN(qu.id) AS somethingunique, qu.component, qu.contextid, " . 57 \context_helper::get_preload_record_columns_sql('ctx') . " 58 FROM {question_usages} qu 59 JOIN {question_attempts} qa ON qa.questionusageid = qu.id 60 JOIN {context} ctx ON ctx.id = qu.contextid 61 WHERE qa.questionid $questionidcondition 62 GROUP BY qu.component, qu.contextid, " . 63 implode(', ', array_keys(\context_helper::get_preload_record_columns('ctx'))) . " 64 ORDER BY qu.contextid ASC 65 ", $params); 66 67 // Strip out the unwanted ids. 68 $places = array_values($places); 69 foreach ($places as $place) { 70 unset($place->somethingunique); 71 \context_helper::preload_from_record($place); 72 } 73 74 return $places; 75 } 76 77 /** 78 * Load the question statistics for all the attempts belonging to a particular component in a particular context. 79 * 80 * @param string $component frankenstyle component name, e.g. 'mod_quiz'. 81 * @param \context $context the context to load the statistics for. 82 * @return all_calculated_for_qubaid_condition|null question statistics. 83 * @deprecated since Moodle 4.3 please use the method from statistics_bulk_loader. 84 * @todo MDL-78090 Final deprecation in Moodle 4.7 85 */ 86 private static function load_statistics_for_place(string $component, \context $context): ?all_calculated_for_qubaid_condition { 87 // This check is basically if (component_exists). 88 if (empty(core_component::get_component_directory($component))) { 89 return null; 90 } 91 92 if (!component_callback_exists($component, 'calculate_question_stats')) { 93 return null; 94 } 95 96 return component_callback($component, 'calculate_question_stats', [$context]); 97 } 98 99 /** 100 * Extract the value for one question and one type of statistic from a set of statistics. 101 * 102 * @param all_calculated_for_qubaid_condition $statistics the batch of statistics. 103 * @param int $questionid a question id. 104 * @param string $item one of the field names in all_calculated_for_qubaid_condition, e.g. 'facility'. 105 * @return float|null the required value. 106 * @deprecated since Moodle 4.3 please use the method from statistics_bulk_loader. 107 * @todo MDL-78090 Final deprecation in Moodle 4.7 108 */ 109 private static function extract_item_value(all_calculated_for_qubaid_condition $statistics, 110 int $questionid, string $item): ?float { 111 112 // Look in main questions. 113 foreach ($statistics->questionstats as $stats) { 114 if ($stats->questionid == $questionid && isset($stats->$item)) { 115 return $stats->$item; 116 } 117 } 118 119 // If not found, look in sub questions. 120 foreach ($statistics->subquestionstats as $stats) { 121 if ($stats->questionid == $questionid && isset($stats->$item)) { 122 return $stats->$item; 123 } 124 } 125 126 return null; 127 } 128 129 /** 130 * Calculate average for a stats item on a list of questions. 131 * 132 * @param int[] $questionids list of ids of the questions we are interested in. 133 * @param string $item one of the field names in all_calculated_for_qubaid_condition, e.g. 'facility'. 134 * @return array array keys are question ids and the corresponding values are the average values. 135 * Only questions for which there are data are included. 136 * @deprecated since Moodle 4.3 please use the method from statistics_bulk_loader. 137 * @todo MDL-78090 Final deprecation in Moodle 4.7 138 */ 139 private static function calculate_average_question_stats_item(array $questionids, string $item): array { 140 $places = self::get_all_places_where_questions_were_attempted($questionids); 141 142 $counts = []; 143 $sums = []; 144 145 foreach ($places as $place) { 146 $statistics = self::load_statistics_for_place($place->component, 147 \context::instance_by_id($place->contextid)); 148 if ($statistics === null) { 149 continue; 150 } 151 152 foreach ($questionids as $questionid) { 153 $value = self::extract_item_value($statistics, $questionid, $item); 154 if ($value === null) { 155 continue; 156 } 157 158 $counts[$questionid] = ($counts[$questionid] ?? 0) + 1; 159 $sums[$questionid] = ($sums[$questionid] ?? 0) + $value; 160 } 161 } 162 163 // Return null if there is no quizzes. 164 $averages = []; 165 foreach ($sums as $questionid => $sum) { 166 $averages[$questionid] = $sum / $counts[$questionid]; 167 } 168 return $averages; 169 } 170 171 /** 172 * Calculate average facility index 173 * 174 * @param int $questionid 175 * @return float|null 176 * @deprecated since Moodle 4.3 please use the method from statistics_bulk_loader. 177 * @todo MDL-78090 Final deprecation in Moodle 4.7 178 */ 179 public static function calculate_average_question_facility(int $questionid): ?float { 180 debugging('Deprecated: please use statistics_bulk_loader instead, ' . 181 'or get_required_statistics_fields in your question bank column class.', DEBUG_DEVELOPER); 182 $averages = self::calculate_average_question_stats_item([$questionid], 'facility'); 183 return $averages[$questionid] ?? null; 184 } 185 186 /** 187 * Calculate average discriminative efficiency 188 * 189 * @param int $questionid question id 190 * @return float|null 191 * @deprecated since Moodle 4.3 please use the method from statistics_bulk_loader. 192 * @todo MDL-78090 Final deprecation in Moodle 4.7 193 */ 194 public static function calculate_average_question_discriminative_efficiency(int $questionid): ?float { 195 debugging('Deprecated: please use statistics_bulk_loader instead, ' . 196 'or get_required_statistics_fields in your question bank column class.', DEBUG_DEVELOPER); 197 $averages = self::calculate_average_question_stats_item([$questionid], 'discriminativeefficiency'); 198 return $averages[$questionid] ?? null; 199 } 200 201 /** 202 * Calculate average discriminative efficiency 203 * 204 * @param int $questionid question id 205 * @return float|null 206 * @deprecated since Moodle 4.3 please use the method from statistics_bulk_loader. 207 * @todo MDL-78090 Final deprecation in Moodle 4.7 208 */ 209 public static function calculate_average_question_discrimination_index(int $questionid): ?float { 210 debugging('Deprecated: please use statistics_bulk_loader instead, ' . 211 'or get_required_statistics_fields in your question bank column class.', DEBUG_DEVELOPER); 212 $averages = self::calculate_average_question_stats_item([$questionid], 'discriminationindex'); 213 return $averages[$questionid] ?? null; 214 } 215 216 /** 217 * Format a number to a localised percentage with specified decimal points. 218 * 219 * @param float|null $number The number being formatted 220 * @param bool $fraction An indicator for whether the number is a fraction or is already multiplied by 100 221 * @param int $decimals Sets the number of decimal points 222 * @return string 223 * @throws \coding_exception 224 */ 225 public static function format_percentage(?float $number, bool $fraction = true, int $decimals = 2): string { 226 if (is_null($number)) { 227 return get_string('na', 'qbank_statistics'); 228 } 229 $coefficient = $fraction ? 100 : 1; 230 return get_string('percents', 'moodle', format_float($number * $coefficient, $decimals)); 231 } 232 233 /** 234 * Format discrimination index (Needs checking?). 235 * 236 * @param float|null $value stats value 237 * @return array 238 */ 239 public static function format_discrimination_index(?float $value): array { 240 if (is_null($value)) { 241 $content = get_string('emptyvalue', 'qbank_statistics'); 242 $classes = ''; 243 } else if ($value < self::NEED_FOR_REVISION_LOWER_THRESHOLD) { 244 $content = get_string('verylikely', 'qbank_statistics'); 245 $classes = 'alert-danger'; 246 } else if ($value < self::NEED_FOR_REVISION_UPPER_THRESHOLD) { 247 $content = get_string('likely', 'qbank_statistics'); 248 $classes = 'alert-warning'; 249 } else { 250 $content = get_string('unlikely', 'qbank_statistics'); 251 $classes = 'alert-success'; 252 } 253 return [$content, $classes]; 254 } 255 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body