Proyectos de Subversion Moodle

Rev

Rev 11 | | 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 mod_quiz;
18
 
19
use moodle_url;
20
use question_bank;
21
use question_engine;
1441 ariadna 22
use mod_quiz\tests\question_helper_test_trait;
1 efrain 23
 
24
/**
25
 * Quiz attempt walk through.
26
 *
27
 * @package   mod_quiz
28
 * @category  test
29
 * @copyright 2013 The Open University
30
 * @author    Jamie Pratt <me@jamiep.org>
31
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
32
 * @covers \mod_quiz\quiz_attempt
33
 */
1441 ariadna 34
final class attempt_walkthrough_test extends \advanced_testcase {
35
    use question_helper_test_trait;
1 efrain 36
 
1441 ariadna 37
    #[\Override]
38
    public static function setUpBeforeClass(): void {
39
        global $CFG;
1 efrain 40
 
1441 ariadna 41
        parent::setUpBeforeClass();
42
 
43
        require_once($CFG->dirroot . '/mod/quiz/locallib.php');
44
    }
45
 
1 efrain 46
    /**
47
     * Create a quiz with questions and walk through a quiz attempt.
48
     */
11 efrain 49
    public function test_quiz_attempt_walkthrough(): void {
1 efrain 50
        global $SITE;
51
 
52
        $this->resetAfterTest(true);
53
 
54
        // Make a quiz.
55
        $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
56
 
57
        $quiz = $quizgenerator->create_instance(['course' => $SITE->id, 'questionsperpage' => 0, 'grade' => 100.0,
58
                                                      'sumgrades' => 3]);
59
 
60
        // Create a couple of questions.
61
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
62
 
63
        $cat = $questiongenerator->create_question_category();
64
        $saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
65
        $numq = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
66
        $matchq = $questiongenerator->create_question('match', null, ['category' => $cat->id]);
67
        $description = $questiongenerator->create_question('description', null, ['category' => $cat->id]);
68
 
69
        // Add them to the quiz.
70
        quiz_add_quiz_question($saq->id, $quiz);
71
        quiz_add_quiz_question($numq->id, $quiz);
72
        quiz_add_quiz_question($matchq->id, $quiz);
73
        quiz_add_quiz_question($description->id, $quiz);
74
 
75
        // Make a user to do the quiz.
76
        $user1 = $this->getDataGenerator()->create_user();
77
 
78
        $quizobj = quiz_settings::create($quiz->id, $user1->id);
79
 
80
        // Start the attempt.
81
        $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
82
        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
83
 
84
        $timenow = time();
85
        $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $user1->id);
86
 
87
        quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
88
        $this->assertEquals('1,2,3,4,0', $attempt->layout);
89
 
90
        quiz_attempt_save_started($quizobj, $quba, $attempt);
91
 
92
        // Process some responses from the student.
93
        $attemptobj = quiz_attempt::create($attempt->id);
94
        $this->assertFalse($attemptobj->has_response_to_at_least_one_graded_question());
95
        // The student has not answered any questions.
96
        $this->assertEquals(3, $attemptobj->get_number_of_unanswered_questions());
97
 
98
        $tosubmit = [1 => ['answer' => 'frog'],
99
                          2 => ['answer' => '3.14']];
100
 
101
        $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
102
        // The student has answered two questions, and only one remaining.
103
        $this->assertEquals(1, $attemptobj->get_number_of_unanswered_questions());
104
 
105
        $tosubmit = [
106
            3 => [
107
                'frog' => 'amphibian',
108
                'cat' => 'mammal',
1441 ariadna 109
                'newt' => '',
110
            ],
1 efrain 111
        ];
112
 
113
        $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
114
        // The student has answered three questions but one is invalid, so there is still one remaining.
115
        $this->assertEquals(1, $attemptobj->get_number_of_unanswered_questions());
116
 
117
        $tosubmit = [
118
            3 => [
119
                'frog' => 'amphibian',
120
                'cat' => 'mammal',
1441 ariadna 121
                'newt' => 'amphibian',
122
            ],
1 efrain 123
        ];
124
 
125
        $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
126
        // The student has answered three questions, so there are no remaining.
127
        $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
128
 
129
        // Finish the attempt.
130
        $attemptobj = quiz_attempt::create($attempt->id);
131
        $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
1441 ariadna 132
        $attemptobj->process_submit($timenow, false);
1 efrain 133
 
134
        // Re-load quiz attempt data.
135
        $attemptobj = quiz_attempt::create($attempt->id);
136
 
137
        // Check that results are stored as expected.
138
        $this->assertEquals(1, $attemptobj->get_attempt_number());
1441 ariadna 139
        $this->assertEquals(false, $attemptobj->is_finished());
1 efrain 140
        $this->assertEquals($timenow, $attemptobj->get_submitted_date());
141
        $this->assertEquals($user1->id, $attemptobj->get_userid());
142
        $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
143
        $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
1441 ariadna 144
        // Check we don't have grades yet.
145
        $this->assertEmpty(quiz_get_user_grades($quiz, $user1->id));
146
        $this->assertNull($attemptobj->get_sum_marks());
1 efrain 147
 
1441 ariadna 148
        // Now grade the submission.
149
        $attemptobj->process_grade_submission($timenow);
150
        $attemptobj = quiz_attempt::create($attempt->id);
151
 
1 efrain 152
        // Check quiz grades.
1441 ariadna 153
        $this->assertEquals(true, $attemptobj->is_finished());
154
        $this->assertEquals(3, $attemptobj->get_sum_marks());
155
 
1 efrain 156
        $grades = quiz_get_user_grades($quiz, $user1->id);
157
        $grade = array_shift($grades);
158
        $this->assertEquals(100.0, $grade->rawgrade);
159
 
160
        // Check grade book.
161
        $gradebookgrades = grade_get_grades($SITE->id, 'mod', 'quiz', $quiz->id, $user1->id);
162
        $gradebookitem = array_shift($gradebookgrades->items);
163
        $gradebookgrade = array_shift($gradebookitem->grades);
164
        $this->assertEquals(100, $gradebookgrade->grade);
165
 
166
        // Update question in quiz.
1441 ariadna 167
        $newsa = $questiongenerator->update_question(
168
            $saq,
169
            null,
170
            ['name' => 'This is the second version of shortanswer']
171
        );
172
        $newnumbq = $questiongenerator->update_question(
173
            $numq,
174
            null,
175
            ['name' => 'This is the second version of numerical']
176
        );
177
        $newmatch = $questiongenerator->update_question(
178
            $matchq,
179
            null,
180
            ['name' => 'This is the second version of match']
181
        );
182
        $newdescription = $questiongenerator->update_question(
183
            $description,
184
            null,
185
            ['name' => 'This is the second version of description']
186
        );
1 efrain 187
 
188
        // Update the attempt to use this questions.
189
        // Would not normally be done for a non-preview, but this is just a unit test.
190
        $attemptobj->update_questions_to_new_version_if_changed();
191
 
192
        // Verify.
193
        $this->assertEquals($newsa->id, $attemptobj->get_question_attempt(1)->get_question_id());
194
        $this->assertEquals($newnumbq->id, $attemptobj->get_question_attempt(2)->get_question_id());
195
        $this->assertEquals($newmatch->id, $attemptobj->get_question_attempt(3)->get_question_id());
196
        $this->assertEquals($newdescription->id, $attemptobj->get_question_attempt(4)->get_question_id());
197
 
198
        // Repeat the checks from above.
199
        $this->assertEquals(1, $attemptobj->get_attempt_number());
200
        $this->assertEquals(3, $attemptobj->get_sum_marks());
201
        $this->assertEquals(true, $attemptobj->is_finished());
202
        $this->assertEquals($timenow, $attemptobj->get_submitted_date());
203
        $this->assertEquals($user1->id, $attemptobj->get_userid());
204
        $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
205
        $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
206
 
207
        // Re-load quiz attempt data and repeat the verification.
208
        $attemptobj = quiz_attempt::create($attempt->id);
209
 
210
        $this->assertEquals($newsa->id, $attemptobj->get_question_attempt(1)->get_question_id());
211
        $this->assertEquals($newnumbq->id, $attemptobj->get_question_attempt(2)->get_question_id());
212
        $this->assertEquals($newmatch->id, $attemptobj->get_question_attempt(3)->get_question_id());
213
        $this->assertEquals($newdescription->id, $attemptobj->get_question_attempt(4)->get_question_id());
214
 
215
        // Repeat the checks from above.
216
        $this->assertEquals(1, $attemptobj->get_attempt_number());
217
        $this->assertEquals(3, $attemptobj->get_sum_marks());
218
        $this->assertEquals(true, $attemptobj->is_finished());
219
        $this->assertEquals($timenow, $attemptobj->get_submitted_date());
220
        $this->assertEquals($user1->id, $attemptobj->get_userid());
221
        $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
222
        $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
223
    }
224
 
225
    /**
226
     * Create a quiz containing one question and a close time.
227
     *
228
     * The question is the standard shortanswer test question.
229
     * The quiz is set to close 1 hour from now.
230
     * The quiz is set to use a grade period of 1 hour once time expires.
231
     *
232
     * @param string $overduehandling value for the overduehandling quiz setting.
233
     * @return \stdClass the quiz that was created.
234
     */
235
    protected function create_quiz_with_one_question(string $overduehandling = 'graceperiod'): \stdClass {
236
        global $SITE;
237
        $this->resetAfterTest();
238
 
239
        // Make a quiz.
240
        $timeclose = time() + HOURSECS;
241
        $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
242
 
243
        $quiz = $quizgenerator->create_instance(
1441 ariadna 244
            ['course' => $SITE->id, 'timeclose' => $timeclose,
245
            'overduehandling' => $overduehandling,
246
            'graceperiod' => HOURSECS]
247
        );
1 efrain 248
 
249
        // Create a question.
250
        /** @var \core_question_generator $questiongenerator */
251
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
252
        $cat = $questiongenerator->create_question_category();
253
        $saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
254
 
255
        // Add them to the quiz.
256
        $quizobj = quiz_settings::create($quiz->id);
257
        quiz_add_quiz_question($saq->id, $quiz, 0, 1);
258
        $quizobj->get_grade_calculator()->recompute_quiz_sumgrades();
259
 
260
        return $quiz;
261
    }
262
 
11 efrain 263
    public function test_quiz_attempt_walkthrough_submit_time_recorded_correctly_when_overdue(): void {
1 efrain 264
 
265
        $quiz = $this->create_quiz_with_one_question();
266
 
267
        // Make a user to do the quiz.
268
        $user = $this->getDataGenerator()->create_user();
269
        $this->setUser($user);
270
        $quizobj = quiz_settings::create($quiz->id, $user->id);
271
 
272
        // Start the attempt.
273
        $attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null);
274
 
275
        // Process some responses from the student.
276
        $attemptobj = quiz_attempt::create($attempt->id);
277
        $this->assertEquals(1, $attemptobj->get_number_of_unanswered_questions());
278
        $attemptobj->process_submitted_actions($quiz->timeclose - 30 * MINSECS, false, [1 => ['answer' => 'frog']]);
279
 
280
        // Attempt goes overdue (e.g. if cron ran).
281
        $attemptobj = quiz_attempt::create($attempt->id);
282
        $attemptobj->process_going_overdue($quiz->timeclose + 2 * get_config('quiz', 'graceperiodmin'), false);
283
 
284
        // Verify the attempt state.
285
        $attemptobj = quiz_attempt::create($attempt->id);
286
        $this->assertEquals(1, $attemptobj->get_attempt_number());
287
        $this->assertEquals(false, $attemptobj->is_finished());
288
        $this->assertEquals(0, $attemptobj->get_submitted_date());
289
        $this->assertEquals($user->id, $attemptobj->get_userid());
290
        $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
291
        $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
292
 
293
        // Student submits the attempt during the grace period.
294
        $attemptobj = quiz_attempt::create($attempt->id);
295
        $attemptobj->process_attempt($quiz->timeclose + 30 * MINSECS, true, false, 1);
296
 
297
        // Verify the attempt state.
298
        $attemptobj = quiz_attempt::create($attempt->id);
299
        $this->assertEquals(1, $attemptobj->get_attempt_number());
300
        $this->assertEquals(true, $attemptobj->is_finished());
301
        $this->assertEquals($quiz->timeclose + 30 * MINSECS, $attemptobj->get_submitted_date());
302
        $this->assertEquals($user->id, $attemptobj->get_userid());
303
        $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
304
        $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
305
    }
306
 
11 efrain 307
    public function test_quiz_attempt_walkthrough_close_time_extended_at_last_minute(): void {
1 efrain 308
        global $DB;
309
 
310
        $quiz = $this->create_quiz_with_one_question();
311
        $originaltimeclose = $quiz->timeclose;
312
 
313
        // Make a user to do the quiz.
314
        $user = $this->getDataGenerator()->create_user();
315
        $this->setUser($user);
316
        $quizobj = quiz_settings::create($quiz->id, $user->id);
317
 
318
        // Start the attempt.
319
        $attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null);
320
 
321
        // Process some responses from the student during the attempt.
322
        $attemptobj = quiz_attempt::create($attempt->id);
323
        $attemptobj->process_submitted_actions($originaltimeclose - 30 * MINSECS, false, [1 => ['answer' => 'frog']]);
324
 
325
        // Teacher edits the quiz to extend the time-limit by one minute.
326
        $DB->set_field('quiz', 'timeclose', $originaltimeclose + MINSECS, ['id' => $quiz->id]);
327
        \course_modinfo::clear_instance_cache($quiz->course);
328
 
329
        // Timer expires in the student browser and thinks it is time to submit the quiz.
330
        // This sets $finishattempt to false - since the student did not click the button, and $timeup to true.
331
        $attemptobj = quiz_attempt::create($attempt->id);
332
        $attemptobj->process_attempt($originaltimeclose, false, true, 1);
333
 
334
        // Verify the attempt state - the $timeup was ignored becuase things have changed server-side.
335
        $attemptobj = quiz_attempt::create($attempt->id);
336
        $this->assertEquals(1, $attemptobj->get_attempt_number());
337
        $this->assertFalse($attemptobj->is_finished());
338
        $this->assertEquals(quiz_attempt::IN_PROGRESS, $attemptobj->get_state());
339
        $this->assertEquals(0, $attemptobj->get_submitted_date());
340
        $this->assertEquals($user->id, $attemptobj->get_userid());
341
    }
342
 
343
    /**
344
     * Create a quiz with a random as well as other questions and walk through quiz attempts.
345
     */
11 efrain 346
    public function test_quiz_with_random_question_attempt_walkthrough(): void {
1 efrain 347
        global $SITE;
348
 
349
        $this->resetAfterTest(true);
350
        question_bank::get_qtype('random')->clear_caches_before_testing();
351
 
352
        $this->setAdminUser();
353
 
354
        // Make a quiz.
355
        $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
356
 
357
        $quiz = $quizgenerator->create_instance(['course' => $SITE->id, 'questionsperpage' => 2, 'grade' => 100.0,
358
                                                      'sumgrades' => 4]);
359
 
360
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
361
 
362
        // Add two questions to question category.
363
        $cat = $questiongenerator->create_question_category();
364
        $saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
365
        $numq = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
366
 
367
        // Add random question to the quiz.
368
        $this->add_random_questions($quiz->id, 0, $cat->id, 1);
369
 
370
        // Make another category.
371
        $cat2 = $questiongenerator->create_question_category();
372
        $match = $questiongenerator->create_question('match', null, ['category' => $cat->id]);
373
 
374
        quiz_add_quiz_question($match->id, $quiz, 0);
375
 
376
        $multichoicemulti = $questiongenerator->create_question('multichoice', 'two_of_four', ['category' => $cat->id]);
377
 
378
        quiz_add_quiz_question($multichoicemulti->id, $quiz, 0);
379
 
380
        $multichoicesingle = $questiongenerator->create_question('multichoice', 'one_of_four', ['category' => $cat->id]);
381
 
382
        quiz_add_quiz_question($multichoicesingle->id, $quiz, 0);
383
 
384
        foreach ([$saq->id => 'frog', $numq->id => '3.14'] as $randomqidtoselect => $randqanswer) {
385
            // Make a new user to do the quiz each loop.
386
            $user1 = $this->getDataGenerator()->create_user();
387
            $this->setUser($user1);
388
 
389
            $quizobj = quiz_settings::create($quiz->id, $user1->id);
390
 
391
            // Start the attempt.
392
            $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
393
            $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
394
 
395
            $timenow = time();
396
            $attempt = quiz_create_attempt($quizobj, 1, false, $timenow);
397
 
398
            quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow, [1 => $randomqidtoselect]);
399
            $this->assertEquals('1,2,0,3,4,0', $attempt->layout);
400
 
401
            quiz_attempt_save_started($quizobj, $quba, $attempt);
402
 
403
            // Process some responses from the student.
404
            $attemptobj = quiz_attempt::create($attempt->id);
405
            $this->assertFalse($attemptobj->has_response_to_at_least_one_graded_question());
406
            $this->assertEquals(4, $attemptobj->get_number_of_unanswered_questions());
407
 
408
            $tosubmit = [];
409
            $selectedquestionid = $quba->get_question_attempt(1)->get_question_id();
410
            $tosubmit[1] = ['answer' => $randqanswer];
411
            $tosubmit[2] = [
412
                'frog' => 'amphibian',
413
                'cat'  => 'mammal',
414
                'newt' => 'amphibian'];
415
            $tosubmit[3] = ['One' => '1', 'Two' => '0', 'Three' => '1', 'Four' => '0']; // First and third choice.
416
            $tosubmit[4] = ['answer' => 'One']; // The first choice.
417
 
418
            $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
419
 
420
            // Finish the attempt.
421
            $attemptobj = quiz_attempt::create($attempt->id);
422
            $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
423
            $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
1441 ariadna 424
            $attemptobj->process_submit($timenow, false);
1 efrain 425
 
426
            // Re-load quiz attempt data.
427
            $attemptobj = quiz_attempt::create($attempt->id);
428
 
429
            // Check that results are stored as expected.
430
            $this->assertEquals(1, $attemptobj->get_attempt_number());
1441 ariadna 431
            $this->assertEquals(false, $attemptobj->is_finished());
1 efrain 432
            $this->assertEquals($timenow, $attemptobj->get_submitted_date());
433
            $this->assertEquals($user1->id, $attemptobj->get_userid());
434
            $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
435
            $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
436
 
1441 ariadna 437
            // Check we don't have grades yet.
438
            $this->assertEmpty(quiz_get_user_grades($quiz, $user1->id));
439
            $this->assertNull($attemptobj->get_sum_marks());
440
 
441
            // Now grade the submission.
442
            $attemptobj->process_grade_submission($timenow);
443
            $attemptobj = quiz_attempt::create($attempt->id);
444
 
1 efrain 445
            // Check quiz grades.
1441 ariadna 446
            $this->assertEquals(true, $attemptobj->is_finished());
447
            $this->assertEquals(4, $attemptobj->get_sum_marks());
448
 
449
            // Check quiz grades.
1 efrain 450
            $grades = quiz_get_user_grades($quiz, $user1->id);
451
            $grade = array_shift($grades);
452
            $this->assertEquals(100.0, $grade->rawgrade);
453
 
454
            // Check grade book.
455
            $gradebookgrades = grade_get_grades($SITE->id, 'mod', 'quiz', $quiz->id, $user1->id);
456
            $gradebookitem = array_shift($gradebookgrades->items);
457
            $gradebookgrade = array_shift($gradebookitem->grades);
458
            $this->assertEquals(100, $gradebookgrade->grade);
459
        }
460
    }
461
 
1441 ariadna 462
    /**
463
     * Get the correct response for variants.
464
     *
465
     * @return array
466
     */
467
    public static function get_correct_response_for_variants(): array {
1 efrain 468
        return [[1, 9.9], [2, 8.5], [5, 14.2], [10, 6.8, true]];
469
    }
470
 
1441 ariadna 471
    /** @var ?\quiz A quiz with variants */
1 efrain 472
    protected $quizwithvariants = null;
473
 
474
    /**
475
     * Create a quiz with a single question with variants and walk through quiz attempts.
476
     *
477
     * @dataProvider get_correct_response_for_variants
478
     */
11 efrain 479
    public function test_quiz_with_question_with_variants_attempt_walkthrough($variantno, $correctresponse, $done = false): void {
1 efrain 480
        global $SITE;
481
 
482
        $this->resetAfterTest($done);
483
 
484
        $this->setAdminUser();
485
 
486
        if ($this->quizwithvariants === null) {
487
            // Make a quiz.
488
            $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
489
 
490
            $this->quizwithvariants = $quizgenerator->create_instance(['course' => $SITE->id,
491
                                                                            'questionsperpage' => 0,
492
                                                                            'grade' => 100.0,
493
                                                                            'sumgrades' => 1]);
494
 
495
            $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
496
 
497
            $cat = $questiongenerator->create_question_category();
498
            $calc = $questiongenerator->create_question('calculatedsimple', 'sumwithvariants', ['category' => $cat->id]);
499
            quiz_add_quiz_question($calc->id, $this->quizwithvariants, 0);
500
        }
501
 
502
 
503
        // Make a new user to do the quiz.
504
        $user1 = $this->getDataGenerator()->create_user();
505
        $this->setUser($user1);
506
        $quizobj = quiz_settings::create($this->quizwithvariants->id, $user1->id);
507
 
508
        // Start the attempt.
509
        $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
510
        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
511
 
512
        $timenow = time();
513
        $attempt = quiz_create_attempt($quizobj, 1, false, $timenow);
514
 
515
        // Select variant.
516
        quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow, [], [1 => $variantno]);
517
        $this->assertEquals('1,0', $attempt->layout);
518
        quiz_attempt_save_started($quizobj, $quba, $attempt);
519
 
520
        // Process some responses from the student.
521
        $attemptobj = quiz_attempt::create($attempt->id);
522
        $this->assertFalse($attemptobj->has_response_to_at_least_one_graded_question());
523
        $this->assertEquals(1, $attemptobj->get_number_of_unanswered_questions());
524
 
525
        $tosubmit = [1 => ['answer' => $correctresponse]];
526
        $attemptobj->process_submitted_actions($timenow, false, $tosubmit);
527
 
528
        // Finish the attempt.
529
        $attemptobj = quiz_attempt::create($attempt->id);
530
        $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
531
        $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
532
 
1441 ariadna 533
        $attemptobj->process_submit($timenow, false);
1 efrain 534
 
535
        // Re-load quiz attempt data.
536
        $attemptobj = quiz_attempt::create($attempt->id);
537
 
538
        // Check that results are stored as expected.
539
        $this->assertEquals(1, $attemptobj->get_attempt_number());
1441 ariadna 540
        $this->assertEquals(false, $attemptobj->is_finished());
1 efrain 541
        $this->assertEquals($timenow, $attemptobj->get_submitted_date());
542
        $this->assertEquals($user1->id, $attemptobj->get_userid());
543
        $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
544
        $this->assertEquals(0, $attemptobj->get_number_of_unanswered_questions());
545
 
1441 ariadna 546
        // Check we don't have grades yet.
547
        $this->assertEmpty(quiz_get_user_grades($this->quizwithvariants, $user1->id));
548
        $this->assertNull($attemptobj->get_sum_marks());
549
 
550
        // Now grade the submission.
551
        $attemptobj->process_grade_submission($timenow);
552
        $attemptobj = quiz_attempt::create($attempt->id);
553
 
1 efrain 554
        // Check quiz grades.
1441 ariadna 555
        $this->assertEquals(true, $attemptobj->is_finished());
556
        $this->assertEquals(1, $attemptobj->get_sum_marks());
557
        // Check quiz grades.
1 efrain 558
        $grades = quiz_get_user_grades($this->quizwithvariants, $user1->id);
559
        $grade = array_shift($grades);
560
        $this->assertEquals(100.0, $grade->rawgrade);
561
 
562
        // Check grade book.
563
        $gradebookgrades = grade_get_grades($SITE->id, 'mod', 'quiz', $this->quizwithvariants->id, $user1->id);
564
        $gradebookitem = array_shift($gradebookgrades->items);
565
        $gradebookgrade = array_shift($gradebookitem->grades);
566
        $this->assertEquals(100, $gradebookgrade->grade);
567
    }
568
 
11 efrain 569
    public function test_quiz_attempt_walkthrough_abandoned_attempt_reopened_with_timelimit_override(): void {
1 efrain 570
        global $DB;
571
 
572
        $quiz = $this->create_quiz_with_one_question('autoabandon');
573
        $originaltimeclose = $quiz->timeclose;
574
 
575
        // Make a user to do the quiz.
576
        $user = $this->getDataGenerator()->create_user();
577
        $this->setUser($user);
578
        $quizobj = quiz_settings::create($quiz->id, $user->id);
579
 
580
        // Start the attempt.
581
        $attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null);
582
 
583
        // Process some responses from the student during the attempt.
584
        $attemptobj = quiz_attempt::create($attempt->id);
585
        $attemptobj->process_submitted_actions($originaltimeclose - 30 * MINSECS, false, [1 => ['answer' => 'frog']]);
586
 
587
        // Student leaves, so cron closes the attempt when time expires.
588
        $attemptobj->process_abandon($originaltimeclose + 5 * MINSECS, false);
589
 
590
        // Verify the attempt state.
591
        $attemptobj = quiz_attempt::create($attempt->id);
592
        $this->assertEquals(quiz_attempt::ABANDONED, $attemptobj->get_state());
593
        $this->assertEquals(0, $attemptobj->get_submitted_date());
594
        $this->assertEquals($user->id, $attemptobj->get_userid());
595
 
596
        // The teacher feels kind, so adds an override for the student, and re-opens the attempt.
597
        $sink = $this->redirectEvents();
598
        $overriddentimeclose = $originaltimeclose + HOURSECS;
599
        $DB->insert_record('quiz_overrides', [
600
            'quiz' => $quiz->id,
601
            'userid' => $user->id,
602
            'timeclose' => $overriddentimeclose,
603
        ]);
604
        $attemptobj = quiz_attempt::create($attempt->id);
605
        $reopentime = $originaltimeclose + 10 * MINSECS;
606
        $attemptobj->process_reopen_abandoned($reopentime);
607
 
608
        // Verify the attempt state.
609
        $attemptobj = quiz_attempt::create($attempt->id);
610
        $this->assertEquals(1, $attemptobj->get_attempt_number());
611
        $this->assertFalse($attemptobj->is_finished());
612
        $this->assertEquals(quiz_attempt::IN_PROGRESS, $attemptobj->get_state());
613
        $this->assertEquals(0, $attemptobj->get_submitted_date());
614
        $this->assertEquals($user->id, $attemptobj->get_userid());
1441 ariadna 615
        $this->assertEquals(
616
            $overriddentimeclose,
617
            $attemptobj->get_access_manager($reopentime)->get_end_time($attemptobj->get_attempt())
618
        );
1 efrain 619
 
620
        // Verify this was logged correctly.
621
        $events = $sink->get_events();
622
        $this->assertCount(1, $events);
623
 
624
        $reopenedevent = array_shift($events);
625
        $this->assertInstanceOf('\mod_quiz\event\attempt_reopened', $reopenedevent);
626
        $this->assertEquals($attemptobj->get_context(), $reopenedevent->get_context());
1441 ariadna 627
        $this->assertEquals(
628
            new moodle_url('/mod/quiz/review.php', ['attempt' => $attemptobj->get_attemptid()]),
629
            $reopenedevent->get_url()
630
        );
1 efrain 631
    }
632
 
11 efrain 633
    public function test_quiz_attempt_walkthrough_abandoned_attempt_reopened_after_close_time(): void {
1 efrain 634
        $quiz = $this->create_quiz_with_one_question('autoabandon');
635
        $originaltimeclose = $quiz->timeclose;
636
 
637
        // Make a user to do the quiz.
638
        $user = $this->getDataGenerator()->create_user();
639
        $this->setUser($user);
640
        $quizobj = quiz_settings::create($quiz->id, $user->id);
641
 
642
        // Start the attempt.
643
        $attempt = quiz_prepare_and_start_new_attempt($quizobj, 1, null);
644
 
645
        // Process some responses from the student during the attempt.
646
        $attemptobj = quiz_attempt::create($attempt->id);
647
        $attemptobj->process_submitted_actions($originaltimeclose - 30 * MINSECS, false, [1 => ['answer' => 'frog']]);
648
 
649
        // Student leaves, so cron closes the attempt when time expires.
650
        $attemptobj->process_abandon($originaltimeclose + 5 * MINSECS, false);
651
 
652
        // Verify the attempt state.
653
        $attemptobj = quiz_attempt::create($attempt->id);
654
        $this->assertEquals(quiz_attempt::ABANDONED, $attemptobj->get_state());
655
        $this->assertEquals(0, $attemptobj->get_submitted_date());
656
        $this->assertEquals($user->id, $attemptobj->get_userid());
657
 
658
        // The teacher reopens the attempt without granting more time, so previously submitted responess are graded.
659
        $sink = $this->redirectEvents();
660
        $reopentime = $originaltimeclose + 10 * MINSECS;
661
        $attemptobj->process_reopen_abandoned($reopentime);
662
 
663
        // Verify the attempt state.
664
        $attemptobj = quiz_attempt::create($attempt->id);
665
        $this->assertEquals(1, $attemptobj->get_attempt_number());
666
        $this->assertTrue($attemptobj->is_finished());
667
        $this->assertEquals(quiz_attempt::FINISHED, $attemptobj->get_state());
668
        $this->assertEquals($originaltimeclose, $attemptobj->get_submitted_date());
669
        $this->assertEquals($user->id, $attemptobj->get_userid());
670
        $this->assertEquals(1, $attemptobj->get_sum_marks());
671
 
672
        // Verify this was logged correctly - there are some gradebook events between the two we want to check.
673
        $events = $sink->get_events();
1441 ariadna 674
        $this->assertGreaterThanOrEqual(3, $events);
1 efrain 675
 
1441 ariadna 676
        $attempturl = new moodle_url(
677
            '/mod/quiz/review.php',
678
            ['attempt' => $attemptobj->get_attemptid()],
679
        );
1 efrain 680
        $reopenedevent = array_shift($events);
681
        $this->assertInstanceOf('\mod_quiz\event\attempt_reopened', $reopenedevent);
682
        $this->assertEquals($attemptobj->get_context(), $reopenedevent->get_context());
683
 
1441 ariadna 684
        $this->assertEquals($attempturl, $reopenedevent->get_url());
685
 
686
        $submittedevent = array_shift($events);
1 efrain 687
        $this->assertInstanceOf('\mod_quiz\event\attempt_submitted', $submittedevent);
688
        $this->assertEquals($attemptobj->get_context(), $submittedevent->get_context());
1441 ariadna 689
 
690
        $this->assertEquals($attempturl, $submittedevent->get_url());
691
 
692
        $gradedevent = array_pop($events);
693
        $this->assertInstanceOf('\mod_quiz\event\attempt_graded', $gradedevent);
694
        $this->assertEquals($attemptobj->get_context(), $gradedevent->get_context());
695
        $this->assertEquals($attempturl, $gradedevent->get_url());
1 efrain 696
    }
1441 ariadna 697
 
698
    /**
699
     * Create a quiz with questions, pre-create an attempt, edit a question, then begin the attempt.
700
     */
701
    public function test_quiz_attempt_walkthrough_update_question_after_precreate(): void {
702
        global $SITE;
703
 
704
        $this->resetAfterTest(true);
705
 
706
        // Make a quiz.
707
        $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
708
 
709
        $quiz = $quizgenerator->create_instance(
710
            [
711
                'course' => $SITE->id,
712
                'questionsperpage' => 0,
713
                'grade' => 100.0,
714
                'sumgrades' => 3,
715
            ],
716
        );
717
 
718
        // Create a couple of questions.
719
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
720
 
721
        $cat = $questiongenerator->create_question_category();
722
        $saq = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
723
        $numq = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
724
        $matchq = $questiongenerator->create_question('match', null, ['category' => $cat->id]);
725
        $description = $questiongenerator->create_question('description', null, ['category' => $cat->id]);
726
 
727
        // Add them to the quiz.
728
        quiz_add_quiz_question($saq->id, $quiz);
729
        quiz_add_quiz_question($numq->id, $quiz);
730
        quiz_add_quiz_question($matchq->id, $quiz);
731
        quiz_add_quiz_question($description->id, $quiz);
732
 
733
        // Make a user to do the quiz.
734
        $user1 = $this->getDataGenerator()->create_user();
735
 
736
        $quizobj = quiz_settings::create($quiz->id, $user1->id);
737
 
738
        // Start the attempt.
739
        $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
740
        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
741
 
742
        $timenow = time();
743
        $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $user1->id);
744
 
745
        quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
746
        $this->assertEquals('1,2,3,4,0', $attempt->layout);
747
 
748
        quiz_attempt_save_not_started($quba, $attempt);
749
 
750
        $attemptobj = quiz_attempt::create($attempt->id);
751
 
752
        // Update question in quiz.
753
        $newsa = $questiongenerator->update_question($saq, null,
754
            ['name' => 'This is the second version of shortanswer']);
755
        $newnumbq = $questiongenerator->update_question($numq, null,
756
            ['name' => 'This is the second version of numerical']);
757
        $newmatch = $questiongenerator->update_question($matchq, null,
758
            ['name' => 'This is the second version of match']);
759
        $newdescription = $questiongenerator->update_question($description, null,
760
            ['name' => 'This is the second version of description']);
761
 
762
        $this->assertEquals($saq->id, $attemptobj->get_question_attempt(1)->get_question_id());
763
        $this->assertEquals($numq->id, $attemptobj->get_question_attempt(2)->get_question_id());
764
        $this->assertEquals($matchq->id, $attemptobj->get_question_attempt(3)->get_question_id());
765
        $this->assertEquals($description->id, $attemptobj->get_question_attempt(4)->get_question_id());
766
 
767
        quiz_attempt_save_started($quizobj, $quba, $attempt);
768
 
769
        // Verify that the started attempt contains the new questions.
770
        $attemptobj = quiz_attempt::create($attempt->id);
771
        $this->assertEquals($newsa->id, $attemptobj->get_question_attempt(1)->get_question_id());
772
        $this->assertEquals($newnumbq->id, $attemptobj->get_question_attempt(2)->get_question_id());
773
        $this->assertEquals($newmatch->id, $attemptobj->get_question_attempt(3)->get_question_id());
774
        $this->assertEquals($newdescription->id, $attemptobj->get_question_attempt(4)->get_question_id());
775
    }
1 efrain 776
}