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
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
 */
43
class custom_completion_test extends advanced_testcase {
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());
135
        $attemptobj->process_finish($timenow, false);
136
    }
137
 
138
    /**
139
     * Test checking the completion state of a quiz base on core's completionpassgrade criteria.
140
     * The quiz requires a passing grade to be completed.
141
     */
11 efrain 142
    public function test_completionpass(): void {
1 efrain 143
        list($students, $quiz, $cm) = $this->setup_quiz_for_testing_completion([
144
            'nbstudents' => 2,
145
            'qtype' => 'numerical',
146
            'quizoptions' => [
147
                'completionusegrade' => 1,
148
                'completionpassgrade' => 1
149
            ]
150
        ]);
151
 
152
        list($passstudent, $failstudent) = $students;
153
 
154
        // Do a passing attempt.
155
        $this->do_attempt_quiz([
156
            'quiz' => $quiz,
157
            'student' => $passstudent,
158
            'attemptnumber' => 1,
159
            'tosubmit' => [1 => ['answer' => '3.14']]
160
        ]);
161
 
162
        $completioninfo = new \completion_info($cm->get_course());
163
        $completiondetails = new cm_completion_details($completioninfo, $cm, (int) $passstudent->id);
164
 
165
        // Check the results.
166
        $this->assertEquals(COMPLETION_COMPLETE_PASS, $completiondetails->get_details()['completionpassgrade']->status);
167
        $this->assertEquals(
168
            'Receive a passing grade',
169
            $completiondetails->get_details()['completionpassgrade']->description
170
        );
171
 
172
        // Do a failing attempt.
173
        $this->do_attempt_quiz([
174
            'quiz' => $quiz,
175
            'student' => $failstudent,
176
            'attemptnumber' => 1,
177
            'tosubmit' => [1 => ['answer' => '0']]
178
        ]);
179
 
180
        $completiondetails = new cm_completion_details($completioninfo, $cm, (int) $failstudent->id);
181
 
182
        // Check the results.
183
        $this->assertEquals(COMPLETION_COMPLETE_FAIL, $completiondetails->get_details()['completionpassgrade']->status);
184
        $this->assertEquals(
185
            'Receive a passing grade',
186
            $completiondetails->get_details()['completionpassgrade']->description
187
        );
188
    }
189
 
190
    /**
191
     * Test checking the completion state of a quiz.
192
     * To be completed, this quiz requires either a passing grade or for all attempts to be used up.
193
     *
194
     * @covers ::get_state
195
     * @covers ::get_custom_rule_descriptions
196
     */
11 efrain 197
    public function test_completionexhausted(): void {
1 efrain 198
        list($students, $quiz, $cm) = $this->setup_quiz_for_testing_completion([
199
            'nbstudents' => 2,
200
            'qtype' => 'numerical',
201
            'quizoptions' => [
202
                'attempts' => 2,
203
                'completionusegrade' => 1,
204
                'completionpassgrade' => 1,
205
                'completionattemptsexhausted' => 1
206
            ]
207
        ]);
208
 
209
        list($passstudent, $exhauststudent) = $students;
210
 
211
        // Start a passing attempt.
212
        $this->do_attempt_quiz([
213
            'quiz' => $quiz,
214
            'student' => $passstudent,
215
            'attemptnumber' => 1,
216
            'tosubmit' => [1 => ['answer' => '3.14']]
217
        ]);
218
 
219
        $completioninfo = new \completion_info($cm->get_course());
220
 
221
        // Check the results. Quiz is completed by $passstudent because of passing grade.
222
        $studentid = (int) $passstudent->id;
223
        $customcompletion = new custom_completion($cm, $studentid, $completioninfo->get_core_completion_state($cm, $studentid));
224
        $this->assertArrayHasKey('completionpassorattemptsexhausted', $cm->customdata['customcompletionrules']);
225
        $this->assertEquals(COMPLETION_COMPLETE, $customcompletion->get_state('completionpassorattemptsexhausted'));
226
        $this->assertEquals(
227
            'Receive a pass grade or complete all available attempts',
228
            $customcompletion->get_custom_rule_descriptions()['completionpassorattemptsexhausted']
229
        );
230
 
231
        // Do a failing attempt.
232
        $this->do_attempt_quiz([
233
            'quiz' => $quiz,
234
            'student' => $exhauststudent,
235
            'attemptnumber' => 1,
236
            'tosubmit' => [1 => ['answer' => '0']]
237
        ]);
238
 
239
        // Check the results. Quiz is not completed by $exhauststudent yet because of failing grade and of remaining attempts.
240
        $studentid = (int) $exhauststudent->id;
241
        $customcompletion = new custom_completion($cm, $studentid, $completioninfo->get_core_completion_state($cm, $studentid));
242
        $this->assertArrayHasKey('completionpassorattemptsexhausted', $cm->customdata['customcompletionrules']);
243
        $this->assertEquals(COMPLETION_INCOMPLETE, $customcompletion->get_state('completionpassorattemptsexhausted'));
244
        $this->assertEquals(
245
            'Receive a pass grade or complete all available attempts',
246
            $customcompletion->get_custom_rule_descriptions()['completionpassorattemptsexhausted']
247
        );
248
 
249
        // Do a second failing attempt.
250
        $this->do_attempt_quiz([
251
            'quiz' => $quiz,
252
            'student' => $exhauststudent,
253
            'attemptnumber' => 2,
254
            'tosubmit' => [1 => ['answer' => '0']]
255
        ]);
256
 
257
        // Check the results. Quiz is completed by $exhauststudent because there are no remaining attempts.
258
        $customcompletion = new custom_completion($cm, $studentid, $completioninfo->get_core_completion_state($cm, $studentid));
259
        $this->assertArrayHasKey('completionpassorattemptsexhausted', $cm->customdata['customcompletionrules']);
260
        $this->assertEquals(COMPLETION_COMPLETE, $customcompletion->get_state('completionpassorattemptsexhausted'));
261
        $this->assertEquals(
262
            'Receive a pass grade or complete all available attempts',
263
            $customcompletion->get_custom_rule_descriptions()['completionpassorattemptsexhausted']
264
        );
265
 
266
    }
267
 
268
    /**
269
     * Test checking the completion state of a quiz.
270
     * To be completed, this quiz requires a minimum number of attempts.
271
     *
272
     * @covers ::get_state
273
     * @covers ::get_custom_rule_descriptions
274
     */
11 efrain 275
    public function test_completionminattempts(): void {
1 efrain 276
        list($students, $quiz, $cm) = $this->setup_quiz_for_testing_completion([
277
            'nbstudents' => 1,
278
            'qtype' => 'essay',
279
            'quizoptions' => [
280
                'completionminattemptsenabled' => 1,
281
                'completionminattempts' => 2
282
            ]
283
        ]);
284
 
285
        list($student) = $students;
286
 
287
        // Do a first attempt.
288
        $this->do_attempt_quiz([
289
            'quiz' => $quiz,
290
            'student' => $student,
291
            'attemptnumber' => 1,
292
            'tosubmit' => [1 => ['answer' => 'Lorem ipsum.', 'answerformat' => '1']]
293
        ]);
294
 
295
        // Check the results. Quiz is not completed yet because only one attempt was done.
296
        $customcompletion = new custom_completion($cm, (int) $student->id);
297
        $this->assertArrayHasKey('completionminattempts', $cm->customdata['customcompletionrules']);
298
        $this->assertEquals(COMPLETION_INCOMPLETE, $customcompletion->get_state('completionminattempts'));
299
        $this->assertEquals(
300
            'Make attempts: 2',
301
            $customcompletion->get_custom_rule_descriptions()['completionminattempts']
302
        );
303
 
304
        // Do a second attempt.
305
        $this->do_attempt_quiz([
306
            'quiz' => $quiz,
307
            'student' => $student,
308
            'attemptnumber' => 2,
309
            'tosubmit' => [1 => ['answer' => 'Lorem ipsum.', 'answerformat' => '1']]
310
        ]);
311
 
312
        // Check the results. Quiz is completed by $student because two attempts were done.
313
        $customcompletion = new custom_completion($cm, (int) $student->id);
314
        $this->assertArrayHasKey('completionminattempts', $cm->customdata['customcompletionrules']);
315
        $this->assertEquals(COMPLETION_COMPLETE, $customcompletion->get_state('completionminattempts'));
316
        $this->assertEquals(
317
            'Make attempts: 2',
318
            $customcompletion->get_custom_rule_descriptions()['completionminattempts']
319
        );
320
    }
321
 
322
    /**
323
     * Test for get_defined_custom_rules().
324
     *
325
     * @covers ::get_defined_custom_rules
326
     */
11 efrain 327
    public function test_get_defined_custom_rules(): void {
1 efrain 328
        $rules = custom_completion::get_defined_custom_rules();
329
        $this->assertCount(2, $rules);
330
        $this->assertEquals(
331
            ['completionpassorattemptsexhausted', 'completionminattempts'],
332
            $rules
333
        );
334
    }
335
 
336
    /**
337
     * Test update moduleinfo.
338
     *
339
     * @covers \update_moduleinfo
340
     */
11 efrain 341
    public function test_update_moduleinfo(): void {
1 efrain 342
        $this->setAdminUser();
343
        // We need lite cm object not a full cm because update_moduleinfo is not allow some properties to be updated.
344
        list($students, $quiz, $cm, $litecm) = $this->setup_quiz_for_testing_completion([
345
            'nbstudents' => 1,
346
            'qtype' => 'numerical',
347
            'nbquestions' => 2,
348
            'sumgrades' => 100,
349
            'questiondefaultmarks' => [20, 80],
350
            'quizoptions' => [
351
                'completionusegrade' => 1,
352
                'completionpassgrade' => 1,
353
                'completionview' => 0,
354
            ]
355
        ]);
356
        $course = $cm->get_course();
357
 
358
        list($student) = $students;
359
        // Do a first attempt with a pass marks = 20.
360
        $this->do_attempt_quiz([
361
            'quiz' => $quiz,
362
            'student' => $student,
363
            'attemptnumber' => 1,
364
            'tosubmit' => [1 => ['answer' => '3.14']]
365
        ]);
366
        $completioninfo = new \completion_info($course);
367
        $cminfo = \cm_info::create($cm);
368
        $completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id);
369
 
370
        // Check the results. Completion is fail because gradepass = 80.
371
        $this->assertEquals(COMPLETION_COMPLETE_FAIL, $completiondetails->get_details()['completionpassgrade']->status);
372
        $this->assertEquals(
373
            'Receive a passing grade',
374
            $completiondetails->get_details()['completionpassgrade']->description
375
        );
376
 
377
        // Update quiz with passgrade = 20 and use highest grade to calculate.
378
        $moduleinfo = $this->prepare_module_info($cm, $quiz, $course, 20, QUIZ_GRADEHIGHEST);
379
        update_moduleinfo($litecm, $moduleinfo, $course, null);
380
 
381
        $completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id);
382
 
383
        // Check the results. Completion is pass.
384
        $this->assertEquals(COMPLETION_COMPLETE_PASS, $completiondetails->get_details()['completionpassgrade']->status);
385
        $this->assertEquals(
386
            'Receive a passing grade',
387
            $completiondetails->get_details()['completionpassgrade']->description
388
        );
389
 
390
        // Do a second attempt with pass marks = 80.
391
        $this->do_attempt_quiz([
392
            'quiz' => $quiz,
393
            'student' => $student,
394
            'attemptnumber' => 2,
395
            'tosubmit' => [2 => ['answer' => '3.14']]
396
        ]);
397
 
398
        // Update quiz with gradepass = 80 and use highest grade to calculate completion.
399
        $moduleinfo = $this->prepare_module_info($cm, $quiz, $course, 80, QUIZ_GRADEHIGHEST);
400
        update_moduleinfo($litecm, $moduleinfo, $course, null);
401
 
402
        $completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id);
403
 
404
        // Check the results. Completion is pass.
405
        $this->assertEquals(COMPLETION_COMPLETE_PASS, $completiondetails->get_details()['completionpassgrade']->status);
406
        $this->assertEquals(
407
            'Receive a passing grade',
408
            $completiondetails->get_details()['completionpassgrade']->description
409
        );
410
 
411
        // Update quiz with gradepass = 80 and use average grade to calculate completion.
412
        $moduleinfo = $this->prepare_module_info($cm, $quiz, $course, 80, QUIZ_GRADEAVERAGE);
413
        update_moduleinfo($litecm, $moduleinfo, $course, null);
414
 
415
        $completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id);
416
 
417
        // Check the results. Completion is fail because student grade = 50.
418
        $this->assertEquals(COMPLETION_COMPLETE_FAIL, $completiondetails->get_details()['completionpassgrade']->status);
419
        $this->assertEquals(
420
            'Receive a passing grade',
421
            $completiondetails->get_details()['completionpassgrade']->description
422
        );
423
 
424
        // Update quiz with gradepass = 50 and use average grade to calculate completion.
425
        $moduleinfo = $this->prepare_module_info($cm, $quiz, $course, 50, QUIZ_GRADEAVERAGE);
426
        update_moduleinfo($litecm, $moduleinfo, $course, null);
427
 
428
        $completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id);
429
 
430
        // Check the results. Completion is pass.
431
        $this->assertEquals(COMPLETION_COMPLETE_PASS, $completiondetails->get_details()['completionpassgrade']->status);
432
        $this->assertEquals(
433
            'Receive a passing grade',
434
            $completiondetails->get_details()['completionpassgrade']->description
435
        );
436
 
437
        // Update quiz with gradepass = 50 and use first attempt grade to calculate completion.
438
        $moduleinfo = $this->prepare_module_info($cm, $quiz, $course, 50, QUIZ_ATTEMPTFIRST);
439
        update_moduleinfo($litecm, $moduleinfo, $course, null);
440
 
441
        $completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id);
442
 
443
        // Check the results. Completion is fail.
444
        $this->assertEquals(COMPLETION_COMPLETE_FAIL, $completiondetails->get_details()['completionpassgrade']->status);
445
        $this->assertEquals(
446
            'Receive a passing grade',
447
            $completiondetails->get_details()['completionpassgrade']->description
448
        );
449
        // Update quiz with gradepass = 50 and use last attempt grade to calculate completion.
450
        $moduleinfo = $this->prepare_module_info($cm, $quiz, $course, 50, QUIZ_ATTEMPTLAST);
451
        update_moduleinfo($litecm, $moduleinfo, $course, null);
452
 
453
        $completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id);
454
 
455
        // Check the results. Completion is fail.
456
        $this->assertEquals(COMPLETION_COMPLETE_PASS, $completiondetails->get_details()['completionpassgrade']->status);
457
        $this->assertEquals(
458
            'Receive a passing grade',
459
            $completiondetails->get_details()['completionpassgrade']->description
460
        );
461
    }
462
 
463
    /**
464
     * Set up moduleinfo object sample data for quiz instance.
465
     *
466
     * @param cm_info $cm course-module instance
467
     * @param stdClass $quiz quiz instance data.
468
     * @param stdClass $course Course related data.
469
     * @param int $gradepass Grade to pass and completed completion.
470
     * @param string $grademethod grade attempt method.
471
     * @return stdClass
472
     */
473
    private function prepare_module_info(cm_info $cm, stdClass $quiz, stdClass $course,
474
            int $gradepass, string $grademethod): \stdClass {
475
        $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]);
476
        // Module test values.
477
        $moduleinfo = new \stdClass();
478
        $moduleinfo->coursemodule = $cm->id;
479
        $moduleinfo->section = 1;
480
        $moduleinfo->course = $course->id;
481
        $moduleinfo->groupingid = $grouping->id;
482
        $draftideditor = 0;
483
        file_prepare_draft_area($draftideditor, null, null, null, null);
484
        $moduleinfo->introeditor = ['text' => 'This is a module', 'format' => FORMAT_HTML, 'itemid' => $draftideditor];
485
        $moduleinfo->modulename = 'quiz';
486
        $moduleinfo->quizpassword = '';
487
        $moduleinfo->cmidnumber = '';
488
        $moduleinfo->maxmarksopen = 1;
489
        $moduleinfo->marksopen = 1;
490
        $moduleinfo->visible = 1;
491
        $moduleinfo->visibleoncoursepage = 1;
492
        $moduleinfo->completion = COMPLETION_TRACKING_AUTOMATIC;
493
        $moduleinfo->completionview = COMPLETION_VIEW_NOT_REQUIRED;
494
        $moduleinfo->name = $quiz->name;
495
        $moduleinfo->timeopen = $quiz->timeopen;
496
        $moduleinfo->timeclose = $quiz->timeclose;
497
        $moduleinfo->timelimit = $quiz->timelimit;
498
        $moduleinfo->graceperiod = $quiz->graceperiod;
499
        $moduleinfo->decimalpoints = $quiz->decimalpoints;
500
        $moduleinfo->questiondecimalpoints = $quiz->questiondecimalpoints;
501
        $moduleinfo->gradepass = $gradepass;
502
        $moduleinfo->grademethod = $grademethod;
503
 
504
        return $moduleinfo;
505
    }
506
}