MDL-46921 lib: Update get_all_user_name_fields() plus unit tests.
[moodle.git] / mod / lesson / report.php
blob41e03d9c106451c239e720268276a9cd3ad3be7d
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');
29 $id = required_param('id', PARAM_INT); // Course Module ID
30 $pageid = optional_param('pageid', null, PARAM_INT); // Lesson Page ID
31 $action = optional_param('action', 'reportoverview', PARAM_ALPHA); // action to take
32 $nothingtodisplay = false;
34 $cm = get_coursemodule_from_id('lesson', $id, 0, false, MUST_EXIST);
35 $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST);
36 $lesson = new lesson($DB->get_record('lesson', array('id' => $cm->instance), '*', MUST_EXIST));
38 require_login($course, false, $cm);
40 $context = context_module::instance($cm->id);
41 require_capability('mod/lesson:manage', $context);
43 $ufields = user_picture::fields('u'); // These fields are enough
44 $params = array("lessonid" => $lesson->id);
45 list($sort, $sortparams) = users_order_by_sql('u');
46 $params = array_merge($params, $sortparams);
47 // TODO: Improve this. Fetching all students always is crazy!
48 if (!empty($cm->groupingid)) {
49 $params["groupingid"] = $cm->groupingid;
50 $sql = "SELECT DISTINCT $ufields
51 FROM {lesson_attempts} a
52 INNER JOIN {user} u ON u.id = a.userid
53 INNER JOIN {groups_members} gm ON gm.userid = u.id
54 INNER JOIN {groupings_groups} gg ON gm.groupid = gg.groupid
55 WHERE a.lessonid = :lessonid AND
56 gg.groupingid = :groupingid
57 ORDER BY $sort";
58 } else {
59 $sql = "SELECT DISTINCT $ufields
60 FROM {user} u,
61 {lesson_attempts} a
62 WHERE a.lessonid = :lessonid and
63 u.id = a.userid
64 ORDER BY $sort";
67 if (! $students = $DB->get_records_sql($sql, $params)) {
68 $nothingtodisplay = true;
71 $url = new moodle_url('/mod/lesson/report.php', array('id'=>$id));
72 if ($action !== 'reportoverview') {
73 $url->param('action', $action);
75 if ($pageid !== null) {
76 $url->param('pageid', $pageid);
78 $PAGE->set_url($url);
79 if ($action == 'reportoverview') {
80 $PAGE->navbar->add(get_string('reports', 'lesson'));
81 $PAGE->navbar->add(get_string('overview', 'lesson'));
84 $lessonoutput = $PAGE->get_renderer('mod_lesson');
86 if (! $attempts = $DB->get_records('lesson_attempts', array('lessonid' => $lesson->id), 'timeseen')) {
87 $nothingtodisplay = true;
90 if (! $grades = $DB->get_records('lesson_grades', array('lessonid' => $lesson->id), 'completed')) {
91 $grades = array();
94 if (! $times = $DB->get_records('lesson_timer', array('lessonid' => $lesson->id), 'starttime')) {
95 $times = array();
98 if ($nothingtodisplay) {
99 echo $lessonoutput->header($lesson, $cm, $action, false, null, get_string('nolessonattempts', 'lesson'));
100 echo $OUTPUT->notification(get_string('nolessonattempts', 'lesson'));
101 echo $OUTPUT->footer();
102 exit();
105 if ($action === 'delete') {
106 /// Process any form data before fetching attempts, grades and times
107 if (has_capability('mod/lesson:edit', $context) and $form = data_submitted() and confirm_sesskey()) {
108 /// Cycle through array of userids with nested arrays of tries
109 if (!empty($form->attempts)) {
110 foreach ($form->attempts as $userid => $tries) {
111 // Modifier IS VERY IMPORTANT! What does it do?
112 // Well, it is for when you delete multiple attempts for the same user.
113 // If you delete try 1 and 3 for a user, then after deleting try 1, try 3 then
114 // becomes try 2 (because try 1 is gone and all tries after try 1 get decremented).
115 // So, the modifier makes sure that the submitted try refers to the current try in the
116 // database - hope this all makes sense :)
117 $modifier = 0;
119 foreach ($tries as $try => $junk) {
120 $try -= $modifier;
122 /// Clean up the timer table by removing using the order - this is silly, it should be linked to specific attempt (skodak)
123 $params = array ("userid" => $userid, "lessonid" => $lesson->id);
124 $timers = $DB->get_records_sql("SELECT id FROM {lesson_timer}
125 WHERE userid = :userid AND lessonid = :lessonid
126 ORDER BY starttime", $params, $try, 1);
127 if ($timers) {
128 $timer = reset($timers);
129 $DB->delete_records('lesson_timer', array('id' => $timer->id));
132 /// Remove the grade from the grades and high_scores tables - this is silly, it should be linked to specific attempt (skodak)
133 $grades = $DB->get_records_sql("SELECT id FROM {lesson_grades}
134 WHERE userid = :userid AND lessonid = :lessonid
135 ORDER BY completed", $params, $try, 1);
137 if ($grades) {
138 $grade = reset($grades);
139 $DB->delete_records('lesson_grades', array('id' => $grade->id));
140 $DB->delete_records('lesson_high_scores', array('gradeid' => $grade->id, 'lessonid' => $lesson->id, 'userid' => $userid));
143 /// Remove attempts and update the retry number
144 $DB->delete_records('lesson_attempts', array('userid' => $userid, 'lessonid' => $lesson->id, 'retry' => $try));
145 $DB->execute("UPDATE {lesson_attempts} SET retry = retry - 1 WHERE userid = ? AND lessonid = ? AND retry > ?", array($userid, $lesson->id, $try));
147 /// Remove seen branches and update the retry number
148 $DB->delete_records('lesson_branch', array('userid' => $userid, 'lessonid' => $lesson->id, 'retry' => $try));
149 $DB->execute("UPDATE {lesson_branch} SET retry = retry - 1 WHERE userid = ? AND lessonid = ? AND retry > ?", array($userid, $lesson->id, $try));
151 /// update central gradebook
152 lesson_update_grades($lesson, $userid);
154 $modifier++;
159 redirect(new moodle_url($PAGE->url, array('action'=>'reportoverview')));
161 } else if ($action === 'reportoverview') {
162 /**************************************************************************
163 this action is for default view and overview view
164 **************************************************************************/
165 echo $lessonoutput->header($lesson, $cm, $action, false, null, get_string('overview', 'lesson'));
167 $course_context = context_course::instance($course->id);
168 if (has_capability('gradereport/grader:view', $course_context) && has_capability('moodle/grade:viewall', $course_context)) {
169 $seeallgradeslink = new moodle_url('/grade/report/grader/index.php', array('id'=>$course->id));
170 $seeallgradeslink = html_writer::link($seeallgradeslink, get_string('seeallcoursegrades', 'grades'));
171 echo $OUTPUT->box($seeallgradeslink, 'allcoursegrades');
174 $studentdata = array();
176 // build an array for output
177 foreach ($attempts as $attempt) {
178 // if the user is not in the array or if the retry number is not in the sub array, add the data for that try.
179 if (!array_key_exists($attempt->userid, $studentdata) || !array_key_exists($attempt->retry, $studentdata[$attempt->userid])) {
180 // restore/setup defaults
181 $n = 0;
182 $timestart = 0;
183 $timeend = 0;
184 $usergrade = null;
186 // search for the grade record for this try. if not there, the nulls defined above will be used.
187 foreach($grades as $grade) {
188 // check to see if the grade matches the correct user
189 if ($grade->userid == $attempt->userid) {
190 // see if n is = to the retry
191 if ($n == $attempt->retry) {
192 // get grade info
193 $usergrade = round($grade->grade, 2); // round it here so we only have to do it once
194 break;
196 $n++; // if not equal, then increment n
199 $n = 0;
200 // search for the time record for this try. if not there, the nulls defined above will be used.
201 foreach($times as $time) {
202 // check to see if the grade matches the correct user
203 if ($time->userid == $attempt->userid) {
204 // see if n is = to the retry
205 if ($n == $attempt->retry) {
206 // get grade info
207 $timeend = $time->lessontime;
208 $timestart = $time->starttime;
209 break;
211 $n++; // if not equal, then increment n
215 // build up the array.
216 // this array represents each student and all of their tries at the lesson
217 $studentdata[$attempt->userid][$attempt->retry] = array( "timestart" => $timestart,
218 "timeend" => $timeend,
219 "grade" => $usergrade,
220 "try" => $attempt->retry,
221 "userid" => $attempt->userid);
224 // set all the stats variables
225 $numofattempts = 0;
226 $avescore = 0;
227 $avetime = 0;
228 $highscore = null;
229 $lowscore = null;
230 $hightime = null;
231 $lowtime = null;
233 $table = new html_table();
235 // set up the table object
236 $table->head = array(get_string('name'), get_string('attempts', 'lesson'), get_string('highscore', 'lesson'));
237 $table->align = array('center', 'left', 'left');
238 $table->wrap = array('nowrap', 'nowrap', 'nowrap');
239 $table->attributes['class'] = 'standardtable generaltable';
240 $table->size = array(null, '70%', null);
242 // print out the $studentdata array
243 // going through each student that has attempted the lesson, so, each student should have something to be displayed
244 foreach ($students as $student) {
245 // check to see if the student has attempts to print out
246 if (array_key_exists($student->id, $studentdata)) {
247 // set/reset some variables
248 $attempts = array();
249 // gather the data for each user attempt
250 $bestgrade = 0;
251 $bestgradefound = false;
252 // $tries holds all the tries/retries a student has done
253 $tries = $studentdata[$student->id];
254 $studentname = "{$student->lastname},&nbsp;$student->firstname";
255 foreach ($tries as $try) {
256 // start to build up the checkbox and link
257 if (has_capability('mod/lesson:edit', $context)) {
258 $temp = '<input type="checkbox" id="attempts" name="attempts['.$try['userid'].']['.$try['try'].']" /> ';
259 } else {
260 $temp = '';
263 $temp .= "<a href=\"report.php?id=$cm->id&amp;action=reportdetail&amp;userid=".$try['userid'].'&amp;try='.$try['try'].'">';
264 if ($try["grade"] !== null) { // if null then not done yet
265 // this is what the link does when the user has completed the try
266 $timetotake = $try["timeend"] - $try["timestart"];
268 $temp .= $try["grade"]."%";
269 $bestgradefound = true;
270 if ($try["grade"] > $bestgrade) {
271 $bestgrade = $try["grade"];
273 $temp .= "&nbsp;".userdate($try["timestart"]);
274 $temp .= ",&nbsp;(".format_time($timetotake).")</a>";
275 } else {
276 // this is what the link does/looks like when the user has not completed the try
277 $temp .= get_string("notcompleted", "lesson");
278 $temp .= "&nbsp;".userdate($try["timestart"])."</a>";
279 $timetotake = null;
281 // build up the attempts array
282 $attempts[] = $temp;
284 // run these lines for the stats only if the user finnished the lesson
285 if ($try["grade"] !== null) {
286 $numofattempts++;
287 $avescore += $try["grade"];
288 $avetime += $timetotake;
289 if ($try["grade"] > $highscore || $highscore === null) {
290 $highscore = $try["grade"];
292 if ($try["grade"] < $lowscore || $lowscore === null) {
293 $lowscore = $try["grade"];
295 if ($timetotake > $hightime || $hightime == null) {
296 $hightime = $timetotake;
298 if ($timetotake < $lowtime || $lowtime == null) {
299 $lowtime = $timetotake;
303 // get line breaks in after each attempt
304 $attempts = implode("<br />\n", $attempts);
305 // add it to the table data[] object
306 $table->data[] = array($studentname, $attempts, $bestgrade."%");
309 // print it all out !
310 if (has_capability('mod/lesson:edit', $context)) {
311 echo "<form id=\"theform\" method=\"post\" action=\"report.php\">\n
312 <input type=\"hidden\" name=\"sesskey\" value=\"".sesskey()."\" />\n
313 <input type=\"hidden\" name=\"id\" value=\"$cm->id\" />\n";
315 echo html_writer::table($table);
316 if (has_capability('mod/lesson:edit', $context)) {
317 $checklinks = '<a href="javascript: checkall();">'.get_string('selectall').'</a> / ';
318 $checklinks .= '<a href="javascript: checknone();">'.get_string('deselectall').'</a>';
319 $checklinks .= html_writer::label('action', 'menuaction', false, array('class' => 'accesshide'));
320 $checklinks .= html_writer::select(array('delete' => get_string('deleteselected')), 'action', 0, array(''=>'choosedots'), array('id'=>'actionid', 'class' => 'autosubmit'));
321 $PAGE->requires->yui_module('moodle-core-formautosubmit',
322 'M.core.init_formautosubmit',
323 array(array('selectid' => 'actionid', 'nothing' => false))
325 echo $OUTPUT->box($checklinks, 'center');
326 echo '</form>';
329 // some stat calculations
330 if ($numofattempts == 0) {
331 $avescore = get_string("notcompleted", "lesson");
332 } else {
333 $avescore = format_float($avescore/$numofattempts, 2);
335 if ($avetime == null) {
336 $avetime = get_string("notcompleted", "lesson");
337 } else {
338 $avetime = format_float($avetime/$numofattempts, 0);
339 $avetime = format_time($avetime);
341 if ($hightime == null) {
342 $hightime = get_string("notcompleted", "lesson");
343 } else {
344 $hightime = format_time($hightime);
346 if ($lowtime == null) {
347 $lowtime = get_string("notcompleted", "lesson");
348 } else {
349 $lowtime = format_time($lowtime);
351 if ($highscore === null) {
352 $highscore = get_string("notcompleted", "lesson");
354 if ($lowscore === null) {
355 $lowscore = get_string("notcompleted", "lesson");
358 // output the stats
359 echo $OUTPUT->heading(get_string('lessonstats', 'lesson'), 3);
360 $stattable = new html_table();
361 $stattable->head = array(get_string('averagescore', 'lesson'), get_string('averagetime', 'lesson'),
362 get_string('highscore', 'lesson'), get_string('lowscore', 'lesson'),
363 get_string('hightime', 'lesson'), get_string('lowtime', 'lesson'));
364 $stattable->align = array('center', 'center', 'center', 'center', 'center', 'center');
365 $stattable->wrap = array('nowrap', 'nowrap', 'nowrap', 'nowrap', 'nowrap', 'nowrap');
366 $stattable->attributes['class'] = 'standardtable generaltable';
368 if (is_numeric($highscore)) {
369 $highscore .= '%';
371 if (is_numeric($lowscore)) {
372 $lowscore .= '%';
374 $stattable->data[] = array($avescore.'%', $avetime, $highscore, $lowscore, $hightime, $lowtime);
376 echo html_writer::table($stattable);
377 } else if ($action === 'reportdetail') {
378 /**************************************************************************
379 this action is for a student detailed view and for the general detailed view
381 General flow of this section of the code
382 1. Generate a object which holds values for the statistics for each question/answer
383 2. Cycle through all the pages to create a object. Foreach page, see if the student actually answered
384 the page. Then process the page appropriatly. Display all info about the question,
385 Highlight correct answers, show how the user answered the question, and display statistics
386 about each page
387 3. Print out info about the try (if needed)
388 4. Print out the object which contains all the try info
390 **************************************************************************/
391 echo $lessonoutput->header($lesson, $cm, $action, false, null, get_string('detailedstats', 'lesson'));
393 $course_context = context_course::instance($course->id);
394 if (has_capability('gradereport/grader:view', $course_context) && has_capability('moodle/grade:viewall', $course_context)) {
395 $seeallgradeslink = new moodle_url('/grade/report/grader/index.php', array('id'=>$course->id));
396 $seeallgradeslink = html_writer::link($seeallgradeslink, get_string('seeallcoursegrades', 'grades'));
397 echo $OUTPUT->box($seeallgradeslink, 'allcoursegrades');
400 $formattextdefoptions = new stdClass;
401 $formattextdefoptions->para = false; //I'll use it widely in this page
402 $formattextdefoptions->overflowdiv = true;
404 $userid = optional_param('userid', null, PARAM_INT); // if empty, then will display the general detailed view
405 $try = optional_param('try', null, PARAM_INT);
407 $lessonpages = $lesson->load_all_pages();
408 foreach ($lessonpages as $lessonpage) {
409 if ($lessonpage->prevpageid == 0) {
410 $pageid = $lessonpage->id;
414 // now gather the stats into an object
415 $firstpageid = $pageid;
416 $pagestats = array();
417 while ($pageid != 0) { // EOL
418 $page = $lessonpages[$pageid];
419 $params = array ("lessonid" => $lesson->id, "pageid" => $page->id);
420 if ($allanswers = $DB->get_records_select("lesson_attempts", "lessonid = :lessonid AND pageid = :pageid", $params, "timeseen")) {
421 // get them ready for processing
422 $orderedanswers = array();
423 foreach ($allanswers as $singleanswer) {
424 // ordering them like this, will help to find the single attempt record that we want to keep.
425 $orderedanswers[$singleanswer->userid][$singleanswer->retry][] = $singleanswer;
427 // this is foreach user and for each try for that user, keep one attempt record
428 foreach ($orderedanswers as $orderedanswer) {
429 foreach($orderedanswer as $tries) {
430 $page->stats($pagestats, $tries);
433 } else {
434 // no one answered yet...
436 //unset($orderedanswers); initialized above now
437 $pageid = $page->nextpageid;
440 $manager = lesson_page_type_manager::get($lesson);
441 $qtypes = $manager->get_page_type_strings();
443 $answerpages = array();
444 $answerpage = "";
445 $pageid = $firstpageid;
446 // cycle through all the pages
447 // foreach page, add to the $answerpages[] array all the data that is needed
448 // from the question, the users attempt, and the statistics
449 // grayout pages that the user did not answer and Branch, end of branch, cluster
450 // and end of cluster pages
451 while ($pageid != 0) { // EOL
452 $page = $lessonpages[$pageid];
453 $answerpage = new stdClass;
454 $data ='';
456 $answerdata = new stdClass;
457 // Set some defaults for the answer data.
458 $answerdata->score = null;
459 $answerdata->response = null;
460 $answerdata->responseformat = FORMAT_PLAIN;
462 $answerpage->title = format_string($page->title);
464 $options = new stdClass;
465 $options->noclean = true;
466 $options->overflowdiv = true;
467 $answerpage->contents = format_text($page->contents, $page->contentsformat, $options);
469 $answerpage->qtype = $qtypes[$page->qtype].$page->option_description_string();
470 $answerpage->grayout = $page->grayout;
471 $answerpage->context = $context;
473 if (empty($userid)) {
474 // there is no userid, so set these vars and display stats.
475 $answerpage->grayout = 0;
476 $useranswer = null;
477 } elseif ($useranswers = $DB->get_records("lesson_attempts",array("lessonid"=>$lesson->id, "userid"=>$userid, "retry"=>$try,"pageid"=>$page->id), "timeseen")) {
478 // get the user's answer for this page
479 // need to find the right one
480 $i = 0;
481 foreach ($useranswers as $userattempt) {
482 $useranswer = $userattempt;
483 $i++;
484 if ($lesson->maxattempts == $i) {
485 break; // reached maxattempts, break out
488 } else {
489 // user did not answer this page, gray it out and set some nulls
490 $answerpage->grayout = 1;
491 $useranswer = null;
493 $i = 0;
494 $n = 0;
495 $answerpages[] = $page->report_answers(clone($answerpage), clone($answerdata), $useranswer, $pagestats, $i, $n);
496 $pageid = $page->nextpageid;
499 /// actually start printing something
500 $table = new html_table();
501 $table->wrap = array();
502 $table->width = "60%";
503 if (!empty($userid)) {
504 // if looking at a students try, print out some basic stats at the top
506 // print out users name
507 //$headingobject->lastname = $students[$userid]->lastname;
508 //$headingobject->firstname = $students[$userid]->firstname;
509 //$headingobject->attempt = $try + 1;
510 //print_heading(get_string("studentattemptlesson", "lesson", $headingobject));
511 echo $OUTPUT->heading(get_string('attempt', 'lesson', $try+1), 3);
513 $table->head = array();
514 $table->align = array('right', 'left');
515 $table->attributes['class'] = 'compacttable generaltable';
517 $params = array("lessonid"=>$lesson->id, "userid"=>$userid);
518 if (!$grades = $DB->get_records_select("lesson_grades", "lessonid = :lessonid and userid = :userid", $params, "completed", "*", $try, 1)) {
519 $grade = -1;
520 $completed = -1;
521 } else {
522 $grade = current($grades);
523 $completed = $grade->completed;
524 $grade = round($grade->grade, 2);
526 if (!$times = $DB->get_records_select("lesson_timer", "lessonid = :lessonid and userid = :userid", $params, "starttime", "*", $try, 1)) {
527 $timetotake = -1;
528 } else {
529 $timetotake = current($times);
530 $timetotake = $timetotake->lessontime - $timetotake->starttime;
533 if ($timetotake == -1 || $completed == -1 || $grade == -1) {
534 $table->align = array("center");
536 $table->data[] = array(get_string("notcompleted", "lesson"));
537 } else {
538 $user = $students[$userid];
540 $gradeinfo = lesson_grade($lesson, $try, $user->id);
542 $table->data[] = array(get_string('name').':', $OUTPUT->user_picture($user, array('courseid'=>$course->id)).fullname($user, true));
543 $table->data[] = array(get_string("timetaken", "lesson").":", format_time($timetotake));
544 $table->data[] = array(get_string("completed", "lesson").":", userdate($completed));
545 $table->data[] = array(get_string('rawgrade', 'lesson').':', $gradeinfo->earned.'/'.$gradeinfo->total);
546 $table->data[] = array(get_string("grade", "lesson").":", $grade."%");
548 echo html_writer::table($table);
550 // Don't want this class for later tables
551 $table->attributes['class'] = '';
555 $table->align = array('left', 'left');
556 $table->size = array('70%', null);
557 $table->attributes['class'] = 'compacttable generaltable';
559 foreach ($answerpages as $page) {
560 unset($table->data);
561 if ($page->grayout) { // set the color of text
562 $fontstart = "<span class=\"dimmed\">";
563 $fontend = "</font>";
564 $fontstart2 = $fontstart;
565 $fontend2 = $fontend;
566 } else {
567 $fontstart = "";
568 $fontend = "";
569 $fontstart2 = "";
570 $fontend2 = "";
573 $table->head = array($fontstart2.$page->qtype.": ".format_string($page->title).$fontend2, $fontstart2.get_string("classstats", "lesson").$fontend2);
574 $table->data[] = array($fontstart.get_string("question", "lesson").": <br />".$fontend.$fontstart2.$page->contents.$fontend2, " ");
575 $table->data[] = array($fontstart.get_string("answer", "lesson").":".$fontend, ' ');
576 // apply the font to each answer
577 if (!empty($page->answerdata)) {
578 foreach ($page->answerdata->answers as $answer){
579 $modified = array();
580 foreach ($answer as $single) {
581 // need to apply a font to each one
582 $modified[] = $fontstart2.$single.$fontend2;
584 $table->data[] = $modified;
586 if (isset($page->answerdata->response)) {
587 $table->data[] = array($fontstart.get_string("response", "lesson").": <br />".$fontend.$fontstart2.format_text($page->answerdata->response,$page->answerdata->responseformat,$formattextdefoptions).$fontend2, " ");
589 $table->data[] = array($page->answerdata->score, " ");
590 } else {
591 $table->data[] = array(get_string('didnotanswerquestion', 'lesson'), " ");
593 echo html_writer::start_tag('div', array('class' => 'no-overflow'));
594 echo html_writer::table($table);
595 echo html_writer::end_tag('div');
597 } else {
598 print_error('unknowaction');
601 /// Finish the page
602 echo $OUTPUT->footer();