Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 311 and 403] [Versions 400 and 403] [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  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  }