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