MDL-53936 reports: State default index page explicitly in pagination url
[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(__DIR__.'/../../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_NOTAGS);
66 $silast  = optional_param('silast', 'all', PARAM_NOTAGS);
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('nocriteriaset', '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-]/', '_',core_text::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     // Handle groups (if enabled)
160     groups_print_course_menu($course, $CFG->wwwroot.'/report/completion/index.php?course='.$course->id);
163 if ($sifirst !== 'all') {
164     set_user_preference('ifirst', $sifirst);
166 if ($silast !== 'all') {
167     set_user_preference('ilast', $silast);
170 if (!empty($USER->preference['ifirst'])) {
171     $sifirst = $USER->preference['ifirst'];
172 } else {
173     $sifirst = 'all';
176 if (!empty($USER->preference['ilast'])) {
177     $silast = $USER->preference['ilast'];
178 } else {
179     $silast = 'all';
182 // Generate where clause
183 $where = array();
184 $where_params = array();
186 if ($sifirst !== 'all') {
187     $where[] = $DB->sql_like('u.firstname', ':sifirst', false);
188     $where_params['sifirst'] = $sifirst.'%';
191 if ($silast !== 'all') {
192     $where[] = $DB->sql_like('u.lastname', ':silast', false);
193     $where_params['silast'] = $silast.'%';
196 // Get user match count
197 $total = $completion->get_num_tracked_users(implode(' AND ', $where), $where_params, $group);
199 // Total user count
200 $grandtotal = $completion->get_num_tracked_users('', array(), $group);
202 // If no users in this course what-so-ever
203 if (!$grandtotal) {
204     echo $OUTPUT->container(get_string('err_nousers', 'completion'), 'errorbox errorboxcontent');
205     echo $OUTPUT->footer();
206     exit;
209 // Get user data
210 $progress = array();
212 if ($total) {
213     $progress = $completion->get_progress_all(
214         implode(' AND ', $where),
215         $where_params,
216         $group,
217         $firstnamesort ? 'u.firstname ASC' : 'u.lastname ASC',
218         $csv ? 0 : COMPLETION_REPORT_PAGE,
219         $csv ? 0 : $start,
220         $context
221     );
224 // Build link for paging
225 $link = $CFG->wwwroot.'/report/completion/index.php?course='.$course->id;
226 if (strlen($sort)) {
227     $link .= '&amp;sort='.$sort;
229 $link .= '&amp;start=';
231 $pagingbar = '';
233 // Initials bar.
234 $prefixfirst = 'sifirst';
235 $prefixlast = 'silast';
236 $pagingbar .= $OUTPUT->initials_bar($sifirst, 'firstinitial', get_string('firstname'), $prefixfirst, $url);
237 $pagingbar .= $OUTPUT->initials_bar($silast, 'lastinitial', get_string('lastname'), $prefixlast, $url);
239 // Do we need a paging bar?
240 if ($total > COMPLETION_REPORT_PAGE) {
242     // Paging bar
243     $pagingbar .= '<div class="paging">';
244     $pagingbar .= get_string('page').': ';
246     $sistrings = array();
247     if ($sifirst != 'all') {
248         $sistrings[] =  "sifirst={$sifirst}";
249     }
250     if ($silast != 'all') {
251         $sistrings[] =  "silast={$silast}";
252     }
253     $sistring = !empty($sistrings) ? '&amp;'.implode('&amp;', $sistrings) : '';
255     // Display previous link
256     if ($start > 0) {
257         $pstart = max($start - COMPLETION_REPORT_PAGE, 0);
258         $pagingbar .= "(<a class=\"previous\" href=\"{$link}{$pstart}{$sistring}\">".get_string('previous').'</a>)&nbsp;';
259     }
261     // Create page links
262     $curstart = 0;
263     $curpage = 0;
264     while ($curstart < $total) {
265         $curpage++;
267         if ($curstart == $start) {
268             $pagingbar .= '&nbsp;'.$curpage.'&nbsp;';
269         }
270         else {
271             $pagingbar .= "&nbsp;<a href=\"{$link}{$curstart}{$sistring}\">$curpage</a>&nbsp;";
272         }
274         $curstart += COMPLETION_REPORT_PAGE;
275     }
277     // Display next link
278     $nstart = $start + COMPLETION_REPORT_PAGE;
279     if ($nstart < $total) {
280         $pagingbar .= "&nbsp;(<a class=\"next\" href=\"{$link}{$nstart}{$sistring}\">".get_string('next').'</a>)';
281     }
283     $pagingbar .= '</div>';
286 /*
287  * Draw table header
288  */
290 // Start of table
291 if (!$csv) {
292     print '<br class="clearer"/>'; // ugh
294     $total_header = ($total == $grandtotal) ? $total : "{$total}/{$grandtotal}";
295     echo $OUTPUT->heading(get_string('allparticipants').": {$total_header}", 3);
297     print $pagingbar;
299     if (!$total) {
300         echo $OUTPUT->heading(get_string('nothingtodisplay'), 2);
301         echo $OUTPUT->footer();
302         exit;
303     }
305     print '<table id="completion-progress" class="table table-bordered generaltable flexible boxaligncenter
306         completionreport" style="text-align: left" cellpadding="5" border="1">';
308     // Print criteria group names
309     print PHP_EOL.'<thead><tr style="vertical-align: top">';
310     echo '<th scope="row" class="rowheader" colspan="' . $leftcols . '">' .
311             get_string('criteriagroup', 'completion') . '</th>';
313     $current_group = false;
314     $col_count = 0;
315     for ($i = 0; $i <= count($criteria); $i++) {
317         if (isset($criteria[$i])) {
318             $criterion = $criteria[$i];
320             if ($current_group && $criterion->criteriatype === $current_group->criteriatype) {
321                 ++$col_count;
322                 continue;
323             }
324         }
326         // Print header cell
327         if ($col_count) {
328             print '<th scope="col" colspan="'.$col_count.'" class="colheader criteriagroup">'.$current_group->get_type_title().'</th>';
329         }
331         if (isset($criteria[$i])) {
332             // Move to next criteria type
333             $current_group = $criterion;
334             $col_count = 1;
335         }
336     }
338     // Overall course completion status
339     print '<th style="text-align: center;">'.get_string('course').'</th>';
341     print '</tr>';
343     // Print aggregation methods
344     print PHP_EOL.'<tr style="vertical-align: top">';
345     echo '<th scope="row" class="rowheader" colspan="' . $leftcols . '">' .
346             get_string('aggregationmethod', 'completion').'</th>';
348     $current_group = false;
349     $col_count = 0;
350     for ($i = 0; $i <= count($criteria); $i++) {
352         if (isset($criteria[$i])) {
353             $criterion = $criteria[$i];
355             if ($current_group && $criterion->criteriatype === $current_group->criteriatype) {
356                 ++$col_count;
357                 continue;
358             }
359         }
361         // Print header cell
362         if ($col_count) {
363             $has_agg = array(
364                 COMPLETION_CRITERIA_TYPE_COURSE,
365                 COMPLETION_CRITERIA_TYPE_ACTIVITY,
366                 COMPLETION_CRITERIA_TYPE_ROLE,
367             );
369             if (in_array($current_group->criteriatype, $has_agg)) {
370                 // Try load a aggregation method
371                 $method = $completion->get_aggregation_method($current_group->criteriatype);
373                 $method = $method == 1 ? get_string('all') : get_string('any');
375             } else {
376                 $method = '-';
377             }
379             print '<th scope="col" colspan="'.$col_count.'" class="colheader aggheader">'.$method.'</th>';
380         }
382         if (isset($criteria[$i])) {
383             // Move to next criteria type
384             $current_group = $criterion;
385             $col_count = 1;
386         }
387     }
389     // Overall course aggregation method
390     print '<th scope="col" class="colheader aggheader aggcriteriacourse">';
392     // Get course aggregation
393     $method = $completion->get_aggregation_method();
395     print $method == 1 ? get_string('all') : get_string('any');
396     print '</th>';
398     print '</tr>';
400     // Print criteria titles
401     if (COMPLETION_REPORT_COL_TITLES) {
403         print PHP_EOL.'<tr>';
404         echo '<th scope="row" class="rowheader" colspan="' . $leftcols . '">' .
405                 get_string('criteria', 'completion') . '</th>';
407         foreach ($criteria as $criterion) {
408             // Get criteria details
409             $details = $criterion->get_title_detailed();
410             print '<th scope="col" class="colheader criterianame">';
411             print '<div class="rotated-text-container"><span class="rotated-text">'.$details.'</span></div>';
412             print '</th>';
413         }
415         // Overall course completion status
416         print '<th scope="col" class="colheader criterianame">';
417         print '<div class="rotated-text-container"><span class="rotated-text">'.get_string('coursecomplete', 'completion').'</span></div>';
418         print '</th></tr>';
419     }
421     // Print user heading and icons
422     print '<tr>';
424     // User heading / sort option
425     print '<th scope="col" class="completion-sortchoice" style="clear: both;">';
427     $sistring = "&amp;silast={$silast}&amp;sifirst={$sifirst}";
429     if ($firstnamesort) {
430         print
431             get_string('firstname')." / <a href=\"./index.php?course={$course->id}{$sistring}\">".
432             get_string('lastname').'</a>';
433     } else {
434         print "<a href=\"./index.php?course={$course->id}&amp;sort=firstname{$sistring}\">".
435             get_string('firstname').'</a> / '.
436             get_string('lastname');
437     }
438     print '</th>';
440     // Print user identity columns
441     foreach ($extrafields as $field) {
442         echo '<th scope="col" class="completion-identifyfield">' .
443                 get_user_field_name($field) . '</th>';
444     }
446     ///
447     /// Print criteria icons
448     ///
449     foreach ($criteria as $criterion) {
451         // Generate icon details
452         $iconlink = '';
453         $iconalt = ''; // Required
454         $iconattributes = array('class' => 'icon');
455         switch ($criterion->criteriatype) {
457             case COMPLETION_CRITERIA_TYPE_ACTIVITY:
459                 // Display icon
460                 $iconlink = $CFG->wwwroot.'/mod/'.$criterion->module.'/view.php?id='.$criterion->moduleinstance;
461                 $iconattributes['title'] = $modinfo->cms[$criterion->moduleinstance]->get_formatted_name();
462                 $iconalt = get_string('modulename', $criterion->module);
463                 break;
465             case COMPLETION_CRITERIA_TYPE_COURSE:
466                 // Load course
467                 $crs = $DB->get_record('course', array('id' => $criterion->courseinstance));
469                 // Display icon
470                 $iconlink = $CFG->wwwroot.'/course/view.php?id='.$criterion->courseinstance;
471                 $iconattributes['title'] = format_string($crs->fullname, true, array('context' => context_course::instance($crs->id, MUST_EXIST)));
472                 $iconalt = format_string($crs->shortname, true, array('context' => context_course::instance($crs->id)));
473                 break;
475             case COMPLETION_CRITERIA_TYPE_ROLE:
476                 // Load role
477                 $role = $DB->get_record('role', array('id' => $criterion->role));
479                 // Display icon
480                 $iconalt = $role->name;
481                 break;
482         }
484         // Create icon alt if not supplied
485         if (!$iconalt) {
486             $iconalt = $criterion->get_title();
487         }
489         // Print icon and cell
490         print '<th class="criteriaicon">';
492         print ($iconlink ? '<a href="'.$iconlink.'" title="'.$iconattributes['title'].'">' : '');
493         print $OUTPUT->render($criterion->get_icon($iconalt, $iconattributes));
494         print ($iconlink ? '</a>' : '');
496         print '</th>';
497     }
499     // Overall course completion status
500     print '<th class="criteriaicon">';
501     print $OUTPUT->pix_icon('i/course', get_string('coursecomplete', 'completion'));
502     print '</th>';
504     print '</tr></thead>';
506     echo '<tbody>';
507 } else {
508     // The CSV headers
509     $row = array();
511     $row[] = get_string('id', 'report_completion');
512     $row[] = get_string('name', 'report_completion');
513     foreach ($extrafields as $field) {
514        $row[] = get_user_field_name($field);
515     }
517     // Add activity headers
518     foreach ($criteria as $criterion) {
520         // Handle activity completion differently
521         if ($criterion->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
523             // Load activity
524             $mod = $criterion->get_mod_instance();
525             $row[] = $formattedname = format_string($mod->name, true,
526                     array('context' => context_module::instance($criterion->moduleinstance)));
527             $row[] = $formattedname . ' - ' . get_string('completiondate', 'report_completion');
528         }
529         else {
530             // Handle all other criteria
531             $row[] = strip_tags($criterion->get_title_detailed());
532         }
533     }
535     $row[] = get_string('coursecomplete', 'completion');
537     $export->add_data($row);
540 ///
541 /// Display a row for each user
542 ///
543 foreach ($progress as $user) {
545     // User name
546     if ($csv) {
547         $row = array();
548         $row[] = $user->id;
549         $row[] = fullname($user);
550         foreach ($extrafields as $field) {
551             $row[] = $user->{$field};
552         }
553     } else {
554         print PHP_EOL.'<tr id="user-'.$user->id.'">';
556         if (completion_can_view_data($user->id, $course)) {
557             $userurl = new moodle_url('/blocks/completionstatus/details.php', array('course' => $course->id, 'user' => $user->id));
558         } else {
559             $userurl = new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $course->id));
560         }
562         print '<th scope="row"><a href="'.$userurl->out().'">'.fullname($user).'</a></th>';
563         foreach ($extrafields as $field) {
564             echo '<td>'.s($user->{$field}).'</td>';
565         }
566     }
568     // Progress for each course completion criteria
569     foreach ($criteria as $criterion) {
571         $criteria_completion = $completion->get_user_completion($user->id, $criterion);
572         $is_complete = $criteria_completion->is_complete();
574         // Handle activity completion differently
575         if ($criterion->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
577             // Load activity
578             $activity = $modinfo->cms[$criterion->moduleinstance];
580             // Get progress information and state
581             if (array_key_exists($activity->id, $user->progress)) {
582                 $state = $user->progress[$activity->id]->completionstate;
583             } else if ($is_complete) {
584                 $state = COMPLETION_COMPLETE;
585             } else {
586                 $state = COMPLETION_INCOMPLETE;
587             }
588             if ($is_complete) {
589                 $date = userdate($criteria_completion->timecompleted, get_string('strftimedatetimeshort', 'langconfig'));
590             } else {
591                 $date = '';
592             }
594             // Work out how it corresponds to an icon
595             switch($state) {
596                 case COMPLETION_INCOMPLETE    : $completiontype = 'n';    break;
597                 case COMPLETION_COMPLETE      : $completiontype = 'y';    break;
598                 case COMPLETION_COMPLETE_PASS : $completiontype = 'pass'; break;
599                 case COMPLETION_COMPLETE_FAIL : $completiontype = 'fail'; break;
600             }
602             $auto = $activity->completion == COMPLETION_TRACKING_AUTOMATIC;
603             $completionicon = 'completion-'.($auto ? 'auto' : 'manual').'-'.$completiontype;
605             $describe = get_string('completion-'.$completiontype, 'completion');
606             $a = new StdClass();
607             $a->state     = $describe;
608             $a->date      = $date;
609             $a->user      = fullname($user);
610             $a->activity  = $activity->get_formatted_name();
611             $fulldescribe = get_string('progress-title', 'completion', $a);
613             if ($csv) {
614                 $row[] = $describe;
615                 $row[] = $date;
616             } else {
617                 print '<td class="completion-progresscell">';
619                 print $OUTPUT->pix_icon('i/' . $completionicon, $fulldescribe);
621                 print '</td>';
622             }
624             continue;
625         }
627         // Handle all other criteria
628         $completiontype = $is_complete ? 'y' : 'n';
629         $completionicon = 'completion-auto-'.$completiontype;
631         $describe = get_string('completion-'.$completiontype, 'completion');
633         $a = new stdClass();
634         $a->state    = $describe;
636         if ($is_complete) {
637             $a->date = userdate($criteria_completion->timecompleted, get_string('strftimedatetimeshort', 'langconfig'));
638         } else {
639             $a->date = '';
640         }
642         $a->user     = fullname($user);
643         $a->activity = strip_tags($criterion->get_title());
644         $fulldescribe = get_string('progress-title', 'completion', $a);
646         if ($csv) {
647             $row[] = $a->date;
648         } else {
650             print '<td class="completion-progresscell">';
652             if ($allow_marking_criteria === $criterion->id) {
653                 $describe = get_string('completion-'.$completiontype, 'completion');
655                 $toggleurl = new moodle_url(
656                     '/course/togglecompletion.php',
657                     array(
658                         'user' => $user->id,
659                         'course' => $course->id,
660                         'rolec' => $allow_marking_criteria,
661                         'sesskey' => sesskey()
662                     )
663                 );
665                 print '<a href="'.$toggleurl->out().'" title="'.s(get_string('clicktomarkusercomplete', 'report_completion')).'">' .
666                     $OUTPUT->pix_icon('i/completion-manual-' . ($is_complete ? 'y' : 'n'), $describe) . '</a></td>';
667             } else {
668                 print $OUTPUT->pix_icon('i/' . $completionicon, $fulldescribe) . '</td>';
669             }
671             print '</td>';
672         }
673     }
675     // Handle overall course completion
677     // Load course completion
678     $params = array(
679         'userid'    => $user->id,
680         'course'    => $course->id
681     );
683     $ccompletion = new completion_completion($params);
684     $completiontype =  $ccompletion->is_complete() ? 'y' : 'n';
686     $describe = get_string('completion-'.$completiontype, 'completion');
688     $a = new StdClass;
690     if ($ccompletion->is_complete()) {
691         $a->date = userdate($ccompletion->timecompleted, get_string('strftimedatetimeshort', 'langconfig'));
692     } else {
693         $a->date = '';
694     }
696     $a->state    = $describe;
697     $a->user     = fullname($user);
698     $a->activity = strip_tags(get_string('coursecomplete', 'completion'));
699     $fulldescribe = get_string('progress-title', 'completion', $a);
701     if ($csv) {
702         $row[] = $a->date;
703     } else {
705         print '<td class="completion-progresscell">';
707         // Display course completion status icon
708         print $OUTPUT->pix_icon('i/completion-auto-' . $completiontype, $fulldescribe);
710         print '</td>';
711     }
713     if ($csv) {
714         $export->add_data($row);
715     } else {
716         print '</tr>';
717     }
720 if ($csv) {
721     $export->download_file();
722 } else {
723     echo '</tbody>';
726 print '</table>';
727 print $pagingbar;
729 $csvurl = new moodle_url('/report/completion/index.php', array('course' => $course->id, 'format' => 'csv'));
730 $excelurl = new moodle_url('/report/completion/index.php', array('course' => $course->id, 'format' => 'excelcsv'));
732 print '<ul class="export-actions">';
733 print '<li><a href="'.$csvurl->out().'">'.get_string('csvdownload','completion').'</a></li>';
734 print '<li><a href="'.$excelurl->out().'">'.get_string('excelcsvdownload','completion').'</a></li>';
735 print '</ul>';
737 echo $OUTPUT->footer($course);
739 // Trigger a report viewed event.
740 $event = \report_completion\event\report_viewed::create(array('context' => $context));
741 $event->trigger();