Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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