Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 311 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]

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