MDL-57754 mod_lesson: Move overview report code to function
[moodle.git] / mod / lesson / report.php
blob1aa2258beaca571ed36caba6bc6d23af8c02cc28
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 /**
19 * Displays the lesson statistics.
21 * @package mod_lesson
22 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or late
24 **/
26 require_once('../../config.php');
27 require_once($CFG->dirroot.'/mod/lesson/locallib.php');
28 require_once($CFG->dirroot.'/mod/lesson/pagetypes/branchtable.php'); // Needed for constant.
30 $id = required_param('id', PARAM_INT); // Course Module ID
31 $pageid = optional_param('pageid', null, PARAM_INT); // Lesson Page ID
32 $action = optional_param('action', 'reportoverview', PARAM_ALPHA); // action to take
33 $nothingtodisplay = false;
35 $cm = get_coursemodule_from_id('lesson', $id, 0, false, MUST_EXIST);
36 $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST);
37 $lesson = new lesson($DB->get_record('lesson', array('id' => $cm->instance), '*', MUST_EXIST));
39 require_login($course, false, $cm);
41 $currentgroup = groups_get_activity_group($cm, true);
43 $context = context_module::instance($cm->id);
44 require_capability('mod/lesson:viewreports', $context);
46 $url = new moodle_url('/mod/lesson/report.php', array('id'=>$id));
47 $url->param('action', $action);
48 if ($pageid !== null) {
49 $url->param('pageid', $pageid);
51 $PAGE->set_url($url);
52 if ($action == 'reportoverview') {
53 $PAGE->navbar->add(get_string('reports', 'lesson'));
54 $PAGE->navbar->add(get_string('overview', 'lesson'));
57 $lessonoutput = $PAGE->get_renderer('mod_lesson');
59 if ($action === 'delete') {
60 /// Process any form data before fetching attempts, grades and times
61 if (has_capability('mod/lesson:edit', $context) and $form = data_submitted() and confirm_sesskey()) {
62 /// Cycle through array of userids with nested arrays of tries
63 if (!empty($form->attempts)) {
64 foreach ($form->attempts as $userid => $tries) {
65 // Modifier IS VERY IMPORTANT! What does it do?
66 // Well, it is for when you delete multiple attempts for the same user.
67 // If you delete try 1 and 3 for a user, then after deleting try 1, try 3 then
68 // becomes try 2 (because try 1 is gone and all tries after try 1 get decremented).
69 // So, the modifier makes sure that the submitted try refers to the current try in the
70 // database - hope this all makes sense :)
71 $modifier = 0;
73 foreach ($tries as $try => $junk) {
74 $try -= $modifier;
76 /// Clean up the timer table by removing using the order - this is silly, it should be linked to specific attempt (skodak)
77 $timers = $lesson->get_user_timers($userid, 'starttime', 'id', $try, 1);
78 if ($timers) {
79 $timer = reset($timers);
80 $DB->delete_records('lesson_timer', array('id' => $timer->id));
83 $params = array ("userid" => $userid, "lessonid" => $lesson->id);
84 // Remove the grade from the grades tables - this is silly, it should be linked to specific attempt (skodak).
85 $grades = $DB->get_records_sql("SELECT id FROM {lesson_grades}
86 WHERE userid = :userid AND lessonid = :lessonid
87 ORDER BY completed", $params, $try, 1);
89 if ($grades) {
90 $grade = reset($grades);
91 $DB->delete_records('lesson_grades', array('id' => $grade->id));
94 /// Remove attempts and update the retry number
95 $DB->delete_records('lesson_attempts', array('userid' => $userid, 'lessonid' => $lesson->id, 'retry' => $try));
96 $DB->execute("UPDATE {lesson_attempts} SET retry = retry - 1 WHERE userid = ? AND lessonid = ? AND retry > ?", array($userid, $lesson->id, $try));
98 /// Remove seen branches and update the retry number
99 $DB->delete_records('lesson_branch', array('userid' => $userid, 'lessonid' => $lesson->id, 'retry' => $try));
100 $DB->execute("UPDATE {lesson_branch} SET retry = retry - 1 WHERE userid = ? AND lessonid = ? AND retry > ?", array($userid, $lesson->id, $try));
102 /// update central gradebook
103 lesson_update_grades($lesson, $userid);
105 $modifier++;
110 redirect(new moodle_url($PAGE->url, array('action'=>'reportoverview')));
112 } else if ($action === 'reportoverview') {
113 /**************************************************************************
114 this action is for default view and overview view
115 **************************************************************************/
117 // Get the table and data for build statistics.
118 list($table, $data) = lesson_get_overview_report_table_and_data($lesson, $currentgroup);
120 if ($table === false) {
121 echo $lessonoutput->header($lesson, $cm, $action, false, null, get_string('nolessonattempts', 'lesson'));
122 if (!empty($currentgroup)) {
123 $groupname = groups_get_group_name($currentgroup);
124 echo $OUTPUT->notification(get_string('nolessonattemptsgroup', 'lesson', $groupname));
125 } else {
126 echo $OUTPUT->notification(get_string('nolessonattempts', 'lesson'));
128 groups_print_activity_menu($cm, $url);
129 echo $OUTPUT->footer();
130 exit();
133 echo $lessonoutput->header($lesson, $cm, $action, false, null, get_string('overview', 'lesson'));
134 groups_print_activity_menu($cm, $url);
136 $course_context = context_course::instance($course->id);
137 if (has_capability('gradereport/grader:view', $course_context) && has_capability('moodle/grade:viewall', $course_context)) {
138 $seeallgradeslink = new moodle_url('/grade/report/grader/index.php', array('id'=>$course->id));
139 $seeallgradeslink = html_writer::link($seeallgradeslink, get_string('seeallcoursegrades', 'grades'));
140 echo $OUTPUT->box($seeallgradeslink, 'allcoursegrades');
143 // Print it all out!
144 if (has_capability('mod/lesson:edit', $context)) {
145 echo "<form id=\"mod-lesson-report-form\" method=\"post\" action=\"report.php\">\n
146 <input type=\"hidden\" name=\"sesskey\" value=\"".sesskey()."\" />\n
147 <input type=\"hidden\" name=\"id\" value=\"$cm->id\" />\n";
150 echo html_writer::table($table);
152 if (has_capability('mod/lesson:edit', $context)) {
153 $checklinks = '<a id="checkall" href="#">'.get_string('selectall').'</a> / ';
154 $checklinks .= '<a id="checknone" href="#">'.get_string('deselectall').'</a>';
155 $checklinks .= html_writer::label('action', 'menuaction', false, array('class' => 'accesshide'));
156 $options = array('delete' => get_string('deleteselected'));
157 $attributes = array('id' => 'actionid', 'class' => 'custom-select m-l-1');
158 $checklinks .= html_writer::select($options, 'action', 0, array('' => 'choosedots'), $attributes);
159 $PAGE->requires->js_amd_inline("
160 require(['jquery'], function($) {
161 $('#actionid').change(function() {
162 $('#mod-lesson-report-form').submit();
164 $('#checkall').click(function(e) {
165 $('#mod-lesson-report-form').find('input:checkbox').prop('checked', true);
166 e.preventDefault();
168 $('#checknone').click(function(e) {
169 $('#mod-lesson-report-form').find('input:checkbox').prop('checked', false);
170 e.preventDefault();
172 });");
173 echo $OUTPUT->box($checklinks, 'center');
174 echo '</form>';
177 // Calculate the Statistics.
178 if ($data->avetime == null) {
179 $data->avetime = get_string("notcompleted", "lesson");
180 } else {
181 $data->avetime = format_float($data->avetime / $data->numofattempts, 0);
182 $data->avetime = format_time($data->avetime);
184 if ($data->hightime == null) {
185 $data->hightime = get_string("notcompleted", "lesson");
186 } else {
187 $data->hightime = format_time($data->hightime);
189 if ($data->lowtime == null) {
190 $data->lowtime = get_string("notcompleted", "lesson");
191 } else {
192 $data->lowtime = format_time($data->lowtime);
195 if ($data->lessonscored) {
196 if ($data->numofattempts == 0) {
197 $data->avescore = get_string("notcompleted", "lesson");
198 } else {
199 $data->avescore = format_float($data->avescore, 2) . '%';
201 if ($data->highscore === null) {
202 $data->highscore = get_string("notcompleted", "lesson");
203 } else {
204 $data->highscore .= '%';
206 if ($data->lowscore === null) {
207 $data->lowscore = get_string("notcompleted", "lesson");
208 } else {
209 $data->lowscore .= '%';
212 // Display the full stats for the lesson.
213 echo $OUTPUT->heading(get_string('lessonstats', 'lesson'), 3);
214 $stattable = new html_table();
215 $stattable->head = array(get_string('averagescore', 'lesson'), get_string('averagetime', 'lesson'),
216 get_string('highscore', 'lesson'), get_string('lowscore', 'lesson'),
217 get_string('hightime', 'lesson'), get_string('lowtime', 'lesson'));
218 $stattable->align = array('center', 'center', 'center', 'center', 'center', 'center');
219 $stattable->wrap = array('nowrap', 'nowrap', 'nowrap', 'nowrap', 'nowrap', 'nowrap');
220 $stattable->attributes['class'] = 'standardtable generaltable';
221 $stattable->data[] = array($data->avescore, $data->avetime, $data->highscore, $data->lowscore, $data->hightime, $data->lowtime);
223 } else {
224 // Display simple stats for the lesson.
225 echo $OUTPUT->heading(get_string('lessonstats', 'lesson'), 3);
226 $stattable = new html_table();
227 $stattable->head = array(get_string('averagetime', 'lesson'), get_string('hightime', 'lesson'),
228 get_string('lowtime', 'lesson'));
229 $stattable->align = array('center', 'center', 'center');
230 $stattable->wrap = array('nowrap', 'nowrap', 'nowrap');
231 $stattable->attributes['class'] = 'standardtable generaltable';
232 $stattable->data[] = array($data->avetime, $data->hightime, $data->lowtime);
235 echo html_writer::table($stattable);
236 } else if ($action === 'reportdetail') {
237 /**************************************************************************
238 this action is for a student detailed view and for the general detailed view
240 General flow of this section of the code
241 1. Generate a object which holds values for the statistics for each question/answer
242 2. Cycle through all the pages to create a object. Foreach page, see if the student actually answered
243 the page. Then process the page appropriatly. Display all info about the question,
244 Highlight correct answers, show how the user answered the question, and display statistics
245 about each page
246 3. Print out info about the try (if needed)
247 4. Print out the object which contains all the try info
249 **************************************************************************/
250 echo $lessonoutput->header($lesson, $cm, $action, false, null, get_string('detailedstats', 'lesson'));
251 groups_print_activity_menu($cm, $url);
253 $course_context = context_course::instance($course->id);
254 if (has_capability('gradereport/grader:view', $course_context) && has_capability('moodle/grade:viewall', $course_context)) {
255 $seeallgradeslink = new moodle_url('/grade/report/grader/index.php', array('id'=>$course->id));
256 $seeallgradeslink = html_writer::link($seeallgradeslink, get_string('seeallcoursegrades', 'grades'));
257 echo $OUTPUT->box($seeallgradeslink, 'allcoursegrades');
260 $formattextdefoptions = new stdClass;
261 $formattextdefoptions->para = false; //I'll use it widely in this page
262 $formattextdefoptions->overflowdiv = true;
264 $userid = optional_param('userid', null, PARAM_INT); // if empty, then will display the general detailed view
265 $try = optional_param('try', null, PARAM_INT);
267 if (!empty($userid)) {
268 // Apply overrides.
269 $lesson->update_effective_access($userid);
272 $lessonpages = $lesson->load_all_pages();
273 foreach ($lessonpages as $lessonpage) {
274 if ($lessonpage->prevpageid == 0) {
275 $pageid = $lessonpage->id;
279 // now gather the stats into an object
280 $firstpageid = $pageid;
281 $pagestats = array();
282 while ($pageid != 0) { // EOL
283 $page = $lessonpages[$pageid];
284 $params = array ("lessonid" => $lesson->id, "pageid" => $page->id);
285 if ($allanswers = $DB->get_records_select("lesson_attempts", "lessonid = :lessonid AND pageid = :pageid", $params, "timeseen")) {
286 // get them ready for processing
287 $orderedanswers = array();
288 foreach ($allanswers as $singleanswer) {
289 // ordering them like this, will help to find the single attempt record that we want to keep.
290 $orderedanswers[$singleanswer->userid][$singleanswer->retry][] = $singleanswer;
292 // this is foreach user and for each try for that user, keep one attempt record
293 foreach ($orderedanswers as $orderedanswer) {
294 foreach($orderedanswer as $tries) {
295 $page->stats($pagestats, $tries);
298 } else {
299 // no one answered yet...
301 //unset($orderedanswers); initialized above now
302 $pageid = $page->nextpageid;
305 $manager = lesson_page_type_manager::get($lesson);
306 $qtypes = $manager->get_page_type_strings();
308 $answerpages = array();
309 $answerpage = "";
310 $pageid = $firstpageid;
311 // cycle through all the pages
312 // foreach page, add to the $answerpages[] array all the data that is needed
313 // from the question, the users attempt, and the statistics
314 // grayout pages that the user did not answer and Branch, end of branch, cluster
315 // and end of cluster pages
316 while ($pageid != 0) { // EOL
317 $page = $lessonpages[$pageid];
318 $answerpage = new stdClass;
319 $data ='';
321 $answerdata = new stdClass;
322 // Set some defaults for the answer data.
323 $answerdata->score = null;
324 $answerdata->response = null;
325 $answerdata->responseformat = FORMAT_PLAIN;
327 $answerpage->title = format_string($page->title);
329 $options = new stdClass;
330 $options->noclean = true;
331 $options->overflowdiv = true;
332 $options->context = $context;
333 $answerpage->contents = format_text($page->contents, $page->contentsformat, $options);
335 $answerpage->qtype = $qtypes[$page->qtype].$page->option_description_string();
336 $answerpage->grayout = $page->grayout;
337 $answerpage->context = $context;
339 if (empty($userid)) {
340 // there is no userid, so set these vars and display stats.
341 $answerpage->grayout = 0;
342 $useranswer = null;
343 } elseif ($useranswers = $DB->get_records("lesson_attempts",array("lessonid"=>$lesson->id, "userid"=>$userid, "retry"=>$try,"pageid"=>$page->id), "timeseen")) {
344 // get the user's answer for this page
345 // need to find the right one
346 $i = 0;
347 foreach ($useranswers as $userattempt) {
348 $useranswer = $userattempt;
349 $i++;
350 if ($lesson->maxattempts == $i) {
351 break; // reached maxattempts, break out
354 } else {
355 // user did not answer this page, gray it out and set some nulls
356 $answerpage->grayout = 1;
357 $useranswer = null;
359 $i = 0;
360 $n = 0;
361 $answerpages[] = $page->report_answers(clone($answerpage), clone($answerdata), $useranswer, $pagestats, $i, $n);
362 $pageid = $page->nextpageid;
365 /// actually start printing something
366 $table = new html_table();
367 $table->wrap = array();
368 $table->width = "60%";
369 if (!empty($userid)) {
370 // if looking at a students try, print out some basic stats at the top
372 // print out users name
373 //$headingobject->lastname = $students[$userid]->lastname;
374 //$headingobject->firstname = $students[$userid]->firstname;
375 //$headingobject->attempt = $try + 1;
376 //print_heading(get_string("studentattemptlesson", "lesson", $headingobject));
377 echo $OUTPUT->heading(get_string('attempt', 'lesson', $try+1), 3);
379 $table->head = array();
380 $table->align = array('right', 'left');
381 $table->attributes['class'] = 'compacttable generaltable form-inline';
383 $params = array("lessonid"=>$lesson->id, "userid"=>$userid);
384 if (!$grades = $DB->get_records_select("lesson_grades", "lessonid = :lessonid and userid = :userid", $params, "completed", "*", $try, 1)) {
385 $grade = -1;
386 $completed = -1;
387 } else {
388 $grade = current($grades);
389 $completed = $grade->completed;
390 $grade = round($grade->grade, 2);
393 if (!$times = $lesson->get_user_timers($userid, 'starttime', '*', $try, 1)) {
394 $timetotake = -1;
395 } else {
396 $timetotake = current($times);
397 $timetotake = $timetotake->lessontime - $timetotake->starttime;
400 if ($timetotake == -1 || $completed == -1 || $grade == -1) {
401 $table->align = array("center");
403 $table->data[] = array(get_string("notcompleted", "lesson"));
404 } else {
405 $user = $DB->get_record('user', array('id' => $userid));
407 $gradeinfo = lesson_grade($lesson, $try, $user->id);
409 $table->data[] = array(get_string('name').':', $OUTPUT->user_picture($user, array('courseid'=>$course->id)).fullname($user, true));
410 $table->data[] = array(get_string("timetaken", "lesson").":", format_time($timetotake));
411 $table->data[] = array(get_string("completed", "lesson").":", userdate($completed));
412 $table->data[] = array(get_string('rawgrade', 'lesson').':', $gradeinfo->earned.'/'.$gradeinfo->total);
413 $table->data[] = array(get_string("grade", "lesson").":", $grade."%");
415 echo html_writer::table($table);
417 // Don't want this class for later tables
418 $table->attributes['class'] = '';
422 $table->align = array('left', 'left');
423 $table->size = array('70%', null);
424 $table->attributes['class'] = 'compacttable generaltable form-inline';
426 foreach ($answerpages as $page) {
427 unset($table->data);
428 if ($page->grayout) { // set the color of text
429 $fontstart = "<span class=\"dimmed\">";
430 $fontend = "</font>";
431 $fontstart2 = $fontstart;
432 $fontend2 = $fontend;
433 } else {
434 $fontstart = "";
435 $fontend = "";
436 $fontstart2 = "";
437 $fontend2 = "";
440 $table->head = array($fontstart2.$page->qtype.": ".format_string($page->title).$fontend2, $fontstart2.get_string("classstats", "lesson").$fontend2);
441 $table->data[] = array($fontstart.get_string("question", "lesson").": <br />".$fontend.$fontstart2.$page->contents.$fontend2, " ");
442 $table->data[] = array($fontstart.get_string("answer", "lesson").":".$fontend, ' ');
443 // apply the font to each answer
444 if (!empty($page->answerdata)) {
445 foreach ($page->answerdata->answers as $answer){
446 $modified = array();
447 foreach ($answer as $single) {
448 // need to apply a font to each one
449 $modified[] = $fontstart2.$single.$fontend2;
451 $table->data[] = $modified;
453 if (isset($page->answerdata->response)) {
454 $table->data[] = array($fontstart.get_string("response", "lesson").": <br />".$fontend
455 .$fontstart2.$page->answerdata->response.$fontend2, " ");
457 $table->data[] = array($page->answerdata->score, " ");
458 } else {
459 $table->data[] = array(get_string('didnotanswerquestion', 'lesson'), " ");
461 echo html_writer::start_tag('div', array('class' => 'no-overflow'));
462 echo html_writer::table($table);
463 echo html_writer::end_tag('div');
465 } else {
466 print_error('unknowaction');
469 /// Finish the page
470 echo $OUTPUT->footer();