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
/**
18
 * Lesson module external functions tests
19
 *
20
 * @package    mod_lesson
21
 * @category   external
22
 * @copyright  2017 Juan Leyva <juan@moodle.com>
23
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24
 * @since      Moodle 3.3
25
 */
26
 
27
namespace mod_lesson\external;
28
 
29
use externallib_advanced_testcase;
30
use mod_lesson_external;
31
use lesson;
32
use core_external\external_api;
33
use core_external\external_settings;
34
 
35
defined('MOODLE_INTERNAL') || die();
36
 
37
global $CFG;
38
 
39
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
40
require_once($CFG->dirroot . '/mod/lesson/locallib.php');
41
 
42
/**
43
 * Silly class to access mod_lesson_external internal methods.
44
 *
45
 * @package mod_lesson
46
 * @copyright 2017 Juan Leyva <juan@moodle.com>
47
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
48
 * @since  Moodle 3.3
49
 */
50
class testable_mod_lesson_external extends mod_lesson_external {
51
 
52
    /**
53
     * Validates a new attempt.
54
     *
55
     * @param  lesson  $lesson lesson instance
56
     * @param  array   $params request parameters
57
     * @param  boolean $return whether to return the errors or throw exceptions
58
     * @return [array          the errors (if return set to true)
59
     * @since  Moodle 3.3
60
     */
61
    public static function validate_attempt(lesson $lesson, $params, $return = false) {
62
        return parent::validate_attempt($lesson, $params, $return);
63
    }
64
}
65
 
66
/**
67
 * Lesson module external functions tests
68
 *
69
 * @package    mod_lesson
70
 * @category   external
71
 * @copyright  2017 Juan Leyva <juan@moodle.com>
72
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
73
 * @since      Moodle 3.3
74
 */
1441 ariadna 75
final class external_test extends externallib_advanced_testcase {
1 efrain 76
 
77
    /** @var \stdClass course record. */
78
    protected \stdClass $course;
79
 
80
    /** @var \stdClass */
81
    protected \stdClass $lesson;
82
 
83
    /** @var \stdClass a fieldset object, false or exception if error not found. */
84
    protected \stdClass $page1;
85
 
86
    /** @var \stdClass a fieldset object false or exception if error not found. */
87
    protected $page2;
88
 
89
    /** @var \core\context\module context instance. */
90
    protected \core\context\module $context;
91
 
92
    /** @var \stdClass */
93
    protected \stdClass $cm;
94
 
95
    /** @var \stdClass user record. */
96
    protected \stdClass $student;
97
 
98
    /** @var \stdClass user record. */
99
    protected \stdClass $teacher;
100
 
101
    /** @var \stdClass a fieldset object, false or exception if error not found. */
102
    protected \stdClass $studentrole;
103
 
104
    /** @var \stdClass a fieldset object, false or exception if error not found. */
105
    protected \stdClass $teacherrole;
106
 
107
    /**
108
     * Set up for every test
109
     */
110
    public function setUp(): void {
111
        global $DB;
1441 ariadna 112
        parent::setUp();
1 efrain 113
        $this->resetAfterTest();
114
        $this->setAdminUser();
115
 
116
        // Setup test data.
117
        $this->course = $this->getDataGenerator()->create_course();
118
        $this->lesson = $this->getDataGenerator()->create_module('lesson', array('course' => $this->course->id));
119
        $lessongenerator = $this->getDataGenerator()->get_plugin_generator('mod_lesson');
120
        $this->page1 = $lessongenerator->create_content($this->lesson);
121
        $this->page2 = $lessongenerator->create_question_truefalse($this->lesson);
122
        $this->context = \context_module::instance($this->lesson->cmid);
123
        $this->cm = get_coursemodule_from_instance('lesson', $this->lesson->id);
124
 
125
        // Create users.
126
        $this->student = self::getDataGenerator()->create_user();
127
        $this->teacher = self::getDataGenerator()->create_user();
128
 
129
        // Users enrolments.
130
        $this->studentrole = $DB->get_record('role', array('shortname' => 'student'));
131
        $this->teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
132
        $this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->studentrole->id, 'manual');
133
        $this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $this->teacherrole->id, 'manual');
134
    }
135
 
136
 
137
    /**
138
     * Test test_mod_lesson_get_lessons_by_courses
139
     */
11 efrain 140
    public function test_mod_lesson_get_lessons_by_courses(): void {
1 efrain 141
        global $DB;
142
 
143
        // Create additional course.
144
        $course2 = self::getDataGenerator()->create_course();
145
 
146
        // Second lesson.
147
        $record = new \stdClass();
148
        $record->course = $course2->id;
149
        $record->name = '<span lang="en" class="multilang">English</span><span lang="es" class="multilang">Español</span>';
150
        $lesson2 = self::getDataGenerator()->create_module('lesson', $record);
151
 
152
        // Execute real Moodle enrolment as we'll call unenrol() method on the instance later.
153
        $enrol = enrol_get_plugin('manual');
154
        $enrolinstances = enrol_get_instances($course2->id, true);
155
        foreach ($enrolinstances as $courseenrolinstance) {
156
            if ($courseenrolinstance->enrol == "manual") {
157
                $instance2 = $courseenrolinstance;
158
                break;
159
            }
160
        }
161
        $enrol->enrol_user($instance2, $this->student->id, $this->studentrole->id);
162
 
163
        self::setUser($this->student);
164
 
165
        // Enable multilang filter to on content and heading.
166
        \filter_manager::reset_caches();
167
        filter_set_global_state('multilang', TEXTFILTER_ON);
168
        filter_set_applies_to_strings('multilang', true);
169
        // Set WS filtering.
170
        $wssettings = external_settings::get_instance();
171
        $wssettings->set_filter(true);
172
 
173
        $returndescription = mod_lesson_external::get_lessons_by_courses_returns();
174
 
175
        // Create what we expect to be returned when querying the two courses.
176
        // First for the student user.
177
        $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'lang',
178
                                'practice', 'modattempts', 'usepassword', 'grade', 'custom', 'ongoing', 'usemaxgrade',
179
                                'maxanswers', 'maxattempts', 'review', 'nextpagedefault', 'feedback', 'minquestions',
180
                                'maxpages', 'timelimit', 'retake', 'mediafile', 'mediafiles', 'mediaheight', 'mediawidth',
181
                                'mediaclose', 'slideshow', 'width', 'height', 'bgcolor', 'displayleft', 'displayleftif',
182
                                'progressbar', 'allowofflineattempts');
183
 
184
        // Add expected coursemodule and data.
185
        $lesson1 = $this->lesson;
186
        $lesson1->coursemodule = $lesson1->cmid;
187
        $lesson1->introformat = 1;
188
        $lesson1->introfiles = [];
189
        $lesson1->mediafiles = [];
190
        $lesson1->lang = '';
191
 
192
        $lesson2->coursemodule = $lesson2->cmid;
193
        $lesson2->introformat = 1;
194
        $lesson2->introfiles = [];
195
        $lesson2->mediafiles = [];
196
        $lesson2->lang = '';
197
 
198
        $booltypes = array('practice', 'modattempts', 'usepassword', 'custom', 'ongoing', 'review', 'feedback', 'retake',
199
            'slideshow', 'displayleft', 'progressbar', 'allowofflineattempts');
200
 
201
        foreach ($expectedfields as $field) {
202
            if (in_array($field, $booltypes)) {
203
                $lesson1->{$field} = (bool) $lesson1->{$field};
204
                $lesson2->{$field} = (bool) $lesson2->{$field};
205
            }
206
            $expected1[$field] = $lesson1->{$field};
207
            $expected2[$field] = $lesson2->{$field};
208
        }
209
 
210
        $expected2['name'] = 'English';  // Lang filtered expected.
211
        $expectedlessons = array($expected2, $expected1);
212
 
213
        // Call the external function passing course ids.
214
        $result = mod_lesson_external::get_lessons_by_courses(array($course2->id, $this->course->id));
215
        $result = external_api::clean_returnvalue($returndescription, $result);
216
 
217
        $this->assertEquals($expectedlessons, $result['lessons']);
218
        $this->assertCount(0, $result['warnings']);
219
 
220
        // Call the external function without passing course id.
221
        $result = mod_lesson_external::get_lessons_by_courses();
222
        $result = external_api::clean_returnvalue($returndescription, $result);
223
        $this->assertEquals($expectedlessons, $result['lessons']);
224
        $this->assertCount(0, $result['warnings']);
225
 
226
        // Unenrol user from second course and alter expected lessons.
227
        $enrol->unenrol_user($instance2, $this->student->id);
228
        array_shift($expectedlessons);
229
 
230
        // Call the external function without passing course id.
231
        $result = mod_lesson_external::get_lessons_by_courses();
232
        $result = external_api::clean_returnvalue($returndescription, $result);
233
        $this->assertEquals($expectedlessons, $result['lessons']);
234
 
235
        // Call for the second course we unenrolled the user from, expected warning.
236
        $result = mod_lesson_external::get_lessons_by_courses(array($course2->id));
237
        $this->assertCount(1, $result['warnings']);
238
        $this->assertEquals('1', $result['warnings'][0]['warningcode']);
239
        $this->assertEquals($course2->id, $result['warnings'][0]['itemid']);
240
 
241
        // Now, try as a teacher for getting all the additional fields.
242
        self::setUser($this->teacher);
243
 
244
        $additionalfields = array('password', 'dependency', 'conditions', 'activitylink', 'available', 'deadline',
245
                                    'timemodified', 'completionendreached', 'completiontimespent');
246
 
247
        foreach ($additionalfields as $field) {
248
            $expectedlessons[0][$field] = $lesson1->{$field};
249
        }
250
 
251
        $result = mod_lesson_external::get_lessons_by_courses();
252
        $result = external_api::clean_returnvalue($returndescription, $result);
253
        $this->assertEquals($expectedlessons, $result['lessons']);
254
 
255
        // Admin also should get all the information.
256
        self::setAdminUser();
257
 
258
        $result = mod_lesson_external::get_lessons_by_courses(array($this->course->id));
259
        $result = external_api::clean_returnvalue($returndescription, $result);
260
        $this->assertEquals($expectedlessons, $result['lessons']);
261
 
262
        // Now, add a restriction.
263
        $this->setUser($this->student);
264
        $DB->set_field('lesson', 'usepassword', 1, array('id' => $lesson1->id));
265
        $DB->set_field('lesson', 'password', 'abc', array('id' => $lesson1->id));
266
 
267
        $lessons = mod_lesson_external::get_lessons_by_courses(array($this->course->id));
268
        $lessons = external_api::clean_returnvalue(mod_lesson_external::get_lessons_by_courses_returns(), $lessons);
269
        $this->assertFalse(isset($lessons['lessons'][0]['intro']));
270
    }
271
 
272
    /**
273
     * Test the validate_attempt function.
274
     */
11 efrain 275
    public function test_validate_attempt(): void {
1 efrain 276
        global $DB;
277
 
278
        $this->setUser($this->student);
279
        // Test deadline.
280
        $oldtime = time() - DAYSECS;
281
        $DB->set_field('lesson', 'deadline', $oldtime, array('id' => $this->lesson->id));
282
 
283
        $lesson = new lesson($DB->get_record('lesson', array('id' => $this->lesson->id)));
284
        $validation = testable_mod_lesson_external::validate_attempt($lesson, ['password' => ''], true);
285
        $this->assertEquals('lessonclosed', key($validation));
286
        $this->assertCount(1, $validation);
287
 
288
        // Test not available yet.
289
        $futuretime = time() + DAYSECS;
290
        $DB->set_field('lesson', 'deadline', 0, array('id' => $this->lesson->id));
291
        $DB->set_field('lesson', 'available', $futuretime, array('id' => $this->lesson->id));
292
 
293
        $lesson = new lesson($DB->get_record('lesson', array('id' => $this->lesson->id)));
294
        $validation = testable_mod_lesson_external::validate_attempt($lesson, ['password' => ''], true);
295
        $this->assertEquals('lessonopen', key($validation));
296
        $this->assertCount(1, $validation);
297
 
298
        // Test password.
299
        $DB->set_field('lesson', 'deadline', 0, array('id' => $this->lesson->id));
300
        $DB->set_field('lesson', 'available', 0, array('id' => $this->lesson->id));
301
        $DB->set_field('lesson', 'usepassword', 1, array('id' => $this->lesson->id));
302
        $DB->set_field('lesson', 'password', 'abc', array('id' => $this->lesson->id));
303
 
304
        $lesson = new lesson($DB->get_record('lesson', array('id' => $this->lesson->id)));
305
        $validation = testable_mod_lesson_external::validate_attempt($lesson, ['password' => ''], true);
306
        $this->assertEquals('passwordprotectedlesson', key($validation));
307
        $this->assertCount(1, $validation);
308
 
309
        $lesson = new lesson($DB->get_record('lesson', array('id' => $this->lesson->id)));
310
        $validation = testable_mod_lesson_external::validate_attempt($lesson, ['password' => 'abc'], true);
311
        $this->assertCount(0, $validation);
312
 
313
        // Dependencies.
314
        $record = new \stdClass();
315
        $record->course = $this->course->id;
316
        $lesson2 = self::getDataGenerator()->create_module('lesson', $record);
317
        $DB->set_field('lesson', 'usepassword', 0, array('id' => $this->lesson->id));
318
        $DB->set_field('lesson', 'password', '', array('id' => $this->lesson->id));
319
        $DB->set_field('lesson', 'dependency', $lesson->id, array('id' => $this->lesson->id));
320
 
321
        $lesson = new lesson($DB->get_record('lesson', array('id' => $this->lesson->id)));
322
        $lesson->conditions = serialize((object) ['completed' => true, 'timespent' => 0, 'gradebetterthan' => 0]);
323
        $validation = testable_mod_lesson_external::validate_attempt($lesson, ['password' => ''], true);
324
        $this->assertEquals('completethefollowingconditions', key($validation));
325
        $this->assertCount(1, $validation);
326
 
327
        // Lesson withou pages.
328
        $lesson = new lesson($lesson2);
329
        $validation = testable_mod_lesson_external::validate_attempt($lesson, ['password' => ''], true);
330
        $this->assertEquals('lessonnotready2', key($validation));
331
        $this->assertCount(1, $validation);
332
 
333
        // Test retakes.
334
        $DB->set_field('lesson', 'dependency', 0, array('id' => $this->lesson->id));
335
        $DB->set_field('lesson', 'retake', 0, array('id' => $this->lesson->id));
336
        $record = [
337
            'lessonid' => $this->lesson->id,
338
            'userid' => $this->student->id,
339
            'grade' => 100,
340
            'late' => 0,
341
            'completed' => 1,
342
        ];
343
        $DB->insert_record('lesson_grades', (object) $record);
344
        $lesson = new lesson($DB->get_record('lesson', array('id' => $this->lesson->id)));
345
        $validation = testable_mod_lesson_external::validate_attempt($lesson, ['password' => ''], true);
346
        $this->assertEquals('noretake', key($validation));
347
        $this->assertCount(1, $validation);
348
 
349
        // Test time limit restriction.
350
        $timenow = time();
351
        // Create a timer for the current user.
352
        $timer1 = new \stdClass;
353
        $timer1->lessonid = $this->lesson->id;
354
        $timer1->userid = $this->student->id;
355
        $timer1->completed = 0;
356
        $timer1->starttime = $timenow - DAYSECS;
357
        $timer1->lessontime = $timenow;
358
        $timer1->id = $DB->insert_record("lesson_timer", $timer1);
359
 
360
        // Out of time.
361
        $DB->set_field('lesson', 'timelimit', HOURSECS, array('id' => $this->lesson->id));
362
        $lesson = new lesson($DB->get_record('lesson', array('id' => $this->lesson->id)));
363
        $validation = testable_mod_lesson_external::validate_attempt($lesson, ['password' => '', 'pageid' => 1], true);
364
        $this->assertEquals('eolstudentoutoftime', key($validation));
365
        $this->assertCount(1, $validation);
366
    }
367
 
368
    /**
369
     * Test the get_lesson_access_information function.
370
     */
11 efrain 371
    public function test_get_lesson_access_information(): void {
1 efrain 372
        global $DB;
373
 
374
        $this->setUser($this->student);
375
        // Add previous attempt.
376
        $record = [
377
            'lessonid' => $this->lesson->id,
378
            'userid' => $this->student->id,
379
            'grade' => 100,
380
            'late' => 0,
381
            'completed' => 1,
382
        ];
383
        $DB->insert_record('lesson_grades', (object) $record);
384
 
385
        $result = mod_lesson_external::get_lesson_access_information($this->lesson->id);
386
        $result = external_api::clean_returnvalue(mod_lesson_external::get_lesson_access_information_returns(), $result);
387
        $this->assertFalse($result['canmanage']);
388
        $this->assertFalse($result['cangrade']);
389
        $this->assertFalse($result['canviewreports']);
390
 
391
        $this->assertFalse($result['leftduringtimedsession']);
392
        $this->assertEquals(1, $result['reviewmode']);
393
        $this->assertEquals(1, $result['attemptscount']);
394
        $this->assertEquals(0, $result['lastpageseen']);
395
        $this->assertEquals($this->page2->id, $result['firstpageid']);
396
        $this->assertCount(1, $result['preventaccessreasons']);
397
        $this->assertEquals('noretake', $result['preventaccessreasons'][0]['reason']);
398
        $this->assertEquals(null, $result['preventaccessreasons'][0]['data']);
399
        $this->assertEquals(get_string('noretake', 'lesson'), $result['preventaccessreasons'][0]['message']);
400
 
401
        // Now check permissions as admin.
402
        $this->setAdminUser();
403
        $result = mod_lesson_external::get_lesson_access_information($this->lesson->id);
404
        $result = external_api::clean_returnvalue(mod_lesson_external::get_lesson_access_information_returns(), $result);
405
        $this->assertTrue($result['canmanage']);
406
        $this->assertTrue($result['cangrade']);
407
        $this->assertTrue($result['canviewreports']);
408
    }
409
 
410
    /**
411
     * Test test_view_lesson invalid id.
412
     */
11 efrain 413
    public function test_view_lesson_invalid_id(): void {
1 efrain 414
        $this->expectException('moodle_exception');
415
        mod_lesson_external::view_lesson(0);
416
    }
417
 
418
    /**
419
     * Test test_view_lesson user not enrolled.
420
     */
11 efrain 421
    public function test_view_lesson_user_not_enrolled(): void {
1 efrain 422
        // Test not-enrolled user.
423
        $usernotenrolled = self::getDataGenerator()->create_user();
424
        $this->setUser($usernotenrolled);
425
        $this->expectException('moodle_exception');
426
        mod_lesson_external::view_lesson($this->lesson->id);
427
    }
428
 
429
    /**
430
     * Test test_view_lesson user student.
431
     */
11 efrain 432
    public function test_view_lesson_user_student(): void {
1 efrain 433
        // Test user with full capabilities.
434
        $this->setUser($this->student);
435
 
436
        // Trigger and capture the event.
437
        $sink = $this->redirectEvents();
438
 
439
        $result = mod_lesson_external::view_lesson($this->lesson->id);
440
        $result = external_api::clean_returnvalue(mod_lesson_external::view_lesson_returns(), $result);
441
        $this->assertTrue($result['status']);
442
 
443
        $events = $sink->get_events();
444
        $this->assertCount(1, $events);
445
        $event = array_shift($events);
446
 
447
        // Checking that the event contains the expected values.
448
        $this->assertInstanceOf('\mod_lesson\event\course_module_viewed', $event);
449
        $this->assertEquals($this->context, $event->get_context());
450
        $moodlelesson = new \moodle_url('/mod/lesson/view.php', array('id' => $this->cm->id));
451
        $this->assertEquals($moodlelesson, $event->get_url());
452
        $this->assertEventContextNotUsed($event);
453
        $this->assertNotEmpty($event->get_name());
454
    }
455
 
456
    /**
457
     * Test test_view_lesson user missing capabilities.
458
     */
11 efrain 459
    public function test_view_lesson_user_missing_capabilities(): void {
1 efrain 460
        // Test user with no capabilities.
461
        // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles.
462
        assign_capability('mod/lesson:view', CAP_PROHIBIT, $this->studentrole->id, $this->context->id);
463
        // Empty all the caches that may be affected  by this change.
464
        accesslib_clear_all_caches_for_unit_testing();
465
        \course_modinfo::clear_instance_cache();
466
 
467
        $this->setUser($this->student);
468
        $this->expectException('moodle_exception');
469
        mod_lesson_external::view_lesson($this->lesson->id);
470
    }
471
 
472
    /**
473
     * Test for get_questions_attempts
474
     */
11 efrain 475
    public function test_get_questions_attempts(): void {
1 efrain 476
        global $DB;
477
 
478
        $this->setUser($this->student);
479
        $attemptnumber = 1;
480
 
481
        // Test lesson without page attempts.
482
        $result = mod_lesson_external::get_questions_attempts($this->lesson->id, $attemptnumber);
483
        $result = external_api::clean_returnvalue(mod_lesson_external::get_questions_attempts_returns(), $result);
484
        $this->assertCount(0, $result['warnings']);
485
        $this->assertCount(0, $result['attempts']);
486
 
487
        // Create a fake attempt for the first possible answer.
488
        $p2answers = $DB->get_records('lesson_answers', array('lessonid' => $this->lesson->id, 'pageid' => $this->page2->id), 'id');
489
        $answerid = reset($p2answers)->id;
490
 
491
        $newpageattempt = [
492
            'lessonid' => $this->lesson->id,
493
            'pageid' => $this->page2->id,
494
            'userid' => $this->student->id,
495
            'answerid' => $answerid,
496
            'retry' => $attemptnumber,
497
            'correct' => 1,
498
            'useranswer' => '1',
499
            'timeseen' => time(),
500
        ];
501
        $DB->insert_record('lesson_attempts', (object) $newpageattempt);
502
 
503
        $result = mod_lesson_external::get_questions_attempts($this->lesson->id, $attemptnumber);
504
        $result = external_api::clean_returnvalue(mod_lesson_external::get_questions_attempts_returns(), $result);
505
        $this->assertCount(0, $result['warnings']);
506
        $this->assertCount(1, $result['attempts']);
507
 
508
        $newpageattempt['id'] = $result['attempts'][0]['id'];
509
        $this->assertEquals($newpageattempt, $result['attempts'][0]);
510
 
511
        // Test filtering. Only correct.
512
        $result = mod_lesson_external::get_questions_attempts($this->lesson->id, $attemptnumber, true);
513
        $result = external_api::clean_returnvalue(mod_lesson_external::get_questions_attempts_returns(), $result);
514
        $this->assertCount(0, $result['warnings']);
515
        $this->assertCount(1, $result['attempts']);
516
 
517
        // Test filtering. Only correct only for page 2.
518
        $result = mod_lesson_external::get_questions_attempts($this->lesson->id, $attemptnumber, true, $this->page2->id);
519
        $result = external_api::clean_returnvalue(mod_lesson_external::get_questions_attempts_returns(), $result);
520
        $this->assertCount(0, $result['warnings']);
521
        $this->assertCount(1, $result['attempts']);
522
 
523
        // Teacher retrieve student page attempts.
524
        $this->setUser($this->teacher);
525
        $result = mod_lesson_external::get_questions_attempts($this->lesson->id, $attemptnumber, false, null, $this->student->id);
526
        $result = external_api::clean_returnvalue(mod_lesson_external::get_questions_attempts_returns(), $result);
527
        $this->assertCount(0, $result['warnings']);
528
        $this->assertCount(1, $result['attempts']);
529
 
530
        // Test exception.
531
        $this->setUser($this->student);
532
        $this->expectException('moodle_exception');
533
        $result = mod_lesson_external::get_questions_attempts($this->lesson->id, $attemptnumber, false, null, $this->teacher->id);
534
    }
535
 
536
    /**
537
     * Test get user grade.
538
     */
11 efrain 539
    public function test_get_user_grade(): void {
1 efrain 540
        global $DB;
541
 
542
        // Add grades for the user.
543
        $newgrade = [
544
            'lessonid' => $this->lesson->id,
545
            'userid' => $this->student->id,
546
            'grade' => 50,
547
            'late' => 0,
548
            'completed' => time(),
549
        ];
550
        $DB->insert_record('lesson_grades', (object) $newgrade);
551
 
552
        $newgrade = [
553
            'lessonid' => $this->lesson->id,
554
            'userid' => $this->student->id,
555
            'grade' => 100,
556
            'late' => 0,
557
            'completed' => time(),
558
        ];
559
        $DB->insert_record('lesson_grades', (object) $newgrade);
560
 
561
        $this->setUser($this->student);
562
 
563
        // Test lesson without multiple attemps. The first result must be returned.
564
        $result = mod_lesson_external::get_user_grade($this->lesson->id);
565
        $result = external_api::clean_returnvalue(mod_lesson_external::get_user_grade_returns(), $result);
566
        $this->assertCount(0, $result['warnings']);
567
        $this->assertEquals(50, $result['grade']);
568
        $this->assertEquals('50.00', $result['formattedgrade']);
569
 
570
        // With retakes. By default average.
571
        $DB->set_field('lesson', 'retake', 1, array('id' => $this->lesson->id));
572
        $result = mod_lesson_external::get_user_grade($this->lesson->id, $this->student->id);
573
        $result = external_api::clean_returnvalue(mod_lesson_external::get_user_grade_returns(), $result);
574
        $this->assertCount(0, $result['warnings']);
575
        $this->assertEquals(75, $result['grade']);
576
        $this->assertEquals('75.00', $result['formattedgrade']);
577
 
578
        // With retakes. With max grade setting.
579
        $DB->set_field('lesson', 'usemaxgrade', 1, array('id' => $this->lesson->id));
580
        $result = mod_lesson_external::get_user_grade($this->lesson->id, $this->student->id);
581
        $result = external_api::clean_returnvalue(mod_lesson_external::get_user_grade_returns(), $result);
582
        $this->assertCount(0, $result['warnings']);
583
        $this->assertEquals(100, $result['grade']);
584
        $this->assertEquals('100.00', $result['formattedgrade']);
585
 
586
        // Test as teacher we get the same result.
587
        $this->setUser($this->teacher);
588
        $result = mod_lesson_external::get_user_grade($this->lesson->id, $this->student->id);
589
        $result = external_api::clean_returnvalue(mod_lesson_external::get_user_grade_returns(), $result);
590
        $this->assertCount(0, $result['warnings']);
591
        $this->assertEquals(100, $result['grade']);
592
        $this->assertEquals('100.00', $result['formattedgrade']);
593
 
594
        // Test exception. As student try to retrieve grades from teacher.
595
        $this->setUser($this->student);
596
        $this->expectException('moodle_exception');
597
        $result = mod_lesson_external::get_user_grade($this->lesson->id, $this->teacher->id);
598
    }
599
 
600
    /**
601
     * Test get_user_attempt_grade
602
     */
11 efrain 603
    public function test_get_user_attempt_grade(): void {
1 efrain 604
        global $DB;
605
 
606
        // Create a fake attempt for the first possible answer.
607
        $attemptnumber = 1;
608
        $p2answers = $DB->get_records('lesson_answers', array('lessonid' => $this->lesson->id, 'pageid' => $this->page2->id), 'id');
609
        $answerid = reset($p2answers)->id;
610
 
611
        $newpageattempt = [
612
            'lessonid' => $this->lesson->id,
613
            'pageid' => $this->page2->id,
614
            'userid' => $this->student->id,
615
            'answerid' => $answerid,
616
            'retry' => $attemptnumber,
617
            'correct' => 1,
618
            'useranswer' => '1',
619
            'timeseen' => time(),
620
        ];
621
        $DB->insert_record('lesson_attempts', (object) $newpageattempt);
622
 
623
        // Test first without custom scoring. All questions receive the same value if correctly responsed.
624
        $DB->set_field('lesson', 'custom', 0, array('id' => $this->lesson->id));
625
        $this->setUser($this->student);
626
        $result = mod_lesson_external::get_user_attempt_grade($this->lesson->id, $attemptnumber, $this->student->id);
627
        $result = external_api::clean_returnvalue(mod_lesson_external::get_user_attempt_grade_returns(), $result);
628
        $this->assertCount(0, $result['warnings']);
629
        $this->assertEquals(1, $result['grade']['nquestions']);
630
        $this->assertEquals(1, $result['grade']['attempts']);
631
        $this->assertEquals(1, $result['grade']['total']);
632
        $this->assertEquals(1, $result['grade']['earned']);
633
        $this->assertEquals(100, $result['grade']['grade']);
634
        $this->assertEquals(0, $result['grade']['nmanual']);
635
        $this->assertEquals(0, $result['grade']['manualpoints']);
636
 
637
        // With custom scoring, in this case, we don't retrieve any values since we are using questions without particular score.
638
        $DB->set_field('lesson', 'custom', 1, array('id' => $this->lesson->id));
639
        $result = mod_lesson_external::get_user_attempt_grade($this->lesson->id, $attemptnumber, $this->student->id);
640
        $result = external_api::clean_returnvalue(mod_lesson_external::get_user_attempt_grade_returns(), $result);
641
        $this->assertCount(0, $result['warnings']);
642
        $this->assertEquals(1, $result['grade']['nquestions']);
643
        $this->assertEquals(1, $result['grade']['attempts']);
644
        $this->assertEquals(0, $result['grade']['total']);
645
        $this->assertEquals(0, $result['grade']['earned']);
646
        $this->assertEquals(0, $result['grade']['grade']);
647
        $this->assertEquals(0, $result['grade']['nmanual']);
648
        $this->assertEquals(0, $result['grade']['manualpoints']);
649
    }
650
 
651
    /**
652
     * Test get_content_pages_viewed
653
     */
11 efrain 654
    public function test_get_content_pages_viewed(): void {
1 efrain 655
        global $DB;
656
 
657
        // Create another content pages.
658
        $lessongenerator = $this->getDataGenerator()->get_plugin_generator('mod_lesson');
659
        $page3 = $lessongenerator->create_content($this->lesson);
660
 
661
        $branch1 = new \stdClass;
662
        $branch1->lessonid = $this->lesson->id;
663
        $branch1->userid = $this->student->id;
664
        $branch1->pageid = $this->page1->id;
665
        $branch1->retry = 1;
666
        $branch1->flag = 0;
667
        $branch1->timeseen = time();
668
        $branch1->nextpageid = $page3->id;
669
        $branch1->id = $DB->insert_record("lesson_branch", $branch1);
670
 
671
        $branch2 = new \stdClass;
672
        $branch2->lessonid = $this->lesson->id;
673
        $branch2->userid = $this->student->id;
674
        $branch2->pageid = $page3->id;
675
        $branch2->retry = 1;
676
        $branch2->flag = 0;
677
        $branch2->timeseen = time() + 1;
678
        $branch2->nextpageid = 0;
679
        $branch2->id = $DB->insert_record("lesson_branch", $branch2);
680
 
681
        // Test first attempt.
682
        $result = mod_lesson_external::get_content_pages_viewed($this->lesson->id, 1, $this->student->id);
683
        $result = external_api::clean_returnvalue(mod_lesson_external::get_content_pages_viewed_returns(), $result);
684
        $this->assertCount(0, $result['warnings']);
685
        $this->assertCount(2, $result['pages']);
686
        foreach ($result['pages'] as $page) {
687
            if ($page['id'] == $branch1->id) {
688
                $this->assertEquals($branch1, (object) $page);
689
            } else {
690
                $this->assertEquals($branch2, (object) $page);
691
            }
692
        }
693
 
694
        // Attempt without pages viewed.
695
        $result = mod_lesson_external::get_content_pages_viewed($this->lesson->id, 3, $this->student->id);
696
        $result = external_api::clean_returnvalue(mod_lesson_external::get_content_pages_viewed_returns(), $result);
697
        $this->assertCount(0, $result['warnings']);
698
        $this->assertCount(0, $result['pages']);
699
    }
700
 
701
    /**
702
     * Test get_user_timers
703
     */
11 efrain 704
    public function test_get_user_timers(): void {
1 efrain 705
        global $DB;
706
 
707
        // Create a couple of timers for the current user.
708
        $timer1 = new \stdClass;
709
        $timer1->lessonid = $this->lesson->id;
710
        $timer1->userid = $this->student->id;
711
        $timer1->completed = 1;
712
        $timer1->starttime = time() - WEEKSECS;
713
        $timer1->lessontime = time();
714
        $timer1->timemodifiedoffline = time();
715
        $timer1->id = $DB->insert_record("lesson_timer", $timer1);
716
 
717
        $timer2 = new \stdClass;
718
        $timer2->lessonid = $this->lesson->id;
719
        $timer2->userid = $this->student->id;
720
        $timer2->completed = 0;
721
        $timer2->starttime = time() - DAYSECS;
722
        $timer2->lessontime = time() + 1;
723
        $timer2->timemodifiedoffline = time() + 1;
724
        $timer2->id = $DB->insert_record("lesson_timer", $timer2);
725
 
726
        // Test retrieve timers.
727
        $result = mod_lesson_external::get_user_timers($this->lesson->id, $this->student->id);
728
        $result = external_api::clean_returnvalue(mod_lesson_external::get_user_timers_returns(), $result);
729
        $this->assertCount(0, $result['warnings']);
730
        $this->assertCount(2, $result['timers']);
731
        foreach ($result['timers'] as $timer) {
732
            if ($timer['id'] == $timer1->id) {
733
                $this->assertEquals($timer1, (object) $timer);
734
            } else {
735
                $this->assertEquals($timer2, (object) $timer);
736
            }
737
        }
738
    }
739
 
740
    /**
741
     * Test for get_pages
742
     */
11 efrain 743
    public function test_get_pages(): void {
1 efrain 744
        global $DB;
745
 
746
        $this->setAdminUser();
747
        // Create another content page.
748
        $lessongenerator = $this->getDataGenerator()->get_plugin_generator('mod_lesson');
749
        $page3 = $lessongenerator->create_content($this->lesson);
750
 
751
        $p2answers = $DB->get_records('lesson_answers', array('lessonid' => $this->lesson->id, 'pageid' => $this->page2->id), 'id');
752
 
753
        // Add files everywhere.
754
        $fs = get_file_storage();
755
 
756
        $filerecord = array(
757
            'contextid' => $this->context->id,
758
            'component' => 'mod_lesson',
759
            'filearea'  => 'page_contents',
760
            'itemid'    => $this->page1->id,
761
            'filepath'  => '/',
762
            'filename'  => 'file.txt',
763
            'sortorder' => 1
764
        );
765
        $fs->create_file_from_string($filerecord, 'Test resource file');
766
 
767
        $filerecord['itemid'] = $page3->id;
768
        $fs->create_file_from_string($filerecord, 'Test resource file');
769
 
770
        foreach ($p2answers as $answer) {
771
            $filerecord['filearea'] = 'page_answers';
772
            $filerecord['itemid'] = $answer->id;
773
            $fs->create_file_from_string($filerecord, 'Test resource file');
774
 
775
            $filerecord['filearea'] = 'page_responses';
776
            $fs->create_file_from_string($filerecord, 'Test resource file');
777
        }
778
 
779
        $result = mod_lesson_external::get_pages($this->lesson->id);
780
        $result = external_api::clean_returnvalue(mod_lesson_external::get_pages_returns(), $result);
781
        $this->assertCount(0, $result['warnings']);
782
        $this->assertCount(3, $result['pages']);
783
 
784
        // Check pages and values.
785
        foreach ($result['pages'] as $page) {
786
            if ($page['page']['id'] == $this->page2->id) {
787
                $this->assertEquals(2 * count($page['answerids']), $page['filescount']);
788
                $this->assertEquals('Lesson TF question 2', $page['page']['title']);
789
            } else {
790
                // Content page, no  answers.
791
                $this->assertCount(0, $page['answerids']);
792
                $this->assertEquals(1, $page['filescount']);
793
            }
794
        }
795
 
796
        // Now, as student without pages menu.
797
        $this->setUser($this->student);
798
        $DB->set_field('lesson', 'displayleft', 0, array('id' => $this->lesson->id));
799
        $result = mod_lesson_external::get_pages($this->lesson->id);
800
        $result = external_api::clean_returnvalue(mod_lesson_external::get_pages_returns(), $result);
801
        $this->assertCount(0, $result['warnings']);
802
        $this->assertCount(3, $result['pages']);
803
 
804
        foreach ($result['pages'] as $page) {
805
            $this->assertArrayNotHasKey('title', $page['page']);
806
        }
807
    }
808
 
809
    /**
810
     * Test launch_attempt. Time restrictions already tested in test_validate_attempt.
811
     */
11 efrain 812
    public function test_launch_attempt(): void {
1 efrain 813
        global $DB, $SESSION;
814
 
815
        // Test time limit restriction.
816
        $timenow = time();
817
        // Create a timer for the current user.
818
        $timer1 = new \stdClass;
819
        $timer1->lessonid = $this->lesson->id;
820
        $timer1->userid = $this->student->id;
821
        $timer1->completed = 0;
822
        $timer1->starttime = $timenow;
823
        $timer1->lessontime = $timenow;
824
        $timer1->id = $DB->insert_record("lesson_timer", $timer1);
825
 
826
        $DB->set_field('lesson', 'timelimit', 30, array('id' => $this->lesson->id));
827
 
828
        unset($SESSION->lesson_messages);
829
        $result = mod_lesson_external::launch_attempt($this->lesson->id, '', 1);
830
        $result = external_api::clean_returnvalue(mod_lesson_external::launch_attempt_returns(), $result);
831
 
832
        $this->assertCount(0, $result['warnings']);
833
        $this->assertCount(2, $result['messages']);
834
        $messages = [];
835
        foreach ($result['messages'] as $message) {
836
            $messages[] = $message['type'];
837
        }
838
        sort($messages);
839
        $this->assertEquals(['center', 'notifyproblem'], $messages);
840
    }
841
 
842
    /**
843
     * Test launch_attempt not finished forcing review mode.
844
     */
11 efrain 845
    public function test_launch_attempt_not_finished_in_review_mode(): void {
1 efrain 846
        global $DB, $SESSION;
847
 
848
        // Create a timer for the current user.
849
        $timenow = time();
850
        $timer1 = new \stdClass;
851
        $timer1->lessonid = $this->lesson->id;
852
        $timer1->userid = $this->student->id;
853
        $timer1->completed = 0;
854
        $timer1->starttime = $timenow;
855
        $timer1->lessontime = $timenow;
856
        $timer1->id = $DB->insert_record("lesson_timer", $timer1);
857
 
858
        unset($SESSION->lesson_messages);
859
        $this->setUser($this->teacher);
860
        $result = mod_lesson_external::launch_attempt($this->lesson->id, '', 1, true);
861
        $result = external_api::clean_returnvalue(mod_lesson_external::launch_attempt_returns(), $result);
862
        // Everything ok as teacher.
863
        $this->assertCount(0, $result['warnings']);
864
        $this->assertCount(0, $result['messages']);
865
        // Should fails as student.
866
        $this->setUser($this->student);
867
        // Now, try to review this attempt. We should not be able because is a non-finished attempt.
868
        $this->expectException('moodle_exception');
869
        mod_lesson_external::launch_attempt($this->lesson->id, '', 1, true);
870
    }
871
 
872
    /**
873
     * Test launch_attempt just finished forcing review mode.
874
     */
11 efrain 875
    public function test_launch_attempt_just_finished_in_review_mode(): void {
1 efrain 876
        global $DB, $SESSION, $USER;
877
 
878
        // Create a timer for the current user.
879
        $timenow = time();
880
        $timer1 = new \stdClass;
881
        $timer1->lessonid = $this->lesson->id;
882
        $timer1->userid = $this->student->id;
883
        $timer1->completed = 1;
884
        $timer1->starttime = $timenow;
885
        $timer1->lessontime = $timenow;
886
        $timer1->id = $DB->insert_record("lesson_timer", $timer1);
887
 
888
        // Create attempt.
889
        $newpageattempt = [
890
            'lessonid' => $this->lesson->id,
891
            'pageid' => $this->page2->id,
892
            'userid' => $this->student->id,
893
            'answerid' => 0,
894
            'retry' => 0,   // First attempt is always 0.
895
            'correct' => 1,
896
            'useranswer' => '1',
897
            'timeseen' => time(),
898
        ];
899
        $DB->insert_record('lesson_attempts', (object) $newpageattempt);
900
        // Create grade.
901
        $record = [
902
            'lessonid' => $this->lesson->id,
903
            'userid' => $this->student->id,
904
            'grade' => 100,
905
            'late' => 0,
906
            'completed' => 1,
907
        ];
908
        $DB->insert_record('lesson_grades', (object) $record);
909
 
910
        unset($SESSION->lesson_messages);
911
 
912
        $this->setUser($this->student);
913
        $result = mod_lesson_external::launch_attempt($this->lesson->id, '', $this->page2->id, true);
914
        $result = external_api::clean_returnvalue(mod_lesson_external::launch_attempt_returns(), $result);
915
        // Everything ok as student.
916
        $this->assertCount(0, $result['warnings']);
917
        $this->assertCount(0, $result['messages']);
918
    }
919
 
920
    /**
921
     * Test launch_attempt not just finished forcing review mode.
922
     */
11 efrain 923
    public function test_launch_attempt_not_just_finished_in_review_mode(): void {
1 efrain 924
        global $DB, $CFG, $SESSION;
925
 
926
        // Create a timer for the current user.
927
        $timenow = time();
928
        $timer1 = new \stdClass;
929
        $timer1->lessonid = $this->lesson->id;
930
        $timer1->userid = $this->student->id;
931
        $timer1->completed = 1;
932
        $timer1->starttime = $timenow - DAYSECS;
933
        $timer1->lessontime = $timenow - $CFG->sessiontimeout - HOURSECS;
934
        $timer1->id = $DB->insert_record("lesson_timer", $timer1);
935
 
936
        unset($SESSION->lesson_messages);
937
 
938
        // Everything ok as teacher.
939
        $this->setUser($this->teacher);
940
        $result = mod_lesson_external::launch_attempt($this->lesson->id, '', 1, true);
941
        $result = external_api::clean_returnvalue(mod_lesson_external::launch_attempt_returns(), $result);
942
        $this->assertCount(0, $result['warnings']);
943
        $this->assertCount(0, $result['messages']);
944
 
945
        // Fail as student.
946
        $this->setUser($this->student);
947
        $this->expectException('moodle_exception');
948
        mod_lesson_external::launch_attempt($this->lesson->id, '', 1, true);
949
    }
950
 
951
    /*
952
     * Test get_page_data
953
     */
11 efrain 954
    public function test_get_page_data(): void {
1 efrain 955
        global $DB;
956
 
957
        // Test a content page first (page1).
958
        $result = mod_lesson_external::get_page_data($this->lesson->id, $this->page1->id, '', false, true);
959
        $result = external_api::clean_returnvalue(mod_lesson_external::get_page_data_returns(), $result);
960
 
961
        $this->assertCount(0, $result['warnings']);
962
        $this->assertCount(0, $result['answers']);  // No answers, auto-generated content page.
963
        $this->assertEmpty($result['ongoingscore']);
964
        $this->assertEmpty($result['progress']);
965
        $this->assertEquals($this->page1->id, $result['newpageid']);    // No answers, so is pointing to the itself.
966
        $this->assertEquals($this->page1->id, $result['page']['id']);
967
        $this->assertEquals(0, $result['page']['nextpageid']);  // Is the last page.
968
        $this->assertEquals('Content', $result['page']['typestring']);
969
        $this->assertEquals($this->page2->id, $result['page']['prevpageid']);    // Previous page.
970
        // Check contents.
971
        $this->assertTrue(strpos($result['pagecontent'], $this->page1->title) !== false);
972
        $this->assertTrue(strpos($result['pagecontent'], $this->page1->contents) !== false);
973
        // Check menu availability.
974
        $this->assertFalse($result['displaymenu']);
975
 
976
        // Check now a page with answers (true / false) and with menu available.
977
        $DB->set_field('lesson', 'displayleft', 1, array('id' => $this->lesson->id));
978
        $result = mod_lesson_external::get_page_data($this->lesson->id, $this->page2->id, '', false, true);
979
        $result = external_api::clean_returnvalue(mod_lesson_external::get_page_data_returns(), $result);
980
        $this->assertCount(0, $result['warnings']);
981
        $this->assertCount(2, $result['answers']);  // One for true, one for false.
982
        // Check menu availability.
983
        $this->assertTrue($result['displaymenu']);
984
 
985
        // Check contents.
986
        $this->assertTrue(strpos($result['pagecontent'], $this->page2->contents) !== false);
987
 
988
        $this->assertEquals(0, $result['page']['prevpageid']);    // Previous page.
989
        $this->assertEquals($this->page1->id, $result['page']['nextpageid']);    // Next page.
990
    }
991
 
992
    /**
993
     * Test get_page_data as student
994
     */
11 efrain 995
    public function test_get_page_data_student(): void {
1 efrain 996
        // Now check using a normal student account.
997
        $this->setUser($this->student);
998
        // First we need to launch the lesson so the timer is on.
999
        mod_lesson_external::launch_attempt($this->lesson->id);
1000
        $result = mod_lesson_external::get_page_data($this->lesson->id, $this->page2->id, '', false, true);
1001
        $result = external_api::clean_returnvalue(mod_lesson_external::get_page_data_returns(), $result);
1002
        $this->assertCount(0, $result['warnings']);
1003
        $this->assertCount(2, $result['answers']);  // One for true, one for false.
1004
        // Check contents.
1005
        $this->assertTrue(strpos($result['pagecontent'], $this->page2->contents) !== false);
1006
        // Check we don't see answer information.
1007
        $this->assertArrayNotHasKey('jumpto', $result['answers'][0]);
1008
        $this->assertArrayNotHasKey('score', $result['answers'][0]);
1009
        $this->assertArrayNotHasKey('jumpto', $result['answers'][1]);
1010
        $this->assertArrayNotHasKey('score', $result['answers'][1]);
1011
    }
1012
 
1013
    /**
1014
     * Test get_page_data without launching attempt.
1015
     */
11 efrain 1016
    public function test_get_page_data_without_launch(): void {
1 efrain 1017
        // Now check using a normal student account.
1018
        $this->setUser($this->student);
1019
 
1020
        $this->expectException('moodle_exception');
1021
        $result = mod_lesson_external::get_page_data($this->lesson->id, $this->page2->id, '', false, true);
1022
    }
1023
 
1024
    /**
1025
     * Creates an attempt for the given userwith a correct or incorrect answer and optionally finishes it.
1026
     *
1027
     * @param  \stdClass $user    Create an attempt for this user
1028
     * @param  boolean $correct  If the answer should be correct
1029
     * @param  boolean $finished If we should finish the attempt
1030
     * @return array the result of the attempt creation or finalisation
1031
     */
1032
    protected function create_attempt($user, $correct = true, $finished = false) {
1033
        global $DB;
1034
 
1035
        $this->setUser($user);
1036
 
1037
        // First we need to launch the lesson so the timer is on.
1038
        mod_lesson_external::launch_attempt($this->lesson->id);
1039
 
1040
        $DB->set_field('lesson', 'feedback', 1, array('id' => $this->lesson->id));
1041
        $DB->set_field('lesson', 'progressbar', 1, array('id' => $this->lesson->id));
1042
        $DB->set_field('lesson', 'custom', 0, array('id' => $this->lesson->id));
1043
        $DB->set_field('lesson', 'maxattempts', 3, array('id' => $this->lesson->id));
1044
 
1045
        $answercorrect = 0;
1046
        $answerincorrect = 0;
1047
        $p2answers = $DB->get_records('lesson_answers', array('lessonid' => $this->lesson->id, 'pageid' => $this->page2->id), 'id');
1048
        foreach ($p2answers as $answer) {
1049
            if ($answer->jumpto == 0) {
1050
                $answerincorrect = $answer->id;
1051
            } else {
1052
                $answercorrect = $answer->id;
1053
            }
1054
        }
1055
 
1056
        $data = array(
1057
            array(
1058
                'name' => 'answerid',
1059
                'value' => $correct ? $answercorrect : $answerincorrect,
1060
            ),
1061
            array(
1062
                'name' => '_qf__lesson_display_answer_form_truefalse',
1063
                'value' => 1,
1064
            )
1065
        );
1066
        $result = mod_lesson_external::process_page($this->lesson->id, $this->page2->id, $data);
1067
        $result = external_api::clean_returnvalue(mod_lesson_external::process_page_returns(), $result);
1068
 
1069
        if ($finished) {
1070
            $result = mod_lesson_external::finish_attempt($this->lesson->id);
1071
            $result = external_api::clean_returnvalue(mod_lesson_external::finish_attempt_returns(), $result);
1072
        }
1073
        return $result;
1074
    }
1075
 
1076
    /**
1077
     * Test process_page
1078
     */
11 efrain 1079
    public function test_process_page(): void {
1 efrain 1080
        global $DB;
1081
 
1082
        // Attempt first with incorrect response.
1083
        $result = $this->create_attempt($this->student, false, false);
1084
 
1085
        $this->assertEquals($this->page2->id, $result['newpageid']);    // Same page, since the answer was incorrect.
1086
        $this->assertFalse($result['correctanswer']);   // Incorrect answer.
1087
        $this->assertEquals(50, $result['progress']);
1088
 
1089
        // Attempt with correct response.
1090
        $result = $this->create_attempt($this->student, true, false);
1091
 
1092
        $this->assertEquals($this->page1->id, $result['newpageid']);    // Next page, the answer was correct.
1093
        $this->assertTrue($result['correctanswer']);    // Correct response.
1094
        $this->assertFalse($result['maxattemptsreached']);  // Still one attempt.
1095
        $this->assertEquals(50, $result['progress']);
1096
    }
1097
 
1098
    /**
1099
     * Test finish attempt not doing anything.
1100
     */
11 efrain 1101
    public function test_finish_attempt_not_doing_anything(): void {
1 efrain 1102
 
1103
        $this->setUser($this->student);
1104
        // First we need to launch the lesson so the timer is on.
1105
        mod_lesson_external::launch_attempt($this->lesson->id);
1106
 
1107
        $result = mod_lesson_external::finish_attempt($this->lesson->id);
1108
        $result = external_api::clean_returnvalue(mod_lesson_external::finish_attempt_returns(), $result);
1109
 
1110
        $this->assertCount(0, $result['warnings']);
1111
        $returneddata = [];
1112
        foreach ($result['data'] as $data) {
1113
            $returneddata[$data['name']] = $data['value'];
1114
        }
1115
        $this->assertEquals(1, $returneddata['gradelesson']);   // Graded lesson.
1116
        $this->assertEquals(1, $returneddata['welldone']);      // Finished correctly (even without grades).
1117
        $gradeinfo = json_decode($returneddata['gradeinfo']);
1118
        $expectedgradeinfo = (object) [
1119
            'nquestions' => 0,
1120
            'attempts' => 0,
1121
            'total' => 0,
1122
            'earned' => 0,
1123
            'grade' => 0,
1124
            'nmanual' => 0,
1125
            'manualpoints' => 0,
1126
        ];
1127
    }
1128
 
1129
    /**
1130
     * Test finish attempt with correct answer.
1131
     */
11 efrain 1132
    public function test_finish_attempt_with_correct_answer(): void {
1 efrain 1133
        // Create a finished attempt.
1134
        $result = $this->create_attempt($this->student, true, true);
1135
 
1136
        $this->assertCount(0, $result['warnings']);
1137
        $returneddata = [];
1138
        foreach ($result['data'] as $data) {
1139
            $returneddata[$data['name']] = $data['value'];
1140
        }
1141
        $this->assertEquals(1, $returneddata['gradelesson']);   // Graded lesson.
1142
        $this->assertEquals(1, $returneddata['numberofpagesviewed']);
1143
        $this->assertEquals(1, $returneddata['numberofcorrectanswers']);
1144
        $gradeinfo = json_decode($returneddata['gradeinfo']);
1145
        $expectedgradeinfo = (object) [
1146
            'nquestions' => 1,
1147
            'attempts' => 1,
1148
            'total' => 1,
1149
            'earned' => 1,
1150
            'grade' => 100,
1151
            'nmanual' => 0,
1152
            'manualpoints' => 0,
1153
        ];
1154
    }
1155
 
1156
    /**
1157
     * Test get_attempts_overview
1158
     */
11 efrain 1159
    public function test_get_attempts_overview(): void {
1 efrain 1160
        global $DB;
1161
 
1162
        // Create a finished attempt with incorrect answer.
1163
        $this->setCurrentTimeStart();
1164
        $this->create_attempt($this->student, false, true);
1165
 
1166
        $this->setAdminUser();
1167
        $result = mod_lesson_external::get_attempts_overview($this->lesson->id);
1168
        $result = external_api::clean_returnvalue(mod_lesson_external::get_attempts_overview_returns(), $result);
1169
 
1170
        // One attempt, 0 for grade (incorrect response) in overal statistics.
1171
        $this->assertEquals(1, $result['data']['numofattempts']);
1172
        $this->assertEquals(0, $result['data']['avescore']);
1173
        $this->assertEquals(0, $result['data']['highscore']);
1174
        $this->assertEquals(0, $result['data']['lowscore']);
1175
        // Check one student, finished attempt, 0 for grade.
1176
        $this->assertCount(1, $result['data']['students']);
1177
        $this->assertEquals($this->student->id, $result['data']['students'][0]['id']);
1178
        $this->assertEquals(0, $result['data']['students'][0]['bestgrade']);
1179
        $this->assertCount(1, $result['data']['students'][0]['attempts']);
1180
        $this->assertEquals(1, $result['data']['students'][0]['attempts'][0]['end']);
1181
        $this->assertEquals(0, $result['data']['students'][0]['attempts'][0]['grade']);
1182
        $this->assertTimeCurrent($result['data']['students'][0]['attempts'][0]['timestart']);
1183
        $this->assertTimeCurrent($result['data']['students'][0]['attempts'][0]['timeend']);
1184
 
1185
        // Add a new attempt (same user).
1186
        sleep(1);
1187
        // Allow first retake.
1188
        $DB->set_field('lesson', 'retake', 1, array('id' => $this->lesson->id));
1189
        // Create a finished attempt with correct answer.
1190
        $this->setCurrentTimeStart();
1191
        $this->create_attempt($this->student, true, true);
1192
 
1193
        $this->setAdminUser();
1194
        $result = mod_lesson_external::get_attempts_overview($this->lesson->id);
1195
        $result = external_api::clean_returnvalue(mod_lesson_external::get_attempts_overview_returns(), $result);
1196
 
1197
        // Two attempts with maximum grade.
1198
        $this->assertEquals(2, $result['data']['numofattempts']);
1199
        $this->assertEquals(50.00, format_float($result['data']['avescore'], 2));
1200
        $this->assertEquals(100, $result['data']['highscore']);
1201
        $this->assertEquals(0, $result['data']['lowscore']);
1202
        // Check one student, finished two attempts, 100 for final grade.
1203
        $this->assertCount(1, $result['data']['students']);
1204
        $this->assertEquals($this->student->id, $result['data']['students'][0]['id']);
1205
        $this->assertEquals(100, $result['data']['students'][0]['bestgrade']);
1206
        $this->assertCount(2, $result['data']['students'][0]['attempts']);
1207
        foreach ($result['data']['students'][0]['attempts'] as $attempt) {
1208
            if ($attempt['try'] == 0) {
1209
                // First attempt, 0 for grade.
1210
                $this->assertEquals(0, $attempt['grade']);
1211
            } else {
1212
                $this->assertEquals(100, $attempt['grade']);
1213
            }
1214
        }
1215
 
1216
        // Now, add other user failed attempt.
1217
        $student2 = self::getDataGenerator()->create_user();
1218
        $this->getDataGenerator()->enrol_user($student2->id, $this->course->id, $this->studentrole->id, 'manual');
1219
        $this->create_attempt($student2, false, true);
1220
 
1221
        // Now check we have two students and the statistics changed.
1222
        $this->setAdminUser();
1223
        $result = mod_lesson_external::get_attempts_overview($this->lesson->id);
1224
        $result = external_api::clean_returnvalue(mod_lesson_external::get_attempts_overview_returns(), $result);
1225
 
1226
        // Total of 3 attempts with maximum grade.
1227
        $this->assertEquals(3, $result['data']['numofattempts']);
1228
        $this->assertEquals(33.33, format_float($result['data']['avescore'], 2));
1229
        $this->assertEquals(100, $result['data']['highscore']);
1230
        $this->assertEquals(0, $result['data']['lowscore']);
1231
        // Check students.
1232
        $this->assertCount(2, $result['data']['students']);
1233
    }
1234
 
1235
    /**
1236
     * Test get_attempts_overview when there aren't attempts.
1237
     */
11 efrain 1238
    public function test_get_attempts_overview_no_attempts(): void {
1 efrain 1239
        $this->setAdminUser();
1240
        $result = mod_lesson_external::get_attempts_overview($this->lesson->id);
1241
        $result = external_api::clean_returnvalue(mod_lesson_external::get_attempts_overview_returns(), $result);
1242
        $this->assertCount(0, $result['warnings']);
1243
        $this->assertArrayNotHasKey('data', $result);
1244
    }
1245
 
1246
    /**
1247
     * Test get_user_attempt
1248
     */
11 efrain 1249
    public function test_get_user_attempt(): void {
1 efrain 1250
        global $DB;
1251
 
1252
        // Create a finished and unfinished attempt with incorrect answer.
1253
        $this->setCurrentTimeStart();
1254
        $this->create_attempt($this->student, true, true);
1255
 
1256
        $DB->set_field('lesson', 'retake', 1, array('id' => $this->lesson->id));
1257
        sleep(1);
1258
        $this->create_attempt($this->student, false, false);
1259
 
1260
        $this->setAdminUser();
1261
        // Test first attempt finished.
1262
        $result = mod_lesson_external::get_user_attempt($this->lesson->id, $this->student->id, 0);
1263
        $result = external_api::clean_returnvalue(mod_lesson_external::get_user_attempt_returns(), $result);
1264
 
1265
        $this->assertCount(2, $result['answerpages']);  // 2 pages in the lesson.
1266
        $this->assertCount(2, $result['answerpages'][0]['answerdata']['answers']);  // 2 possible answers in true/false.
1267
        $this->assertEquals(100, $result['userstats']['grade']);    // Correct answer.
1268
        $this->assertEquals(1, $result['userstats']['gradeinfo']['total']);     // Total correct answers.
1269
        $this->assertEquals(100, $result['userstats']['gradeinfo']['grade']);   // Correct answer.
1270
 
1271
        // Check page object contains the lesson pages answered.
1272
        $pagesanswered = array();
1273
        foreach ($result['answerpages'] as $answerp) {
1274
            $pagesanswered[] = $answerp['page']['id'];
1275
        }
1276
        sort($pagesanswered);
1277
        $this->assertEquals(array($this->page1->id, $this->page2->id), $pagesanswered);
1278
 
1279
        // Test second attempt unfinished.
1280
        $result = mod_lesson_external::get_user_attempt($this->lesson->id, $this->student->id, 1);
1281
        $result = external_api::clean_returnvalue(mod_lesson_external::get_user_attempt_returns(), $result);
1282
 
1283
        $this->assertCount(2, $result['answerpages']);  // 2 pages in the lesson.
1284
        $this->assertCount(2, $result['answerpages'][0]['answerdata']['answers']);  // 2 possible answers in true/false.
1285
        $this->assertArrayNotHasKey('gradeinfo', $result['userstats']);    // No grade info since it not finished.
1286
 
1287
        // Check as student I can get this information for only me.
1288
        $this->setUser($this->student);
1289
        // Test first attempt finished.
1290
        $result = mod_lesson_external::get_user_attempt($this->lesson->id, $this->student->id, 0);
1291
        $result = external_api::clean_returnvalue(mod_lesson_external::get_user_attempt_returns(), $result);
1292
 
1293
        $this->assertCount(2, $result['answerpages']);  // 2 pages in the lesson.
1294
        $this->assertCount(2, $result['answerpages'][0]['answerdata']['answers']);  // 2 possible answers in true/false.
1295
        $this->assertEquals(100, $result['userstats']['grade']);    // Correct answer.
1296
        $this->assertEquals(1, $result['userstats']['gradeinfo']['total']);     // Total correct answers.
1297
        $this->assertEquals(100, $result['userstats']['gradeinfo']['grade']);   // Correct answer.
1298
 
1299
        $this->expectException('moodle_exception');
1300
        $result = mod_lesson_external::get_user_attempt($this->lesson->id, $this->teacher->id, 0);
1301
    }
1302
 
1303
    /**
1304
     * Test get_pages_possible_jumps
1305
     */
11 efrain 1306
    public function test_get_pages_possible_jumps(): void {
1 efrain 1307
        $this->setAdminUser();
1308
        $result = mod_lesson_external::get_pages_possible_jumps($this->lesson->id);
1309
        $result = external_api::clean_returnvalue(mod_lesson_external::get_pages_possible_jumps_returns(), $result);
1310
 
1311
        $this->assertCount(0, $result['warnings']);
1312
        $this->assertCount(3, $result['jumps']);    // 3 jumps, 2 from the question page and 1 from the content.
1313
        foreach ($result['jumps'] as $jump) {
1314
            if ($jump['answerid'] != 0) {
1315
                // Check only pages with answers.
1316
                if ($jump['jumpto'] == 0) {
1317
                    $this->assertEquals($jump['pageid'], $jump['calculatedjump']);    // 0 means to jump to current page.
1318
                } else {
1319
                    // Question is configured to jump to next page if correct.
1320
                    $this->assertEquals($this->page1->id, $jump['calculatedjump']);
1321
                }
1322
            }
1323
        }
1324
    }
1325
 
1326
    /**
1327
     * Test get_pages_possible_jumps when offline attemps are disabled for a normal user
1328
     */
11 efrain 1329
    public function test_get_pages_possible_jumps_with_offlineattemps_disabled(): void {
1 efrain 1330
        $this->setUser($this->student->id);
1331
        $result = mod_lesson_external::get_pages_possible_jumps($this->lesson->id);
1332
        $result = external_api::clean_returnvalue(mod_lesson_external::get_pages_possible_jumps_returns(), $result);
1333
        $this->assertCount(0, $result['jumps']);
1334
    }
1335
 
1336
    /**
1337
     * Test get_pages_possible_jumps when offline attemps are enabled for a normal user
1338
     */
11 efrain 1339
    public function test_get_pages_possible_jumps_with_offlineattemps_enabled(): void {
1 efrain 1340
        global $DB;
1341
 
1342
        $DB->set_field('lesson', 'allowofflineattempts', 1, array('id' => $this->lesson->id));
1343
        $this->setUser($this->student->id);
1344
        $result = mod_lesson_external::get_pages_possible_jumps($this->lesson->id);
1345
        $result = external_api::clean_returnvalue(mod_lesson_external::get_pages_possible_jumps_returns(), $result);
1346
        $this->assertCount(3, $result['jumps']);
1347
    }
1348
 
1349
    /*
1350
     * Test get_lesson user student.
1351
     */
11 efrain 1352
    public function test_get_lesson_user_student(): void {
1 efrain 1353
        // Test user with full capabilities.
1354
        $this->setUser($this->student);
1355
 
1356
        // Lesson not using password.
1357
        $result = mod_lesson_external::get_lesson($this->lesson->id);
1358
        $result = external_api::clean_returnvalue(mod_lesson_external::get_lesson_returns(), $result);
1359
        $this->assertCount(37, $result['lesson']);  // Expect most of the fields.
1360
        $this->assertFalse(isset($result['password']));
1361
    }
1362
 
1363
    /**
1364
     * Test get_lesson user student with missing password.
1365
     */
11 efrain 1366
    public function test_get_lesson_user_student_with_missing_password(): void {
1 efrain 1367
        global $DB;
1368
 
1369
        // Test user with full capabilities.
1370
        $this->setUser($this->student);
1371
        $DB->set_field('lesson', 'usepassword', 1, array('id' => $this->lesson->id));
1372
        $DB->set_field('lesson', 'password', 'abc', array('id' => $this->lesson->id));
1373
 
1374
        // Lesson not using password.
1375
        $result = mod_lesson_external::get_lesson($this->lesson->id);
1376
        $result = external_api::clean_returnvalue(mod_lesson_external::get_lesson_returns(), $result);
1377
        $this->assertCount(7, $result['lesson']);   // Expect just this few fields.
1378
        $this->assertFalse(isset($result['intro']));
1379
    }
1380
 
1381
    /**
1382
     * Test get_lesson user student with correct password.
1383
     */
11 efrain 1384
    public function test_get_lesson_user_student_with_correct_password(): void {
1 efrain 1385
        global $DB;
1386
        // Test user with full capabilities.
1387
        $this->setUser($this->student);
1388
        $password = 'abc';
1389
        $DB->set_field('lesson', 'usepassword', 1, array('id' => $this->lesson->id));
1390
        $DB->set_field('lesson', 'password', $password, array('id' => $this->lesson->id));
1391
 
1392
        // Lesson not using password.
1393
        $result = mod_lesson_external::get_lesson($this->lesson->id, $password);
1394
        $result = external_api::clean_returnvalue(mod_lesson_external::get_lesson_returns(), $result);
1395
        $this->assertCount(37 , $result['lesson']);
1396
        $this->assertFalse(isset($result['intro']));
1397
    }
1398
 
1399
    /**
1400
     * Test get_lesson teacher.
1401
     */
11 efrain 1402
    public function test_get_lesson_teacher(): void {
1 efrain 1403
        global $DB;
1404
        // Test user with full capabilities.
1405
        $this->setUser($this->teacher);
1406
        $password = 'abc';
1407
        $DB->set_field('lesson', 'usepassword', 1, array('id' => $this->lesson->id));
1408
        $DB->set_field('lesson', 'password', $password, array('id' => $this->lesson->id));
1409
 
1410
        // Lesson not passing a valid password (but we are teachers, we should see all the info).
1411
        $result = mod_lesson_external::get_lesson($this->lesson->id);
1412
        $result = external_api::clean_returnvalue(mod_lesson_external::get_lesson_returns(), $result);
1413
        $this->assertCount(46, $result['lesson']);  // Expect all the fields.
1414
        $this->assertEquals($result['lesson']['password'], $password);
1415
    }
1416
}