Commit 9c2ca400 authored by Andrew Nicols's avatar Andrew Nicols
Browse files

Merge branch 'MDL-57643-master' of git://github.com/jleyva/moodle

parents eb6f0473 630f0e3b
......@@ -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(),
)
);
}
}
......@@ -71,11 +71,7 @@ if (!$canmanage) {
// record answer (if necessary) and show response (if none say if answer is correct or not)
$page = $lesson->load_page(required_param('pageid', PARAM_INT));
$userhasgrade = $DB->count_records("lesson_grades", array("lessonid"=>$lesson->id, "userid"=>$USER->id));
$reviewmode = false;
if ($userhasgrade && !$lesson->retake) {
$reviewmode = true;
}
$reviewmode = $lesson->is_in_review_mode();
// Check the page has answers [MDL-25632]
if (count($page->answers) > 0) {
......
......@@ -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),
),
);
......@@ -378,7 +378,7 @@ switch ($mode) {
$essaylinks = array();
// Number of attempts on the lesson
$attempts = $DB->count_records('lesson_grades', array('userid'=>$userid, 'lessonid'=>$lesson->id));
$attempts = $lesson->count_user_retries($userid);
// Go through each essay page
foreach ($studentessays[$userid] as $page => $tries) {
......
......@@ -2255,6 +2255,108 @@ class lesson extends lesson_base {
}
return false;
}
/**
* Check if the lesson is in review mode. (The user already finished it and retakes are not allowed).
*
* @return bool true if is in review mode
* @since Moodle 3.3
*/
public function is_in_review_mode() {
global $DB, $USER;
$userhasgrade = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
if ($userhasgrade && !$this->properties->retake) {
return true;
}
return false;
}
/**
* Return the last page the current user saw.
*
* @param int $retriescount the number of retries for the lesson (the last retry number).
* @return mixed false if the user didn't see the lesson or the last page id
*/
public function get_last_page_seen($retriescount) {
global $DB, $USER;
$lastpageseen = false;
$allattempts = $this->get_attempts($retriescount);
if (!empty($allattempts)) {
$attempt = end($allattempts);
$attemptpage = $this->load_page($attempt->pageid);
$jumpto = $DB->get_field('lesson_answers', 'jumpto', array('id' => $attempt->answerid));
// Convert the jumpto to a proper page id.
if ($jumpto == 0) {
// Check if a question has been incorrectly answered AND no more attempts at it are left.
$nattempts = $this->get_attempts($attempt->retry, false, $attempt->pageid, $USER->id);
if (count($nattempts) >= $this->properties->maxattempts) {
$lastpageseen = $this->get_next_page($attemptpage->nextpageid);
} else {
$lastpageseen = $attempt->pageid;
}
} else if ($jumpto == LESSON_NEXTPAGE) {
$lastpageseen = $this->get_next_page($attemptpage->nextpageid);
} else if ($jumpto == LESSON_CLUSTERJUMP) {
$lastpageseen = $this->cluster_jump($attempt->pageid);
} else {
$lastpageseen = $jumpto;
}
}
if ($branchtables = $DB->get_records('lesson_branch', array("lessonid" => $this->properties->id, "userid" => $USER->id,
"retry" => $retriescount), 'timeseen DESC')) {
// In here, user has viewed a branch table.
$lastbranchtable = current($branchtables);
if (count($allattempts) > 0) {
if ($lastbranchtable->timeseen > $attempt->timeseen) {
// This branch table was viewed more recently than the question page.
if (!empty($lastbranchtable->nextpageid)) {
$lastpageseen = $lastbranchtable->nextpageid;
} else {
// Next page ID did not exist prior to MDL-34006.
$lastpageseen = $lastbranchtable->pageid;
}
}
} else {
// Has not answered any questions but has viewed a branch table.
if (!empty($lastbranchtable->nextpageid)) {
$lastpageseen = $lastbranchtable->nextpageid;
} else {
// Next page ID did not exist prior to MDL-34006.
$lastpageseen = $lastbranchtable->pageid;
}
}
}
return $lastpageseen;
}
/**
* Return the number of retries in a lesson for a given user.
*
* @param int $userid the user id
* @return int the retries count
* @since Moodle 3.3
*/
public function count_user_retries($userid) {
global $DB;
return $DB->count_records('lesson_grades', array("lessonid" => $this->properties->id, "userid" => $userid));
}
/**
* Check if a user left a timed session.
*
* @param int $retriescount the number of retries for the lesson (the last retry number).
* @return true if the user left the timed session
*/
public function left_during_timed_session($retriescount) {
global $DB, $USER;
$conditions = array('lessonid' => $this->properties->id, 'userid' => $USER->id, 'retry' => $retriescount);
return $DB->count_records('lesson_attempts', $conditions) > 0 || $DB->count_records('lesson_branch', $conditions) > 0;
}
}
......
......@@ -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']);
}
}
......@@ -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;
......@@ -64,11 +64,7 @@ $canmanage = $lesson->can_manage();
$lessonoutput = $PAGE->get_renderer('mod_lesson');
$reviewmode = false;
$userhasgrade = $DB->count_records("lesson_grades", array("lessonid"=>$lesson->id, "userid"=>$USER->id));
if ($userhasgrade && !$lesson->retake) {
$reviewmode = true;
}
$reviewmode = $lesson->is_in_review_mode();
if ($lesson->usepassword && !empty($userpassword)) {
require_sesskey();
......@@ -97,13 +93,17 @@ if ($pageid == LESSON_UNSEENBRANCHPAGE) {
$pageid = lesson_unseen_question_jump($lesson, $USER->id, $pageid);
}
// To avoid multiple calls, store the magic property firstpage.
$lessonfirstpage = $lesson->firstpage;
$lessonfirstpageid = $lessonfirstpage ? $lessonfirstpage->id : false;
// display individual pages and their sets of answers
// if pageid is EOL then the end of the lesson has been reached
// for flow, changed to simple echo for flow styles, michaelp, moved lesson name and page title down
$attemptflag = false;
if (empty($pageid)) {
// make sure there are pages to view
if (!$DB->get_field('lesson_pages', 'id', array('lessonid' => $lesson->id, 'prevpageid' => 0))) {
if (!$lessonfirstpageid) {
if (!$canmanage) {
$lesson->add_message(get_string('lessonnotready2', 'lesson')); // a nice message to the student
} else {
......@@ -116,7 +116,7 @@ if (empty($pageid)) {
}
// if no pageid given see if the lesson has been started
$retries = $DB->count_records('lesson_grades', array("lessonid" => $lesson->id, "userid" => $USER->id));
$retries = $lesson->count_user_retries($USER->id);
if ($retries > 0) {
$attemptflag = true;
}
......@@ -125,57 +125,12 @@ if (empty($pageid)) {
unset($USER->modattempts[$lesson->id]); // if no pageid, then student is NOT reviewing
}
// If there are any questions that have been answered correctly (or not) in this attempt.
$allattempts = $lesson->get_attempts($retries);
if (!empty($allattempts)) {
$attempt = end($allattempts);
$attemptpage = $lesson->load_page($attempt->pageid);
$jumpto = $DB->get_field('lesson_answers', 'jumpto', array('id' => $attempt->answerid));
// convert the jumpto to a proper page id
if ($jumpto == 0) {
// Check if a question has been incorrectly answered AND no more attempts at it are left.
$nattempts = $lesson->get_attempts($attempt->retry, false, $attempt->pageid, $USER->id);
if (count($nattempts) >= $lesson->maxattempts) {
$lastpageseen = $lesson->get_next_page($attemptpage->nextpageid);