See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [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 namespace core_question; 18 19 use quiz_statistics\tests\statistics_helper; 20 use qubaid_list; 21 use question_bank; 22 use question_engine; 23 use question_engine_data_mapper; 24 use question_state; 25 use quiz; 26 use quiz_attempt; 27 28 defined('MOODLE_INTERNAL') || die(); 29 30 global $CFG; 31 require_once (__DIR__ . '/../lib.php'); 32 require_once (__DIR__ . '/helpers.php'); 33 34 /** 35 * Unit tests for the parts of {@link question_engine_data_mapper} related to reporting. 36 * 37 * @package core_question 38 * @category test 39 * @copyright 2013 The Open University 40 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 41 */ 42 class datalib_reporting_queries_test extends \qbehaviour_walkthrough_test_base { 43 44 /** @var question_engine_data_mapper */ 45 protected $dm; 46 47 /** @var qtype_shortanswer_question */ 48 protected $sa; 49 50 /** @var qtype_essay_question */ 51 protected $essay; 52 53 /** @var array */ 54 protected $usageids = array(); 55 56 /** @var qubaid_condition */ 57 protected $bothusages; 58 59 /** @var array */ 60 protected $allslots = array(); 61 62 /** 63 * Test the various methods that load data for reporting. 64 * 65 * Since these methods need an expensive set-up, and then only do read-only 66 * operations on the data, we use a single method to do the set-up, which 67 * calls diffents methods to test each query. 68 */ 69 public function test_reporting_queries() { 70 // We create two usages, each with two questions, a short-answer marked 71 // out of 5, and and essay marked out of 10. 72 // 73 // In the first usage, the student answers the short-answer 74 // question correctly, and enters something in the essay. 75 // 76 // In the second useage, the student answers the short-answer question 77 // wrongly, and leaves the essay blank. 78 $this->resetAfterTest(); 79 $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); 80 $cat = $generator->create_question_category(); 81 $this->sa = $generator->create_question('shortanswer', null, 82 array('category' => $cat->id)); 83 $this->essay = $generator->create_question('essay', null, 84 array('category' => $cat->id)); 85 86 $this->usageids = array(); 87 88 // Create the first usage. 89 $q = question_bank::load_question($this->sa->id); 90 $this->start_attempt_at_question($q, 'interactive', 5); 91 $this->allslots[] = $this->slot; 92 $this->process_submission(array('answer' => 'cat')); 93 $this->process_submission(array('answer' => 'frog', '-submit' => 1)); 94 95 $q = question_bank::load_question($this->essay->id); 96 $this->start_attempt_at_question($q, 'interactive', 10); 97 $this->allslots[] = $this->slot; 98 $this->process_submission(array('answer' => '<p>The cat sat on the mat.</p>', 'answerformat' => FORMAT_HTML)); 99 100 $this->finish(); 101 $this->save_quba(); 102 $this->usageids[] = $this->quba->get_id(); 103 104 // Create the second usage. 105 $this->quba = question_engine::make_questions_usage_by_activity('unit_test', 106 \context_system::instance()); 107 108 $q = question_bank::load_question($this->sa->id); 109 $this->start_attempt_at_question($q, 'interactive', 5); 110 $this->process_submission(array('answer' => 'fish')); 111 112 $q = question_bank::load_question($this->essay->id); 113 $this->start_attempt_at_question($q, 'interactive', 10); 114 115 $this->finish(); 116 $this->save_quba(); 117 $this->usageids[] = $this->quba->get_id(); 118 119 // Set up some things the tests will need. 120 $this->dm = new question_engine_data_mapper(); 121 $this->bothusages = new qubaid_list($this->usageids); 122 123 // Now test the various queries. 124 $this->dotest_load_questions_usages_latest_steps($this->allslots); 125 $this->dotest_load_questions_usages_latest_steps(null); 126 $this->dotest_load_questions_usages_question_state_summary($this->allslots); 127 $this->dotest_load_questions_usages_question_state_summary(null); 128 $this->dotest_load_questions_usages_where_question_in_state(); 129 $this->dotest_load_average_marks($this->allslots); 130 $this->dotest_load_average_marks(null); 131 $this->dotest_sum_usage_marks_subquery(); 132 $this->dotest_question_attempt_latest_state_view(); 133 } 134 135 /** 136 * This test is executed by {@link test_reporting_queries()}. 137 * 138 * @param array|null $slots list of slots to use in the call. 139 */ 140 protected function dotest_load_questions_usages_latest_steps($slots) { 141 $rawstates = $this->dm->load_questions_usages_latest_steps($this->bothusages, $slots, 142 'qa.id AS questionattemptid, qa.questionusageid, qa.slot, ' . 143 'qa.questionid, qa.maxmark, qas.sequencenumber, qas.state'); 144 145 $states = array(); 146 foreach ($rawstates as $state) { 147 $states[$state->questionusageid][$state->slot] = $state; 148 unset($state->questionattemptid); 149 unset($state->questionusageid); 150 unset($state->slot); 151 } 152 153 $state = $states[$this->usageids[0]][$this->allslots[0]]; 154 $this->assertEquals((object) array( 155 'questionid' => $this->sa->id, 156 'maxmark' => 5.0, 157 'sequencenumber' => 2, 158 'state' => (string) question_state::$gradedright, 159 ), $state); 160 161 $state = $states[$this->usageids[0]][$this->allslots[1]]; 162 $this->assertEquals((object) array( 163 'questionid' => $this->essay->id, 164 'maxmark' => 10.0, 165 'sequencenumber' => 2, 166 'state' => (string) question_state::$needsgrading, 167 ), $state); 168 169 $state = $states[$this->usageids[1]][$this->allslots[0]]; 170 $this->assertEquals((object) array( 171 'questionid' => $this->sa->id, 172 'maxmark' => 5.0, 173 'sequencenumber' => 2, 174 'state' => (string) question_state::$gradedwrong, 175 ), $state); 176 177 $state = $states[$this->usageids[1]][$this->allslots[1]]; 178 $this->assertEquals((object) array( 179 'questionid' => $this->essay->id, 180 'maxmark' => 10.0, 181 'sequencenumber' => 1, 182 'state' => (string) question_state::$gaveup, 183 ), $state); 184 } 185 186 /** 187 * This test is executed by {@link test_reporting_queries()}. 188 * 189 * @param array|null $slots list of slots to use in the call. 190 */ 191 protected function dotest_load_questions_usages_question_state_summary($slots) { 192 $summary = $this->dm->load_questions_usages_question_state_summary( 193 $this->bothusages, $slots); 194 195 $this->assertEquals($summary[$this->allslots[0] . ',' . $this->sa->id], 196 (object) array( 197 'slot' => $this->allslots[0], 198 'questionid' => $this->sa->id, 199 'name' => $this->sa->name, 200 'inprogress' => 0, 201 'needsgrading' => 0, 202 'autograded' => 2, 203 'manuallygraded' => 0, 204 'all' => 2, 205 )); 206 $this->assertEquals($summary[$this->allslots[1] . ',' . $this->essay->id], 207 (object) array( 208 'slot' => $this->allslots[1], 209 'questionid' => $this->essay->id, 210 'name' => $this->essay->name, 211 'inprogress' => 0, 212 'needsgrading' => 1, 213 'autograded' => 1, 214 'manuallygraded' => 0, 215 'all' => 2, 216 )); 217 } 218 219 /** 220 * This test is executed by {@link test_reporting_queries()}. 221 */ 222 protected function dotest_load_questions_usages_where_question_in_state() { 223 $this->assertEquals( 224 array(array($this->usageids[0], $this->usageids[1]), 2), 225 $this->dm->load_questions_usages_where_question_in_state($this->bothusages, 226 'all', $this->allslots[1], null, 'questionusageid')); 227 228 $this->assertEquals( 229 array(array($this->usageids[0], $this->usageids[1]), 2), 230 $this->dm->load_questions_usages_where_question_in_state($this->bothusages, 231 'autograded', $this->allslots[0], null, 'questionusageid')); 232 233 $this->assertEquals( 234 array(array($this->usageids[0]), 1), 235 $this->dm->load_questions_usages_where_question_in_state($this->bothusages, 236 'needsgrading', $this->allslots[1], null, 'questionusageid')); 237 } 238 239 /** 240 * This test is executed by {@link test_reporting_queries()}. 241 * 242 * @param array|null $slots list of slots to use in the call. 243 */ 244 protected function dotest_load_average_marks($slots) { 245 $averages = $this->dm->load_average_marks($this->bothusages, $slots); 246 247 $this->assertEquals(array( 248 $this->allslots[0] => (object) array( 249 'slot' => $this->allslots[0], 250 'averagefraction' => 0.5, 251 'numaveraged' => 2, 252 ), 253 $this->allslots[1] => (object) array( 254 'slot' => $this->allslots[1], 255 'averagefraction' => 0, 256 'numaveraged' => 1, 257 ), 258 ), $averages); 259 } 260 261 /** 262 * This test is executed by {@link test_reporting_queries()}. 263 */ 264 protected function dotest_sum_usage_marks_subquery() { 265 global $DB; 266 267 $totals = $DB->get_records_sql_menu("SELECT qu.id, ({$this->dm->sum_usage_marks_subquery('qu.id')}) AS totalmark 268 FROM {question_usages} qu 269 WHERE qu.id IN ({$this->usageids[0]}, {$this->usageids[1]})"); 270 271 $this->assertNull($totals[$this->usageids[0]]); // Since a question requires grading. 272 273 $this->assertNotNull($totals[$this->usageids[1]]); // Grrr! PHP null == 0 makes this hard. 274 $this->assertEquals(0, $totals[$this->usageids[1]]); 275 } 276 277 /** 278 * This test is executed by {@link test_reporting_queries()}. 279 */ 280 protected function dotest_question_attempt_latest_state_view() { 281 global $DB; 282 283 list($inlineview, $viewparams) = $this->dm->question_attempt_latest_state_view( 284 'lateststate', $this->bothusages); 285 286 $rawstates = $DB->get_records_sql(" 287 SELECT lateststate.questionattemptid, 288 qu.id AS questionusageid, 289 lateststate.slot, 290 lateststate.questionid, 291 lateststate.maxmark, 292 lateststate.sequencenumber, 293 lateststate.state 294 FROM {question_usages} qu 295 LEFT JOIN $inlineview ON lateststate.questionusageid = qu.id 296 WHERE qu.id IN ({$this->usageids[0]}, {$this->usageids[1]})", $viewparams); 297 298 $states = array(); 299 foreach ($rawstates as $state) { 300 $states[$state->questionusageid][$state->slot] = $state; 301 unset($state->questionattemptid); 302 unset($state->questionusageid); 303 unset($state->slot); 304 } 305 306 $state = $states[$this->usageids[0]][$this->allslots[0]]; 307 $this->assertEquals((object) array( 308 'questionid' => $this->sa->id, 309 'maxmark' => 5.0, 310 'sequencenumber' => 2, 311 'state' => (string) question_state::$gradedright, 312 ), $state); 313 314 $state = $states[$this->usageids[0]][$this->allslots[1]]; 315 $this->assertEquals((object) array( 316 'questionid' => $this->essay->id, 317 'maxmark' => 10.0, 318 'sequencenumber' => 2, 319 'state' => (string) question_state::$needsgrading, 320 ), $state); 321 322 $state = $states[$this->usageids[1]][$this->allslots[0]]; 323 $this->assertEquals((object) array( 324 'questionid' => $this->sa->id, 325 'maxmark' => 5.0, 326 'sequencenumber' => 2, 327 'state' => (string) question_state::$gradedwrong, 328 ), $state); 329 330 $state = $states[$this->usageids[1]][$this->allslots[1]]; 331 $this->assertEquals((object) array( 332 'questionid' => $this->essay->id, 333 'maxmark' => 10.0, 334 'sequencenumber' => 1, 335 'state' => (string) question_state::$gaveup, 336 ), $state); 337 } 338 339 /** 340 * Test that a Quiz with only description questions wont break \quiz_statistics\task\recalculate. 341 * 342 * @covers \quiz_statistics\task\recalculate::execute 343 */ 344 public function test_quiz_with_description_questions_recalculate_statistics(): void { 345 $this->resetAfterTest(); 346 347 // Create course with quiz module. 348 $course = $this->getDataGenerator()->create_course(); 349 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 350 $layout = '1'; 351 $quiz = $quizgenerator->create_instance([ 352 'course' => $course->id, 353 'grade' => 0.0, 'sumgrades' => 1, 354 'layout' => $layout 355 ]); 356 357 // Add question of type description to quiz. 358 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 359 $cat = $questiongenerator->create_question_category(); 360 $question = $questiongenerator->create_question('description', null, ['category' => $cat->id]); 361 quiz_add_quiz_question($question->id, $quiz); 362 363 // Create attempt. 364 $user = $this->getDataGenerator()->create_user(); 365 $quizobj = quiz::create($quiz->id, $user->id); 366 $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 367 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 368 $timenow = time(); 369 $attempt = quiz_create_attempt($quizobj, 1, null, $timenow, false, $user->id); 370 quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow); 371 quiz_attempt_save_started($quizobj, $quba, $attempt); 372 373 // Submit attempt. 374 $attemptobj = quiz_attempt::create($attempt->id); 375 $attemptobj->process_submitted_actions($timenow, false); 376 $attemptobj->process_finish($timenow, false); 377 378 // Calculate the statistics. 379 $this->expectOutputRegex('~.*Calculations completed.*~'); 380 statistics_helper::run_pending_recalculation_tasks(); 381 } 382 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body