Merge branch 'MDL-79667-main' of https://github.com/mihailges/moodle
[moodle.git] / report / completion / index.php
bloba6fad00e663358b01358efba005c14c9e4e7b4e0
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.
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
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
27 use core\report_helper;
29 require_once(__DIR__.'/../../config.php');
30 require_once("{$CFG->libdir}/completionlib.php");
32 /**
33 * Configuration
35 define('COMPLETION_REPORT_PAGE', 25);
36 define('COMPLETION_REPORT_COL_TITLES', true);
39 * Setup page, check permissions
42 // Get course
43 $courseid = required_param('course', PARAM_INT);
44 $format = optional_param('format','',PARAM_ALPHA);
45 $sort = optional_param('sort','',PARAM_ALPHA);
46 $edituser = optional_param('edituser', 0, PARAM_INT);
49 $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
50 $context = context_course::instance($course->id);
52 $url = new moodle_url('/report/completion/index.php', array('course'=>$course->id));
53 $PAGE->set_url($url);
54 $PAGE->set_pagelayout('report');
56 $firstnamesort = ($sort == 'firstname');
57 $excel = ($format == 'excelcsv');
58 $csv = ($format == 'csv' || $excel);
59 if ($csv) {
60 $dateformat = "%F %T";
61 } else {
62 $dateformat = get_string('strftimedatetimeshort', 'langconfig');
65 // Load CSV library
66 if ($csv) {
67 require_once("{$CFG->libdir}/csvlib.class.php");
70 // Paging
71 $start = optional_param('start', 0, PARAM_INT);
72 $sifirst = optional_param('sifirst', 'all', PARAM_NOTAGS);
73 $silast = optional_param('silast', 'all', PARAM_NOTAGS);
75 // Whether to show extra user identity information.
76 $extrafields = \core_user\fields::get_identity_fields($context, true);
77 $leftcols = 1 + count($extrafields);
79 // Check permissions
80 require_login($course);
82 require_capability('report/completion:view', $context);
84 // Get group mode
85 $group = groups_get_course_group($course, true); // Supposed to verify group
86 if ($group === 0 && $course->groupmode == SEPARATEGROUPS) {
87 require_capability('moodle/site:accessallgroups',$context);
90 /**
91 * Load data
94 // Retrieve course_module data for all modules in the course
95 $modinfo = get_fast_modinfo($course);
97 // Get criteria for course
98 $completion = new completion_info($course);
100 if (!$completion->has_criteria()) {
101 throw new \moodle_exception('nocriteriaset', 'completion', $CFG->wwwroot.'/course/report.php?id='.$course->id);
104 // Get criteria and put in correct order
105 $criteria = array();
107 foreach ($completion->get_criteria(COMPLETION_CRITERIA_TYPE_COURSE) as $criterion) {
108 $criteria[] = $criterion;
111 foreach ($completion->get_criteria(COMPLETION_CRITERIA_TYPE_ACTIVITY) as $criterion) {
112 $criteria[] = $criterion;
115 foreach ($completion->get_criteria() as $criterion) {
116 if (!in_array($criterion->criteriatype, array(
117 COMPLETION_CRITERIA_TYPE_COURSE, COMPLETION_CRITERIA_TYPE_ACTIVITY))) {
118 $criteria[] = $criterion;
122 // Can logged in user mark users as complete?
123 // (if the logged in user has a role defined in the role criteria)
124 $allow_marking = false;
125 $allow_marking_criteria = null;
127 if (!$csv) {
128 // Get role criteria
129 $rcriteria = $completion->get_criteria(COMPLETION_CRITERIA_TYPE_ROLE);
131 if (!empty($rcriteria)) {
133 foreach ($rcriteria as $rcriterion) {
134 $users = get_role_users($rcriterion->role, $context, true);
136 // If logged in user has this role, allow marking complete
137 if ($users && in_array($USER->id, array_keys($users))) {
138 $allow_marking = true;
139 $allow_marking_criteria = $rcriterion->id;
140 break;
147 * Setup page header
149 if ($csv) {
151 $shortname = format_string($course->shortname, true, array('context' => $context));
152 $shortname = preg_replace('/[^a-z0-9-]/', '_',core_text::strtolower(strip_tags($shortname)));
154 $export = new csv_export_writer('comma', '"', 'application/download', $excel);
155 $export->set_filename('completion-'.$shortname);
157 } else {
158 // Navigation and header
159 $strcompletion = get_string('coursecompletion');
161 $PAGE->set_title($strcompletion);
162 $PAGE->set_heading($course->fullname);
164 echo $OUTPUT->header();
165 // Print the selected dropdown.
166 $pluginname = get_string('pluginname', 'report_completion');
167 report_helper::print_report_selector($pluginname);
169 // Handle groups (if enabled)
170 groups_print_course_menu($course, $CFG->wwwroot.'/report/completion/index.php?course='.$course->id);
173 if ($sifirst !== 'all') {
174 set_user_preference('ifirst', $sifirst);
176 if ($silast !== 'all') {
177 set_user_preference('ilast', $silast);
180 if (!empty($USER->preference['ifirst'])) {
181 $sifirst = $USER->preference['ifirst'];
182 } else {
183 $sifirst = 'all';
186 if (!empty($USER->preference['ilast'])) {
187 $silast = $USER->preference['ilast'];
188 } else {
189 $silast = 'all';
192 // Generate where clause
193 $where = array();
194 $where_params = array();
196 if ($sifirst !== 'all') {
197 $where[] = $DB->sql_like('u.firstname', ':sifirst', false, false);
198 $where_params['sifirst'] = $sifirst.'%';
201 if ($silast !== 'all') {
202 $where[] = $DB->sql_like('u.lastname', ':silast', false, false);
203 $where_params['silast'] = $silast.'%';
206 // Get user match count
207 $total = $completion->get_num_tracked_users(implode(' AND ', $where), $where_params, $group);
209 // Total user count
210 $grandtotal = $completion->get_num_tracked_users('', array(), $group);
212 // If no users in this course what-so-ever
213 if (!$grandtotal) {
214 echo $OUTPUT->container(get_string('err_nousers', 'completion'), 'errorbox errorboxcontent');
215 echo $OUTPUT->footer();
216 exit;
219 // Get user data
220 $progress = array();
222 if ($total) {
223 $progress = $completion->get_progress_all(
224 implode(' AND ', $where),
225 $where_params,
226 $group,
227 $firstnamesort ? 'u.firstname ASC' : 'u.lastname ASC',
228 $csv ? 0 : COMPLETION_REPORT_PAGE,
229 $csv ? 0 : $start,
230 $context
234 // Build link for paging
235 $link = $CFG->wwwroot.'/report/completion/index.php?course='.$course->id;
236 if (strlen($sort)) {
237 $link .= '&amp;sort='.$sort;
239 $link .= '&amp;start=';
241 $pagingbar = '';
243 // Initials bar.
244 $prefixfirst = 'sifirst';
245 $prefixlast = 'silast';
246 $pagingbar .= $OUTPUT->initials_bar($sifirst, 'firstinitial', get_string('firstname'), $prefixfirst, $url);
247 $pagingbar .= $OUTPUT->initials_bar($silast, 'lastinitial', get_string('lastname'), $prefixlast, $url);
249 // Do we need a paging bar?
250 if ($total > COMPLETION_REPORT_PAGE) {
252 // Paging bar
253 $pagingbar .= '<div class="paging">';
254 $pagingbar .= get_string('page').': ';
256 $sistrings = array();
257 if ($sifirst != 'all') {
258 $sistrings[] = "sifirst={$sifirst}";
260 if ($silast != 'all') {
261 $sistrings[] = "silast={$silast}";
263 $sistring = !empty($sistrings) ? '&amp;'.implode('&amp;', $sistrings) : '';
265 // Display previous link
266 if ($start > 0) {
267 $pstart = max($start - COMPLETION_REPORT_PAGE, 0);
268 $pagingbar .= "(<a class=\"previous\" href=\"{$link}{$pstart}{$sistring}\">".get_string('previous').'</a>)&nbsp;';
271 // Create page links
272 $curstart = 0;
273 $curpage = 0;
274 while ($curstart < $total) {
275 $curpage++;
277 if ($curstart == $start) {
278 $pagingbar .= '&nbsp;'.$curpage.'&nbsp;';
280 else {
281 $pagingbar .= "&nbsp;<a href=\"{$link}{$curstart}{$sistring}\">$curpage</a>&nbsp;";
284 $curstart += COMPLETION_REPORT_PAGE;
287 // Display next link
288 $nstart = $start + COMPLETION_REPORT_PAGE;
289 if ($nstart < $total) {
290 $pagingbar .= "&nbsp;(<a class=\"next\" href=\"{$link}{$nstart}{$sistring}\">".get_string('next').'</a>)';
293 $pagingbar .= '</div>';
297 * Draw table header
300 // Start of table
301 if (!$csv) {
302 print '<br class="clearer"/>'; // ugh
304 $total_header = ($total == $grandtotal) ? $total : "{$total}/{$grandtotal}";
305 echo $OUTPUT->heading(get_string('allparticipants').": {$total_header}", 3);
307 print $pagingbar;
309 if (!$total) {
310 echo $OUTPUT->notification(get_string('nothingtodisplay'), 'info', false);
311 echo $OUTPUT->footer();
312 exit;
315 print '<table id="completion-progress" class="table table-bordered generaltable flexible boxaligncenter
316 completionreport" cellpadding="5" border="1">';
318 // Print criteria group names
319 print PHP_EOL.'<thead><tr style="vertical-align: top">';
320 echo '<th scope="row" class="rowheader" colspan="' . $leftcols . '">' .
321 get_string('criteriagroup', 'completion') . '</th>';
323 $current_group = false;
324 $col_count = 0;
325 for ($i = 0; $i <= count($criteria); $i++) {
327 if (isset($criteria[$i])) {
328 $criterion = $criteria[$i];
330 if ($current_group && $criterion->criteriatype === $current_group->criteriatype) {
331 ++$col_count;
332 continue;
336 // Print header cell
337 if ($col_count) {
338 print '<th scope="col" colspan="'.$col_count.'" class="colheader criteriagroup">'.$current_group->get_type_title().'</th>';
341 if (isset($criteria[$i])) {
342 // Move to next criteria type
343 $current_group = $criterion;
344 $col_count = 1;
348 // Overall course completion status
349 print '<th style="text-align: center;">'.get_string('course').'</th>';
351 print '</tr>';
353 // Print aggregation methods
354 print PHP_EOL.'<tr style="vertical-align: top">';
355 echo '<th scope="row" class="rowheader" colspan="' . $leftcols . '">' .
356 get_string('aggregationmethod', 'completion').'</th>';
358 $current_group = false;
359 $col_count = 0;
360 for ($i = 0; $i <= count($criteria); $i++) {
362 if (isset($criteria[$i])) {
363 $criterion = $criteria[$i];
365 if ($current_group && $criterion->criteriatype === $current_group->criteriatype) {
366 ++$col_count;
367 continue;
371 // Print header cell
372 if ($col_count) {
373 $has_agg = array(
374 COMPLETION_CRITERIA_TYPE_COURSE,
375 COMPLETION_CRITERIA_TYPE_ACTIVITY,
376 COMPLETION_CRITERIA_TYPE_ROLE,
379 if (in_array($current_group->criteriatype, $has_agg)) {
380 // Try load a aggregation method
381 $method = $completion->get_aggregation_method($current_group->criteriatype);
383 $method = $method == 1 ? get_string('all') : get_string('any');
385 } else {
386 $method = '-';
389 print '<th scope="col" colspan="'.$col_count.'" class="colheader aggheader">'.$method.'</th>';
392 if (isset($criteria[$i])) {
393 // Move to next criteria type
394 $current_group = $criterion;
395 $col_count = 1;
399 // Overall course aggregation method
400 print '<th scope="col" class="colheader aggheader aggcriteriacourse">';
402 // Get course aggregation
403 $method = $completion->get_aggregation_method();
405 print $method == 1 ? get_string('all') : get_string('any');
406 print '</th>';
408 print '</tr>';
410 // Print criteria titles
411 if (COMPLETION_REPORT_COL_TITLES) {
413 print PHP_EOL.'<tr>';
414 echo '<th scope="row" class="rowheader" colspan="' . $leftcols . '">' .
415 get_string('criteria', 'completion') . '</th>';
417 foreach ($criteria as $criterion) {
418 // Get criteria details
419 $details = $criterion->get_title_detailed();
420 print '<th scope="col" class="colheader criterianame">';
421 print '<div class="rotated-text-container"><span class="rotated-text">'.$details.'</span></div>';
422 print '</th>';
425 // Overall course completion status
426 print '<th scope="col" class="colheader criterianame">';
427 print '<div class="rotated-text-container"><span class="rotated-text">'.get_string('coursecomplete', 'completion').'</span></div>';
428 print '</th></tr>';
431 // Print user heading and icons
432 print '<tr>';
434 // User heading / sort option
435 print '<th scope="col" class="completion-sortchoice" style="clear: both;">';
437 $sistring = "&amp;silast={$silast}&amp;sifirst={$sifirst}";
439 if ($firstnamesort) {
440 print
441 get_string('firstname')." / <a href=\"./index.php?course={$course->id}{$sistring}\">".
442 get_string('lastname').'</a>';
443 } else {
444 print "<a href=\"./index.php?course={$course->id}&amp;sort=firstname{$sistring}\">".
445 get_string('firstname').'</a> / '.
446 get_string('lastname');
448 print '</th>';
450 // Print user identity columns
451 foreach ($extrafields as $field) {
452 echo '<th scope="col" class="completion-identifyfield">' .
453 \core_user\fields::get_display_name($field) . '</th>';
457 /// Print criteria icons
459 foreach ($criteria as $criterion) {
461 // Generate icon details
462 $iconlink = '';
463 $iconalt = ''; // Required
464 $iconattributes = array('class' => 'icon');
465 switch ($criterion->criteriatype) {
467 case COMPLETION_CRITERIA_TYPE_ACTIVITY:
469 // Display icon
470 $iconlink = $CFG->wwwroot.'/mod/'.$criterion->module.'/view.php?id='.$criterion->moduleinstance;
471 $iconattributes['title'] = $modinfo->cms[$criterion->moduleinstance]->get_formatted_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 $iconattributes['title'] = 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;
494 // Create icon alt if not supplied
495 if (!$iconalt) {
496 $iconalt = $criterion->get_title();
499 // Print icon and cell
500 print '<th class="criteriaicon">';
502 print ($iconlink ? '<a href="'.$iconlink.'" title="'.$iconattributes['title'].'">' : '');
503 print $OUTPUT->render($criterion->get_icon($iconalt, $iconattributes));
504 print ($iconlink ? '</a>' : '');
506 print '</th>';
509 // Overall course completion status
510 print '<th class="criteriaicon">';
511 print $OUTPUT->pix_icon('i/course', get_string('coursecomplete', 'completion'));
512 print '</th>';
514 print '</tr></thead>';
516 echo '<tbody>';
517 } else {
518 // The CSV headers
519 $row = array();
521 $row[] = get_string('id', 'report_completion');
522 $row[] = get_string('name', 'report_completion');
523 foreach ($extrafields as $field) {
524 $row[] = \core_user\fields::get_display_name($field);
527 // Add activity headers
528 foreach ($criteria as $criterion) {
530 // Handle activity completion differently
531 if ($criterion->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
533 // Load activity
534 $mod = $criterion->get_mod_instance();
535 $row[] = $formattedname = format_string($mod->name, true,
536 array('context' => context_module::instance($criterion->moduleinstance)));
537 $row[] = $formattedname . ' - ' . get_string('completiondate', 'report_completion');
539 else {
540 // Handle all other criteria
541 $row[] = strip_tags($criterion->get_title_detailed());
545 $row[] = get_string('coursecomplete', 'completion');
547 $export->add_data($row);
551 /// Display a row for each user
553 foreach ($progress as $user) {
555 // User name
556 if ($csv) {
557 $row = array();
558 $row[] = $user->id;
559 $row[] = fullname($user, has_capability('moodle/site:viewfullnames', $context));
560 foreach ($extrafields as $field) {
561 $row[] = $user->{$field};
563 } else {
564 print PHP_EOL.'<tr id="user-'.$user->id.'">';
566 if (completion_can_view_data($user->id, $course)) {
567 $userurl = new moodle_url('/blocks/completionstatus/details.php', array('course' => $course->id, 'user' => $user->id));
568 } else {
569 $userurl = new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $course->id));
572 print '<th scope="row"><a href="' . $userurl->out() . '">' .
573 fullname($user, has_capability('moodle/site:viewfullnames', $context)) . '</a></th>';
574 foreach ($extrafields as $field) {
575 echo '<td>'.s($user->{$field}).'</td>';
579 // Progress for each course completion criteria
580 foreach ($criteria as $criterion) {
582 $criteria_completion = $completion->get_user_completion($user->id, $criterion);
583 $is_complete = $criteria_completion->is_complete();
585 // Handle activity completion differently
586 if ($criterion->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
588 // Load activity
589 $activity = $modinfo->cms[$criterion->moduleinstance];
591 // Get progress information and state
592 if (array_key_exists($activity->id, $user->progress)) {
593 $state = $user->progress[$activity->id]->completionstate;
594 } else if ($is_complete) {
595 $state = COMPLETION_COMPLETE;
596 } else {
597 $state = COMPLETION_INCOMPLETE;
599 if ($is_complete) {
600 $date = userdate($criteria_completion->timecompleted, $dateformat);
601 } else {
602 $date = '';
605 // Work out how it corresponds to an icon
606 switch($state) {
607 case COMPLETION_INCOMPLETE : $completiontype = 'n'; break;
608 case COMPLETION_COMPLETE : $completiontype = 'y'; break;
609 case COMPLETION_COMPLETE_PASS : $completiontype = 'pass'; break;
610 case COMPLETION_COMPLETE_FAIL : $completiontype = 'fail'; break;
613 $auto = $activity->completion == COMPLETION_TRACKING_AUTOMATIC;
614 $completionicon = 'completion-'.($auto ? 'auto' : 'manual').'-'.$completiontype;
616 $describe = get_string('completion-'.$completiontype, 'completion');
617 $a = new StdClass();
618 $a->state = $describe;
619 $a->date = $date;
620 $a->user = fullname($user);
621 $a->activity = $activity->get_formatted_name();
622 $fulldescribe = get_string('progress-title', 'completion', $a);
624 if ($csv) {
625 $row[] = $describe;
626 $row[] = $date;
627 } else {
628 print '<td class="completion-progresscell">';
630 print $OUTPUT->pix_icon('i/' . $completionicon, $fulldescribe);
632 print '</td>';
635 continue;
638 // Handle all other criteria
639 $completiontype = $is_complete ? 'y' : 'n';
640 $completionicon = 'completion-auto-'.$completiontype;
642 $describe = get_string('completion-'.$completiontype, 'completion');
644 $a = new stdClass();
645 $a->state = $describe;
647 if ($is_complete) {
648 $a->date = userdate($criteria_completion->timecompleted, $dateformat);
649 } else {
650 $a->date = '';
653 $a->user = fullname($user);
654 $a->activity = strip_tags($criterion->get_title());
655 $fulldescribe = get_string('progress-title', 'completion', $a);
657 if ($csv) {
658 $row[] = $a->date;
659 } else {
661 print '<td class="completion-progresscell">';
663 if ($allow_marking_criteria === $criterion->id) {
664 $describe = get_string('completion-'.$completiontype, 'completion');
666 $toggleurl = new moodle_url(
667 '/course/togglecompletion.php',
668 array(
669 'user' => $user->id,
670 'course' => $course->id,
671 'rolec' => $allow_marking_criteria,
672 'sesskey' => sesskey()
676 print '<a href="'.$toggleurl->out().'" title="'.s(get_string('clicktomarkusercomplete', 'report_completion')).'">' .
677 $OUTPUT->pix_icon('i/completion-manual-' . ($is_complete ? 'y' : 'n'), $describe) . '</a></td>';
678 } else {
679 print $OUTPUT->pix_icon('i/' . $completionicon, $fulldescribe) . '</td>';
682 print '</td>';
686 // Handle overall course completion
688 // Load course completion
689 $params = array(
690 'userid' => $user->id,
691 'course' => $course->id
694 $ccompletion = new completion_completion($params);
695 $completiontype = $ccompletion->is_complete() ? 'y' : 'n';
697 $describe = get_string('completion-'.$completiontype, 'completion');
699 $a = new StdClass;
701 if ($ccompletion->is_complete()) {
702 $a->date = userdate($ccompletion->timecompleted, $dateformat);
703 } else {
704 $a->date = '';
707 $a->state = $describe;
708 $a->user = fullname($user);
709 $a->activity = strip_tags(get_string('coursecomplete', 'completion'));
710 $fulldescribe = get_string('progress-title', 'completion', $a);
712 if ($csv) {
713 $row[] = $a->date;
714 } else {
716 print '<td class="completion-progresscell">';
718 // Display course completion status icon
719 print $OUTPUT->pix_icon('i/completion-auto-' . $completiontype, $fulldescribe);
721 print '</td>';
724 if ($csv) {
725 $export->add_data($row);
726 } else {
727 print '</tr>';
731 if ($csv) {
732 $export->download_file();
733 } else {
734 echo '</tbody>';
737 print '</table>';
739 $csvurl = new moodle_url('/report/completion/index.php', array('course' => $course->id, 'format' => 'csv'));
740 $excelurl = new moodle_url('/report/completion/index.php', array('course' => $course->id, 'format' => 'excelcsv'));
742 print '<ul class="export-actions">';
743 print '<li><a href="'.$csvurl->out().'">'.get_string('csvdownload','completion').'</a></li>';
744 print '<li><a href="'.$excelurl->out().'">'.get_string('excelcsvdownload','completion').'</a></li>';
745 print '</ul>';
747 echo $OUTPUT->footer($course);
749 // Trigger a report viewed event.
750 $event = \report_completion\event\report_viewed::create(array('context' => $context));
751 $event->trigger();