course completion MDL-2631 Course completion feature - Thanks to Aaron Barnes and...
[moodle.git] / course / report / completion / index.php
CommitLineData
2be4d090
MD
1<?php
2
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/>.
17
18
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 */
27require_once('../../../config.php');
28require_once($CFG->libdir.'/completionlib.php');
29
30
31/**
32 * Configuration
33 */
34define('COMPLETION_REPORT_PAGE', 25);
35define('COMPLETION_REPORT_COL_TITLES', true);
36
37
38/**
39 * Setup page, check permissions
40 */
41
42// Get course
43$course = $DB->get_record('course', array('id' => required_param('course', PARAM_INT)));
44if(!$course) {
45 print_error('invalidcourseid');
46}
47
48// Non-js edit
49$edituser = optional_param('edituser', 0, PARAM_INT);
50
51// Sort (default lastname, optionally firstname)
52$sort = optional_param('sort','',PARAM_ALPHA);
53$firstnamesort = $sort == 'firstname';
54
55// CSV format
56$format = optional_param('format','',PARAM_ALPHA);
57$excel = $format == 'excelcsv';
58$csv = $format == 'csv' || $excel;
59
60// Whether to start at a particular position
61$start = optional_param('start',0,PARAM_INT);
62
63// Whether to show idnumber
64$idnumbers = $CFG->grade_report_showuseridnumber;
65
66// Function for quoting csv cell values
67function csv_quote($value) {
68 global $excel;
69 if($excel) {
70 $tl=textlib_get_instance();
71 return $tl->convert('"'.str_replace('"',"'",$value).'"','UTF-8','UTF-16LE');
72 } else {
73 return '"'.str_replace('"',"'",$value).'"';
74 }
75}
76
77
78// Check permissions
79require_login($course);
80
81$context=get_context_instance(CONTEXT_COURSE, $course->id);
82require_capability('coursereport/completion:view', $context);
83
84// Get group mode
85$group = groups_get_course_group($course, true); // Supposed to verify group
86if($group === 0 && $course->groupmode == SEPARATEGROUPS) {
87 require_capability('moodle/site:accessallgroups',$context);
88}
89
90
91$url = new moodle_url('/course/report/completion/index.php', array('course'=>$course->id));
92$PAGE->set_url($url);
93
94/**
95 * Load data
96 */
97
98// Get criteria for course
99$completion = new completion_info($course);
100
101if (!$completion->has_criteria()) {
102 print_error('err_nocriteria', 'completion', $CFG->wwwroot.'/course/report.php?id='.$course->id);
103}
104
105// Get criteria and put in correct order
106$criteria = array();
107
108foreach ($completion->get_criteria(COMPLETION_CRITERIA_TYPE_COURSE) as $criterion) {
109 $criteria[] = $criterion;
110}
111
112foreach ($completion->get_criteria(COMPLETION_CRITERIA_TYPE_ACTIVITY) as $criterion) {
113 $criteria[] = $criterion;
114}
115
116foreach ($completion->get_criteria() as $criterion) {
117 if (!in_array($criterion->criteriatype, array(
118 COMPLETION_CRITERIA_TYPE_COURSE, COMPLETION_CRITERIA_TYPE_ACTIVITY))) {
119 $criteria[] = $criterion;
120 }
121}
122
123// Can logged in user mark users as complete?
124// (if the logged in user has a role defined in the role criteria)
125$allow_marking = false;
126$allow_marking_criteria = null;
127
128if (!$csv) {
129 // Get role criteria
130 $rcriteria = $completion->get_criteria(COMPLETION_CRITERIA_TYPE_ROLE);
131
132 if (!empty($rcriteria)) {
133
134 foreach ($rcriteria as $rcriterion) {
135 $users = get_role_users($rcriterion->role, $context, true);
136
137 // If logged in user has this role, allow marking complete
138 if ($users && in_array($USER->id, array_keys($users))) {
139 $allow_marking = true;
140 $allow_marking_criteria = $rcriterion->id;
141 break;
142 }
143 }
144 }
145}
146
147// Get user data
148$progress = $completion->get_progress_all(
149 $firstnamesort, $group,
150 $csv ? 0 : COMPLETION_REPORT_PAGE,
151 $csv ? 0 : $start);
152
153
154/**
155 * Setup page header
156 */
157if ($csv) {
158 header('Content-Disposition: attachment; filename=progress.'.
159 preg_replace('/[^a-z0-9-]/','_',strtolower($course->shortname)).'.csv');
160 // Unicode byte-order mark for Excel
161 if($excel) {
162 header('Content-Type: text/csv; charset=UTF-16LE');
163 print chr(0xFF).chr(0xFE);
164 $sep="\t".chr(0);
165 $line="\n".chr(0);
166 } else {
167 header('Content-Type: text/csv; charset=UTF-8');
168 $sep=",";
169 $line="\n";
170 }
171
172} else {
173 // Navigation and header
174 $strcompletion = get_string('completionreport','completion');
175
176 $PAGE->set_title($strcompletion);
177 $PAGE->set_heading($course->fullname);
178
179 echo $OUTPUT->header();
180
181 $PAGE->requires->yui2_lib(
182 array(
183 'yahoo',
184 'dom',
185 'element',
186 'event',
187 )
188 );
189
190 $PAGE->requires->js('/course/report/completion/textrotate.js');
191
192 // Handle groups (if enabled)
193 groups_print_course_menu($course, $CFG->wwwroot.'/course/report/progress/?course='.$course->id);
194}
195
196// Do we need a paging bar?
197if($progress->total > COMPLETION_REPORT_PAGE) {
198 $pagingbar='<div class="completion_pagingbar">';
199
200 if($start>0) {
201 $newstart=$start-COMPLETION_REPORT_PAGE;
202 if($newstart<0) {
203 $newstart=0;
204 }
205 $pagingbar.=link_arrow_left(get_string('previous'),'./?course='.$course->id.
206 ($newstart ? '&amp;start='.$newstart : ''),false,'completion_prev');
207 }
208
209 $a=new StdClass;
210 $a->from=$start+1;
211 $a->to=$start+COMPLETION_REPORT_PAGE;
212 $a->total=$progress->total;
213 $pagingbar.='<p>'.get_string('reportpage','completion',$a).'</p>';
214
215 if($start+COMPLETION_REPORT_PAGE < $progress->total) {
216 $pagingbar.=link_arrow_right(get_string('next'),'./?course='.$course->id.
217 '&amp;start='.($start+COMPLETION_REPORT_PAGE),false,'completion_next');
218 }
219
220 $pagingbar.='</div>';
221} else {
222 $pagingbar='';
223}
224
225
226/**
227 * Draw table header
228 */
229
230// Start of table
231if(!$csv) {
232 print '<br class="clearer"/>'; // ugh
233
234 if(count($progress->users)==0) {
235 echo $OUTPUT->box_start('errorbox errorboxcontent boxaligncenter boxwidthnormal');
236 print '<p class="nousers">'.get_string('err_nousers','completion').'</p>';
237 print '<p><a href="'.$CFG->wwwroot.'/course/report.php?id='.$course->id.'">'.get_string('continue').'</a></p>';
238 echo $OUTPUT->box_end();
239 echo $OUTPUT->footer($course);
240 exit;
241 }
242
243 print $pagingbar;
244 print '<table id="completion-progress" class="generaltable flexible boxaligncenter completionreport" style="text-align: left" cellpadding="5" border="1">';
245
246 // Print criteria group names
247 print PHP_EOL.'<tr style="vertical-align: top">';
248 print '<th scope="row" class="rowheader">'.get_string('criteriagroup', 'completion').'</th>';
249
250 $current_group = false;
251 $col_count = 0;
252 for ($i = 0; $i <= count($criteria); $i++) {
253
254 if (isset($criteria[$i])) {
255 $criterion = $criteria[$i];
256
257 if ($current_group && $criterion->criteriatype === $current_group->criteriatype) {
258 ++$col_count;
259 continue;
260 }
261 }
262
263 // Print header cell
264 if ($col_count) {
265 print '<th scope="col" colspan="'.$col_count.'" class="colheader criteriagroup">'.$current_group->get_type_title().'</th>';
266 }
267
268 if (isset($criteria[$i])) {
269 // Move to next criteria type
270 $current_group = $criterion;
271 $col_count = 1;
272 }
273 }
274
275 // Overall course completion status
276 print '<th style="text-align: center;">'.get_string('course').'</th>';
277
278 print '</tr>';
279
280 // Print aggregation methods
281 print PHP_EOL.'<tr style="vertical-align: top">';
282 print '<th scope="row" class="rowheader">'.get_string('aggregationmethod', 'completion').'</th>';
283
284 $current_group = false;
285 $col_count = 0;
286 for ($i = 0; $i <= count($criteria); $i++) {
287
288 if (isset($criteria[$i])) {
289 $criterion = $criteria[$i];
290
291 if ($current_group && $criterion->criteriatype === $current_group->criteriatype) {
292 ++$col_count;
293 continue;
294 }
295 }
296
297 // Print header cell
298 if ($col_count) {
299 $has_agg = array(
300 COMPLETION_CRITERIA_TYPE_COURSE,
301 COMPLETION_CRITERIA_TYPE_ACTIVITY,
302 COMPLETION_CRITERIA_TYPE_ROLE,
303 );
304
305 if (in_array($current_group->criteriatype, $has_agg)) {
306 // Try load a aggregation method
307 $method = $completion->get_aggregation_method($current_group->criteriatype);
308
309 $method = $method == 1 ? 'All' : 'Any';
310
311 } else {
312 $method = '-';
313 }
314
315 print '<th scope="col" colspan="'.$col_count.'" class="colheader aggheader">'.$method.'</th>';
316 }
317
318 if (isset($criteria[$i])) {
319 // Move to next criteria type
320 $current_group = $criterion;
321 $col_count = 1;
322 }
323 }
324
325 // Overall course aggregation method
326 print '<th scope="col" class="colheader aggheader aggcriteriacourse">';
327
328 // Get course aggregation
329 $method = $completion->get_aggregation_method();
330
331 print $method == 1 ? 'All' : 'Any';
332 print '</th>';
333
334 print '</tr>';
335
336
337 // Print criteria titles
338 if (COMPLETION_REPORT_COL_TITLES) {
339
340 print PHP_EOL.'<tr>';
341 print '<th scope="row" class="rowheader">'.get_string('criteria', 'completion').'</th>';
342
343 foreach ($criteria as $criterion) {
344 // Get criteria details
345 $details = $criterion->get_title_detailed();
346 print '<th scope="col" class="colheader criterianame">';
347 print '<span class="completion-criterianame">'.$details.'</span>';
348 print '</th>';
349 }
350
351 // Overall course completion status
352 print '<th scope="col" class="colheader criterianame">';
353
354 print '<span class="completion-criterianame">'.get_string('coursecomplete', 'completion').'</span>';
355
356 print '</th></tr>';
357 }
358
359 // Print user heading and icons
360 print '<tr>';
361
362 // User heading / sort option
363 print '<th scope="col" class="completion-sortchoice" style="clear: both;">';
364 if($firstnamesort) {
365 print
366 get_string('firstname').' / <a href="./?course='.$course->id.'">'.
367 get_string('lastname').'</a>';
368 } else {
369 print '<a href="./?course='.$course->id.'&amp;sort=firstname">'.
370 get_string('firstname').'</a> / '.
371 get_string('lastname');
372 }
373 print '</th>';
374
375
376 // Print user id number column
377 if($idnumbers) {
378 print '<th>'.get_string('idnumber').'</th>';
379 }
380
381 ///
382 /// Print criteria icons
383 ///
384 foreach ($criteria as $criterion) {
385
386 // Generate icon details
387 $icon = '';
388 $iconlink = '';
389 $icontitle = ''; // Required if $iconlink set
390 $iconalt = ''; // Required
391 switch ($criterion->criteriatype) {
392
393 case COMPLETION_CRITERIA_TYPE_ACTIVITY:
394 // Load activity
395 $activity = $criterion->get_mod_instance();
396
397 // Display icon
398 $icon = $OUTPUT->pix_url('icon', $criterion->module).'/icon.gif';
399 $iconlink = $CFG->wwwroot.'/mod/'.$criterion->module.'/view.php?id='.$activity->id;
400 $icontitle = $activity->name;
401 $iconalt = get_string('modulename', $criterion->module);
402 break;
403
404 case COMPLETION_CRITERIA_TYPE_COURSE:
405 // Load course
406 $crs = get_record('course', 'id', $criterion->courseinstance);
407
408 // Display icon
409 $iconlink = $CFG->wwwroot.'/course/view.php?id='.$criterion->courseinstance;
410 $icontitle = $crs->fullname;
411 $iconalt = $crs->shortname;
412 break;
413
414 case COMPLETION_CRITERIA_TYPE_ROLE:
415 // Load role
416 $role = $DB->get_record('role', array('id' => $criterion->role));
417
418 // Display icon
419 $iconalt = $role->name;
420 break;
421 }
422
423 // Print icon and cell
424 print '<th class="criteriaicon">';
425
426 // Create icon if not supplied
427 if (!$icon) {
428 $icon = $OUTPUT->pix_url('i/'.$COMPLETION_CRITERIA_TYPES[$criterion->criteriatype].'.gif');
429 }
430
431 print ($iconlink ? '<a href="'.$iconlink.'" title="'.$icontitle.'">' : '');
432 print '<img src="'.$icon.'" class="icon" alt="'.$iconalt.'" '.(!$iconlink ? 'title="'.$iconalt.'"' : '').' />';
433 print ($iconlink ? '</a>' : '');
434
435 print '</th>';
436 }
437
438 // Overall course completion status
439 print '<th class="criteriaicon">';
440 print '<img src="'.$OUTPUT->pix_url('i/course.gif').'" class="icon" alt="Course" title="Course Complete" />';
441 print '</th>';
442
443 print '</tr>';
444
445
446} else {
447 // TODO
448 if($idnumbers) {
449 print $sep;
450 }
451}
452
453
454///
455/// Display a row for each user
456///
457foreach($progress->users as $user) {
458
459 // User name
460 if($csv) {
461 print csv_quote(fullname($user));
462 if($idnumbers) {
463 print $sep.csv_quote($user->idnumber);
464 }
465 } else {
466 print PHP_EOL.'<tr id="user-'.$user->id.'">';
467
468 print '<th scope="row"><a href="'.$CFG->wwwroot.'/user/view.php?id='.
469 $user->id.'&amp;course='.$course->id.'">'.fullname($user).'</a></th>';
470 if($idnumbers) {
471 print '<td>'.htmlspecialchars($user->idnumber).'</td>';
472 }
473 }
474
475 // Progress for each course completion criteria
476 foreach ($criteria as $criterion) {
477
478 // Handle activity completion differently
479 if ($criterion->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
480
481 // Load activity
482 $mod = $criterion->get_mod_instance();
483 $activity = $DB->get_record('course_modules', array('id' => $criterion->moduleinstance));
484 $activity->name = $mod->name;
485
486
487 // Get progress information and state
488 if(array_key_exists($activity->id,$user->progress)) {
489 $thisprogress=$user->progress[$activity->id];
490 $state=$thisprogress->completionstate;
491 $date=userdate($thisprogress->timemodified);
492 } else {
493 $state=COMPLETION_INCOMPLETE;
494 $date='';
495 }
496
497 $criteria_completion = $completion->get_user_completion($user->id, $criterion);
498
499 // Work out how it corresponds to an icon
500 switch($state) {
501 case COMPLETION_INCOMPLETE : $completiontype='n'; break;
502 case COMPLETION_COMPLETE : $completiontype='y'; break;
503 case COMPLETION_COMPLETE_PASS : $completiontype='pass'; break;
504 case COMPLETION_COMPLETE_FAIL : $completiontype='fail'; break;
505 }
506
507 $completionicon='completion-'.
508 ($activity->completion==COMPLETION_TRACKING_AUTOMATIC ? 'auto' : 'manual').
509 '-'.$completiontype;
510
511 $describe=get_string('completion-alt-auto-'.$completiontype,'completion');
512 $a=new StdClass;
513 $a->state=$describe;
514 $a->date=$date;
515 $a->user=fullname($user);
516 $a->activity=strip_tags($activity->name);
517 $fulldescribe=get_string('progress-title','completion',$a);
518
519 if($csv) {
520 print $sep.csv_quote($describe).$sep.csv_quote($date);
521 } else {
522 print '<td class="completion-progresscell">';
523
524 print '<img src="'.$OUTPUT->pix_url('i/'.$completionicon).
525 '" alt="'.$describe.'" class="icon" title="'.$fulldescribe.'" />';
526
527 print '</td>';
528 }
529
530 continue;
531 }
532
533 // Handle all other criteria
534 $criteria_completion = $completion->get_user_completion($user->id, $criterion);
535 $is_complete = $criteria_completion->is_complete();
536
537 $completiontype = $is_complete ? 'y' : 'n';
538 $completionicon = 'completion-auto-'.$completiontype;
539
540 $describe = get_string('completion-alt-auto-'.$completiontype, 'completion');
541
542 $a = new Object();
543 $a->state = $describe;
544 $a->date = $is_complete ? userdate($criteria_completion->timecompleted) : '';
545 $a->user = fullname($user);
546 $a->activity = strip_tags($criterion->get_title());
547 $fulldescribe = get_string('progress-title', 'completion', $a);
548
549 if ($csv) {
550 print $sep.csv_quote($describe);
551 } else {
552
553 if ($allow_marking_criteria === $criterion->id) {
554 $describe = get_string('completion-alt-auto-'.$completiontype,'completion');
555
556 print '<td class="completion-progresscell">'.
557 '<a href="'.$CFG->wwwroot.'/course/togglecompletion.php?user='.$user->id.'&course='.$course->id.'&rolec='.$allow_marking_criteria.'">'.
558 '<img src="'.$OUTPUT->pix_url('i/completion-manual-'.($is_complete ? 'y' : 'n')).
559 '" alt="'.$describe.'" class="icon" title="Mark as complete" /></a></td>';
560 } else {
561 print '<td class="completion-progresscell">'.
562 '<img src="'.$OUTPUT->pix_url('i/'.$completionicon).
563 '" alt="'.$describe.'" class="icon" title="'.$fulldescribe.'" /></td>';
564 }
565 }
566 }
567
568 // Handle overall course completion
569
570 // Load course completion
571 $params = array(
572 'userid' => $user->id,
573 'course' => $course->id
574 );
575
576 $ccompletion = new completion_completion($params);
577 $completiontype = $ccompletion->is_complete() ? 'y' : 'n';
578
579 $describe = get_string('completion-alt-auto-'.$completiontype, 'completion');
580
581 $a = new StdClass;
582 $a->state = $describe;
583 $a->date = '';
584 $a->user = fullname($user);
585 $a->activity = strip_tags(get_string('coursecomplete', 'completion'));
586 $fulldescribe = get_string('progress-title', 'completion', $a);
587
588 if ($csv) {
589 print $sep.csv_quote($describe);
590 } else {
591
592 print '<td class="completion-progresscell">';
593
594 // Display course completion status icon
595 print '<img src="'.$OUTPUT->pix_url('i/completion-auto-'.$completiontype).
596 '" alt="'.$describe.'" class="icon" title="'.$fulldescribe.'" />';
597
598 print '</td>';
599 }
600
601 if($csv) {
602 print $line;
603 } else {
604 print '</tr>';
605 }
606}
607
608if($csv) {
609 exit;
610}
611print '</table>';
612print $pagingbar;
613
614print '<ul class="progress-actions"><li><a href="index.php?course='.$course->id.
615 '&amp;format=csv">'.get_string('csvdownload','completion').'</a></li>
616 <li><a href="index.php?course='.$course->id.'&amp;format=excelcsv">'.
617 get_string('excelcsvdownload','completion').'</a></li></ul>';
618
619echo $OUTPUT->footer($course);