completion: MDL-2631 Improve queries, reports and fix problems checking for tracked...
[moodle.git] / course / report / completion / index.php
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.
14 //
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 /**
20  * Course completion progress report
21  *
22  * @package   moodlecore
23  * @copyright 2009 Catalyst IT Ltd
24  * @author    Aaron Barnes <aaronb@catalyst.net.nz>
25  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26  */
27 require_once('../../../config.php');
28 require_once($CFG->libdir.'/completionlib.php');
30 /**
31  * Configuration
32  */
33 define('COMPLETION_REPORT_PAGE',        25);
34 define('COMPLETION_REPORT_COL_TITLES',  true);
36 /**
37  * Setup page, check permissions
38  */
40 // Get course
41 $courseid = required_param('course', PARAM_INT);
42 $format = optional_param('format','',PARAM_ALPHA);
43 $sort = optional_param('sort','',PARAM_ALPHA);
44 $edituser = optional_param('edituser', 0, PARAM_INT);
47 $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
49 $url = new moodle_url('/course/report/completion/index.php', array('course'=>$course->id));
50 $PAGE->set_url($url);
51 $PAGE->set_pagelayout('standard');
53 $firstnamesort = ($sort == 'firstname');
54 $excel = ($format == 'excelcsv');
55 $csv = ($format == 'csv' || $excel);
57 // Paging
58 $start   = optional_param('start', 0, PARAM_INT);
59 $sifirst = optional_param('sifirst', 'all', PARAM_ALPHA);
60 $silast  = optional_param('silast', 'all', PARAM_ALPHA);
62 // Whether to show idnumber
63 $idnumbers = $CFG->grade_report_showuseridnumber;
65 // Function for quoting csv cell values
66 function csv_quote($value) {
67     global $excel;
68     if($excel) {
69         $tl=textlib_get_instance();
70         return $tl->convert('"'.str_replace('"',"'",$value).'"','UTF-8','UTF-16LE');
71     } else {
72         return '"'.str_replace('"',"'",$value).'"';
73     }
74 }
77 // Check permissions
78 require_login($course);
80 $context=get_context_instance(CONTEXT_COURSE, $course->id);
81 require_capability('coursereport/completion:view', $context);
83 // Get group mode
84 $group = groups_get_course_group($course, true); // Supposed to verify group
85 if($group === 0 && $course->groupmode == SEPARATEGROUPS) {
86     require_capability('moodle/site:accessallgroups',$context);
87 }
89 /**
90  * Load data
91  */
93 // Get criteria for course
94 $completion = new completion_info($course);
96 if (!$completion->has_criteria()) {
97     print_error('err_nocriteria', 'completion', $CFG->wwwroot.'/course/report.php?id='.$course->id);
98 }
100 // Get criteria and put in correct order
101 $criteria = array();
103 foreach ($completion->get_criteria(COMPLETION_CRITERIA_TYPE_COURSE) as $criterion) {
104     $criteria[] = $criterion;
107 foreach ($completion->get_criteria(COMPLETION_CRITERIA_TYPE_ACTIVITY) as $criterion) {
108     $criteria[] = $criterion;
111 foreach ($completion->get_criteria() as $criterion) {
112     if (!in_array($criterion->criteriatype, array(
113             COMPLETION_CRITERIA_TYPE_COURSE, COMPLETION_CRITERIA_TYPE_ACTIVITY))) {
114         $criteria[] = $criterion;
115     }
118 // Can logged in user mark users as complete?
119 // (if the logged in user has a role defined in the role criteria)
120 $allow_marking = false;
121 $allow_marking_criteria = null;
123 if (!$csv) {
124     // Get role criteria
125     $rcriteria = $completion->get_criteria(COMPLETION_CRITERIA_TYPE_ROLE);
127     if (!empty($rcriteria)) {
129         foreach ($rcriteria as $rcriterion) {
130             $users = get_role_users($rcriterion->role, $context, true);
132             // If logged in user has this role, allow marking complete
133             if ($users && in_array($USER->id, array_keys($users))) {
134                 $allow_marking = true;
135                 $allow_marking_criteria = $rcriterion->id;
136                 break;
137             }
138         }
139     }
142 /**
143  * Setup page header
144  */
145 if ($csv) {
146     header('Content-Disposition: attachment; filename=progress.'.
147         preg_replace('/[^a-z0-9-]/','_',strtolower($course->shortname)).'.csv');
148     // Unicode byte-order mark for Excel
149     if($excel) {
150         header('Content-Type: text/csv; charset=UTF-16LE');
151         print chr(0xFF).chr(0xFE);
152         $sep="\t".chr(0);
153         $line="\n".chr(0);
154     } else {
155         header('Content-Type: text/csv; charset=UTF-8');
156         $sep=",";
157         $line="\n";
158     }
160 } else {
161     // Navigation and header
162     $strcompletion = get_string('completionreport','completion');
164     $PAGE->set_title($strcompletion);
165     $PAGE->set_heading($course->fullname);
167     echo $OUTPUT->header();
169     $PAGE->requires->yui2_lib(
170         array(
171             'yahoo',
172             'dom',
173             'element',
174             'event',
175         )
176     );
178     $PAGE->requires->js('/course/report/completion/textrotate.js');
180     // Handle groups (if enabled)
181     groups_print_course_menu($course, $CFG->wwwroot.'/course/report/completion/?course='.$course->id);
185 // Generate where clause
186 $where = array();
187 $ilike = $DB->sql_ilike();
189 if ($sifirst !== 'all') {
190     $where[] = "u.firstname $ilike '$sifirst%'";
193 if ($silast !== 'all') {
194     $where[] = "u.lastname $ilike '$silast%'";
197 // Get user match count
198 $total = $completion->get_num_tracked_users(implode(' AND ', $where), $group);
200 // Total user count
201 $grandtotal = $completion->get_num_tracked_users('', $group);
203 // If no users in this course what-so-ever
204 if (!$grandtotal) {
205     echo $OUTPUT->container(get_string('err_nousers', 'completion'), 'errorbox errorboxcontent');
206     echo $OUTPUT->footer();
207     exit;
210 // Get user data
211 $progress = array();
213 if ($total) {
214     $progress = $completion->get_progress_all(
215         implode(' AND ', $where),
216         $group,
217         $firstnamesort ? 'u.firstname ASC' : 'u.lastname ASC',
218         $csv ? 0 : COMPLETION_REPORT_PAGE,
219         $csv ? 0 : $start
220     );
224 // Build link for paging
225 $link = $CFG->wwwroot.'/course/report/completion/?course='.$course->id;
226 if (strlen($sort)) {
227     $link .= '&amp;sort='.$sort;
229 $link .= '&amp;start=';
231 // Build the the page by Initial bar
232 $initials = array('first', 'last');
233 $alphabet = explode(',', get_string('alphabet', 'langconfig'));
235 $pagingbar = '';
236 foreach ($initials as $initial) {
237     $var = 'si'.$initial;
239     $pagingbar .= ' <div class="initialbar '.$initial.'initial">';
240     $pagingbar .= get_string($initial.'name').':&nbsp;';
242     if ($$var == 'all') {
243         $pagingbar .= '<strong>'.get_string('all').'</strong> ';
244     }
245     else {
246         $pagingbar .= '<a href="'.$link.'">'.get_string('all').'</a> ';
247     }
249     foreach ($alphabet as $letter) {
250         if ($$var === $letter) {
251             $pagingbar .= '<strong>'.$letter.'</strong> ';
252         }
253         else {
254             $pagingbar .= '<a href="'.$link.'&amp;'.$var.'='.$letter.'">'.$letter.'</a> ';
255         }
256     }
258     $pagingbar .= '</div>';
261 // Do we need a paging bar?
262 if($total > COMPLETION_REPORT_PAGE) {
264     // Paging bar
265     $pagingbar .= '<div class="paging">';
266     $pagingbar .= get_string('page').': ';
268     // Display previous link
269     if ($start > 0) {
270         $pstart = max($start - COMPLETION_REPORT_PAGE, 0);
271         $pagingbar .= '(<a class="previous" href="'.$link.$pstart.'">'.get_string('previous').'</a>)&nbsp;';
272     }
274     // Create page links
275     $curstart = 0;
276     $curpage = 0;
277     while ($curstart < $total) {
278         $curpage++;
280         if ($curstart == $start) {
281             $pagingbar .= '&nbsp;'.$curpage.'&nbsp;';
282         }
283         else {
284             $pagingbar .= '&nbsp;<a href="'.$link.$curstart.'">'.$curpage.'</a>&nbsp;';
285         }
287         $curstart += COMPLETION_REPORT_PAGE;
288     }
290     // Display next link
291     $nstart = $start + COMPLETION_REPORT_PAGE;
292     if ($nstart < $total) {
293         $pagingbar .= '&nbsp;(<a class="next" href="'.$link.$nstart.'">'.get_string('next').'</a>)';
294     }
296     $pagingbar .= '</div>';
300 /**
301  * Draw table header
302  */
304 // Start of table
305 if(!$csv) {
306     print '<br class="clearer"/>'; // ugh
308     $total_header = ($total == $grandtotal) ? $total : "{$total}/{$grandtotal}";
309     echo $OUTPUT->heading(get_string('allparticipants').": {$total_header}", 3);
311     print $pagingbar;
313     if (!$total) {
314         echo $OUTPUT->heading(get_string('nothingtodisplay'), 2);
315         echo $OUTPUT->footer();
316         exit;
317     }
319     print '<table id="completion-progress" class="generaltable flexible boxaligncenter completionreport" style="text-align: left" cellpadding="5" border="1">';
321     // Print criteria group names
322     print PHP_EOL.'<tr style="vertical-align: top">';
323     print '<th scope="row" class="rowheader">'.get_string('criteriagroup', 'completion').'</th>';
325     $current_group = false;
326     $col_count = 0;
327     for ($i = 0; $i <= count($criteria); $i++) {
329         if (isset($criteria[$i])) {
330             $criterion = $criteria[$i];
332             if ($current_group && $criterion->criteriatype === $current_group->criteriatype) {
333                 ++$col_count;
334                 continue;
335             }
336         }
338         // Print header cell
339         if ($col_count) {
340             print '<th scope="col" colspan="'.$col_count.'" class="colheader criteriagroup">'.$current_group->get_type_title().'</th>';
341         }
343         if (isset($criteria[$i])) {
344             // Move to next criteria type
345             $current_group = $criterion;
346             $col_count = 1;
347         }
348     }
350     // Overall course completion status
351     print '<th style="text-align: center;">'.get_string('course').'</th>';
353     print '</tr>';
355     // Print aggregation methods
356     print PHP_EOL.'<tr style="vertical-align: top">';
357     print '<th scope="row" class="rowheader">'.get_string('aggregationmethod', 'completion').'</th>';
359     $current_group = false;
360     $col_count = 0;
361     for ($i = 0; $i <= count($criteria); $i++) {
363         if (isset($criteria[$i])) {
364             $criterion = $criteria[$i];
366             if ($current_group && $criterion->criteriatype === $current_group->criteriatype) {
367                 ++$col_count;
368                 continue;
369             }
370         }
372         // Print header cell
373         if ($col_count) {
374             $has_agg = array(
375                 COMPLETION_CRITERIA_TYPE_COURSE,
376                 COMPLETION_CRITERIA_TYPE_ACTIVITY,
377                 COMPLETION_CRITERIA_TYPE_ROLE,
378             );
380             if (in_array($current_group->criteriatype, $has_agg)) {
381                 // Try load a aggregation method
382                 $method = $completion->get_aggregation_method($current_group->criteriatype);
384                 $method = $method == 1 ? 'All' : 'Any';
386             } else {
387                 $method = '-';
388             }
390             print '<th scope="col" colspan="'.$col_count.'" class="colheader aggheader">'.$method.'</th>';
391         }
393         if (isset($criteria[$i])) {
394             // Move to next criteria type
395             $current_group = $criterion;
396             $col_count = 1;
397         }
398     }
400     // Overall course aggregation method
401     print '<th scope="col" class="colheader aggheader aggcriteriacourse">';
403     // Get course aggregation
404     $method = $completion->get_aggregation_method();
406     print $method == 1 ? 'All' : 'Any';
407     print '</th>';
409     print '</tr>';
412     // Print criteria titles
413     if (COMPLETION_REPORT_COL_TITLES) {
415         print PHP_EOL.'<tr>';
416         print '<th scope="row" class="rowheader">'.get_string('criteria', 'completion').'</th>';
418         foreach ($criteria as $criterion) {
419             // Get criteria details
420             $details = $criterion->get_title_detailed();
421             print '<th scope="col" class="colheader criterianame">';
422             print '<span class="completion-criterianame">'.$details.'</span>';
423             print '</th>';
424         }
426         // Overall course completion status
427         print '<th scope="col" class="colheader criterianame">';
429         print '<span class="completion-criterianame">'.get_string('coursecomplete', 'completion').'</span>';
431         print '</th></tr>';
432     }
434     // Print user heading and icons
435     print '<tr>';
437     // User heading / sort option
438     print '<th scope="col" class="completion-sortchoice" style="clear: both;">';
439     if($firstnamesort) {
440         print
441             get_string('firstname').' / <a href="./?course='.$course->id.'">'.
442             get_string('lastname').'</a>';
443     } else {
444         print '<a href="./?course='.$course->id.'&amp;sort=firstname">'.
445             get_string('firstname').'</a> / '.
446             get_string('lastname');
447     }
448     print '</th>';
451     // Print user id number column
452     if($idnumbers) {
453         print '<th>'.get_string('idnumber').'</th>';
454     }
456     ///
457     /// Print criteria icons
458     ///
459     foreach ($criteria as $criterion) {
461         // Generate icon details
462         $icon = '';
463         $iconlink = '';
464         $icontitle = ''; // Required if $iconlink set
465         $iconalt = ''; // Required
466         switch ($criterion->criteriatype) {
468             case COMPLETION_CRITERIA_TYPE_ACTIVITY:
469                 // Load activity
470                 $activity = $criterion->get_mod_instance();
472                 // Display icon
473                 $icon = $OUTPUT->pix_url('icon', $criterion->module).'/icon.gif';
474                 $iconlink = $CFG->wwwroot.'/mod/'.$criterion->module.'/view.php?id='.$activity->id;
475                 $icontitle = $activity->name;
476                 $iconalt = get_string('modulename', $criterion->module);
477                 break;
479             case COMPLETION_CRITERIA_TYPE_COURSE:
480                 // Load course
481                 $crs = $DB->get_record('course', array('id' => $criterion->courseinstance));
483                 // Display icon
484                 $iconlink = $CFG->wwwroot.'/course/view.php?id='.$criterion->courseinstance;
485                 $icontitle = $crs->fullname;
486                 $iconalt = $crs->shortname;
487                 break;
489             case COMPLETION_CRITERIA_TYPE_ROLE:
490                 // Load role
491                 $role = $DB->get_record('role', array('id' => $criterion->role));
493                 // Display icon
494                 $iconalt = $role->name;
495                 break;
496         }
498         // Print icon and cell
499         print '<th class="criteriaicon">';
501         // Create icon if not supplied
502         if (!$icon) {
503             $icon = $OUTPUT->pix_url('i/'.$COMPLETION_CRITERIA_TYPES[$criterion->criteriatype].'.gif');
504         }
506         print ($iconlink ? '<a href="'.$iconlink.'" title="'.$icontitle.'">' : '');
507         print '<img src="'.$icon.'" class="icon" alt="'.$iconalt.'" '.(!$iconlink ? 'title="'.$iconalt.'"' : '').' />';
508         print ($iconlink ? '</a>' : '');
510         print '</th>';
511     }
513     // Overall course completion status
514     print '<th class="criteriaicon">';
515     print '<img src="'.$OUTPUT->pix_url('i/course.gif').'" class="icon" alt="Course" title="Course Complete" />';
516     print '</th>';
518     print '</tr>';
521 } else {
522     // TODO
523     if($idnumbers) {
524         print $sep;
525     }
529 ///
530 /// Display a row for each user
531 ///
532 foreach ($progress as $user) {
534     // User name
535     if($csv) {
536         print csv_quote(fullname($user));
537         if($idnumbers) {
538             print $sep.csv_quote($user->idnumber);
539         }
540     } else {
541         print PHP_EOL.'<tr id="user-'.$user->id.'">';
543         print '<th scope="row"><a href="'.$CFG->wwwroot.'/user/view.php?id='.
544             $user->id.'&amp;course='.$course->id.'">'.fullname($user).'</a></th>';
545         if($idnumbers) {
546             print '<td>'.htmlspecialchars($user->idnumber).'</td>';
547         }
548     }
550     // Progress for each course completion criteria
551     foreach ($criteria as $criterion) {
553         // Handle activity completion differently
554         if ($criterion->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
556             // Load activity
557             $mod = $criterion->get_mod_instance();
558             $activity = $DB->get_record('course_modules', array('id' => $criterion->moduleinstance));
559             $activity->name = $mod->name;
562             // Get progress information and state
563             if(array_key_exists($activity->id,$user->progress)) {
564                 $thisprogress=$user->progress[$activity->id];
565                 $state=$thisprogress->completionstate;
566                 $date=userdate($thisprogress->timemodified);
567             } else {
568                 $state=COMPLETION_INCOMPLETE;
569                 $date='';
570             }
572             $criteria_completion = $completion->get_user_completion($user->id, $criterion);
574             // Work out how it corresponds to an icon
575             switch($state) {
576                 case COMPLETION_INCOMPLETE : $completiontype='n'; break;
577                 case COMPLETION_COMPLETE : $completiontype='y'; break;
578                 case COMPLETION_COMPLETE_PASS : $completiontype='pass'; break;
579                 case COMPLETION_COMPLETE_FAIL : $completiontype='fail'; break;
580             }
582             $completionicon='completion-'.
583                 ($activity->completion==COMPLETION_TRACKING_AUTOMATIC ? 'auto' : 'manual').
584                 '-'.$completiontype;
586             $describe=get_string('completion-alt-auto-'.$completiontype,'completion');
587             $a=new StdClass;
588             $a->state=$describe;
589             $a->date=$date;
590             $a->user=fullname($user);
591             $a->activity=strip_tags($activity->name);
592             $fulldescribe=get_string('progress-title','completion',$a);
594             if($csv) {
595                 print $sep.csv_quote($describe).$sep.csv_quote($date);
596             } else {
597                 print '<td class="completion-progresscell">';
599                 print '<img src="'.$OUTPUT->pix_url('i/'.$completionicon).
600                       '" alt="'.$describe.'" class="icon" title="'.$fulldescribe.'" />';
602                 print '</td>';
603             }
605             continue;
606         }
608         // Handle all other criteria
609         $criteria_completion = $completion->get_user_completion($user->id, $criterion);
610         $is_complete = $criteria_completion->is_complete();
612         $completiontype = $is_complete ? 'y' : 'n';
613         $completionicon = 'completion-auto-'.$completiontype;
615         $describe = get_string('completion-alt-auto-'.$completiontype, 'completion');
617         $a = new Object();
618         $a->state    = $describe;
619         $a->date     = $is_complete ? userdate($criteria_completion->timecompleted) : '';
620         $a->user     = fullname($user);
621         $a->activity = strip_tags($criterion->get_title());
622         $fulldescribe = get_string('progress-title', 'completion', $a);
624         if ($csv) {
625             print $sep.csv_quote($describe);
626         } else {
628             if ($allow_marking_criteria === $criterion->id) {
629                 $describe = get_string('completion-alt-auto-'.$completiontype,'completion');
631                 print '<td class="completion-progresscell">'.
632                     '<a href="'.$CFG->wwwroot.'/course/togglecompletion.php?user='.$user->id.'&course='.$course->id.'&rolec='.$allow_marking_criteria.'">'.
633                     '<img src="'.$OUTPUT->pix_url('i/completion-manual-'.($is_complete ? 'y' : 'n')).
634                     '" alt="'.$describe.'" class="icon" title="Mark as complete" /></a></td>';
635             } else {
636                 print '<td class="completion-progresscell">'.
637                     '<img src="'.$OUTPUT->pix_url('i/'.$completionicon).
638                     '" alt="'.$describe.'" class="icon" title="'.$fulldescribe.'" /></td>';
639             }
640         }
641     }
643     // Handle overall course completion
645     // Load course completion
646     $params = array(
647         'userid'    => $user->id,
648         'course'    => $course->id
649     );
651     $ccompletion = new completion_completion($params);
652     $completiontype =  $ccompletion->is_complete() ? 'y' : 'n';
654     $describe = get_string('completion-alt-auto-'.$completiontype, 'completion');
656     $a = new StdClass;
657     $a->state    = $describe;
658     $a->date     = '';
659     $a->user     = fullname($user);
660     $a->activity = strip_tags(get_string('coursecomplete', 'completion'));
661     $fulldescribe = get_string('progress-title', 'completion', $a);
663     if ($csv) {
664         print $sep.csv_quote($describe);
665     } else {
667         print '<td class="completion-progresscell">';
669         // Display course completion status icon
670         print '<img src="'.$OUTPUT->pix_url('i/completion-auto-'.$completiontype).
671                '" alt="'.$describe.'" class="icon" title="'.$fulldescribe.'" />';
673         print '</td>';
674     }
676     if($csv) {
677         print $line;
678     } else {
679         print '</tr>';
680     }
683 if($csv) {
684     exit;
686 print '</table>';
687 print $pagingbar;
689 print '<ul class="progress-actions"><li><a href="index.php?course='.$course->id.
690     '&amp;format=csv">'.get_string('csvdownload','completion').'</a></li>
691     <li><a href="index.php?course='.$course->id.'&amp;format=excelcsv">'.
692     get_string('excelcsvdownload','completion').'</a></li></ul>';
694 echo $OUTPUT->footer($course);