diff --git a/mod/lesson/classes/external.php b/mod/lesson/classes/external.php index 9a750c874c2..bf99b78cbd2 100644 --- a/mod/lesson/classes/external.php +++ b/mod/lesson/classes/external.php @@ -1395,4 +1395,146 @@ class mod_lesson_external extends external_api { ) ); } + + /** + * Describes the parameters for process_page. + * + * @return external_external_function_parameters + * @since Moodle 3.3 + */ + public static function process_page_parameters() { + return new external_function_parameters ( + array( + 'lessonid' => new external_value(PARAM_INT, 'lesson instance id'), + 'pageid' => new external_value(PARAM_INT, 'the page id'), + 'data' => new external_multiple_structure( + new external_single_structure( + array( + 'name' => new external_value(PARAM_RAW, 'data name'), + 'value' => new external_value(PARAM_RAW, 'data value'), + ) + ), 'the data to be saved' + ), + 'password' => new external_value(PARAM_RAW, 'optional password (the lesson may be protected)', VALUE_DEFAULT, ''), + 'review' => new external_value(PARAM_BOOL, 'if we want to review just after finishing (1 hour margin)', + VALUE_DEFAULT, false), + ) + ); + } + + /** + * Processes page responses + * + * @param int $lessonid lesson instance id + * @param int $pageid page id + * @param array $data the data to be saved + * @param str $password optional password (the lesson may be protected) + * @param bool $review if we want to review just after finishing (1 hour margin) + * @return array of warnings and status result + * @since Moodle 3.3 + * @throws moodle_exception + */ + public static function process_page($lessonid, $pageid, $data, $password = '', $review = false) { + global $USER; + + $params = array('lessonid' => $lessonid, 'pageid' => $pageid, 'data' => $data, 'password' => $password, + 'review' => $review); + $params = self::validate_parameters(self::process_page_parameters(), $params); + + $warnings = array(); + $pagecontent = $ongoingscore = ''; + $progress = null; + + list($lesson, $course, $cm, $context) = self::validate_lesson($params['lessonid']); + + // Update timer so the validation can check the time restrictions. + $timer = $lesson->update_timer(); + self::validate_attempt($lesson, $params); + + // Create the $_POST object required by the lesson question engine. + $_POST = array(); + foreach ($data as $element) { + // First check if we are handling editor fields like answer[text]. + if (preg_match('/(.+)\[(.+)\]$/', $element['name'], $matches)) { + $_POST[$matches[1]][$matches[2]] = $element['value']; + } else { + $_POST[$element['name']] = $element['value']; + } + } + + // Ignore sesskey (deep in some APIs), the request is already validated. + $USER->ignoresesskey = true; + + // Process page. + $page = $lesson->load_page($params['pageid']); + $result = $lesson->process_page_responses($page); + + // Prepare messages. + $reviewmode = $lesson->is_in_review_mode(); + $lesson->add_messages_on_page_process($page, $result, $reviewmode); + + // Additional lesson information. + if (!$lesson->can_manage()) { + if ($lesson->ongoing && !$reviewmode) { + $ongoingscore = $lesson->get_ongoing_score_message(); + } + if ($lesson->progressbar) { + $progress = $lesson->calculate_progress(); + } + } + + // Check conditionally everything coming from result (except newpageid because is always set). + $result = array( + 'newpageid' => (int) $result->newpageid, + 'inmediatejump' => $result->inmediatejump, + 'nodefaultresponse' => !empty($result->nodefaultresponse), + 'feedback' => (isset($result->feedback)) ? $result->feedback : '', + 'attemptsremaining' => (isset($result->attemptsremaining)) ? $result->attemptsremaining : null, + 'correctanswer' => !empty($result->correctanswer), + 'noanswer' => !empty($result->noanswer), + 'isessayquestion' => !empty($result->isessayquestion), + 'maxattemptsreached' => !empty($result->maxattemptsreached), + 'response' => (isset($result->response)) ? $result->response : '', + 'studentanswer' => (isset($result->studentanswer)) ? $result->studentanswer : '', + 'userresponse' => (isset($result->userresponse)) ? $result->userresponse : '', + 'reviewmode' => $reviewmode, + 'ongoingscore' => $ongoingscore, + 'progress' => $progress, + 'displaymenu' => !empty(lesson_displayleftif($lesson)), + 'messages' => self::format_lesson_messages($lesson), + 'warnings' => $warnings, + ); + return $result; + } + + /** + * Describes the process_page return value. + * + * @return external_single_structure + * @since Moodle 3.3 + */ + public static function process_page_returns() { + return new external_single_structure( + array( + 'newpageid' => new external_value(PARAM_INT, 'New page id (if a jump was made).'), + 'inmediatejump' => new external_value(PARAM_BOOL, 'Whether the page processing redirect directly to anoter page.'), + 'nodefaultresponse' => new external_value(PARAM_BOOL, 'Whether there is not a default response.'), + 'feedback' => new external_value(PARAM_RAW, 'The response feedback.'), + 'attemptsremaining' => new external_value(PARAM_INT, 'Number of attempts remaining.'), + 'correctanswer' => new external_value(PARAM_BOOL, 'Whether the answer is correct.'), + 'noanswer' => new external_value(PARAM_BOOL, 'Whether there aren\'t answers.'), + 'isessayquestion' => new external_value(PARAM_BOOL, 'Whether is a essay question.'), + 'maxattemptsreached' => new external_value(PARAM_BOOL, 'Whether we reachered the max number of attempts.'), + 'response' => new external_value(PARAM_RAW, 'The response.'), + 'studentanswer' => new external_value(PARAM_RAW, 'The student answer.'), + 'userresponse' => new external_value(PARAM_RAW, 'The user response.'), + 'reviewmode' => new external_value(PARAM_BOOL, 'Whether the user is reviewing.'), + 'ongoingscore' => new external_value(PARAM_TEXT, 'The ongoing message.'), + 'progress' => new external_value(PARAM_INT, 'Progress percentage in the lesson.'), + 'displaymenu' => new external_value(PARAM_BOOL, 'Whether we should display the menu or not in this page.'), + 'messages' => self::external_messages(), + 'warnings' => new external_warnings(), + ) + ); + } } diff --git a/mod/lesson/continue.php b/mod/lesson/continue.php index 06396041eab..fe7fe690778 100644 --- a/mod/lesson/continue.php +++ b/mod/lesson/continue.php @@ -31,7 +31,7 @@ $id = required_param('id', PARAM_INT); $cm = get_coursemodule_from_id('lesson', $id, 0, false, MUST_EXIST); $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); -$lesson = new lesson($DB->get_record('lesson', array('id' => $cm->instance), '*', MUST_EXIST)); +$lesson = new lesson($DB->get_record('lesson', array('id' => $cm->instance), '*', MUST_EXIST), $cm, $course); require_login($course, false, $cm); require_sesskey(); @@ -39,8 +39,8 @@ require_sesskey(); // Apply overrides. $lesson->update_effective_access($USER->id); -$context = context_module::instance($cm->id); -$canmanage = has_capability('mod/lesson:manage', $context); +$context = $lesson->context; +$canmanage = $lesson->can_manage(); $lessonoutput = $PAGE->get_renderer('mod_lesson'); $url = new moodle_url('/mod/lesson/continue.php', array('id'=>$cm->id)); @@ -66,97 +66,16 @@ $page = $lesson->load_page(required_param('pageid', PARAM_INT)); $reviewmode = $lesson->is_in_review_mode(); -// Check the page has answers [MDL-25632] -if (count($page->answers) > 0) { - $result = $page->record_attempt($context); -} else { - // The page has no answers so we will just progress to the next page in the - // sequence (as set by newpageid). - $result = new stdClass; - $result->newpageid = optional_param('newpageid', $page->nextpageid, PARAM_INT); - $result->nodefaultresponse = true; -} +// Process the page responses. +$result = $lesson->process_page_responses($page); -if (isset($USER->modattempts[$lesson->id])) { - // make sure if the student is reviewing, that he/she sees the same pages/page path that he/she saw the first time - if ($USER->modattempts[$lesson->id]->pageid == $page->id && $page->nextpageid == 0) { // remember, this session variable holds the pageid of the last page that the user saw - $result->newpageid = LESSON_EOL; - } else { - $nretakes = $DB->count_records("lesson_grades", array("lessonid"=>$lesson->id, "userid"=>$USER->id)); - $nretakes--; // make sure we are looking at the right try. - $attempts = $DB->get_records("lesson_attempts", array("lessonid"=>$lesson->id, "userid"=>$USER->id, "retry"=>$nretakes), "timeseen", "id, pageid"); - $found = false; - $temppageid = 0; - // Make sure that the newpageid always defaults to something valid. - $result->newpageid = LESSON_EOL; - foreach($attempts as $attempt) { - if ($found && $temppageid != $attempt->pageid) { // now try to find the next page, make sure next few attempts do no belong to current page - $result->newpageid = $attempt->pageid; - break; - } - if ($attempt->pageid == $page->id) { - $found = true; // if found current page - $temppageid = $attempt->pageid; - } - } - } -} elseif ($result->newpageid != LESSON_CLUSTERJUMP && $page->id != 0 && $result->newpageid > 0) { - // going to check to see if the page that the user is going to view next, is a cluster page. - // If so, dont display, go into the cluster. The $result->newpageid > 0 is used to filter out all of the negative code jumps. - $newpage = $lesson->load_page($result->newpageid); - if ($newpageid = $newpage->override_next_page($result->newpageid)) { - $result->newpageid = $newpageid; - } -} elseif ($result->newpageid == LESSON_UNSEENBRANCHPAGE) { - if ($canmanage) { - if ($page->nextpageid == 0) { - $result->newpageid = LESSON_EOL; - } else { - $result->newpageid = $page->nextpageid; - } - } else { - $result->newpageid = lesson_unseen_question_jump($lesson, $USER->id, $page->id); - } -} elseif ($result->newpageid == LESSON_PREVIOUSPAGE) { - $result->newpageid = $page->prevpageid; -} elseif ($result->newpageid == LESSON_RANDOMPAGE) { - $result->newpageid = lesson_random_question_jump($lesson, $page->id); -} elseif ($result->newpageid == LESSON_CLUSTERJUMP) { - if ($canmanage) { - if ($page->nextpageid == 0) { // if teacher, go to next page - $result->newpageid = LESSON_EOL; - } else { - $result->newpageid = $page->nextpageid; - } - } else { - $result->newpageid = $lesson->cluster_jump($page->id); - } -} - -if ($result->nodefaultresponse) { - // Don't display feedback +if ($result->nodefaultresponse || $result->inmediatejump) { + // Don't display feedback or force a redirecto to newpageid. redirect(new moodle_url('/mod/lesson/view.php', array('id'=>$cm->id,'pageid'=>$result->newpageid))); } -/// Set Messages - -if ($canmanage) { - // This is the warning msg for teachers to inform them that cluster and unseen does not work while logged in as a teacher - if(lesson_display_teacher_warning($lesson)) { - $warningvars = new stdClass(); - $warningvars->cluster = get_string("clusterjump", "lesson"); - $warningvars->unseen = get_string("unseenpageinbranch", "lesson"); - $lesson->add_message(get_string("teacherjumpwarning", "lesson", $warningvars)); - } - // Inform teacher that s/he will not see the timer - if ($lesson->timelimit) { - $lesson->add_message(get_string("teachertimerwarning", "lesson")); - } -} -// Report attempts remaining -if ($result->attemptsremaining != 0 && $lesson->review && !$reviewmode) { - $lesson->add_message(get_string('attemptsremaining', 'lesson', $result->attemptsremaining)); -} +// Set Messages. +$lesson->add_messages_on_page_process($page, $result, $reviewmode); $PAGE->set_url('/mod/lesson/view.php', array('id' => $cm->id, 'pageid' => $page->id)); $PAGE->set_subpage($page->id); diff --git a/mod/lesson/db/services.php b/mod/lesson/db/services.php index f6cbb10ec2e..09224df9c81 100644 --- a/mod/lesson/db/services.php +++ b/mod/lesson/db/services.php @@ -116,4 +116,12 @@ $functions = array( 'capabilities' => 'mod/lesson:view', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) ), + 'mod_lesson_process_page' => array( + 'classname' => 'mod_lesson_external', + 'methodname' => 'process_page', + 'description' => 'Processes page responses.', + 'type' => 'write', + 'capabilities' => 'mod/lesson:view', + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE) + ), ); diff --git a/mod/lesson/locallib.php b/mod/lesson/locallib.php index 43b3031b5c5..a2ae35d4324 100644 --- a/mod/lesson/locallib.php +++ b/mod/lesson/locallib.php @@ -2675,6 +2675,121 @@ class lesson extends lesson_base { return array($page, $lessoncontent); } + + /** + * Process page responses. + * + * @param lesson_page $page page object + * @since Moodle 3.3 + */ + public function process_page_responses(lesson_page $page) { + global $USER, $DB; + + $canmanage = $this->can_manage(); + $context = $this->get_context(); + + // Check the page has answers [MDL-25632]. + if (count($page->answers) > 0) { + $result = $page->record_attempt($context); + } else { + // The page has no answers so we will just progress to the next page in the + // sequence (as set by newpageid). + $result = new stdClass; + $result->newpageid = optional_param('newpageid', $page->nextpageid, PARAM_INT); + $result->nodefaultresponse = true; + } + + if ($result->inmediatejump) { + return $result; + } else if (isset($USER->modattempts[$this->properties->id])) { + // Make sure if the student is reviewing, that he/she sees the same pages/page path that he/she saw the first time. + if ($USER->modattempts[$this->properties->id]->pageid == $page->id && $page->nextpageid == 0) { + // Remember, this session variable holds the pageid of the last page that the user saw. + $result->newpageid = LESSON_EOL; + } else { + $nretakes = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id)); + $nretakes--; // Make sure we are looking at the right try. + $attempts = $DB->get_records("lesson_attempts", array("lessonid" => $this->properties->id, "userid" => $USER->id, "retry" => $nretakes), "timeseen", "id, pageid"); + $found = false; + $temppageid = 0; + // Make sure that the newpageid always defaults to something valid. + $result->newpageid = LESSON_EOL; + foreach ($attempts as $attempt) { + if ($found && $temppageid != $attempt->pageid) { + // Now try to find the next page, make sure next few attempts do no belong to current page. + $result->newpageid = $attempt->pageid; + break; + } + if ($attempt->pageid == $page->id) { + $found = true; // If found current page. + $temppageid = $attempt->pageid; + } + } + } + } else if ($result->newpageid != LESSON_CLUSTERJUMP && $page->id != 0 && $result->newpageid > 0) { + // Going to check to see if the page that the user is going to view next, is a cluster page. + // If so, dont display, go into the cluster. + // The $result->newpageid > 0 is used to filter out all of the negative code jumps. + $newpage = $this->load_page($result->newpageid); + if ($newpageid = $newpage->override_next_page($result->newpageid)) { + $result->newpageid = $newpageid; + } + } else if ($result->newpageid == LESSON_UNSEENBRANCHPAGE) { + if ($canmanage) { + if ($page->nextpageid == 0) { + $result->newpageid = LESSON_EOL; + } else { + $result->newpageid = $page->nextpageid; + } + } else { + $result->newpageid = lesson_unseen_question_jump($this, $USER->id, $page->id); + } + } else if ($result->newpageid == LESSON_PREVIOUSPAGE) { + $result->newpageid = $page->prevpageid; + } else if ($result->newpageid == LESSON_RANDOMPAGE) { + $result->newpageid = lesson_random_question_jump($this, $page->id); + } else if ($result->newpageid == LESSON_CLUSTERJUMP) { + if ($canmanage) { + if ($page->nextpageid == 0) { // If teacher, go to next page. + $result->newpageid = LESSON_EOL; + } else { + $result->newpageid = $page->nextpageid; + } + } else { + $result->newpageid = $lesson->cluster_jump($page->id); + } + } + return $result; + } + + /** + * Add different informative messages to the given page. + * + * @param lesson_page $page page object + * @param stdClass $result the page processing result object + * @param bool $reviewmode whether we are in review mode or not + * @since Moodle 3.3 + */ + public function add_messages_on_page_process(lesson_page $page, $result, $reviewmode) { + + if ($this->can_manage()) { + // This is the warning msg for teachers to inform them that cluster and unseen does not work while logged in as a teacher. + if (lesson_display_teacher_warning($this)) { + $warningvars = new stdClass(); + $warningvars->cluster = get_string("clusterjump", "lesson"); + $warningvars->unseen = get_string("unseenpageinbranch", "lesson"); + $this->add_message(get_string("teacherjumpwarning", "lesson", $warningvars)); + } + // Inform teacher that s/he will not see the timer. + if ($this->properties->timelimit) { + $lesson->add_message(get_string("teachertimerwarning", "lesson")); + } + } + // Report attempts remaining. + if ($result->attemptsremaining != 0 && $this->properties->review && !$reviewmode) { + $this->add_message(get_string('attemptsremaining', 'lesson', $result->attemptsremaining)); + } + } } @@ -3115,6 +3230,11 @@ abstract class lesson_page extends lesson_base { */ $result = $this->check_answer(); + // Processes inmediate jumps. + if ($result->inmediatejump) { + return $result; + } + $result->attemptsremaining = 0; $result->maxattemptsreached = false; @@ -3630,6 +3750,7 @@ abstract class lesson_page extends lesson_base { $result->userresponse = null; $result->feedback = ''; $result->nodefaultresponse = false; // Flag for redirecting when default feedback is turned off + $result->inmediatejump = false; // Flag to detect when we should do a jump from the page without further processing. return $result; } diff --git a/mod/lesson/pagetypes/branchtable.php b/mod/lesson/pagetypes/branchtable.php index ee1a3c14499..5a43fceaebf 100644 --- a/mod/lesson/pagetypes/branchtable.php +++ b/mod/lesson/pagetypes/branchtable.php @@ -160,6 +160,8 @@ class lesson_page_type_branchtable extends lesson_page { public function check_answer() { global $USER, $DB, $PAGE, $CFG; + $result = parent::check_answer(); + require_sesskey(); $newpageid = optional_param('jumpto', null, PARAM_INT); // going to insert into lesson_branch @@ -210,7 +212,10 @@ class lesson_page_type_branchtable extends lesson_page { $branch->nextpageid = $newpageid; $DB->insert_record("lesson_branch", $branch); - redirect(new moodle_url('/mod/lesson/view.php', array('id' => $PAGE->cm->id, 'pageid' => $newpageid))); + // This will force to redirect to the newpageid. + $result->inmediatejump = true; + $result->newpageid = $newpageid; + return $result; } public function display_answers(html_table $table) { diff --git a/mod/lesson/pagetypes/essay.php b/mod/lesson/pagetypes/essay.php index 9592180f68f..992200d4754 100644 --- a/mod/lesson/pagetypes/essay.php +++ b/mod/lesson/pagetypes/essay.php @@ -120,7 +120,9 @@ class lesson_page_type_essay extends lesson_page { require_sesskey(); if (!$data) { - redirect(new moodle_url('/mod/lesson/view.php', array('id'=>$PAGE->cm->id, 'pageid'=>$this->properties->id))); + $result->inmediatejump = true; + $result->newpageid = $this->properties->id; + return $result; } if (is_array($data->answer)) { diff --git a/mod/lesson/pagetypes/matching.php b/mod/lesson/pagetypes/matching.php index cb8e5221711..c43f7005f44 100644 --- a/mod/lesson/pagetypes/matching.php +++ b/mod/lesson/pagetypes/matching.php @@ -181,7 +181,9 @@ class lesson_page_type_matching extends lesson_page { require_sesskey(); if (!$data) { - redirect(new moodle_url('/mod/lesson/view.php', array('id'=>$PAGE->cm->id, 'pageid'=>$this->properties->id))); + $result->inmediatejump = true; + $result->newpageid = $this->properties->id; + return $result; } $response = $data->response; diff --git a/mod/lesson/pagetypes/multichoice.php b/mod/lesson/pagetypes/multichoice.php index b56fa30cded..37b6a8d9481 100644 --- a/mod/lesson/pagetypes/multichoice.php +++ b/mod/lesson/pagetypes/multichoice.php @@ -135,7 +135,9 @@ class lesson_page_type_multichoice extends lesson_page { require_sesskey(); if (!$data) { - redirect(new moodle_url('/mod/lesson/view.php', array('id'=>$PAGE->cm->id, 'pageid'=>$this->properties->id))); + $result->inmediatejump = true; + $result->newpageid = $this->properties->id; + return $result; } if ($this->properties->qoption) { diff --git a/mod/lesson/tests/external_test.php b/mod/lesson/tests/external_test.php index f44bf3af71e..625aa130ec2 100644 --- a/mod/lesson/tests/external_test.php +++ b/mod/lesson/tests/external_test.php @@ -974,4 +974,72 @@ class mod_lesson_external_testcase extends externallib_advanced_testcase { $this->setExpectedException('moodle_exception'); $result = mod_lesson_external::get_page_data($this->lesson->id, $this->page2->id, '', false, true); } + + /** + * Test process_page + */ + public function test_process_page() { + global $DB; + + $this->setUser($this->student); + // First we need to launch the lesson so the timer is on. + mod_lesson_external::launch_attempt($this->lesson->id); + + // Configure the lesson to return feedback and avoid custom scoring. + $DB->set_field('lesson', 'feedback', 1, array('id' => $this->lesson->id)); + $DB->set_field('lesson', 'progressbar', 1, array('id' => $this->lesson->id)); + $DB->set_field('lesson', 'custom', 0, array('id' => $this->lesson->id)); + $DB->set_field('lesson', 'maxattempts', 3, array('id' => $this->lesson->id)); + + // Now, we can directly launch mocking the data. + + // First incorrect response. + $answerincorrect = 0; + $answercorrect = 0; + $p2answers = $DB->get_records('lesson_answers', array('lessonid' => $this->lesson->id, 'pageid' => $this->page2->id), 'id'); + foreach ($p2answers as $answer) { + if ($answer->jumpto == 0) { + $answerincorrect = $answer->id; + } else { + $answercorrect = $answer->id; + } + } + + $data = array( + array( + 'name' => 'answerid', + 'value' => $answerincorrect, + ), + array( + 'name' => '_qf__lesson_display_answer_form_truefalse', + 'value' => 1, + ) + ); + $result = mod_lesson_external::process_page($this->lesson->id, $this->page2->id, $data); + $result = external_api::clean_returnvalue(mod_lesson_external::process_page_returns(), $result); + + $this->assertEquals($this->page2->id, $result['newpageid']); // Same page, since the answer was incorrect. + $this->assertFalse($result['correctanswer']); // Incorrect answer. + $this->assertEquals(50, $result['progress']); + + // Correct response. + $data = array( + array( + 'name' => 'answerid', + 'value' => $answercorrect, + ), + array( + 'name' => '_qf__lesson_display_answer_form_truefalse', + 'value' => 1, + ) + ); + + $result = mod_lesson_external::process_page($this->lesson->id, $this->page2->id, $data); + $result = external_api::clean_returnvalue(mod_lesson_external::process_page_returns(), $result); + + $this->assertEquals($this->page1->id, $result['newpageid']); // Next page, the answer was correct. + $this->assertTrue($result['correctanswer']); // Correct response. + $this->assertFalse($result['maxattemptsreached']); // Still one attempt. + $this->assertEquals(50, $result['progress']); + } } diff --git a/mod/lesson/version.php b/mod/lesson/version.php index 52f4ce9a82c..d7b95f6c320 100644 --- a/mod/lesson/version.php +++ b/mod/lesson/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2016120510; // The current module version (Date: YYYYMMDDXX) +$plugin->version = 2016120511; // 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;