Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]
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 * Tests for the quiz overview report. 19 * 20 * @package quiz_overview 21 * @copyright 2014 The Open University 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 defined('MOODLE_INTERNAL') || die(); 26 27 global $CFG; 28 require_once($CFG->dirroot . '/mod/quiz/locallib.php'); 29 require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php'); 30 require_once($CFG->dirroot . '/mod/quiz/report/default.php'); 31 require_once($CFG->dirroot . '/mod/quiz/report/overview/report.php'); 32 require_once($CFG->dirroot . '/mod/quiz/report/overview/tests/helpers.php'); 33 34 35 /** 36 * Tests for the quiz overview report. 37 * 38 * @copyright 2014 The Open University 39 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 40 */ 41 class quiz_overview_report_testcase extends advanced_testcase { 42 43 /** 44 * Data provider for test_report_sql. 45 * 46 * @return array the data for the test sub-cases. 47 */ 48 public function report_sql_cases() { 49 return [[null], ['csv']]; // Only need to test on or off, not all download types. 50 } 51 52 /** 53 * Test how the report queries the database. 54 * 55 * @param bool $isdownloading a download type, or null. 56 * @dataProvider report_sql_cases 57 */ 58 public function test_report_sql($isdownloading) { 59 global $DB; 60 $this->resetAfterTest(true); 61 62 // Create a course and a quiz. 63 $generator = $this->getDataGenerator(); 64 $course = $generator->create_course(); 65 $quizgenerator = $generator->get_plugin_generator('mod_quiz'); 66 $quiz = $quizgenerator->create_instance(array('course' => $course->id, 67 'grademethod' => QUIZ_GRADEHIGHEST, 'grade' => 100.0, 'sumgrades' => 10.0, 68 'attempts' => 10)); 69 70 // Add one question. 71 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 72 $cat = $questiongenerator->create_question_category(); 73 $q = $questiongenerator->create_question('essay', 'plain', ['category' => $cat->id]); 74 quiz_add_quiz_question($q->id, $quiz, 0 , 10); 75 76 // Create some students and enrol them in the course. 77 $student1 = $generator->create_user(); 78 $student2 = $generator->create_user(); 79 $student3 = $generator->create_user(); 80 $generator->enrol_user($student1->id, $course->id); 81 $generator->enrol_user($student2->id, $course->id); 82 $generator->enrol_user($student3->id, $course->id); 83 // This line is not really necessary for the test asserts below, 84 // but what it does is add an extra user row returned by 85 // get_enrolled_with_capabilities_join because of a second enrolment. 86 // The extra row returned used to make $table->query_db complain 87 // about duplicate records. So this is really a test that an extra 88 // student enrolment does not cause duplicate records in this query. 89 $generator->enrol_user($student2->id, $course->id, null, 'self'); 90 91 // Also create a user who should not appear in the reports, 92 // because they have a role with neither 'mod/quiz:attempt' 93 // nor 'mod/quiz:reviewmyattempts'. 94 $tutor = $generator->create_user(); 95 $generator->enrol_user($tutor->id, $course->id, 'teacher'); 96 97 // The test data. 98 $timestamp = 1234567890; 99 $attempts = array( 100 array($quiz, $student1, 1, 0.0, quiz_attempt::FINISHED), 101 array($quiz, $student1, 2, 5.0, quiz_attempt::FINISHED), 102 array($quiz, $student1, 3, 8.0, quiz_attempt::FINISHED), 103 array($quiz, $student1, 4, null, quiz_attempt::ABANDONED), 104 array($quiz, $student1, 5, null, quiz_attempt::IN_PROGRESS), 105 array($quiz, $student2, 1, null, quiz_attempt::ABANDONED), 106 array($quiz, $student2, 2, null, quiz_attempt::ABANDONED), 107 array($quiz, $student2, 3, 7.0, quiz_attempt::FINISHED), 108 array($quiz, $student2, 4, null, quiz_attempt::ABANDONED), 109 array($quiz, $student2, 5, null, quiz_attempt::ABANDONED), 110 ); 111 112 // Load it in to quiz attempts table. 113 foreach ($attempts as $attemptdata) { 114 list($quiz, $student, $attemptnumber, $sumgrades, $state) = $attemptdata; 115 $timestart = $timestamp + $attemptnumber * 3600; 116 117 $quizobj = quiz::create($quiz->id, $student->id); 118 $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 119 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 120 121 // Create the new attempt and initialize the question sessions. 122 $attempt = quiz_create_attempt($quizobj, $attemptnumber, null, $timestart, false, $student->id); 123 124 $attempt = quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $timestamp); 125 $attempt = quiz_attempt_save_started($quizobj, $quba, $attempt); 126 127 // Process some responses from the student. 128 $attemptobj = quiz_attempt::create($attempt->id); 129 switch ($state) { 130 case quiz_attempt::ABANDONED: 131 $attemptobj->process_abandon($timestart + 300, false); 132 break; 133 134 case quiz_attempt::IN_PROGRESS: 135 // Do nothing. 136 break; 137 138 case quiz_attempt::FINISHED: 139 // Save answer and finish attempt. 140 $attemptobj->process_submitted_actions($timestart + 300, false, [ 141 1 => ['answer' => 'My essay by ' . $student->firstname, 'answerformat' => FORMAT_PLAIN]]); 142 $attemptobj->process_finish($timestart + 600, false); 143 144 // Manually grade it. 145 $quba = $attemptobj->get_question_usage(); 146 $quba->get_question_attempt(1)->manual_grade( 147 'Comment', $sumgrades, FORMAT_HTML, $timestart + 1200); 148 question_engine::save_questions_usage_by_activity($quba); 149 $update = new stdClass(); 150 $update->id = $attemptobj->get_attemptid(); 151 $update->timemodified = $timestart + 1200; 152 $update->sumgrades = $quba->get_total_mark(); 153 $DB->update_record('quiz_attempts', $update); 154 quiz_save_best_grade($attemptobj->get_quiz(), $student->id); 155 break; 156 } 157 } 158 159 // Actually getting the SQL to run is quite hard. Do a minimal set up of 160 // some objects. 161 $context = context_module::instance($quiz->cmid); 162 $cm = get_coursemodule_from_id('quiz', $quiz->cmid); 163 $qmsubselect = quiz_report_qm_filter_select($quiz); 164 $studentsjoins = get_enrolled_with_capabilities_join($context, '', 165 array('mod/quiz:attempt', 'mod/quiz:reviewmyattempts')); 166 $empty = new \core\dml\sql_join(); 167 168 // Set the options. 169 $reportoptions = new quiz_overview_options('overview', $quiz, $cm, null); 170 $reportoptions->attempts = quiz_attempts_report::ENROLLED_ALL; 171 $reportoptions->onlygraded = true; 172 $reportoptions->states = array(quiz_attempt::IN_PROGRESS, quiz_attempt::OVERDUE, quiz_attempt::FINISHED); 173 174 // Now do a minimal set-up of the table class. 175 $q->slot = 1; 176 $q->maxmark = 10; 177 $table = new quiz_overview_table($quiz, $context, $qmsubselect, $reportoptions, 178 $empty, $studentsjoins, array(1 => $q), null); 179 $table->download = $isdownloading; // Cannot call the is_downloading API, because it gives errors. 180 $table->define_columns(array('fullname')); 181 $table->sortable(true, 'uniqueid'); 182 $table->define_baseurl(new moodle_url('/mod/quiz/report.php')); 183 $table->setup(); 184 185 // Run the query. 186 $table->setup_sql_queries($studentsjoins); 187 $table->query_db(30, false); 188 189 // Should be 4 rows, matching count($table->rawdata) tested below. 190 // The count is only done if not downloading. 191 if (!$isdownloading) { 192 $this->assertEquals(4, $table->totalrows); 193 } 194 195 // Verify what was returned: Student 1's best and in progress attempts. 196 // Student 2's finshed attempt, and Student 3 with no attempt. 197 // The array key is {student id}#{attempt number}. 198 $this->assertEquals(4, count($table->rawdata)); 199 $this->assertArrayHasKey($student1->id . '#3', $table->rawdata); 200 $this->assertEquals(1, $table->rawdata[$student1->id . '#3']->gradedattempt); 201 $this->assertArrayHasKey($student1->id . '#3', $table->rawdata); 202 $this->assertEquals(0, $table->rawdata[$student1->id . '#5']->gradedattempt); 203 $this->assertArrayHasKey($student2->id . '#3', $table->rawdata); 204 $this->assertEquals(1, $table->rawdata[$student2->id . '#3']->gradedattempt); 205 $this->assertArrayHasKey($student3->id . '#0', $table->rawdata); 206 $this->assertEquals(0, $table->rawdata[$student3->id . '#0']->gradedattempt); 207 208 // Check the calculation of averages. 209 $averagerow = $table->compute_average_row('overallaverage', $studentsjoins); 210 $this->assertStringContainsString('75.00', $averagerow['sumgrades']); 211 $this->assertStringContainsString('75.00', $averagerow['qsgrade1']); 212 if (!$isdownloading) { 213 $this->assertStringContainsString('(2)', $averagerow['sumgrades']); 214 $this->assertStringContainsString('(2)', $averagerow['qsgrade1']); 215 } 216 217 // Ensure that filtering by initial does not break it. 218 // This involves setting a private properly of the base class, which is 219 // only really possible using reflection :-(. 220 $reflectionobject = new ReflectionObject($table); 221 while ($parent = $reflectionobject->getParentClass()) { 222 $reflectionobject = $parent; 223 } 224 $prefsproperty = $reflectionobject->getProperty('prefs'); 225 $prefsproperty->setAccessible(true); 226 $prefs = $prefsproperty->getValue($table); 227 $prefs['i_first'] = 'A'; 228 $prefsproperty->setValue($table, $prefs); 229 230 list($fields, $from, $where, $params) = $table->base_sql($studentsjoins); 231 $table->set_count_sql("SELECT COUNT(1) FROM (SELECT $fields FROM $from WHERE $where) temp WHERE 1 = 1", $params); 232 $table->set_sql($fields, $from, $where, $params); 233 $table->query_db(30, false); 234 // Just verify that this does not cause a fatal error. 235 } 236 237 /** 238 * Bands provider. 239 * @return array 240 */ 241 public function get_bands_count_and_width_provider() { 242 return [ 243 [10, [20, .5]], 244 [20, [20, 1]], 245 [30, [15, 2]], 246 // TODO MDL-55068 Handle bands better when grade is 50. 247 // [50, [10, 5]], 248 [100, [20, 5]], 249 [200, [20, 10]], 250 ]; 251 } 252 253 /** 254 * Test bands. 255 * 256 * @dataProvider get_bands_count_and_width_provider 257 * @param int $grade grade 258 * @param array $expected 259 */ 260 public function test_get_bands_count_and_width($grade, $expected) { 261 $this->resetAfterTest(true); 262 $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz'); 263 $quiz = $quizgenerator->create_instance(['course' => SITEID, 'grade' => $grade]); 264 $this->assertEquals($expected, quiz_overview_report::get_bands_count_and_width($quiz)); 265 } 266 267 /** 268 * Test delete_selected_attempts function. 269 */ 270 public function test_delete_selected_attempts() { 271 $this->resetAfterTest(true); 272 273 $timestamp = 1234567890; 274 $timestart = $timestamp + 3600; 275 276 // Create a course and a quiz. 277 $generator = $this->getDataGenerator(); 278 $course = $generator->create_course(); 279 $quizgenerator = $generator->get_plugin_generator('mod_quiz'); 280 $quiz = $quizgenerator->create_instance([ 281 'course' => $course->id, 282 'grademethod' => QUIZ_GRADEHIGHEST, 283 'grade' => 100.0, 284 'sumgrades' => 10.0, 285 'attempts' => 10 286 ]); 287 288 // Add one question. 289 $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); 290 $cat = $questiongenerator->create_question_category(); 291 $q = $questiongenerator->create_question('essay', 'plain', ['category' => $cat->id]); 292 quiz_add_quiz_question($q->id, $quiz, 0 , 10); 293 294 // Create student and enrol them in the course. 295 // Note: we create two enrolments, to test the problem reported in MDL-67942. 296 $student = $generator->create_user(); 297 $generator->enrol_user($student->id, $course->id); 298 $generator->enrol_user($student->id, $course->id, null, 'self'); 299 300 $context = context_module::instance($quiz->cmid); 301 $cm = get_coursemodule_from_id('quiz', $quiz->cmid); 302 $allowedjoins = get_enrolled_with_capabilities_join($context, '', ['mod/quiz:attempt', 'mod/quiz:reviewmyattempts']); 303 $quizattemptsreport = new testable_quiz_attempts_report(); 304 305 // Create the new attempt and initialize the question sessions. 306 $quizobj = quiz::create($quiz->id, $student->id); 307 $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context()); 308 $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour); 309 $attempt = quiz_create_attempt($quizobj, 1, null, $timestart, false, $student->id); 310 $attempt = quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timestamp); 311 $attempt = quiz_attempt_save_started($quizobj, $quba, $attempt); 312 313 // Delete the student's attempt. 314 $quizattemptsreport->delete_selected_attempts($quiz, $cm, [$attempt->id], $allowedjoins); 315 } 316 317 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body