From 630f0e3bced4f023669e1502555407a0d0056c75 Mon Sep 17 00:00:00 2001
From: Juan Leyva <juanleyvadelgado@gmail.com>
Date: Thu, 12 Jan 2017 23:25:17 +0100
Subject: [PATCH] MDL-57643 mod_lesson: New WS
 mod_lesson_get_access_information

---
 mod/lesson/classes/external.php    | 207 +++++++++++++++++++++++++++++
 mod/lesson/db/services.php         |  11 +-
 mod/lesson/tests/external_test.php | 147 ++++++++++++++++++++
 mod/lesson/version.php             |   2 +-
 4 files changed, 365 insertions(+), 2 deletions(-)

diff --git a/mod/lesson/classes/external.php b/mod/lesson/classes/external.php
index f3d10518efa..70cc7028bd1 100644
--- a/mod/lesson/classes/external.php
+++ b/mod/lesson/classes/external.php
@@ -209,4 +209,211 @@ class mod_lesson_external extends external_api {
             )
         );
     }
+
+    /**
+     * Utility function for validating a lesson.
+     *
+     * @param int $lessonid lesson instance id
+     * @return array array containing the lesson, course, context and course module objects
+     * @since  Moodle 3.3
+     */
+    protected static function validate_lesson($lessonid) {
+        global $DB, $USER;
+
+        // Request and permission validation.
+        $lesson = $DB->get_record('lesson', array('id' => $lessonid), '*', MUST_EXIST);
+        list($course, $cm) = get_course_and_cm_from_instance($lesson, 'lesson');
+
+        $lesson = new lesson($lesson, $cm);
+        $lesson->update_effective_access($USER->id);
+
+        $context = $lesson->context;
+        self::validate_context($context);
+
+        return array($lesson, $course, $cm, $context);
+    }
+
+    /**
+     * Validates a new attempt.
+     *
+     * @param  lesson  $lesson lesson instance
+     * @param  array   $params request parameters
+     * @param  boolean $return whether to return the errors or throw exceptions
+     * @return array          the errors (if return set to true)
+     * @since  Moodle 3.3
+     */
+    protected static function validate_attempt(lesson $lesson, $params, $return = false) {
+        global $USER;
+
+        $errors = array();
+
+        // Avoid checkings for managers.
+        if ($lesson->can_manage()) {
+            return [];
+        }
+
+        // Dead line.
+        if ($timerestriction = $lesson->get_time_restriction_status()) {
+            $error = ["$timerestriction->reason" => userdate($timerestriction->time)];
+            if (!$return) {
+                throw new moodle_exception(key($error), 'lesson', '', current($error));
+            }
+            $errors[key($error)] = current($error);
+        }
+
+        // Password protected lesson code.
+        if ($passwordrestriction = $lesson->get_password_restriction_status($params['password'])) {
+            $error = ["passwordprotectedlesson" => external_format_string($lesson->name, $lesson->context->id)];
+            if (!$return) {
+                throw new moodle_exception(key($error), 'lesson', '', current($error));
+            }
+            $errors[key($error)] = current($error);
+        }
+
+        // Check for dependencies.
+        if ($dependenciesrestriction = $lesson->get_dependencies_restriction_status()) {
+            $errorhtmllist = implode(get_string('and', 'lesson') . ', ', $dependenciesrestriction->errors);
+            $error = ["completethefollowingconditions" => $dependenciesrestriction->dependentlesson->name . $errorhtmllist];
+            if (!$return) {
+                throw new moodle_exception(key($error), 'lesson', '', current($error));
+            }
+            $errors[key($error)] = current($error);
+        }
+
+        // To check only when no page is set (starting or continuing a lesson).
+        if (empty($params['pageid'])) {
+            // To avoid multiple calls, store the magic property firstpage.
+            $lessonfirstpage = $lesson->firstpage;
+            $lessonfirstpageid = $lessonfirstpage ? $lessonfirstpage->id : false;
+
+            // Check if the lesson does not have pages.
+            if (!$lessonfirstpageid) {
+                $error = ["lessonnotready2" => null];
+                if (!$return) {
+                    throw new moodle_exception(key($error), 'lesson');
+                }
+                $errors[key($error)] = current($error);
+            }
+
+            // Get the number of retries (also referenced as attempts), and the last page seen.
+            $attemptscount = $lesson->count_user_retries($USER->id);
+            $lastpageseen = $lesson->get_last_page_seen($attemptscount);
+
+            // Check if the user left a timed session with no retakes.
+            if ($lastpageseen !== false && $lastpageseen != LESSON_EOL) {
+                if ($lesson->left_during_timed_session($attemptscount) && $lesson->timelimit && !$lesson->retake) {
+                    $error = ["leftduringtimednoretake" => null];
+                    if (!$return) {
+                        throw new moodle_exception(key($error), 'lesson');
+                    }
+                    $errors[key($error)] = current($error);
+                }
+            } else if ($attemptscount > 0 && !$lesson->retake) {
+                // The user finished the lesson and no retakes are allowed.
+                $error = ["noretake" => null];
+                if (!$return) {
+                    throw new moodle_exception(key($error), 'lesson');
+                }
+                $errors[key($error)] = current($error);
+            }
+        }
+
+        return $errors;
+    }
+
+    /**
+     * Describes the parameters for get_lesson_access_information.
+     *
+     * @return external_external_function_parameters
+     * @since Moodle 3.3
+     */
+    public static function get_lesson_access_information_parameters() {
+        return new external_function_parameters (
+            array(
+                'lessonid' => new external_value(PARAM_INT, 'lesson instance id')
+            )
+        );
+    }
+
+    /**
+     * Return access information for a given lesson.
+     *
+     * @param int $lessonid lesson instance id
+     * @return array of warnings and the access information
+     * @since Moodle 3.3
+     * @throws  moodle_exception
+     */
+    public static function get_lesson_access_information($lessonid) {
+        global $DB, $USER;
+
+        $warnings = array();
+
+        $params = array(
+            'lessonid' => $lessonid
+        );
+        $params = self::validate_parameters(self::get_lesson_access_information_parameters(), $params);
+
+        list($lesson, $course, $cm, $context) = self::validate_lesson($params['lessonid']);
+
+        $result = array();
+        // Capabilities first.
+        $result['canmanage'] = $lesson->can_manage();
+        $result['cangrade'] = has_capability('mod/lesson:grade', $context);
+        $result['canviewreports'] = has_capability('mod/lesson:viewreports', $context);
+
+        // Status information.
+        $result['reviewmode'] = $lesson->is_in_review_mode();
+        $result['attemptscount'] = $lesson->count_user_retries($USER->id);
+        $lastpageseen = $lesson->get_last_page_seen($result['attemptscount']);
+        $result['lastpageseen'] = ($lastpageseen !== false) ? $lastpageseen : 0;
+        $result['leftduringtimedsession'] = $lesson->left_during_timed_session($result['attemptscount']);
+        // To avoid multiple calls, store the magic property firstpage.
+        $lessonfirstpage = $lesson->firstpage;
+        $result['firstpageid'] = $lessonfirstpage ? $lessonfirstpage->id : 0;
+
+        // Access restrictions now, we emulate a new attempt access to get the possible warnings.
+        $result['preventaccessreasons'] = [];
+        $validationerrors = self::validate_attempt($lesson, ['password' => ''], true);
+        foreach ($validationerrors as $reason => $data) {
+            $result['preventaccessreasons'][] = [
+                'reason' => $reason,
+                'data' => $data,
+                'message' => get_string($reason, 'lesson', $data),
+            ];
+        }
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
+    /**
+     * Describes the get_lesson_access_information return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.3
+     */
+    public static function get_lesson_access_information_returns() {
+        return new external_single_structure(
+            array(
+                'canmanage' => new external_value(PARAM_BOOL, 'Whether the user can manage the lesson or not.'),
+                'cangrade' => new external_value(PARAM_BOOL, 'Whether the user can grade the lesson or not.'),
+                'canviewreports' => new external_value(PARAM_BOOL, 'Whether the user can view the lesson reports or not.'),
+                'reviewmode' => new external_value(PARAM_BOOL, 'Whether the lesson is in review mode for the current user.'),
+                'attemptscount' => new external_value(PARAM_INT, 'The number of attempts done by the user.'),
+                'lastpageseen' => new external_value(PARAM_INT, 'The last page seen id.'),
+                'leftduringtimedsession' => new external_value(PARAM_BOOL, 'Whether the user left during a timed session.'),
+                'firstpageid' => new external_value(PARAM_INT, 'The lesson first page id.'),
+                'preventaccessreasons' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'reason' => new external_value(PARAM_ALPHANUMEXT, 'Reason lang string code'),
+                            'data' => new external_value(PARAM_RAW, 'Additional data'),
+                            'message' => new external_value(PARAM_RAW, 'Complete html message'),
+                        ),
+                        'The reasons why the user cannot attempt the lesson'
+                    )
+                ),
+                'warnings' => new external_warnings(),
+            )
+        );
+    }
 }
diff --git a/mod/lesson/db/services.php b/mod/lesson/db/services.php
index 06284be2842..daa5dc61890 100644
--- a/mod/lesson/db/services.php
+++ b/mod/lesson/db/services.php
@@ -30,9 +30,18 @@ $functions = array(
     'mod_lesson_get_lessons_by_courses' => array(
         'classname'     => 'mod_lesson_external',
         'methodname'    => 'get_lessons_by_courses',
-        'description'   => 'Returns a list of lessons in a provided list of courses, if no list is provided all lessons that the user can view will be returned.',
+        'description'   => 'Returns a list of lessons in a provided list of courses,
+                            if no list is provided all lessons that the user can view will be returned.',
         'type'          => 'read',
         'capabilities'  => 'mod/lesson:view',
         'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
+    'mod_lesson_get_lesson_access_information' => array(
+        'classname'     => 'mod_lesson_external',
+        'methodname'    => 'get_lesson_access_information',
+        'description'   => 'Return access information for a given lesson.',
+        'type'          => 'read',
+        'capabilities'  => 'mod/lesson:view',
+        'services'    => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
 );
diff --git a/mod/lesson/tests/external_test.php b/mod/lesson/tests/external_test.php
index b27dc31619e..06267ff95fc 100644
--- a/mod/lesson/tests/external_test.php
+++ b/mod/lesson/tests/external_test.php
@@ -31,6 +31,30 @@ global $CFG;
 require_once($CFG->dirroot . '/webservice/tests/helpers.php');
 require_once($CFG->dirroot . '/mod/lesson/locallib.php');
 
+/**
+ * Silly class to access mod_lesson_external internal methods.
+ *
+ * @package mod_lesson
+ * @copyright 2017 Juan Leyva <juan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since  Moodle 3.3
+ */
+class testable_mod_lesson_external extends mod_lesson_external {
+
+    /**
+     * Validates a new attempt.
+     *
+     * @param  lesson  $lesson lesson instance
+     * @param  array   $params request parameters
+     * @param  boolean $return whether to return the errors or throw exceptions
+     * @return [array          the errors (if return set to true)
+     * @since  Moodle 3.3
+     */
+    public static function validate_attempt(lesson $lesson, $params, $return = false) {
+        return parent::validate_attempt($lesson, $params, $return);
+    }
+}
+
 /**
  * Lesson module external functions tests
  *
@@ -53,6 +77,9 @@ class mod_lesson_external_testcase extends externallib_advanced_testcase {
         // Setup test data.
         $this->course = $this->getDataGenerator()->create_course();
         $this->lesson = $this->getDataGenerator()->create_module('lesson', array('course' => $this->course->id));
+        $lessongenerator = $this->getDataGenerator()->get_plugin_generator('mod_lesson');
+        $this->page1 = $lessongenerator->create_content($this->lesson);
+        $this->page2 = $lessongenerator->create_question_truefalse($this->lesson);
         $this->context = context_module::instance($this->lesson->cmid);
         $this->cm = get_coursemodule_from_instance('lesson', $this->lesson->id);
 
@@ -192,4 +219,124 @@ class mod_lesson_external_testcase extends externallib_advanced_testcase {
         $this->assertFalse(isset($lessons['lessons'][0]['intro']));
     }
 
+    /**
+     * Test the validate_attempt function.
+     */
+    public function test_validate_attempt() {
+        global $DB;
+
+        $this->setUser($this->student);
+        // Test deadline.
+        $oldtime = time() - DAYSECS;
+        $DB->set_field('lesson', 'deadline', $oldtime, array('id' => $this->lesson->id));
+
+        $lesson = new lesson($DB->get_record('lesson', array('id' => $this->lesson->id)));
+        $validation = testable_mod_lesson_external::validate_attempt($lesson, ['password' => ''], true);
+        $this->assertEquals('lessonclosed', key($validation));
+        $this->assertCount(1, $validation);
+
+        // Test not available yet.
+        $futuretime = time() + DAYSECS;
+        $DB->set_field('lesson', 'deadline', 0, array('id' => $this->lesson->id));
+        $DB->set_field('lesson', 'available', $futuretime, array('id' => $this->lesson->id));
+
+        $lesson = new lesson($DB->get_record('lesson', array('id' => $this->lesson->id)));
+        $validation = testable_mod_lesson_external::validate_attempt($lesson, ['password' => ''], true);
+        $this->assertEquals('lessonopen', key($validation));
+        $this->assertCount(1, $validation);
+
+        // Test password.
+        $DB->set_field('lesson', 'deadline', 0, array('id' => $this->lesson->id));
+        $DB->set_field('lesson', 'available', 0, array('id' => $this->lesson->id));
+        $DB->set_field('lesson', 'usepassword', 1, array('id' => $this->lesson->id));
+        $DB->set_field('lesson', 'password', 'abc', array('id' => $this->lesson->id));
+
+        $lesson = new lesson($DB->get_record('lesson', array('id' => $this->lesson->id)));
+        $validation = testable_mod_lesson_external::validate_attempt($lesson, ['password' => ''], true);
+        $this->assertEquals('passwordprotectedlesson', key($validation));
+        $this->assertCount(1, $validation);
+
+        $lesson = new lesson($DB->get_record('lesson', array('id' => $this->lesson->id)));
+        $validation = testable_mod_lesson_external::validate_attempt($lesson, ['password' => 'abc'], true);
+        $this->assertCount(0, $validation);
+
+        // Dependencies.
+        $record = new stdClass();
+        $record->course = $this->course->id;
+        $lesson2 = self::getDataGenerator()->create_module('lesson', $record);
+        $DB->set_field('lesson', 'usepassword', 0, array('id' => $this->lesson->id));
+        $DB->set_field('lesson', 'password', '', array('id' => $this->lesson->id));
+        $DB->set_field('lesson', 'dependency', $lesson->id, array('id' => $this->lesson->id));
+
+        $lesson = new lesson($DB->get_record('lesson', array('id' => $this->lesson->id)));
+        $lesson->conditions = serialize((object) ['completed' => true, 'timespent' => 0, 'gradebetterthan' => 0]);
+        $validation = testable_mod_lesson_external::validate_attempt($lesson, ['password' => ''], true);
+        $this->assertEquals('completethefollowingconditions', key($validation));
+        $this->assertCount(1, $validation);
+
+        // Lesson withou pages.
+        $lesson = new lesson($lesson2);
+        $validation = testable_mod_lesson_external::validate_attempt($lesson, ['password' => ''], true);
+        $this->assertEquals('lessonnotready2', key($validation));
+        $this->assertCount(1, $validation);
+
+        // Test retakes.
+        $DB->set_field('lesson', 'dependency', 0, array('id' => $this->lesson->id));
+        $DB->set_field('lesson', 'retake', 0, array('id' => $this->lesson->id));
+        $record = [
+            'lessonid' => $this->lesson->id,
+            'userid' => $this->student->id,
+            'grade' => 100,
+            'late' => 0,
+            'completed' => 1,
+        ];
+        $DB->insert_record('lesson_grades', (object) $record);
+        $lesson = new lesson($DB->get_record('lesson', array('id' => $this->lesson->id)));
+        $validation = testable_mod_lesson_external::validate_attempt($lesson, ['password' => ''], true);
+        $this->assertEquals('noretake', key($validation));
+        $this->assertCount(1, $validation);
+    }
+
+    /**
+     * Test the get_lesson_access_information function.
+     */
+    public function test_get_lesson_access_information() {
+        global $DB;
+
+        $this->setUser($this->student);
+        // Add previous attempt.
+        $record = [
+            'lessonid' => $this->lesson->id,
+            'userid' => $this->student->id,
+            'grade' => 100,
+            'late' => 0,
+            'completed' => 1,
+        ];
+        $DB->insert_record('lesson_grades', (object) $record);
+
+        $result = mod_lesson_external::get_lesson_access_information($this->lesson->id);
+        $result = external_api::clean_returnvalue(mod_lesson_external::get_lesson_access_information_returns(), $result);
+        $this->assertFalse($result['canmanage']);
+        $this->assertFalse($result['cangrade']);
+        $this->assertFalse($result['canviewreports']);
+
+        $this->assertFalse($result['leftduringtimedsession']);
+        $this->assertEquals(1, $result['reviewmode']);
+        $this->assertEquals(1, $result['attemptscount']);
+        $this->assertEquals(0, $result['lastpageseen']);
+        $this->assertEquals($this->page2->id, $result['firstpageid']);
+        $this->assertCount(1, $result['preventaccessreasons']);
+        $this->assertEquals('noretake', $result['preventaccessreasons'][0]['reason']);
+        $this->assertEquals(null, $result['preventaccessreasons'][0]['data']);
+        $this->assertEquals(get_string('noretake', 'lesson'), $result['preventaccessreasons'][0]['message']);
+
+        // Now check permissions as admin.
+        $this->setAdminUser();
+        $result = mod_lesson_external::get_lesson_access_information($this->lesson->id);
+        $result = external_api::clean_returnvalue(mod_lesson_external::get_lesson_access_information_returns(), $result);
+        $this->assertTrue($result['canmanage']);
+        $this->assertTrue($result['cangrade']);
+        $this->assertTrue($result['canviewreports']);
+    }
+
 }
diff --git a/mod/lesson/version.php b/mod/lesson/version.php
index 4b0e1585426..d6ebb85cb13 100644
--- a/mod/lesson/version.php
+++ b/mod/lesson/version.php
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2016120501;     // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2016120502;     // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2016112900;    // Requires this Moodle version
 $plugin->component = 'mod_lesson'; // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;