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

This commit is contained in:
David Monllao 2017-03-27 11:50:35 +02:00
commit 49ba56cf38
6 changed files with 347 additions and 142 deletions

View file

@ -708,6 +708,28 @@ class mod_lesson_external extends external_api {
);
}
/**
* Describes an attempt grade structure.
*
* @param int $required if the structure is required or optional
* @return external_single_structure the structure
* @since Moodle 3.3
*/
protected static function get_user_attempt_grade_structure($required = VALUE_REQUIRED) {
$data = array(
'nquestions' => new external_value(PARAM_INT, 'Number of questions answered'),
'attempts' => new external_value(PARAM_INT, 'Number of question attempts'),
'total' => new external_value(PARAM_FLOAT, 'Max points possible'),
'earned' => new external_value(PARAM_FLOAT, 'Points earned by student'),
'grade' => new external_value(PARAM_FLOAT, 'Calculated percentage grade'),
'nmanual' => new external_value(PARAM_INT, 'Number of manually graded questions'),
'manualpoints' => new external_value(PARAM_FLOAT, 'Point value for manually graded questions'),
);
return new external_single_structure(
$data, 'Attempt grade', $required
);
}
/**
* Describes the parameters for get_user_attempt_grade.
*
@ -758,7 +780,8 @@ class mod_lesson_external extends external_api {
self::check_can_view_user_data($params['userid'], $course, $cm, $context);
}
$result = (array) lesson_grade($lesson, $params['lessonattempt'], $params['userid']);
$result = array();
$result['grade'] = (array) lesson_grade($lesson, $params['lessonattempt'], $params['userid']);
$result['warnings'] = $warnings;
return $result;
}
@ -772,13 +795,7 @@ class mod_lesson_external extends external_api {
public static function get_user_attempt_grade_returns() {
return new external_single_structure(
array(
'nquestions' => new external_value(PARAM_INT, 'Number of questions answered'),
'attempts' => new external_value(PARAM_INT, 'Number of question attempts'),
'total' => new external_value(PARAM_FLOAT, 'Max points possible'),
'earned' => new external_value(PARAM_FLOAT, 'Points earned by student'),
'grade' => new external_value(PARAM_FLOAT, 'Calculated percentage grade'),
'nmanual' => new external_value(PARAM_INT, 'Number of manually graded questions'),
'manualpoints' => new external_value(PARAM_FLOAT, 'Point value for manually graded questions'),
'grade' => self::get_user_attempt_grade_structure(),
'warnings' => new external_warnings(),
)
);
@ -1761,4 +1778,105 @@ class mod_lesson_external extends external_api {
)
);
}
/**
* Describes the parameters for get_user_attempt.
*
* @return external_external_function_parameters
* @since Moodle 3.3
*/
public static function get_user_attempt_parameters() {
return new external_function_parameters (
array(
'lessonid' => new external_value(PARAM_INT, 'Lesson instance id.'),
'userid' => new external_value(PARAM_INT, 'The user id. 0 for current user.'),
'lessonattempt' => new external_value(PARAM_INT, 'The attempt number.'),
)
);
}
/**
* Return information about the given user attempt (including answers).
*
* @param int $lessonid lesson instance id
* @param int $userid the user id
* @param int $lessonattempt the attempt number
* @return array of warnings and page attempts
* @since Moodle 3.3
* @throws moodle_exception
*/
public static function get_user_attempt($lessonid, $userid, $lessonattempt) {
global $USER;
$params = array(
'lessonid' => $lessonid,
'userid' => $userid,
'lessonattempt' => $lessonattempt,
);
$params = self::validate_parameters(self::get_user_attempt_parameters(), $params);
$warnings = array();
list($lesson, $course, $cm, $context) = self::validate_lesson($params['lessonid']);
// Default value for userid.
if (empty($params['userid'])) {
$params['userid'] = $USER->id;
}
// Extra checks so only users with permissions can view other users attempts.
if ($USER->id != $params['userid']) {
self::check_can_view_user_data($params['userid'], $course, $cm, $context);
}
list($answerpages, $userstats) = lesson_get_user_detailed_report_data($lesson, $userid, $params['lessonattempt']);
$result = array(
'answerpages' => $answerpages,
'userstats' => $userstats,
'warnings' => $warnings,
);
return $result;
}
/**
* Describes the get_user_attempt return value.
*
* @return external_single_structure
* @since Moodle 3.3
*/
public static function get_user_attempt_returns() {
return new external_single_structure(
array(
'answerpages' => new external_multiple_structure(
new external_single_structure(
array(
'title' => new external_value(PARAM_RAW, 'Page title.'),
'contents' => new external_value(PARAM_RAW, 'Page contents.'),
'qtype' => new external_value(PARAM_TEXT, 'Identifies the page type of this page.'),
'grayout' => new external_value(PARAM_INT, 'If is required to apply a grayout.'),
'answerdata' => new external_single_structure(
array(
'score' => new external_value(PARAM_TEXT, 'The score (text version).'),
'response' => new external_value(PARAM_RAW, 'The response text.'),
'responseformat' => new external_format_value('response.'),
'answers' => new external_multiple_structure(
new external_multiple_structure(new external_value(PARAM_RAW, 'Possible answers and info.'))
)
), 'Answer data (empty in content pages created in Moodle 1.x).', VALUE_OPTIONAL
)
)
)
),
'userstats' => new external_single_structure(
array(
'grade' => new external_value(PARAM_FLOAT, 'Attempt final grade.'),
'completed' => new external_value(PARAM_INT, 'Time completed.'),
'timetotake' => new external_value(PARAM_INT, 'Time taken.'),
'gradeinfo' => self::get_user_attempt_grade_structure(VALUE_OPTIONAL)
)
),
'warnings' => new external_warnings(),
)
);
}
}

View file

@ -140,4 +140,12 @@ $functions = array(
'capabilities' => 'mod/lesson:viewreports',
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
'mod_lesson_get_user_attempt' => array(
'classname' => 'mod_lesson_external',
'methodname' => 'get_user_attempt',
'description' => 'Return information about the given user attempt (including answers).',
'type' => 'read',
'capabilities' => 'mod/lesson:viewreports',
'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
),
);

View file

@ -1009,6 +1009,149 @@ function lesson_get_overview_report_table_and_data(lesson $lesson, $currentgroup
return array($table, $data);
}
/**
* Return information about one user attempt (including answers)
* @param lesson $lesson lesson instance
* @param int $userid the user id
* @param int $attempt the attempt number
* @return array the user answers (array) and user data stats (object)
* @since Moodle 3.3
*/
function lesson_get_user_detailed_report_data(lesson $lesson, $userid, $attempt) {
global $DB;
$context = $lesson->context;
if (!empty($userid)) {
// Apply overrides.
$lesson->update_effective_access($userid);
}
$lessonpages = $lesson->load_all_pages();
foreach ($lessonpages as $lessonpage) {
if ($lessonpage->prevpageid == 0) {
$pageid = $lessonpage->id;
}
}
// now gather the stats into an object
$firstpageid = $pageid;
$pagestats = array();
while ($pageid != 0) { // EOL
$page = $lessonpages[$pageid];
$params = array ("lessonid" => $lesson->id, "pageid" => $page->id);
if ($allanswers = $DB->get_records_select("lesson_attempts", "lessonid = :lessonid AND pageid = :pageid", $params, "timeseen")) {
// get them ready for processing
$orderedanswers = array();
foreach ($allanswers as $singleanswer) {
// ordering them like this, will help to find the single attempt record that we want to keep.
$orderedanswers[$singleanswer->userid][$singleanswer->retry][] = $singleanswer;
}
// this is foreach user and for each try for that user, keep one attempt record
foreach ($orderedanswers as $orderedanswer) {
foreach($orderedanswer as $tries) {
$page->stats($pagestats, $tries);
}
}
} else {
// no one answered yet...
}
//unset($orderedanswers); initialized above now
$pageid = $page->nextpageid;
}
$manager = lesson_page_type_manager::get($lesson);
$qtypes = $manager->get_page_type_strings();
$answerpages = array();
$answerpage = "";
$pageid = $firstpageid;
// cycle through all the pages
// foreach page, add to the $answerpages[] array all the data that is needed
// from the question, the users attempt, and the statistics
// grayout pages that the user did not answer and Branch, end of branch, cluster
// and end of cluster pages
while ($pageid != 0) { // EOL
$page = $lessonpages[$pageid];
$answerpage = new stdClass;
$data ='';
$answerdata = new stdClass;
// Set some defaults for the answer data.
$answerdata->score = null;
$answerdata->response = null;
$answerdata->responseformat = FORMAT_PLAIN;
$answerpage->title = format_string($page->title);
$options = new stdClass;
$options->noclean = true;
$options->overflowdiv = true;
$options->context = $context;
$answerpage->contents = format_text($page->contents, $page->contentsformat, $options);
$answerpage->qtype = $qtypes[$page->qtype].$page->option_description_string();
$answerpage->grayout = $page->grayout;
$answerpage->context = $context;
if (empty($userid)) {
// there is no userid, so set these vars and display stats.
$answerpage->grayout = 0;
$useranswer = null;
} elseif ($useranswers = $DB->get_records("lesson_attempts",array("lessonid"=>$lesson->id, "userid"=>$userid, "retry"=>$attempt,"pageid"=>$page->id), "timeseen")) {
// get the user's answer for this page
// need to find the right one
$i = 0;
foreach ($useranswers as $userattempt) {
$useranswer = $userattempt;
$i++;
if ($lesson->maxattempts == $i) {
break; // reached maxattempts, break out
}
}
} else {
// user did not answer this page, gray it out and set some nulls
$answerpage->grayout = 1;
$useranswer = null;
}
$i = 0;
$n = 0;
$answerpages[] = $page->report_answers(clone($answerpage), clone($answerdata), $useranswer, $pagestats, $i, $n);
$pageid = $page->nextpageid;
}
$userstats = new stdClass;
if (!empty($userid)) {
$params = array("lessonid"=>$lesson->id, "userid"=>$userid);
$alreadycompleted = true;
if (!$grades = $DB->get_records_select("lesson_grades", "lessonid = :lessonid and userid = :userid", $params, "completed", "*", $attempt, 1)) {
$userstats->grade = -1;
$userstats->completed = -1;
$alreadycompleted = false;
} else {
$userstats->grade = current($grades);
$userstats->completed = $userstats->grade->completed;
$userstats->grade = round($userstats->grade->grade, 2);
}
if (!$times = $lesson->get_user_timers($userid, 'starttime', '*', $attempt, 1)) {
$userstats->timetotake = -1;
$alreadycompleted = false;
} else {
$userstats->timetotake = current($times);
$userstats->timetotake = $userstats->timetotake->lessontime - $userstats->timetotake->starttime;
}
if ($alreadycompleted) {
$userstats->gradeinfo = lesson_grade($lesson, $attempt, $userid);
}
}
return array($answerpages, $userstats);
}
/**
* Abstract class that page type's MUST inherit from.
*

View file

@ -264,103 +264,7 @@ if ($action === 'delete') {
$userid = optional_param('userid', null, PARAM_INT); // if empty, then will display the general detailed view
$try = optional_param('try', null, PARAM_INT);
if (!empty($userid)) {
// Apply overrides.
$lesson->update_effective_access($userid);
}
$lessonpages = $lesson->load_all_pages();
foreach ($lessonpages as $lessonpage) {
if ($lessonpage->prevpageid == 0) {
$pageid = $lessonpage->id;
}
}
// now gather the stats into an object
$firstpageid = $pageid;
$pagestats = array();
while ($pageid != 0) { // EOL
$page = $lessonpages[$pageid];
$params = array ("lessonid" => $lesson->id, "pageid" => $page->id);
if ($allanswers = $DB->get_records_select("lesson_attempts", "lessonid = :lessonid AND pageid = :pageid", $params, "timeseen")) {
// get them ready for processing
$orderedanswers = array();
foreach ($allanswers as $singleanswer) {
// ordering them like this, will help to find the single attempt record that we want to keep.
$orderedanswers[$singleanswer->userid][$singleanswer->retry][] = $singleanswer;
}
// this is foreach user and for each try for that user, keep one attempt record
foreach ($orderedanswers as $orderedanswer) {
foreach($orderedanswer as $tries) {
$page->stats($pagestats, $tries);
}
}
} else {
// no one answered yet...
}
//unset($orderedanswers); initialized above now
$pageid = $page->nextpageid;
}
$manager = lesson_page_type_manager::get($lesson);
$qtypes = $manager->get_page_type_strings();
$answerpages = array();
$answerpage = "";
$pageid = $firstpageid;
// cycle through all the pages
// foreach page, add to the $answerpages[] array all the data that is needed
// from the question, the users attempt, and the statistics
// grayout pages that the user did not answer and Branch, end of branch, cluster
// and end of cluster pages
while ($pageid != 0) { // EOL
$page = $lessonpages[$pageid];
$answerpage = new stdClass;
$data ='';
$answerdata = new stdClass;
// Set some defaults for the answer data.
$answerdata->score = null;
$answerdata->response = null;
$answerdata->responseformat = FORMAT_PLAIN;
$answerpage->title = format_string($page->title);
$options = new stdClass;
$options->noclean = true;
$options->overflowdiv = true;
$options->context = $context;
$answerpage->contents = format_text($page->contents, $page->contentsformat, $options);
$answerpage->qtype = $qtypes[$page->qtype].$page->option_description_string();
$answerpage->grayout = $page->grayout;
$answerpage->context = $context;
if (empty($userid)) {
// there is no userid, so set these vars and display stats.
$answerpage->grayout = 0;
$useranswer = null;
} elseif ($useranswers = $DB->get_records("lesson_attempts",array("lessonid"=>$lesson->id, "userid"=>$userid, "retry"=>$try,"pageid"=>$page->id), "timeseen")) {
// get the user's answer for this page
// need to find the right one
$i = 0;
foreach ($useranswers as $userattempt) {
$useranswer = $userattempt;
$i++;
if ($lesson->maxattempts == $i) {
break; // reached maxattempts, break out
}
}
} else {
// user did not answer this page, gray it out and set some nulls
$answerpage->grayout = 1;
$useranswer = null;
}
$i = 0;
$n = 0;
$answerpages[] = $page->report_answers(clone($answerpage), clone($answerdata), $useranswer, $pagestats, $i, $n);
$pageid = $page->nextpageid;
}
list($answerpages, $userstats) = lesson_get_user_detailed_report_data($lesson, $userid, $try);
/// actually start printing something
$table = new html_table();
@ -380,24 +284,7 @@ if ($action === 'delete') {
$table->align = array('right', 'left');
$table->attributes['class'] = 'compacttable generaltable form-inline';
$params = array("lessonid"=>$lesson->id, "userid"=>$userid);
if (!$grades = $DB->get_records_select("lesson_grades", "lessonid = :lessonid and userid = :userid", $params, "completed", "*", $try, 1)) {
$grade = -1;
$completed = -1;
} else {
$grade = current($grades);
$completed = $grade->completed;
$grade = round($grade->grade, 2);
}
if (!$times = $lesson->get_user_timers($userid, 'starttime', '*', $try, 1)) {
$timetotake = -1;
} else {
$timetotake = current($times);
$timetotake = $timetotake->lessontime - $timetotake->starttime;
}
if ($timetotake == -1 || $completed == -1 || $grade == -1) {
if (empty($userstats->gradeinfo)) {
$table->align = array("center");
$table->data[] = array(get_string("notcompleted", "lesson"));
@ -407,10 +294,10 @@ if ($action === 'delete') {
$gradeinfo = lesson_grade($lesson, $try, $user->id);
$table->data[] = array(get_string('name').':', $OUTPUT->user_picture($user, array('courseid'=>$course->id)).fullname($user, true));
$table->data[] = array(get_string("timetaken", "lesson").":", format_time($timetotake));
$table->data[] = array(get_string("completed", "lesson").":", userdate($completed));
$table->data[] = array(get_string('rawgrade', 'lesson').':', $gradeinfo->earned.'/'.$gradeinfo->total);
$table->data[] = array(get_string("grade", "lesson").":", $grade."%");
$table->data[] = array(get_string("timetaken", "lesson").":", format_time($userstats->timetotake));
$table->data[] = array(get_string("completed", "lesson").":", userdate($userstats->completed));
$table->data[] = array(get_string('rawgrade', 'lesson').':', $userstats->gradeinfo->earned.'/'.$userstats->gradeinfo->total);
$table->data[] = array(get_string("grade", "lesson").":", $userstats->grade."%");
}
echo html_writer::table($table);

View file

@ -576,26 +576,26 @@ class mod_lesson_external_testcase extends externallib_advanced_testcase {
$result = mod_lesson_external::get_user_attempt_grade($this->lesson->id, $attemptnumber, $this->student->id);
$result = external_api::clean_returnvalue(mod_lesson_external::get_user_attempt_grade_returns(), $result);
$this->assertCount(0, $result['warnings']);
$this->assertEquals(1, $result['nquestions']);
$this->assertEquals(1, $result['attempts']);
$this->assertEquals(1, $result['total']);
$this->assertEquals(1, $result['earned']);
$this->assertEquals(100, $result['grade']);
$this->assertEquals(0, $result['nmanual']);
$this->assertEquals(0, $result['manualpoints']);
$this->assertEquals(1, $result['grade']['nquestions']);
$this->assertEquals(1, $result['grade']['attempts']);
$this->assertEquals(1, $result['grade']['total']);
$this->assertEquals(1, $result['grade']['earned']);
$this->assertEquals(100, $result['grade']['grade']);
$this->assertEquals(0, $result['grade']['nmanual']);
$this->assertEquals(0, $result['grade']['manualpoints']);
// With custom scoring, in this case, we don't retrieve any values since we are using questions without particular score.
$DB->set_field('lesson', 'custom', 1, array('id' => $this->lesson->id));
$result = mod_lesson_external::get_user_attempt_grade($this->lesson->id, $attemptnumber, $this->student->id);
$result = external_api::clean_returnvalue(mod_lesson_external::get_user_attempt_grade_returns(), $result);
$this->assertCount(0, $result['warnings']);
$this->assertEquals(1, $result['nquestions']);
$this->assertEquals(1, $result['attempts']);
$this->assertEquals(0, $result['total']);
$this->assertEquals(0, $result['earned']);
$this->assertEquals(0, $result['grade']);
$this->assertEquals(0, $result['nmanual']);
$this->assertEquals(0, $result['manualpoints']);
$this->assertEquals(1, $result['grade']['nquestions']);
$this->assertEquals(1, $result['grade']['attempts']);
$this->assertEquals(0, $result['grade']['total']);
$this->assertEquals(0, $result['grade']['earned']);
$this->assertEquals(0, $result['grade']['grade']);
$this->assertEquals(0, $result['grade']['nmanual']);
$this->assertEquals(0, $result['grade']['manualpoints']);
}
/**
@ -1185,4 +1185,53 @@ class mod_lesson_external_testcase extends externallib_advanced_testcase {
// Check students.
$this->assertCount(2, $result['data']['students']);
}
/**
* Test get_user_attempt
*/
public function test_get_user_attempt() {
global $DB;
// Create a finished and unfinished attempt with incorrect answer.
$this->setCurrentTimeStart();
$this->create_attempt($this->student, true, true);
$DB->set_field('lesson', 'retake', 1, array('id' => $this->lesson->id));
sleep(1);
$this->create_attempt($this->student, false, false);
$this->setAdminUser();
// Test first attempt finished.
$result = mod_lesson_external::get_user_attempt($this->lesson->id, $this->student->id, 0);
$result = external_api::clean_returnvalue(mod_lesson_external::get_user_attempt_returns(), $result);
$this->assertCount(2, $result['answerpages']); // 2 pages in the lesson.
$this->assertCount(2, $result['answerpages'][0]['answerdata']['answers']); // 2 possible answers in true/false.
$this->assertEquals(100, $result['userstats']['grade']); // Correct answer.
$this->assertEquals(1, $result['userstats']['gradeinfo']['total']); // Total correct answers.
$this->assertEquals(100, $result['userstats']['gradeinfo']['grade']); // Correct answer.
// Test second attempt unfinished.
$result = mod_lesson_external::get_user_attempt($this->lesson->id, $this->student->id, 1);
$result = external_api::clean_returnvalue(mod_lesson_external::get_user_attempt_returns(), $result);
$this->assertCount(2, $result['answerpages']); // 2 pages in the lesson.
$this->assertCount(2, $result['answerpages'][0]['answerdata']['answers']); // 2 possible answers in true/false.
$this->assertArrayNotHasKey('gradeinfo', $result['userstats']); // No grade info since it not finished.
// Check as student I can get this information for only me.
$this->setUser($this->student);
// Test first attempt finished.
$result = mod_lesson_external::get_user_attempt($this->lesson->id, $this->student->id, 0);
$result = external_api::clean_returnvalue(mod_lesson_external::get_user_attempt_returns(), $result);
$this->assertCount(2, $result['answerpages']); // 2 pages in the lesson.
$this->assertCount(2, $result['answerpages'][0]['answerdata']['answers']); // 2 possible answers in true/false.
$this->assertEquals(100, $result['userstats']['grade']); // Correct answer.
$this->assertEquals(1, $result['userstats']['gradeinfo']['total']); // Total correct answers.
$this->assertEquals(100, $result['userstats']['gradeinfo']['grade']); // Correct answer.
$this->setExpectedException('moodle_exception');
$result = mod_lesson_external::get_user_attempt($this->lesson->id, $this->teacher->id, 0);
}
}

View file

@ -24,7 +24,7 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2016120513; // The current module version (Date: YYYYMMDDXX)
$plugin->version = 2016120514; // 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;