MDL-35386 completion report: Update code and improve report/csv
[moodle.git] / report / completion / index.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Course completion progress report
19  *
20  * @package    report
21  * @subpackage completion
22  * @copyright  2009 Catalyst IT Ltd
23  * @author     Aaron Barnes <aaronb@catalyst.net.nz>
24  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
27 require_once(dirname(__FILE__).'/../../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);
48 $context = context_course::instance($course->id);
50 $url = new moodle_url('/report/completion/index.php', array('course'=>$course->id));
51 $PAGE->set_url($url);
52 $PAGE->set_pagelayout('report');
54 $firstnamesort = ($sort == 'firstname');
55 $excel = ($format == 'excelcsv');
56 $csv = ($format == 'csv' || $excel);
58 // Load CSV library
59 if ($csv) {
60     require_once("{$CFG->libdir}/csvlib.class.php");
61 }
63 // Paging
64 $start   = optional_param('start', 0, PARAM_INT);
65 $sifirst = optional_param('sifirst', 'all', PARAM_ALPHA);
66 $silast  = optional_param('silast', 'all', PARAM_ALPHA);
68 // Whether to show extra user identity information
69 $extrafields = get_extra_user_fields($context);
70 $leftcols = 1 + count($extrafields);
72 // Check permissions
73 require_login($course);
75 require_capability('report/completion:view', $context);
77 // Get group mode
78 $group = groups_get_course_group($course, true); // Supposed to verify group
79 if ($group === 0 && $course->groupmode == SEPARATEGROUPS) {
80     require_capability('moodle/site:accessallgroups',$context);
81 }
83 /**
84  * Load data
85  */
87 // Retrieve course_module data for all modules in the course
88 $modinfo = get_fast_modinfo($course);
90 // Get criteria for course
91 $completion = new completion_info($course);
93 if (!$completion->has_criteria()) {
94     print_error('err_nocriteria', 'completion', $CFG->wwwroot.'/course/report.php?id='.$course->id);
95 }
97 // Get criteria and put in correct order
98 $criteria = array();
100 foreach ($completion->get_criteria(COMPLETION_CRITERIA_TYPE_COURSE) as $criterion) {
101     $criteria[] = $criterion;
104 foreach ($completion->get_criteria(COMPLETION_CRITERIA_TYPE_ACTIVITY) as $criterion) {
105     $criteria[] = $criterion;
108 foreach ($completion->get_criteria() as $criterion) {
109     if (!in_array($criterion->criteriatype, array(
110             COMPLETION_CRITERIA_TYPE_COURSE, COMPLETION_CRITERIA_TYPE_ACTIVITY))) {
111         $criteria[] = $criterion;
112     }
115 // Can logged in user mark users as complete?
116 // (if the logged in user has a role defined in the role criteria)
117 $allow_marking = false;
118 $allow_marking_criteria = null;
120 if (!$csv) {
121     // Get role criteria
122     $rcriteria = $completion->get_criteria(COMPLETION_CRITERIA_TYPE_ROLE);
124     if (!empty($rcriteria)) {
126         foreach ($rcriteria as $rcriterion) {
127             $users = get_role_users($rcriterion->role, $context, true);
129             // If logged in user has this role, allow marking complete
130             if ($users && in_array($USER->id, array_keys($users))) {
131                 $allow_marking = true;
132                 $allow_marking_criteria = $rcriterion->id;
133                 break;
134             }
135         }
136     }
139 /*
140  * Setup page header
141  */
142 if ($csv) {
144     $shortname = format_string($course->shortname, true, array('context' => $context));
145     $shortname = preg_replace('/[^a-z0-9-]/', '_',textlib::strtolower(strip_tags($shortname)));
147     $export = new csv_export_writer();
148     $export->set_filename('completion-'.$shortname);
150 } else {
151     // Navigation and header
152     $strcompletion = get_string('coursecompletion');
154     $PAGE->set_title($strcompletion);
155     $PAGE->set_heading($course->fullname);
157     echo $OUTPUT->header();
159     $PAGE->requires->js('/report/completion/textrotate.js');
160     $PAGE->requires->js_function_call('textrotate_init', null, true);
162     // Handle groups (if enabled)
163     groups_print_course_menu($course, $CFG->wwwroot.'/report/completion/?course='.$course->id);
166 // Generate where clause
167 $where = array();
168 $where_params = array();
170 if ($sifirst !== 'all') {
171     $where[] = $DB->sql_like('u.firstname', ':sifirst', false);
172     $where_params['sifirst'] = $sifirst.'%';
175 if ($silast !== 'all') {
176     $where[] = $DB->sql_like('u.lastname', ':silast', false);
177     $where_params['silast'] = $silast.'%';
180 // Get user match count
181 $total = $completion->get_num_tracked_users(implode(' AND ', $where), $where_params, $group);
183 // Total user count
184 $grandtotal = $completion->get_num_tracked_users('', array(), $group);
186 // If no users in this course what-so-ever
187 if (!$grandtotal) {
188     echo $OUTPUT->container(get_string('err_nousers', 'completion'), 'errorbox errorboxcontent');
189     echo $OUTPUT->footer();
190     exit;
193 // Get user data
194 $progress = array();
196 if ($total) {
197     $progress = $completion->get_progress_all(
198         implode(' AND ', $where),
199         $where_params,
200         $group,
201         $firstnamesort ? 'u.firstname ASC' : 'u.lastname ASC',
202         $csv ? 0 : COMPLETION_REPORT_PAGE,
203         $csv ? 0 : $start,
204         $context
205     );
208 // Build link for paging
209 $link = $CFG->wwwroot.'/report/completion/?course='.$course->id;
210 if (strlen($sort)) {
211     $link .= '&amp;sort='.$sort;
213 $link .= '&amp;start=';
215 // Build the the page by Initial bar
216 $initials = array('first', 'last');
217 $alphabet = explode(',', get_string('alphabet', 'langconfig'));
219 $pagingbar = '';
220 foreach ($initials as $initial) {
221     $var = 'si'.$initial;
223     $othervar = $initial == 'first' ? 'silast' : 'sifirst';
224     $othervar = $$othervar != 'all' ? "&amp;{$othervar}={$$othervar}" : '';
226     $pagingbar .= ' <div class="initialbar '.$initial.'initial">';
227     $pagingbar .= get_string($initial.'name').':&nbsp;';
229     if ($$var == 'all') {
230         $pagingbar .= '<strong>'.get_string('all').'</strong> ';
231     }
232     else {
233         $pagingbar .= "<a href=\"{$link}{$othervar}\">".get_string('all').'</a> ';
234     }
236     foreach ($alphabet as $letter) {
237         if ($$var === $letter) {
238             $pagingbar .= '<strong>'.$letter.'</strong> ';
239         }
240         else {
241             $pagingbar .= "<a href=\"$link&amp;$var={$letter}{$othervar}\">$letter</a> ";
242         }
243     }
245     $pagingbar .= '</div>';
248 // Do we need a paging bar?
249 if ($total > COMPLETION_REPORT_PAGE) {
251     // Paging bar
252     $pagingbar .= '<div class="paging">';
253     $pagingbar .= get_string('page').': ';
255     $sistrings = array();
256     if ($sifirst != 'all') {
257         $sistrings[] =  "sifirst={$sifirst}";
258     }
259     if ($silast != 'all') {
260         $sistrings[] =  "silast={$silast}";
261     }
262     $sistring = !empty($sistrings) ? '&amp;'.implode('&amp;', $sistrings) : '';
264     // Display previous link
265     if ($start > 0) {
266         $pstart = max($start - COMPLETION_REPORT_PAGE, 0);
267         $pagingbar .= "(<a class=\"previous\" href=\"{$link}{$pstart}{$sistring}\">".get_string('previous').'</a>)&nbsp;';
268     }
270     // Create page links
271     $curstart = 0;
272     $curpage = 0;
273     while ($curstart < $total) {
274         $curpage++;
276         if ($curstart == $start) {
277             $pagingbar .= '&nbsp;'.$curpage.'&nbsp;';
278         }
279         else {
280             $pagingbar .= "&nbsp;<a href=\"{$link}{$curstart}{$sistring}\">$curpage</a>&nbsp;";
281         }
283         $curstart += COMPLETION_REPORT_PAGE;
284     }
286     // Display next link
287     $nstart = $start + COMPLETION_REPORT_PAGE;
288     if ($nstart < $total) {
289         $pagingbar .= "&nbsp;(<a class=\"next\" href=\"{$link}{$nstart}{$sistring}\">".get_string('next').'</a>)';
290     }
292     $pagingbar .= '</div>';
295 /*
296  * Draw table header
297  */
299 // Start of table
300 if (!$csv) {
301     print '<br class="clearer"/>'; // ugh
303     $total_header = ($total == $grandtotal) ? $total : "{$total}/{$grandtotal}";
304     echo $OUTPUT->heading(get_string('allparticipants').": {$total_header}", 3);
306     print $pagingbar;
308     if (!$total) {
309         echo $OUTPUT->heading(get_string('nothingtodisplay'), 2);
310         echo $OUTPUT->footer();
311         exit;
312     }
314     print '<table id="completion-progress" class="generaltable flexible boxaligncenter completionreport" style="text-align: left" cellpadding="5" border="1">';
316     // Print criteria group names
317     print PHP_EOL.'<tr style="vertical-align: top">';
318     echo '<th scope="row" class="rowheader" colspan="' . $leftcols . '">' .
319             get_string('criteriagroup', 'completion') . '</th>';
321     $current_group = false;
322     $col_count = 0;
323     for ($i = 0; $i <= count($criteria); $i++) {
325         if (isset($criteria[$i])) {
326             $criterion = $criteria[$i];
328             if ($current_group && $criterion->criteriatype === $current_group->criteriatype) {
329                 ++$col_count;
330                 continue;
331             }
332         }
334         // Print header cell
335         if ($col_count) {
336             print '<th scope="col" colspan="'.$col_count.'" class="colheader criteriagroup">'.$current_group->get_type_title().'</th>';
337         }
339         if (isset($criteria[$i])) {
340             // Move to next criteria type
341             $current_group = $criterion;
342             $col_count = 1;
343         }
344     }
346     // Overall course completion status
347     print '<th style="text-align: center;">'.get_string('course').'</th>';
349     print '</tr>';
351     // Print aggregation methods
352     print PHP_EOL.'<tr style="vertical-align: top">';
353     echo '<th scope="row" class="rowheader" colspan="' . $leftcols . '">' .
354             get_string('aggregationmethod', 'completion').'</th>';
356     $current_group = false;
357     $col_count = 0;
358     for ($i = 0; $i <= count($criteria); $i++) {
360         if (isset($criteria[$i])) {
361             $criterion = $criteria[$i];
363             if ($current_group && $criterion->criteriatype === $current_group->criteriatype) {
364                 ++$col_count;
365                 continue;
366             }
367         }
369         // Print header cell
370         if ($col_count) {
371             $has_agg = array(
372                 COMPLETION_CRITERIA_TYPE_COURSE,
373                 COMPLETION_CRITERIA_TYPE_ACTIVITY,
374                 COMPLETION_CRITERIA_TYPE_ROLE,
375             );
377             if (in_array($current_group->criteriatype, $has_agg)) {
378                 // Try load a aggregation method
379                 $method = $completion->get_aggregation_method($current_group->criteriatype);
381                 $method = $method == 1 ? get_string('all') : get_string('any');
383             } else {
384                 $method = '-';
385             }
387             print '<th scope="col" colspan="'.$col_count.'" class="colheader aggheader">'.$method.'</th>';
388         }
390         if (isset($criteria[$i])) {
391             // Move to next criteria type
392             $current_group = $criterion;
393             $col_count = 1;
394         }
395     }
397     // Overall course aggregation method
398     print '<th scope="col" class="colheader aggheader aggcriteriacourse">';
400     // Get course aggregation
401     $method = $completion->get_aggregation_method();
403     print $method == 1 ? get_string('all') : get_string('any');
404     print '</th>';
406     print '</tr>';
408     // Print criteria titles
409     if (COMPLETION_REPORT_COL_TITLES) {
411         print PHP_EOL.'<tr>';
412         echo '<th scope="row" class="rowheader" colspan="' . $leftcols . '">' .
413                 get_string('criteria', 'completion') . '</th>';
415         foreach ($criteria as $criterion) {
416             // Get criteria details
417             $details = $criterion->get_title_detailed();
418             print '<th scope="col" class="colheader criterianame">';
419             print '<span class="completion-criterianame">'.$details.'</span>';
420             print '</th>';
421         }
423         // Overall course completion status
424         print '<th scope="col" class="colheader criterianame">';
425         print '<span class="completion-criterianame">'.get_string('coursecomplete', 'completion').'</span>';
426         print '</th></tr>';
427     }
429     // Print user heading and icons
430     print '<tr>';
432     // User heading / sort option
433     print '<th scope="col" class="completion-sortchoice" style="clear: both;">';
435     $sistring = "&amp;silast={$silast}&amp;sifirst={$sifirst}";
437     if ($firstnamesort) {
438         print
439             get_string('firstname')." / <a href=\"./?course={$course->id}{$sistring}\">".
440             get_string('lastname').'</a>';
441     } else {
442         print "<a href=\"./?course={$course->id}&amp;sort=firstname{$sistring}\">".
443             get_string('firstname').'</a> / '.
444             get_string('lastname');
445     }
446     print '</th>';
448     // Print user identity columns
449     foreach ($extrafields as $field) {
450         echo '<th scope="col" class="completion-identifyfield">' .
451                 get_user_field_name($field) . '</th>';
452     }
454     ///
455     /// Print criteria icons
456     ///
457     foreach ($criteria as $criterion) {
459         // Generate icon details
460         $icon = '';
461         $iconlink = '';
462         $icontitle = ''; // Required if $iconlink set
463         $iconalt = ''; // Required
464         switch ($criterion->criteriatype) {
466             case COMPLETION_CRITERIA_TYPE_ACTIVITY:
468                 // Display icon
469                 $icon = $OUTPUT->pix_url('icon', $criterion->module);
470                 $iconlink = $CFG->wwwroot.'/mod/'.$criterion->module.'/view.php?id='.$criterion->moduleinstance;
471                 $icontitle = $modinfo->cms[$criterion->moduleinstance]->name;
472                 $iconalt = get_string('modulename', $criterion->module);
473                 break;
475             case COMPLETION_CRITERIA_TYPE_COURSE:
476                 // Load course
477                 $crs = $DB->get_record('course', array('id' => $criterion->courseinstance));
479                 // Display icon
480                 $iconlink = $CFG->wwwroot.'/course/view.php?id='.$criterion->courseinstance;
481                 $icontitle = format_string($crs->fullname, true, array('context' => context_course::instance($crs->id, MUST_EXIST)));
482                 $iconalt = format_string($crs->shortname, true, array('context' => context_course::instance($crs->id)));
483                 break;
485             case COMPLETION_CRITERIA_TYPE_ROLE:
486                 // Load role
487                 $role = $DB->get_record('role', array('id' => $criterion->role));
489                 // Display icon
490                 $iconalt = $role->name;
491                 break;
492         }
494         // Print icon and cell
495         print '<th class="criteriaicon">';
497         // Create icon if not supplied
498         if (!$icon) {
499             $icon = $OUTPUT->pix_url('i/'.$COMPLETION_CRITERIA_TYPES[$criterion->criteriatype]);
500         }
502         print ($iconlink ? '<a href="'.$iconlink.'" title="'.$icontitle.'">' : '');
503         print '<img src="'.$icon.'" class="icon" alt="'.$iconalt.'" '.(!$iconlink ? 'title="'.$iconalt.'"' : '').' />';
504         print ($iconlink ? '</a>' : '');
506         print '</th>';
507     }
509     // Overall course completion status
510     print '<th class="criteriaicon">';
511     print '<img src="'.$OUTPUT->pix_url('i/course').'" class="icon" alt="'.get_string('course').'" title="'.get_string('coursecomplete', 'completion').'" />';
512     print '</th>';
514     print '</tr>';
516 } else {
517     // The CSV headers
518     $row = array();
520     $row[] = get_string('id', 'report_completion');
521     $row[] = get_string('name', 'report_completion');
522     foreach ($extrafields as $field) {
523        $row[] = get_user_field_name($field);
524     }
526     // Add activity headers
527     foreach ($criteria as $criterion) {
529         // Handle activity completion differently
530         if ($criterion->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
532             // Load activity
533             $mod = $criterion->get_mod_instance();
534             $row[] = $mod->name;
535             $row[] = $mod->name . ' - ' . get_string('completiondate', 'report_completion');
536         }
537         else {
538             // Handle all other criteria
539             $row[] = strip_tags($criterion->get_title_detailed());
540         }
541     }
543     $row[] = get_string('coursecomplete', 'completion');
545     $export->add_data($row);
548 ///
549 /// Display a row for each user
550 ///
551 foreach ($progress as $user) {
553     // User name
554     if ($csv) {
555         $row = array();
556         $row[] = $user->id;
557         $row[] = fullname($user);
558         foreach ($extrafields as $field) {
559             $row[] = $user->{$field};
560         }
561     } else {
562         print PHP_EOL.'<tr id="user-'.$user->id.'">';
564         $userurl = new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $course->id));
565         print '<th scope="row"><a href="'.$userurl->out().'">'.fullname($user).'</a></th>';
566         foreach ($extrafields as $field) {
567             echo '<td>'.s($user->{$field}).'</td>';
568         }
569     }
571     // Progress for each course completion criteria
572     foreach ($criteria as $criterion) {
574         $criteria_completion = $completion->get_user_completion($user->id, $criterion);
575         $is_complete = $criteria_completion->is_complete();
577         // Handle activity completion differently
578         if ($criterion->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
580             // Load activity
581             $activity = $modinfo->cms[$criterion->moduleinstance];
583             // Get progress information and state
584             if ($is_complete) {
585                 $date = userdate($criteria_completion->timecompleted, get_string('strftimedatetimeshort', 'langconfig'));
587                 if (array_key_exists($activity->id, $user->progress)) {
588                     $thisprogress = $user->progress[$activity->id];
589                     $state = $thisprogress->completionstate;
590                 } else {
591                     $state = COMPLETION_COMPLETE;
592                 }
593             } else {
594                 $date = '';
595                 $state = COMPLETION_INCOMPLETE;
596             }
598             // Work out how it corresponds to an icon
599             switch($state) {
600                 case COMPLETION_INCOMPLETE    : $completiontype = 'n';    break;
601                 case COMPLETION_COMPLETE      : $completiontype = 'y';    break;
602                 case COMPLETION_COMPLETE_PASS : $completiontype = 'pass'; break;
603                 case COMPLETION_COMPLETE_FAIL : $completiontype = 'fail'; break;
604             }
606             $auto = $activity->completion == COMPLETION_TRACKING_AUTOMATIC;
607             $completionicon = 'completion-'.($auto ? 'auto' : 'manual').'-'.$completiontype;
609             $describe = get_string('completion-'.$completiontype, 'completion');
610             $a = new StdClass();
611             $a->state     = $describe;
612             $a->date      = $date;
613             $a->user      = fullname($user);
614             $a->activity  = strip_tags($activity->name);
615             $fulldescribe = get_string('progress-title', 'completion', $a);
617             if ($csv) {
618                 $row[] = $describe;
619                 $row[] = $date;
620             } else {
621                 print '<td class="completion-progresscell">';
623                 print '<img src="'.$OUTPUT->pix_url('i/'.$completionicon).
624                       '" alt="'.$describe.'" class="icon" title="'.$fulldescribe.'" />';
626                 print '</td>';
627             }
629             continue;
630         }
632         // Handle all other criteria
633         $completiontype = $is_complete ? 'y' : 'n';
634         $completionicon = 'completion-auto-'.$completiontype;
636         $describe = get_string('completion-'.$completiontype, 'completion');
638         $a = new stdClass();
639         $a->state    = $describe;
641         if ($is_complete) {
642             $a->date = userdate($criteria_completion->timecompleted, get_string('strftimedatetimeshort', 'langconfig'));
643         } else {
644             $a->date = '';
645         }
647         $a->user     = fullname($user);
648         $a->activity = strip_tags($criterion->get_title());
649         $fulldescribe = get_string('progress-title', 'completion', $a);
651         if ($csv) {
652             $row[] = $a->date;
653         } else {
655             print '<td class="completion-progresscell">';
657             if ($allow_marking_criteria === $criterion->id) {
658                 $describe = get_string('completion-'.$completiontype, 'completion');
660                 $toggleurl = new moodle_url(
661                     '/course/togglecompletion.php',
662                     array(
663                         'user' => $user->id,
664                         'course' => $course->id,
665                         'rolec' => $allow_marking_criteria,
666                         'sesskey' => sesskey()
667                     )
668                 );
670                 print '<a href="'.$toggleurl->out().'"><img src="'.$OUTPUT->pix_url('i/completion-manual-'.($is_complete ? 'y' : 'n')).
671                     '" alt="'.$describe.'" class="icon" title="'.get_string('markcomplete', 'completion').'" /></a></td>';
672             } else {
673                 print '<img src="'.$OUTPUT->pix_url('i/'.$completionicon).'" alt="'.$describe.'" class="icon" title="'.$fulldescribe.'" /></td>';
674             }
676             print '</td>';
677         }
678     }
680     // Handle overall course completion
682     // Load course completion
683     $params = array(
684         'userid'    => $user->id,
685         'course'    => $course->id
686     );
688     $ccompletion = new completion_completion($params);
689     $completiontype =  $ccompletion->is_complete() ? 'y' : 'n';
691     $describe = get_string('completion-'.$completiontype, 'completion');
693     $a = new StdClass;
695     if ($ccompletion->is_complete()) {
696         $a->date = userdate($ccompletion->timecompleted, get_string('strftimedatetimeshort', 'langconfig'));
697     } else {
698         $a->date = '';
699     }
701     $a->state    = $describe;
702     $a->user     = fullname($user);
703     $a->activity = strip_tags(get_string('coursecomplete', 'completion'));
704     $fulldescribe = get_string('progress-title', 'completion', $a);
706     if ($csv) {
707         $row[] = $a->date;
708     } else {
710         print '<td class="completion-progresscell">';
712         // Display course completion status icon
713         print '<img src="'.$OUTPUT->pix_url('i/completion-auto-'.$completiontype).
714                '" alt="'.$describe.'" class="icon" title="'.$fulldescribe.'" />';
716         print '</td>';
717     }
719     if ($csv) {
720         $export->add_data($row);
721     } else {
722         print '</tr>';
723     }
726 if ($csv) {
727     $export->download_file();
730 print '</table>';
731 print $pagingbar;
733 $csvurl = new moodle_url('/report/completion/index.php', array('course' => $course->id, 'format' => 'csv'));
734 $excelurl = new moodle_url('/report/completion/index.php', array('course' => $course->id, 'format' => 'excelcsv'));
736 print '<ul class="export-actions">';
737 print '<li><a href="'.$csvurl->out().'">'.get_string('csvdownload','completion').'</a></li>';
738 print '<li><a href="'.$excelurl->out().'">'.get_string('excelcsvdownload','completion').'</a></li>';
739 print '</ul>';
741 echo $OUTPUT->footer($course);