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
declare(strict_types=1);
18
 
19
namespace mod_quiz;
20
 
21
use advanced_testcase;
22
use cm_info;
23
use core_completion\cm_completion_details;
24
use grade_item;
25
use mod_quiz\completion\custom_completion;
26
use question_engine;
27
use mod_quiz\quiz_settings;
28
use stdClass;
29
 
30
defined('MOODLE_INTERNAL') || die();
31
 
32
global $CFG;
33
require_once($CFG->libdir . '/completionlib.php');
34
 
35
/**
36
 * Class for unit testing mod_quiz/custom_completion.
37
 *
38
 * @package   mod_quiz
39
 * @copyright 2021 Shamim Rezaie <shamim@moodle.com>
40
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41
 * @coversDefaultClass \mod_quiz\completion\custom_completion
42
 */
1441 ariadna 43
final class custom_completion_test extends advanced_testcase {
1 efrain 44
 
45
    /**
46
     * Setup function for all tests.
47
     *
48
     * @param array $completionoptions ['nbstudents'] => int, ['qtype'] => string, ['quizoptions'] => array
49
     * @return array [$students, $quiz, $cm, $litecm]
50
     */
51
    private function setup_quiz_for_testing_completion(array $completionoptions): array {
52
        global $CFG, $DB;
53
 
54
        $this->resetAfterTest(true);
55
 
56
        // Enable completion before creating modules, otherwise the completion data is not written in DB.
57
        $CFG->enablecompletion = true;
58
 
59
        // Create a course and students.
60
        $studentrole = $DB->get_record('role', ['shortname' => 'student']);
61
        $course = $this->getDataGenerator()->create_course(['enablecompletion' => true]);
62
        $students = [];
63
        $sumgrades = $completionoptions['sumgrades'] ?? 1;
64
        $nbquestions = $completionoptions['nbquestions'] ?? 1;
65
        for ($i = 0; $i < $completionoptions['nbstudents']; $i++) {
66
            $students[$i] = $this->getDataGenerator()->create_user();
67
            $this->assertTrue($this->getDataGenerator()->enrol_user($students[$i]->id, $course->id, $studentrole->id));
68
        }
69
 
70
        // Make a quiz.
71
        $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
72
        $data = array_merge([
73
            'course' => $course->id,
74
            'grade' => 100.0,
75
            'questionsperpage' => 0,
76
            'sumgrades' => $sumgrades,
77
            'completion' => COMPLETION_TRACKING_AUTOMATIC
78
        ], $completionoptions['quizoptions']);
79
        $quiz = $quizgenerator->create_instance($data);
80
        $litecm = get_coursemodule_from_id('quiz', $quiz->cmid);
81
        $cm = cm_info::create($litecm);
82
 
83
        // Create a question.
84
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
85
 
86
        $cat = $questiongenerator->create_question_category();
87
        for ($i = 0; $i < $nbquestions; $i++) {
88
            $overrideparams = ['category' => $cat->id];
89
            if (isset($completionoptions['questiondefaultmarks'][$i])) {
90
                $overrideparams['defaultmark'] = $completionoptions['questiondefaultmarks'][$i];
91
            }
92
            $question = $questiongenerator->create_question($completionoptions['qtype'], null, $overrideparams);
93
            quiz_add_quiz_question($question->id, $quiz);
94
        }
95
 
96
        // Set grade to pass.
97
        $item = grade_item::fetch(['courseid' => $course->id, 'itemtype' => 'mod', 'itemmodule' => 'quiz',
98
            'iteminstance' => $quiz->id, 'outcomeid' => null]);
99
        $item->gradepass = 80;
100
        $item->update();
101
        return [
102
            $students,
103
            $quiz,
104
            $cm,
105
            $litecm
106
        ];
107
    }
108
 
109
    /**
110
     * Helper function for tests.
111
     * Starts an attempt, processes responses and finishes the attempt.
112
     *
113
     * @param array $attemptoptions ['quiz'] => object, ['student'] => object, ['tosubmit'] => array, ['attemptnumber'] => int
114
     */
115
    private function do_attempt_quiz(array $attemptoptions) {
116
        $quizobj = quiz_settings::create((int) $attemptoptions['quiz']->id);
117
 
118
        // Start the passing attempt.
119
        $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
120
        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
121
 
122
        $timenow = time();
123
        $attempt = quiz_create_attempt($quizobj, $attemptoptions['attemptnumber'], false, $timenow, false,
124
            $attemptoptions['student']->id);
125
        quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptoptions['attemptnumber'], $timenow);
126
        quiz_attempt_save_started($quizobj, $quba, $attempt);
127
 
128
        // Process responses from the student.
129
        $attemptobj = quiz_attempt::create($attempt->id);
130
        $attemptobj->process_submitted_actions($timenow, false, $attemptoptions['tosubmit']);
131
 
132
        // Finish the attempt.
133
        $attemptobj = quiz_attempt::create($attempt->id);
134
        $this->assertTrue($attemptobj->has_response_to_at_least_one_graded_question());
1441 ariadna 135
        $attemptobj->process_submit($timenow, false);
136
        $attemptobj->process_grade_submission($timenow);
1 efrain 137
    }
138
 
139
    /**
140
     * Test checking the completion state of a quiz base on core's completionpassgrade criteria.
141
     * The quiz requires a passing grade to be completed.
142
     */
11 efrain 143
    public function test_completionpass(): void {
1 efrain 144
        list($students, $quiz, $cm) = $this->setup_quiz_for_testing_completion([
145
            'nbstudents' => 2,
146
            'qtype' => 'numerical',
147
            'quizoptions' => [
148
                'completionusegrade' => 1,
149
                'completionpassgrade' => 1
150
            ]
151
        ]);
152
 
153
        list($passstudent, $failstudent) = $students;
154
 
155
        // Do a passing attempt.
156
        $this->do_attempt_quiz([
157
            'quiz' => $quiz,
158
            'student' => $passstudent,
159
            'attemptnumber' => 1,
160
            'tosubmit' => [1 => ['answer' => '3.14']]
161
        ]);
162
 
163
        $completioninfo = new \completion_info($cm->get_course());
164
        $completiondetails = new cm_completion_details($completioninfo, $cm, (int) $passstudent->id);
165
 
166
        // Check the results.
167
        $this->assertEquals(COMPLETION_COMPLETE_PASS, $completiondetails->get_details()['completionpassgrade']->status);
168
        $this->assertEquals(
169
            'Receive a passing grade',
170
            $completiondetails->get_details()['completionpassgrade']->description
171
        );
172
 
173
        // Do a failing attempt.
174
        $this->do_attempt_quiz([
175
            'quiz' => $quiz,
176
            'student' => $failstudent,
177
            'attemptnumber' => 1,
178
            'tosubmit' => [1 => ['answer' => '0']]
179
        ]);
180
 
181
        $completiondetails = new cm_completion_details($completioninfo, $cm, (int) $failstudent->id);
182
 
183
        // Check the results.
184
        $this->assertEquals(COMPLETION_COMPLETE_FAIL, $completiondetails->get_details()['completionpassgrade']->status);
185
        $this->assertEquals(
186
            'Receive a passing grade',
187
            $completiondetails->get_details()['completionpassgrade']->description
188
        );
189
    }
190
 
191
    /**
192
     * Test checking the completion state of a quiz.
193
     * To be completed, this quiz requires either a passing grade or for all attempts to be used up.
194
     *
195
     * @covers ::get_state
196
     * @covers ::get_custom_rule_descriptions
197
     */
11 efrain 198
    public function test_completionexhausted(): void {
1 efrain 199
        list($students, $quiz, $cm) = $this->setup_quiz_for_testing_completion([
200
            'nbstudents' => 2,
201
            'qtype' => 'numerical',
202
            'quizoptions' => [
203
                'attempts' => 2,
204
                'completionusegrade' => 1,
205
                'completionpassgrade' => 1,
206
                'completionattemptsexhausted' => 1
207
            ]
208
        ]);
209
 
210
        list($passstudent, $exhauststudent) = $students;
211
 
212
        // Start a passing attempt.
213
        $this->do_attempt_quiz([
214
            'quiz' => $quiz,
215
            'student' => $passstudent,
216
            'attemptnumber' => 1,
217
            'tosubmit' => [1 => ['answer' => '3.14']]
218
        ]);
219
 
220
        $completioninfo = new \completion_info($cm->get_course());
221
 
222
        // Check the results. Quiz is completed by $passstudent because of passing grade.
223
        $studentid = (int) $passstudent->id;
224
        $customcompletion = new custom_completion($cm, $studentid, $completioninfo->get_core_completion_state($cm, $studentid));
225
        $this->assertArrayHasKey('completionpassorattemptsexhausted', $cm->customdata['customcompletionrules']);
226
        $this->assertEquals(COMPLETION_COMPLETE, $customcompletion->get_state('completionpassorattemptsexhausted'));
227
        $this->assertEquals(
228
            'Receive a pass grade or complete all available attempts',
229
            $customcompletion->get_custom_rule_descriptions()['completionpassorattemptsexhausted']
230
        );
231
 
232
        // Do a failing attempt.
233
        $this->do_attempt_quiz([
234
            'quiz' => $quiz,
235
            'student' => $exhauststudent,
236
            'attemptnumber' => 1,
237
            'tosubmit' => [1 => ['answer' => '0']]
238
        ]);
239
 
240
        // Check the results. Quiz is not completed by $exhauststudent yet because of failing grade and of remaining attempts.
241
        $studentid = (int) $exhauststudent->id;
242
        $customcompletion = new custom_completion($cm, $studentid, $completioninfo->get_core_completion_state($cm, $studentid));
243
        $this->assertArrayHasKey('completionpassorattemptsexhausted', $cm->customdata['customcompletionrules']);
244
        $this->assertEquals(COMPLETION_INCOMPLETE, $customcompletion->get_state('completionpassorattemptsexhausted'));
245
        $this->assertEquals(
246
            'Receive a pass grade or complete all available attempts',
247
            $customcompletion->get_custom_rule_descriptions()['completionpassorattemptsexhausted']
248
        );
249
 
250
        // Do a second failing attempt.
251
        $this->do_attempt_quiz([
252
            'quiz' => $quiz,
253
            'student' => $exhauststudent,
254
            'attemptnumber' => 2,
255
            'tosubmit' => [1 => ['answer' => '0']]
256
        ]);
257
 
258
        // Check the results. Quiz is completed by $exhauststudent because there are no remaining attempts.
259
        $customcompletion = new custom_completion($cm, $studentid, $completioninfo->get_core_completion_state($cm, $studentid));
260
        $this->assertArrayHasKey('completionpassorattemptsexhausted', $cm->customdata['customcompletionrules']);
261
        $this->assertEquals(COMPLETION_COMPLETE, $customcompletion->get_state('completionpassorattemptsexhausted'));
262
        $this->assertEquals(
263
            'Receive a pass grade or complete all available attempts',
264
            $customcompletion->get_custom_rule_descriptions()['completionpassorattemptsexhausted']
265
        );
266
 
267
    }
268
 
269
    /**
270
     * Test checking the completion state of a quiz.
271
     * To be completed, this quiz requires a minimum number of attempts.
272
     *
273
     * @covers ::get_state
274
     * @covers ::get_custom_rule_descriptions
275
     */
11 efrain 276
    public function test_completionminattempts(): void {
1 efrain 277
        list($students, $quiz, $cm) = $this->setup_quiz_for_testing_completion([
278
            'nbstudents' => 1,
279
            'qtype' => 'essay',
280
            'quizoptions' => [
281
                'completionminattemptsenabled' => 1,
282
                'completionminattempts' => 2
283
            ]
284
        ]);
285
 
286
        list($student) = $students;
287
 
288
        // Do a first attempt.
289
        $this->do_attempt_quiz([
290
            'quiz' => $quiz,
291
            'student' => $student,
292
            'attemptnumber' => 1,
293
            'tosubmit' => [1 => ['answer' => 'Lorem ipsum.', 'answerformat' => '1']]
294
        ]);
295
 
296
        // Check the results. Quiz is not completed yet because only one attempt was done.
297
        $customcompletion = new custom_completion($cm, (int) $student->id);
298
        $this->assertArrayHasKey('completionminattempts', $cm->customdata['customcompletionrules']);
299
        $this->assertEquals(COMPLETION_INCOMPLETE, $customcompletion->get_state('completionminattempts'));
300
        $this->assertEquals(
301
            'Make attempts: 2',
302
            $customcompletion->get_custom_rule_descriptions()['completionminattempts']
303
        );
304
 
305
        // Do a second attempt.
306
        $this->do_attempt_quiz([
307
            'quiz' => $quiz,
308
            'student' => $student,
309
            'attemptnumber' => 2,
310
            'tosubmit' => [1 => ['answer' => 'Lorem ipsum.', 'answerformat' => '1']]
311
        ]);
312
 
313
        // Check the results. Quiz is completed by $student because two attempts were done.
314
        $customcompletion = new custom_completion($cm, (int) $student->id);
315
        $this->assertArrayHasKey('completionminattempts', $cm->customdata['customcompletionrules']);
316
        $this->assertEquals(COMPLETION_COMPLETE, $customcompletion->get_state('completionminattempts'));
317
        $this->assertEquals(
318
            'Make attempts: 2',
319
            $customcompletion->get_custom_rule_descriptions()['completionminattempts']
320
        );
321
    }
322
 
323
    /**
324
     * Test for get_defined_custom_rules().
325
     *
326
     * @covers ::get_defined_custom_rules
327
     */
11 efrain 328
    public function test_get_defined_custom_rules(): void {
1 efrain 329
        $rules = custom_completion::get_defined_custom_rules();
330
        $this->assertCount(2, $rules);
331
        $this->assertEquals(
332
            ['completionpassorattemptsexhausted', 'completionminattempts'],
333
            $rules
334
        );
335
    }
336
 
337
    /**
338
     * Test update moduleinfo.
339
     *
340
     * @covers \update_moduleinfo
341
     */
11 efrain 342
    public function test_update_moduleinfo(): void {
1 efrain 343
        $this->setAdminUser();
344
        // We need lite cm object not a full cm because update_moduleinfo is not allow some properties to be updated.
345
        list($students, $quiz, $cm, $litecm) = $this->setup_quiz_for_testing_completion([
346
            'nbstudents' => 1,
347
            'qtype' => 'numerical',
348
            'nbquestions' => 2,
349
            'sumgrades' => 100,
350
            'questiondefaultmarks' => [20, 80],
351
            'quizoptions' => [
352
                'completionusegrade' => 1,
353
                'completionpassgrade' => 1,
354
                'completionview' => 0,
355
            ]
356
        ]);
357
        $course = $cm->get_course();
358
 
359
        list($student) = $students;
360
        // Do a first attempt with a pass marks = 20.
361
        $this->do_attempt_quiz([
362
            'quiz' => $quiz,
363
            'student' => $student,
364
            'attemptnumber' => 1,
365
            'tosubmit' => [1 => ['answer' => '3.14']]
366
        ]);
367
        $completioninfo = new \completion_info($course);
368
        $cminfo = \cm_info::create($cm);
369
        $completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id);
370
 
371
        // Check the results. Completion is fail because gradepass = 80.
372
        $this->assertEquals(COMPLETION_COMPLETE_FAIL, $completiondetails->get_details()['completionpassgrade']->status);
373
        $this->assertEquals(
374
            'Receive a passing grade',
375
            $completiondetails->get_details()['completionpassgrade']->description
376
        );
377
 
378
        // Update quiz with passgrade = 20 and use highest grade to calculate.
379
        $moduleinfo = $this->prepare_module_info($cm, $quiz, $course, 20, QUIZ_GRADEHIGHEST);
380
        update_moduleinfo($litecm, $moduleinfo, $course, null);
381
 
382
        $completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id);
383
 
384
        // Check the results. Completion is pass.
385
        $this->assertEquals(COMPLETION_COMPLETE_PASS, $completiondetails->get_details()['completionpassgrade']->status);
386
        $this->assertEquals(
387
            'Receive a passing grade',
388
            $completiondetails->get_details()['completionpassgrade']->description
389
        );
390
 
391
        // Do a second attempt with pass marks = 80.
392
        $this->do_attempt_quiz([
393
            'quiz' => $quiz,
394
            'student' => $student,
395
            'attemptnumber' => 2,
396
            'tosubmit' => [2 => ['answer' => '3.14']]
397
        ]);
398
 
399
        // Update quiz with gradepass = 80 and use highest grade to calculate completion.
400
        $moduleinfo = $this->prepare_module_info($cm, $quiz, $course, 80, QUIZ_GRADEHIGHEST);
401
        update_moduleinfo($litecm, $moduleinfo, $course, null);
402
 
403
        $completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id);
404
 
405
        // Check the results. Completion is pass.
406
        $this->assertEquals(COMPLETION_COMPLETE_PASS, $completiondetails->get_details()['completionpassgrade']->status);
407
        $this->assertEquals(
408
            'Receive a passing grade',
409
            $completiondetails->get_details()['completionpassgrade']->description
410
        );
411
 
412
        // Update quiz with gradepass = 80 and use average grade to calculate completion.
413
        $moduleinfo = $this->prepare_module_info($cm, $quiz, $course, 80, QUIZ_GRADEAVERAGE);
414
        update_moduleinfo($litecm, $moduleinfo, $course, null);
415
 
416
        $completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id);
417
 
418
        // Check the results. Completion is fail because student grade = 50.
419
        $this->assertEquals(COMPLETION_COMPLETE_FAIL, $completiondetails->get_details()['completionpassgrade']->status);
420
        $this->assertEquals(
421
            'Receive a passing grade',
422
            $completiondetails->get_details()['completionpassgrade']->description
423
        );
424
 
425
        // Update quiz with gradepass = 50 and use average grade to calculate completion.
426
        $moduleinfo = $this->prepare_module_info($cm, $quiz, $course, 50, QUIZ_GRADEAVERAGE);
427
        update_moduleinfo($litecm, $moduleinfo, $course, null);
428
 
429
        $completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id);
430
 
431
        // Check the results. Completion is pass.
432
        $this->assertEquals(COMPLETION_COMPLETE_PASS, $completiondetails->get_details()['completionpassgrade']->status);
433
        $this->assertEquals(
434
            'Receive a passing grade',
435
            $completiondetails->get_details()['completionpassgrade']->description
436
        );
437
 
438
        // Update quiz with gradepass = 50 and use first attempt grade to calculate completion.
439
        $moduleinfo = $this->prepare_module_info($cm, $quiz, $course, 50, QUIZ_ATTEMPTFIRST);
440
        update_moduleinfo($litecm, $moduleinfo, $course, null);
441
 
442
        $completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id);
443
 
444
        // Check the results. Completion is fail.
445
        $this->assertEquals(COMPLETION_COMPLETE_FAIL, $completiondetails->get_details()['completionpassgrade']->status);
446
        $this->assertEquals(
447
            'Receive a passing grade',
448
            $completiondetails->get_details()['completionpassgrade']->description
449
        );
450
        // Update quiz with gradepass = 50 and use last attempt grade to calculate completion.
451
        $moduleinfo = $this->prepare_module_info($cm, $quiz, $course, 50, QUIZ_ATTEMPTLAST);
452
        update_moduleinfo($litecm, $moduleinfo, $course, null);
453
 
454
        $completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id);
455
 
456
        // Check the results. Completion is fail.
457
        $this->assertEquals(COMPLETION_COMPLETE_PASS, $completiondetails->get_details()['completionpassgrade']->status);
458
        $this->assertEquals(
459
            'Receive a passing grade',
460
            $completiondetails->get_details()['completionpassgrade']->description
461
        );
462
    }
463
 
464
    /**
465
     * Set up moduleinfo object sample data for quiz instance.
466
     *
467
     * @param cm_info $cm course-module instance
468
     * @param stdClass $quiz quiz instance data.
469
     * @param stdClass $course Course related data.
470
     * @param int $gradepass Grade to pass and completed completion.
471
     * @param string $grademethod grade attempt method.
472
     * @return stdClass
473
     */
474
    private function prepare_module_info(cm_info $cm, stdClass $quiz, stdClass $course,
475
            int $gradepass, string $grademethod): \stdClass {
476
        $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]);
477
        // Module test values.
478
        $moduleinfo = new \stdClass();
479
        $moduleinfo->coursemodule = $cm->id;
480
        $moduleinfo->section = 1;
481
        $moduleinfo->course = $course->id;
482
        $moduleinfo->groupingid = $grouping->id;
483
        $draftideditor = 0;
484
        file_prepare_draft_area($draftideditor, null, null, null, null);
485
        $moduleinfo->introeditor = ['text' => 'This is a module', 'format' => FORMAT_HTML, 'itemid' => $draftideditor];
486
        $moduleinfo->modulename = 'quiz';
487
        $moduleinfo->quizpassword = '';
488
        $moduleinfo->cmidnumber = '';
489
        $moduleinfo->maxmarksopen = 1;
490
        $moduleinfo->marksopen = 1;
491
        $moduleinfo->visible = 1;
492
        $moduleinfo->visibleoncoursepage = 1;
493
        $moduleinfo->completion = COMPLETION_TRACKING_AUTOMATIC;
494
        $moduleinfo->completionview = COMPLETION_VIEW_NOT_REQUIRED;
495
        $moduleinfo->name = $quiz->name;
496
        $moduleinfo->timeopen = $quiz->timeopen;
497
        $moduleinfo->timeclose = $quiz->timeclose;
498
        $moduleinfo->timelimit = $quiz->timelimit;
499
        $moduleinfo->graceperiod = $quiz->graceperiod;
500
        $moduleinfo->decimalpoints = $quiz->decimalpoints;
501
        $moduleinfo->questiondecimalpoints = $quiz->questiondecimalpoints;
502
        $moduleinfo->gradepass = $gradepass;
503
        $moduleinfo->grademethod = $grademethod;
504
 
505
        return $moduleinfo;
506
    }
507
}