From 630f0e3bced4f023669e1502555407a0d0056c75 Mon Sep 17 00:00:00 2001 From: Juan Leyva 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 + * @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;