Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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 mod_quiz\quiz_attempt;
21
use mod_quiz\quiz_settings;
22
use qubaid_list;
23
use question_bank;
24
use question_engine;
25
use question_engine_data_mapper;
26
use question_state;
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
     */
11 efrain 69
    public function test_reporting_queries(): void {
1 efrain 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_settings::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
}