Proyectos de Subversion Moodle

Rev

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