3 // This file is part of Moodle - http://moodle.org/
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.
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/>.
19 * Displays the lesson statistics.
22 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or late
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
59 $sql = "SELECT DISTINCT $ufields
62 WHERE a.lessonid = :lessonid and
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);
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')) {
94 if (! $times = $DB->get_records('lesson_timer', array('lessonid' => $lesson->id
), 'starttime')) {
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();
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 :)
119 foreach ($tries as $try => $junk) {
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);
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);
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);
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
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
) {
193 $usergrade = round($grade->grade
, 2); // round it here so we only have to do it once
196 $n++
; // if not equal, then increment n
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
) {
207 $timeend = $time->lessontime
;
208 $timestart = $time->starttime
;
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
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
249 // gather the data for each user attempt
251 $bestgradefound = false;
252 // $tries holds all the tries/retries a student has done
253 $tries = $studentdata[$student->id
];
254 $studentname = "{$student->lastname}, $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'].']" /> ';
263 $temp .= "<a href=\"report.php?id=$cm->id&action=reportdetail&userid=".$try['userid'].'&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 .= " ".userdate($try["timestart"]);
274 $temp .= ", (".format_time($timetotake).")</a>";
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 .= " ".userdate($try["timestart"])."</a>";
281 // build up the attempts array
284 // run these lines for the stats only if the user finnished the lesson
285 if ($try["grade"] !== null) {
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');
329 // some stat calculations
330 if ($numofattempts == 0) {
331 $avescore = get_string("notcompleted", "lesson");
333 $avescore = format_float($avescore/$numofattempts, 2);
335 if ($avetime == null) {
336 $avetime = get_string("notcompleted", "lesson");
338 $avetime = format_float($avetime/$numofattempts, 0);
339 $avetime = format_time($avetime);
341 if ($hightime == null) {
342 $hightime = get_string("notcompleted", "lesson");
344 $hightime = format_time($hightime);
346 if ($lowtime == null) {
347 $lowtime = get_string("notcompleted", "lesson");
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");
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)) {
371 if (is_numeric($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
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);
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();
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
;
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;
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
481 foreach ($useranswers as $userattempt) {
482 $useranswer = $userattempt;
484 if ($lesson->maxattempts
== $i) {
485 break; // reached maxattempts, break out
489 // user did not answer this page, gray it out and set some nulls
490 $answerpage->grayout
= 1;
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)) {
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)) {
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"));
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) {
561 if ($page->grayout
) { // set the color of text
562 $fontstart = "<span class=\"dimmed\">";
563 $fontend = "</font>";
564 $fontstart2 = $fontstart;
565 $fontend2 = $fontend;
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){
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
, " ");
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');
598 print_error('unknowaction');
602 echo $OUTPUT->footer();