2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * Unit tests for the parts of {@link question_engine_data_mapper} related to reporting.
20 * @package core_question
22 * @copyright 2013 The Open University
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 defined('MOODLE_INTERNAL') || die();
30 require_once(__DIR__ . '/../lib.php');
31 require_once(__DIR__ . '/helpers.php');
35 * Unit tests for the parts of {@link question_engine_data_mapper} related to reporting.
37 * @copyright 2013 The Open University
38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40 class question_engine_data_mapper_reporting_testcase extends qbehaviour_walkthrough_test_base {
42 /** @var question_engine_data_mapper */
45 /** @var qtype_shortanswer_question */
48 /** @var qtype_essay_question */
52 protected $usageids = array();
54 /** @var qubaid_condition */
55 protected $bothusages;
58 protected $allslots = array();
61 * Test the various methods that load data for reporting.
63 * Since these methods need an expensive set-up, and then only do read-only
64 * operations on the data, we use a single method to do the set-up, which
65 * calls diffents methods to test each query.
67 public function test_reporting_queries() {
68 // We create two usages, each with two questions, a short-answer marked
69 // out of 5, and and essay marked out of 10.
71 // In the first usage, the student answers the short-answer
72 // question correctly, and enters something in the essay.
74 // In the second useage, the student answers the short-answer question
75 // wrongly, and leaves the essay blank.
76 $this->resetAfterTest();
77 $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
78 $cat = $generator->create_question_category();
79 $this->sa = $generator->create_question('shortanswer', null,
80 array('category' => $cat->id));
81 $this->essay = $generator->create_question('essay', null,
82 array('category' => $cat->id));
84 $this->usageids = array();
86 // Create the first usage.
87 $q = question_bank::load_question($this->sa->id);
88 $this->start_attempt_at_question($q, 'interactive', 5);
89 $this->allslots[] = $this->slot;
90 $this->process_submission(array('answer' => 'cat'));
91 $this->process_submission(array('answer' => 'frog', '-submit' => 1));
93 $q = question_bank::load_question($this->essay->id);
94 $this->start_attempt_at_question($q, 'interactive', 10);
95 $this->allslots[] = $this->slot;
96 $this->process_submission(array('answer' => '<p>The cat sat on the mat.</p>', 'answerformat' => FORMAT_HTML));
100 $this->usageids[] = $this->quba->get_id();
102 // Create the second usage.
103 $this->quba = question_engine::make_questions_usage_by_activity('unit_test',
104 context_system::instance());
106 $q = question_bank::load_question($this->sa->id);
107 $this->start_attempt_at_question($q, 'interactive', 5);
108 $this->process_submission(array('answer' => 'fish'));
110 $q = question_bank::load_question($this->essay->id);
111 $this->start_attempt_at_question($q, 'interactive', 10);
115 $this->usageids[] = $this->quba->get_id();
117 // Set up some things the tests will need.
118 $this->dm = new question_engine_data_mapper();
119 $this->bothusages = new qubaid_list($this->usageids);
121 // Now test the various queries.
122 $this->dotest_load_questions_usages_latest_steps();
123 $this->dotest_load_questions_usages_question_state_summary();
124 $this->dotest_load_questions_usages_where_question_in_state();
125 $this->dotest_load_average_marks();
126 $this->dotest_sum_usage_marks_subquery();
127 $this->dotest_question_attempt_latest_state_view();
131 * This test is executed by {@link test_reporting_queries()}.
133 protected function dotest_load_questions_usages_latest_steps() {
134 $rawstates = $this->dm->load_questions_usages_latest_steps($this->bothusages, $this->allslots,
135 'qa.id AS questionattemptid, qa.questionusageid, qa.slot, ' .
136 'qa.questionid, qa.maxmark, qas.sequencenumber, qas.state');
139 foreach ($rawstates as $state) {
140 $states[$state->questionusageid][$state->slot] = $state;
141 unset($state->questionattemptid);
142 unset($state->questionusageid);
146 $state = $states[$this->usageids[0]][$this->allslots[0]];
147 $this->assertEquals((object) array(
148 'questionid' => $this->sa->id,
150 'sequencenumber' => 2,
151 'state' => (string) question_state::$gradedright,
154 $state = $states[$this->usageids[0]][$this->allslots[1]];
155 $this->assertEquals((object) array(
156 'questionid' => $this->essay->id,
158 'sequencenumber' => 2,
159 'state' => (string) question_state::$needsgrading,
162 $state = $states[$this->usageids[1]][$this->allslots[0]];
163 $this->assertEquals((object) array(
164 'questionid' => $this->sa->id,
166 'sequencenumber' => 2,
167 'state' => (string) question_state::$gradedwrong,
170 $state = $states[$this->usageids[1]][$this->allslots[1]];
171 $this->assertEquals((object) array(
172 'questionid' => $this->essay->id,
174 'sequencenumber' => 1,
175 'state' => (string) question_state::$gaveup,
180 * This test is executed by {@link test_reporting_queries()}.
182 protected function dotest_load_questions_usages_question_state_summary() {
183 $summary = $this->dm->load_questions_usages_question_state_summary(
184 $this->bothusages, $this->allslots);
186 $this->assertEquals($summary[$this->allslots[0] . ',' . $this->sa->id],
188 'slot' => $this->allslots[0],
189 'questionid' => $this->sa->id,
190 'name' => $this->sa->name,
194 'manuallygraded' => 0,
197 $this->assertEquals($summary[$this->allslots[1] . ',' . $this->essay->id],
199 'slot' => $this->allslots[1],
200 'questionid' => $this->essay->id,
201 'name' => $this->essay->name,
205 'manuallygraded' => 0,
211 * This test is executed by {@link test_reporting_queries()}.
213 protected function dotest_load_questions_usages_where_question_in_state() {
215 array(array($this->usageids[0], $this->usageids[1]), 2),
216 $this->dm->load_questions_usages_where_question_in_state($this->bothusages,
217 'all', $this->allslots[1], null, 'questionusageid'));
220 array(array($this->usageids[0], $this->usageids[1]), 2),
221 $this->dm->load_questions_usages_where_question_in_state($this->bothusages,
222 'autograded', $this->allslots[0], null, 'questionusageid'));
225 array(array($this->usageids[0]), 1),
226 $this->dm->load_questions_usages_where_question_in_state($this->bothusages,
227 'needsgrading', $this->allslots[1], null, 'questionusageid'));
231 * This test is executed by {@link test_reporting_queries()}.
233 protected function dotest_load_average_marks() {
234 $averages = $this->dm->load_average_marks($this->bothusages);
236 $this->assertEquals(array(
237 $this->allslots[0] => (object) array(
238 'slot' => $this->allslots[0],
239 'averagefraction' => 0.5,
242 $this->allslots[1] => (object) array(
243 'slot' => $this->allslots[1],
244 'averagefraction' => 0,
251 * This test is executed by {@link test_reporting_queries()}.
253 protected function dotest_sum_usage_marks_subquery() {
256 $totals = $DB->get_records_sql_menu("SELECT qu.id, ({$this->dm->sum_usage_marks_subquery('qu.id')}) AS totalmark
257 FROM {question_usages} qu
258 WHERE qu.id IN ({$this->usageids[0]}, {$this->usageids[1]})");
260 $this->assertNull($totals[$this->usageids[0]]); // Since a question requires grading.
262 $this->assertNotNull($totals[$this->usageids[1]]); // Grrr! PHP null == 0 makes this hard.
263 $this->assertEquals(0, $totals[$this->usageids[1]]);
267 * This test is executed by {@link test_reporting_queries()}.
269 protected function dotest_question_attempt_latest_state_view() {
272 list($inlineview, $viewparams) = $this->dm->question_attempt_latest_state_view(
273 'lateststate', $this->bothusages);
275 $rawstates = $DB->get_records_sql("
276 SELECT lateststate.questionattemptid,
277 qu.id AS questionusageid,
279 lateststate.questionid,
281 lateststate.sequencenumber,
283 FROM {question_usages} qu
284 LEFT JOIN $inlineview ON lateststate.questionusageid = qu.id
285 WHERE qu.id IN ({$this->usageids[0]}, {$this->usageids[1]})", $viewparams);
288 foreach ($rawstates as $state) {
289 $states[$state->questionusageid][$state->slot] = $state;
290 unset($state->questionattemptid);
291 unset($state->questionusageid);
295 $state = $states[$this->usageids[0]][$this->allslots[0]];
296 $this->assertEquals((object) array(
297 'questionid' => $this->sa->id,
299 'sequencenumber' => 2,
300 'state' => (string) question_state::$gradedright,
303 $state = $states[$this->usageids[0]][$this->allslots[1]];
304 $this->assertEquals((object) array(
305 'questionid' => $this->essay->id,
307 'sequencenumber' => 2,
308 'state' => (string) question_state::$needsgrading,
311 $state = $states[$this->usageids[1]][$this->allslots[0]];
312 $this->assertEquals((object) array(
313 'questionid' => $this->sa->id,
315 'sequencenumber' => 2,
316 'state' => (string) question_state::$gradedwrong,
319 $state = $states[$this->usageids[1]][$this->allslots[1]];
320 $this->assertEquals((object) array(
321 'questionid' => $this->essay->id,
323 'sequencenumber' => 1,
324 'state' => (string) question_state::$gaveup,