Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310]

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