+ * Just add elements to the form as needed and return the list of IDs. The
+ * system will call disabledIf and handle other behaviour for each returned
+ * ID.
+ * @return array Array of string IDs of added items, empty array if none
+ */
+ function add_completion_rules() {
+ return array();
+ }
+
+ /**
+ * Called during validation. Override to indicate, based on the data, whether
+ * a custom completion rule is enabled (selected).
+ *
+ * @param array $data Input data (not yet validated)
+ * @return bool True if one or more rules is enabled, false if none are;
+ * default returns false
+ */
+ function completion_rule_enabled(&$data) {
+ return false;
+ }
function standard_hidden_coursemodule_elements(){
$mform =& $this->_form;
Index: course/view.php
===================================================================
RCS file: /cvsroot/moodle/moodle/course/view.php,v
retrieving revision 1.113
diff -u -r1.113 view.php
--- course/view.php 2 Jun 2008 08:13:25 -0000 1.113
+++ course/view.php 7 Jul 2008 13:21:48 -0000
@@ -195,6 +195,15 @@
$PAGE->print_header(get_string('course').': %fullname%', NULL, '', $bodytags);
+ $completion=new completion_info($course);
+ if($completion->is_enabled() && ajaxenabled()) {
+ require_js(array('yui_yahoo','yui_event','yui_connection','yui_dom'));
+ // Need to do this after the header because it requires the YUI stuff
+ // to be loaded already
+ print ''.
+ '';
+ }
+
// Course wrapper start.
echo '
';
Index: course/edit_form.php
===================================================================
RCS file: /cvsroot/moodle/moodle/course/edit_form.php,v
retrieving revision 1.48
diff -u -r1.48 edit_form.php
--- course/edit_form.php 1 Jun 2008 15:44:59 -0000 1.48
+++ course/edit_form.php 7 Jul 2008 13:21:47 -0000
@@ -351,6 +351,18 @@
$mform->addElement('select', 'lang', get_string('forcelanguage'), $languages);
//--------------------------------------------------------------------------------
+ require_once($CFG->libdir.'/completionlib.php');
+ if(completion_info::is_enabled_for_site()) {
+ $mform->addElement('header','', get_string('progress','completion'));
+ $mform->addElement('select', 'enablecompletion', get_string('completion','completion'),
+ array(0=>get_string('completiondisabled','completion'), 1=>get_string('completionenabled','completion')));
+ $mform->setDefault('enablecompletion',1);
+ } else {
+ $mform->addElement('hidden', 'enablecompletion');
+ $mform->setDefault('enablecompletion',0);
+ }
+
+//--------------------------------------------------------------------------------
if (has_capability('moodle/site:config', $systemcontext) && ((!empty($course->requested) && $CFG->restrictmodulesfor == 'requested') || $CFG->restrictmodulesfor == 'all')) {
$mform->addElement('header', '', get_string('restrictmodules'));
Index: course/modedit.php
===================================================================
RCS file: /cvsroot/moodle/moodle/course/modedit.php,v
retrieving revision 1.54
diff -u -r1.54 modedit.php
--- course/modedit.php 15 Jun 2008 11:34:42 -0000 1.54
+++ course/modedit.php 7 Jul 2008 13:21:47 -0000
@@ -113,6 +113,10 @@
$form->instance = $cm->instance;
$form->return = $return;
$form->update = $update;
+ $form->completion = $cm->completion;
+ $form->completionview = $cm->completionview;
+ $form->completionexpected = $cm->completionexpected;
+ $form->completionusegrade = is_null($cm->completiongradeitemnumber) ? 0 : 1;
if ($items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$form->modulename,
'iteminstance'=>$form->instance, 'courseid'=>$COURSE->id))) {
@@ -236,6 +240,19 @@
if (!isset($fromform->name)) { //label
$fromform->name = $fromform->modulename;
}
+
+ if (!isset($fromform->completion)) {
+ $fromform->completion=COMPLETION_DISABLED;
+ }
+ if (!isset($fromform->completionview)) {
+ $fromform->completionview=COMPLETION_VIEW_NOT_REQUIRED;
+ }
+
+ // Convert the 'use grade' checkbox into a grade-item number: 0 if
+ // checked, null if not
+ $fromform->completiongradeitemnumber =
+ isset($fromform->completionusegrade) && $fromform->completionusegrade
+ ? 0 : null;
if (!empty($fromform->update)) {
@@ -257,6 +274,18 @@
set_coursemodule_groupingid($fromform->coursemodule, $fromform->groupingid);
set_coursemodule_groupmembersonly($fromform->coursemodule, $fromform->groupmembersonly);
+ // Handle completion settings. If necessary, wipe existing completion
+ // data first.
+ if(!empty($fromform->completionunlocked)) {
+ $completion=new completion_info($course);
+ $completion->reset_all_state($cm);
+ }
+ set_coursemodule_completion($fromform->coursemodule, $fromform->completion);
+ set_coursemodule_completionview($fromform->coursemodule, $fromform->completionview);
+ set_coursemodule_completionexpected($fromform->coursemodule, $fromform->completionexpected);
+ set_coursemodule_completiongradeitemnumber(
+ $fromform->coursemodule,$fromform->completiongradeitemnumber);
+
if (isset($fromform->cmidnumber)) { //label
// set cm idnumber
set_coursemodule_idnumber($fromform->coursemodule, $fromform->cmidnumber);
Index: course/lib.php
===================================================================
RCS file: /cvsroot/moodle/moodle/course/lib.php,v
retrieving revision 1.607
diff -u -r1.607 lib.php
--- course/lib.php 6 Jul 2008 17:57:07 -0000 1.607
+++ course/lib.php 7 Jul 2008 13:21:47 -0000
@@ -1,6 +1,7 @@
libdir.'/completionlib.php');
define('COURSE_MAX_LOG_DISPLAY', 150); // days
define('COURSE_MAX_LOGS_PER_PAGE', 1000); // records
@@ -1276,7 +1277,7 @@
/**
* Prints a section full of activity modules
*/
-function print_section($course, $section, $mods, $modnamesused, $absolute=false, $width="100%") {
+function print_section($course, $section, $mods, $modnamesused, $absolute=false, $width="100%", $hidecompletion=false) {
global $CFG, $USER, $DB;
static $initialised;
@@ -1450,6 +1451,66 @@
echo ' ';
echo make_editing_buttons($mod, $absolute, true, $mod->indent, $section->section);
}
+
+ // Completion
+ $completioninfo=new completion_info($course);
+ $completion=$hidecompletion
+ ? COMPLETION_TRACKING_NONE
+ : $completioninfo->is_enabled($mod);
+ if($completion!=COMPLETION_TRACKING_NONE) {
+ $completiondata=$completioninfo->get_data($mod,true);
+ $completionicon='';
+ if($isediting) {
+ switch($completion) {
+ case COMPLETION_TRACKING_MANUAL :
+ $completionicon='manual-enabled'; break;
+ case COMPLETION_TRACKING_AUTOMATIC :
+ $completionicon='auto-enabled'; break;
+ default: // wtf
+ }
+ } else if($completion==COMPLETION_TRACKING_MANUAL) {
+ switch($completiondata->completionstate) {
+ case COMPLETION_INCOMPLETE:
+ $completionicon='manual-n'; break;
+ case COMPLETION_COMPLETE:
+ $completionicon='manual-y'; break;
+ }
+ } else { // Automatic
+ switch($completiondata->completionstate) {
+ case COMPLETION_INCOMPLETE:
+ $completionicon='auto-n'; break;
+ case COMPLETION_COMPLETE:
+ $completionicon='auto-y'; break;
+ case COMPLETION_COMPLETE_PASS:
+ $completionicon='auto-pass'; break;
+ case COMPLETION_COMPLETE_FAIL:
+ $completionicon='auto-fail'; break;
+ }
+ }
+ if($completionicon) {
+ $imgsrc=$CFG->pixpath.'/i/completion-'.$completionicon.'.gif';
+ $imgalt=get_string('completion-alt-'.$completionicon,'completion');
+ if($completion==COMPLETION_TRACKING_MANUAL && !$isediting) {
+ $imgtitle=get_string('completion-title-'.$completionicon,'completion');
+ $newstate=
+ $completiondata->completionstate==COMPLETION_COMPLETE
+ ? COMPLETION_INCOMPLETE
+ : COMPLETION_COMPLETE;
+ // In manual mode the icon is a toggle form.
+ echo "
+
";
+ } else {
+ // In auto mode, or when editing, the icon is just an image
+ echo "
+

";
+ }
+ }
+ }
+
echo "\n";
}
@@ -2225,6 +2286,27 @@
global $DB;
return $DB->set_field("course_modules", "idnumber", $idnumber, array("id"=>$id));
}
+
+function set_coursemodule_completion($id, $completion) {
+ global $DB;
+ return $DB->set_field("course_modules", "completion", $completion, array('id'=>$id));
+}
+
+function set_coursemodule_completionview($id, $completionview) {
+ global $DB;
+ return $DB->set_field("course_modules", "completionview", $completionview, array('id'=>$id));
+}
+
+function set_coursemodule_completiongradeitemnumber($id, $completiongradeitemnumber) {
+ global $DB;
+ return $DB->set_field("course_modules", "completiongradeitemnumber", $completiongradeitemnumber, array('id'=>$id));
+}
+
+function set_coursemodule_completionexpected($id, $completionexpected) {
+ global $DB;
+ return $DB->set_field("course_modules", "completionexpected", $completionexpected, array('id'=>$id));
+}
+
/**
* $prevstateoverrides = true will set the visibility of the course module
* to what is defined in visibleold. This enables us to remember the current
Index: course/report.php
===================================================================
RCS file: /cvsroot/moodle/moodle/course/report.php,v
retrieving revision 1.8
diff -u -r1.8 report.php
--- course/report.php 1 Jun 2008 17:59:13 -0000 1.8
+++ course/report.php 7 Jul 2008 13:21:47 -0000
@@ -11,7 +11,8 @@
require_login($course->id);
- require_capability('moodle/site:viewreports', get_context_instance(CONTEXT_COURSE, $course->id));
+ $context=get_context_instance(CONTEXT_COURSE, $course->id);
+ require_capability('moodle/site:viewreports', $context);
$strreports = get_string('reports');
Index: lib/grade/grade_grade.php
===================================================================
RCS file: /cvsroot/moodle/moodle/lib/grade/grade_grade.php,v
retrieving revision 1.43
diff -u -r1.43 grade_grade.php
--- lib/grade/grade_grade.php 30 May 2008 17:43:00 -0000 1.43
+++ lib/grade/grade_grade.php 7 Jul 2008 13:21:51 -0000
@@ -735,5 +735,59 @@
$this->rawgrademax = grade_floatval($this->rawgrademax);
return parent::update($source);
}
+
+ /**
+ * Used to notify the completion system (if necessary) that a user's grade
+ * has changed.
+ * @param bool deleted True if grade was actually deleted
+ */
+ function notify_changed($deleted) {
+ // Ignore during restore
+ global $restore;
+ if(!empty($restore->backup_unique_code)) {
+ return;
+ }
+ global $CFG,$COURSE,$DB;
+ require_once($CFG->libdir.'/completionlib.php');
+
+ // Use $COURSE if available otherwise get it via item fields
+ if(!empty($COURSE)) {
+ $course=$COURSE;
+ } else {
+ $this->load_grade_item();
+ $course=get_record('course','id',$grade_item->courseid);
+ }
+
+ // Bail out immediately if completion is not enabled for course
+ $completion=new completion_info($course);
+ if(!$completion->is_enabled()) {
+ return;
+ }
+
+ // Get the grade item and course-module which we will need
+ $this->load_grade_item();
+ if($this->grade_item->itemtype!='mod') {
+ return;
+ }
+ $cm=$DB->get_record_sql("
+SELECT
+ cm.*,m.name AS modname
+FROM
+ {$CFG->prefix}modules m
+ INNER JOIN {$CFG->prefix}course_modules cm ON m.id=cm.module
+WHERE
+ m.name=? AND cm.instance=? AND cm.course=?",
+ array($this->grade_item->itemmodule,$this->grade_item->iteminstance,
+ $this->grade_item->courseid));
+ if(!$cm) {
+ debugging("Couldn't find course-module for module
+ '{$this->grade_item->itemmodule}', instance '{$this->grade_item->iteminstance}',
+ course '{$this->grade_item->courseid}'");
+ return;
+ }
+
+ // Pass information on to completion system
+ $completion->inform_grade_changed($cm,$this->grade_item,$this,$deleted);
+ }
}
?>
Index: lib/grade/grade_object.php
===================================================================
RCS file: /cvsroot/moodle/moodle/lib/grade/grade_object.php,v
retrieving revision 1.45
diff -u -r1.45 grade_object.php
--- lib/grade/grade_object.php 9 Jun 2008 16:53:59 -0000 1.45
+++ lib/grade/grade_object.php 7 Jul 2008 13:21:51 -0000
@@ -173,6 +173,7 @@
global $DB;
if ($datas = $DB->get_records_select($table, $wheresql, $params)) {
+
$result = array();
foreach($datas as $data) {
$instance = new $classname();
@@ -182,6 +183,7 @@
return $result;
} else {
+
return false;
}
}
@@ -215,6 +217,7 @@
$DB->insert_record($this->table.'_history', $data);
}
+ $this->notify_changed(false);
return true;
}
@@ -244,6 +247,7 @@
$data->userlogged = $USER->id;
$DB->insert_record($this->table.'_history', $data);
}
+ $this->notify_changed(true);
return true;
} else {
@@ -306,6 +310,7 @@
$DB->insert_record($this->table.'_history', $data);
}
+ $this->notify_changed(false);
return $this->id;
}
@@ -344,5 +349,15 @@
}
}
}
+
+ /**
+ * Called immediately after the object data has been inserted, updated, or
+ * deleted in the database. Default does nothing, can be overridden to
+ * hook in special behaviour.
+ *
+ * @param bool $deleted
+ */
+ function notify_changed($deleted) {
+ }
}
?>
Index: theme/standard/styles_layout.css
===================================================================
RCS file: /cvsroot/moodle/moodle/theme/standard/styles_layout.css,v
retrieving revision 1.583
diff -u -r1.583 styles_layout.css
--- theme/standard/styles_layout.css 15 Jun 2008 11:58:17 -0000 1.583
+++ theme/standard/styles_layout.css 7 Jul 2008 13:21:53 -0000
@@ -1790,6 +1790,29 @@
#course-view .section .weekdates {
}
+#course-view li.activity {
+ margin-right:20px; /* Space allowed for completion icons if enabled */
+ position:relative;
+}
+#course-view li.activity form.togglecompletion,
+#course-view li.activity span.autocompletion {
+ display:inline;
+ position:absolute;
+ right:-20px;
+ z-index:10;
+}
+#course-view li.activity form.togglecompletion div {
+ display:inline;
+}
+#course-view .completion-saved-display {
+ position:absolute;
+ top:0; left:0;
+ border:1px solid black;
+ padding: 1px 2px;
+ background:white;
+ font-size:0.85em;
+}
+
#course-view ul.section,
#site-index ul.section {
margin: 0;
@@ -2680,6 +2703,36 @@
}
/***
+ *** Completion progress report
+ ***/
+
+#course-report-progress-index th,
+#course-report-progress-index td {
+ padding:2px 4px;
+ font-weight:normal;
+ border-right: 1px solid #EEE;
+}
+.completion-expired {
+ background:#fdd;
+}
+.completion-expected {
+ font-size:0.75em;
+}
+.completion-sortchoice {
+ font-size:0.75em;
+ vertical-align:bottom;
+}
+.completion-progresscell {
+ text-align:right;
+}
+.completion-expired .completion-expected {
+ font-weight:bold;
+}
+#course-report-progress-index .progress-actions {
+ text-align:center;
+}
+
+/***
*** Logs
***/
Index: backup/restorelib.php
===================================================================
RCS file: /cvsroot/moodle/moodle/backup/restorelib.php,v
retrieving revision 1.342
diff -u -r1.342 restorelib.php
--- backup/restorelib.php 5 Jul 2008 00:45:44 -0000 1.342
+++ backup/restorelib.php 7 Jul 2008 13:21:47 -0000
@@ -752,6 +752,7 @@
$course->enrolenddate += $restore->course_startdateoffset;
}
$course->enrolperiod = $course_header->course_enrolperiod;
+ $course->enablecompletion = isset($course_header->course_enablecompletion) ? $course_header->course_enablecompletion : 0;
//Put as last course in category
$course->sortorder = $category->sortorder + MAX_COURSES_IN_CATEGORY - 1;
@@ -1119,11 +1120,17 @@
//print_object($course_module); //Debug
//Save it to db
if ($mod->idnumber) {
+ $mod->idnumber=backup_todb($mod->idnumber);
if (grade_verify_idnumber($mod->idnumber, $restore->course_id)) {
$course_module->idnumber = $mod->idnumber;
}
}
+ $course_module->completion=$mod->completion;
+ $course_module->completiongradeitemnumber=backup_todb($mod->completiongradeitemnumber);
+ $course_module->completionview=$mod->completionview;
+ $course_module->completionexpected=$mod->completionexpected;
+
$newidmod = $DB->insert_record("course_modules", $course_module);
if ($newidmod) {
//save old and new module id
@@ -1162,6 +1169,43 @@
}
}
}
+
+ // Now that we have IDs for everything, store any completion data
+ if($status && !empty($info->completiondata)) {
+ foreach($info->completiondata as $data) {
+ // Convert cmid
+ $newcmid=backup_getid($restore->backup_unique_code, 'course_modules', $data->coursemoduleid);
+ if($newcmid) {
+ $data->coursemoduleid=$newcmid->new_id;
+ } else {
+ if (!defined('RESTORE_SILENTLY')) {
+ echo "
Can't find new ID for cm $data->coursemoduleid.
";
+ }
+ $status=false;
+ continue;
+ }
+
+ // Convert userid
+ $newuserid=backup_getid($restore->backup_unique_code, 'user', $data->userid);
+ if($newuserid) {
+ $data->userid=$newuserid->new_id;
+ } else {
+ // Skip missing users
+ debugging("Not restoring completion data for missing user {$data->userid}",DEBUG_DEVELOPER);
+ continue;
+ }
+
+ // Add record
+ if(!$DB->insert_record('course_modules_completion',$data)) {
+ if (!defined('RESTORE_SILENTLY')) {
+ echo "
Failed to insert completion data record.
";
+ }
+ $status=false;
+ continue;
+ }
+ }
+ }
+
} else {
$status = false;
}
@@ -5173,6 +5217,9 @@
case "ENROLPERIOD":
$this->info->course_enrolperiod = $this->getContents();
break;
+ case "ENABLECOMPLETION":
+ $this->info->course_enablecompletion = $this->getContents();
+ break;
}
}
if ($this->tree[4] == "CATEGORY") {
@@ -5548,7 +5595,15 @@
$this->info->tempmod->groupmembersonly;
$this->info->tempsection->mods[$this->info->tempmod->id]->idnumber =
$this->info->tempmod->idnumber;
-
+ $this->info->tempsection->mods[$this->info->tempmod->id]->completion =
+ isset($this->info->tempmod->completion) ? $this->info->tempmod->completion : 0;
+ $this->info->tempsection->mods[$this->info->tempmod->id]->completiongradeitemnumber =
+ isset($this->info->tempmod->completiongradeitemnumber) ? $this->info->tempmod->completiongradeitemnumber : null;
+ $this->info->tempsection->mods[$this->info->tempmod->id]->completionview =
+ isset($this->info->tempmod->completionview) ? $this->info->tempmod->completionview : 0;
+ $this->info->tempsection->mods[$this->info->tempmod->id]->completionexpected =
+ isset($this->info->tempmod->completionexpected) ? $this->info->tempmod->completionexpected : 0;
+
unset($this->info->tempmod);
}
}
@@ -5586,6 +5641,18 @@
case "IDNUMBER":
$this->info->tempmod->idnumber = $this->getContents();
break;
+ case "COMPLETION":
+ $this->info->tempmod->completion = $this->getContents();
+ break;
+ case "COMPLETIONGRADEITEMNUMBER":
+ $this->info->tempmod->completiongradeitemnumber = $this->getContents();
+ break;
+ case "COMPLETIONVIEW":
+ $this->info->tempmod->completionview = $this->getContents();
+ break;
+ case "COMPLETIONEXPECTED":
+ $this->info->tempmod->completionexpected = $this->getContents();
+ break;
default:
break;
}
@@ -5681,6 +5748,36 @@
}
} /// ends role_overrides
+ if (isset($this->tree[7]) && $this->tree[7] == "COMPLETIONDATA") {
+ if($this->level == 8) {
+ switch($tagName) {
+ case 'COMPLETION':
+ // Got all data to make completion entry...
+ $this->info->tempcompletion->coursemoduleid=$this->info->tempmod->id;
+ $this->info->completiondata[]=$this->info->tempcompletion;
+ unset($this->info->tempcompletion);
+ $this->info->tempcompletion=new stdClass;
+ break;
+ }
+ }
+
+ if($this->level == 9) {
+ switch($tagName) {
+ case 'USERID' :
+ $this->info->tempcompletion->userid=$this->getContents();
+ break;
+ case 'COMPLETIONSTATE' :
+ $this->info->tempcompletion->completionstate=$this->getContents();
+ break;
+ case 'VIEWED' :
+ $this->info->tempcompletion->viewed=$this->getContents();
+ break;
+ case 'TIMEMODIFIED' :
+ $this->info->tempcompletion->timemodified=$this->getContents();
+ break;
+ }
+ }
+ }
}
//Stop parsing if todo = SECTIONS and tagName = SECTIONS (en of the tag, of course)
@@ -8347,22 +8444,22 @@
}
/// Now, restore role nameincourse (only if the role had nameincourse in backup)
if (!empty($roledata->nameincourse)) {
- $newrole = backup_getid($restore->backup_unique_code, 'role', $oldroleid); /// Look for target role
- $coursecontext = get_context_instance(CONTEXT_COURSE, $restore->course_id); /// Look for target context
- if (!empty($newrole->new_id) && !empty($coursecontext) && !empty($roledata->nameincourse)) {
- /// Check the role hasn't any custom name in context
- if (!$DB->record_exists('role_names', array('roleid'=>$newrole->new_id, 'contextid'=>$coursecontext->id))) {
- $rolename = new object();
- $rolename->roleid = $newrole->new_id;
- $rolename->contextid = $coursecontext->id;
- $rolename->name = $roledata->nameincourse;
+ $newrole = backup_getid($restore->backup_unique_code, 'role', $oldroleid); /// Look for target role
+ $coursecontext = get_context_instance(CONTEXT_COURSE, $restore->course_id); /// Look for target context
+ if (!empty($newrole->new_id) && !empty($coursecontext) && !empty($roledata->nameincourse)) {
+ /// Check the role hasn't any custom name in context
+ if (!$DB->record_exists('role_names', array('roleid'=>$newrole->new_id, 'contextid'=>$coursecontext->id))) {
+ $rolename = new object();
+ $rolename->roleid = $newrole->new_id;
+ $rolename->contextid = $coursecontext->id;
+ $rolename->name = $roledata->nameincourse;
- $DB->insert_record('role_names', $rolename);
- }
+ $DB->insert_record('role_names', $rolename);
}
}
}
}
+ }
return true;
}
Index: backup/backuplib.php
===================================================================
RCS file: /cvsroot/moodle/moodle/backup/backuplib.php,v
retrieving revision 1.215
diff -u -r1.215 backuplib.php
--- backup/backuplib.php 2 Jul 2008 22:47:57 -0000 1.215
+++ backup/backuplib.php 7 Jul 2008 13:21:45 -0000
@@ -716,7 +716,8 @@
fwrite ($bf,full_tag("ENROLSTARTDATE",3,false,$course->enrolstartdate));
fwrite ($bf,full_tag("ENROLENDDATE",3,false,$course->enrolenddate));
fwrite ($bf,full_tag("ENROLPERIOD",3,false,$course->enrolperiod));
-
+ fwrite ($bf,full_tag("ENABLECOMPLETION",3,false,$course->enablecompletion));
+
/// write local course overrides here?
write_role_overrides_xml($bf, $context, 3);
/// write role_assign code here
@@ -1162,6 +1163,11 @@
$status = true;
$first_record = true;
+
+ $course=$DB->get_record('course',array('id'=>$preferences->backup_course));
+ if(!$course) {
+ return false;
+ }
//Now print the mods in section
//Extracts mod id from sequence
@@ -1221,11 +1227,36 @@
fwrite ($bf,full_tag("GROUPINGID",6,false,$course_module->groupingid));
fwrite ($bf,full_tag("GROUPMEMBERSONLY",6,false,$course_module->groupmembersonly));
fwrite ($bf,full_tag("IDNUMBER",6,false,$course_module->idnumber));
+ fwrite ($bf,full_tag("COMPLETION",6,false,$course_module->completion));
+ fwrite ($bf,full_tag("COMPLETIONGRADEITEMNUMBER",6,false,$course_module->completiongradeitemnumber));
+ fwrite ($bf,full_tag("COMPLETIONVIEW",6,false,$course_module->completionview));
+ fwrite ($bf,full_tag("COMPLETIONEXPECTED",6,false,$course_module->completionexpected));
// get all the role_capabilities overrides in this mod
write_role_overrides_xml($bf, $context, 6);
/// write role_assign code here
- write_role_assignments_xml($bf, $preferences, $context, 6);
- /// write role_assign code here
+ write_role_assignments_xml($bf, $preferences, $context, 6);
+ // write completion data if enabled and user data enabled
+ require_once($CFG->libdir.'/completionlib.php');
+ $completion=new completion_info($course);
+ if($completion->is_enabled($course_module) &&
+ backup_userdata_selected($preferences,$moduletype,$course_module->instance)) {
+ fwrite ($bf,start_tag("COMPLETIONDATA",6,true));
+
+ // Get all completion records for this module and loop
+ $data=$DB->get_records('course_modules_completion',array('coursemoduleid'=>$course_module->id));
+ $data=$data ? $data : array();
+ foreach($data as $completion) {
+ // Write completion record
+ fwrite ($bf,start_tag("COMPLETION",7,true));
+ fwrite ($bf,full_tag("USERID",8,false,$completion->userid));
+ fwrite ($bf,full_tag("COMPLETIONSTATE",8,false,$completion->completionstate));
+ fwrite ($bf,full_tag("VIEWED",8,false,$completion->viewed));
+ fwrite ($bf,full_tag("TIMEMODIFIED",8,false,$completion->timemodified));
+ fwrite ($bf,end_tag("COMPLETION",7,true));
+ }
+
+ fwrite ($bf,end_tag("COMPLETIONDATA",6,true));
+ }
fwrite ($bf,end_tag("MOD",5,true));
}
@@ -1240,7 +1271,7 @@
return $status;
}
-
+
//Print users to xml
//Only users previously calculated in backup_ids will output
//
Index: lang/en_utf8/forum.php
===================================================================
RCS file: /cvsroot/moodle/moodle/lang/en_utf8/forum.php,v
retrieving revision 1.36
diff -u -r1.36 forum.php
--- lang/en_utf8/forum.php 29 May 2008 02:23:08 -0000 1.36
+++ lang/en_utf8/forum.php 7 Jul 2008 13:21:48 -0000
@@ -35,6 +35,15 @@
$string['cannotupdaterate'] = 'Could not update an old rating ($a[0] = $a[1])';
$string['cannotinsertrate'] = 'Could not insert a new rating ($a[0] = $a[1])';
$string['cleanreadtime'] = 'Mark old posts as read hour';
+$string['completiondiscussions'] = 'User must create discussions:';
+$string['completiondiscussionshelp'] = 'requiring discussions to complete';
+$string['completiondiscussionsgroup'] = 'Require discussions';
+$string['completionposts'] = 'User must post discussions or replies:';
+$string['completionpostshelp'] = 'requiring discussions or replies to complete';
+$string['completionpostsgroup'] = 'Require posts';
+$string['completionreplies'] = 'User must post replies:';
+$string['completionreplieshelp'] = 'requiring replies to complete';
+$string['completionrepliesgroup'] = 'Require replies';
$string['configcleanreadtime'] = 'The hour of the day to clean old posts from the \'read\' table.';
$string['configdisplaymode'] = 'The default display mode for discussions if one isn\'t set.';
$string['configenablerssfeeds'] = 'This switch will enable the possibility of RSS feeds for all forums. You will still need to turn feeds on manually in the settings for each forum.';
Index: version.php
===================================================================
RCS file: /cvsroot/moodle/moodle/version.php,v
retrieving revision 1.709
diff -u -r1.709 version.php
--- version.php 7 Jul 2008 01:33:11 -0000 1.709
+++ version.php 7 Jul 2008 13:21:44 -0000
@@ -6,7 +6,7 @@
// This is compared against the values stored in the database to determine
// whether upgrades should be performed (see lib/db/*.php)
- $version = 2008070700; // YYYYMMDD = date of the last version bump
+ $version = 2008070702; // YYYYMMDD = date of the last version bump
// XX = daily increments
$release = '2.0 dev (Build: 20080707)'; // Human-friendly version name
Index: admin/settings/misc.php
===================================================================
RCS file: /cvsroot/moodle/moodle/admin/settings/misc.php,v
retrieving revision 1.19
diff -u -r1.19 misc.php
--- admin/settings/misc.php 25 May 2008 11:37:45 -0000 1.19
+++ admin/settings/misc.php 7 Jul 2008 13:21:44 -0000
@@ -13,6 +13,11 @@
$temp->add($item);
$temp->add(new admin_setting_configcheckbox('enablegroupings', get_string('enablegroupings', 'admin'), get_string('configenablegroupings', 'admin'), 0));
+ // Completion system
+ require_once($CFG->libdir.'/completionlib.php');
+ $temp->add(new admin_setting_configcheckbox('enablecompletion',get_string('enablecompletion','completion'),get_string('configenablecompletion','completion'),COMPLETION_ENABLED));
+ $temp->add(new admin_setting_pickroles('progresstrackedroles',get_string('progresstrackedroles','completion'),get_string('configprogresstrackedroles','completion')));
+
$ADMIN->add('misc', $temp);
// XMLDB editor
Index: lib/simpletest/testcompletionlib.php
===================================================================
RCS file: lib/simpletest/testcompletionlib.php
diff -N lib/simpletest/testcompletionlib.php
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ lib/simpletest/testcompletionlib.php 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,676 @@
+libdir.'/completionlib.php');
+
+global $DB;
+Mock::generate(get_class($DB), 'mock_database');
+
+Mock::generatePartial('completion_info','completion_cutdown',
+ array('delete_all_state','internal_get_tracked_users','update_state',
+ 'internal_get_grade_state','is_enabled','get_data','internal_get_state','internal_set_data'));
+Mock::generatePartial('completion_info','completion_cutdown2',
+ array('is_enabled','get_data','internal_get_state','internal_set_data'));
+Mock::generatePartial('completion_info','completion_cutdown3',
+ array('internal_get_grade_state'));
+
+class fake_recordset implements Iterator {
+ var $closed;
+ var $values,$index;
+
+ function fake_recordset($values) {
+ $this->values=$values;
+ $this->index=0;
+ }
+
+ function current() {
+ return $this->values[$this->index];
+ }
+
+ function key() {
+ return $this->values[$this->index];
+ }
+
+ function next() {
+ $this->index++;
+ }
+
+ function rewind() {
+ $this->index=0;
+ }
+
+ function valid() {
+ return count($this->values) > $this->index;
+ }
+
+ function close() {
+ $closed=true;
+ }
+
+ function was_closed() {
+ return $closed;
+ }
+}
+
+/**
+ * Expectation that checks an object for given values (normal equality test)
+ * plus a 'timemodified' field that is current (last second or two).
+ */
+class TimeModifiedExpectation extends SimpleExpectation {
+ private $otherfields;
+
+ /**
+ * @param array $otherfields Array key=>value of required object fields
+ */
+ function TimeModifiedExpectation($otherfields) {
+ $this->otherfields=$otherfields;
+ }
+
+ function test($thing) {
+ $thingfields=(array)$thing;
+ foreach($this->otherfields as $key=>$value) {
+ if(!array_key_exists($key,$thingfields)) {
+ return false;
+ }
+ if($thingfields[$key]!=$value) {
+ return false;
+ }
+ }
+
+ $timedifference=time()-$thing->timemodified;
+ return ($timedifference < 2 && $timedifference>=0);
+ }
+
+ function testMessage($thing) {
+ return "Object does not match fields/time requirement";
+ }
+}
+
+class completionlib_test extends UnitTestCase {
+ var $realdb,$realcfg,$realsession,$realuser;
+
+ function setUp() {
+ global $DB,$CFG,$SESSION,$USER;
+ $this->realdb=$DB;
+ $this->realcfg=$CFG;
+ $this->realuser=$USER;
+ $DB=new mock_database();
+ $CFG=clone($this->realcfg);
+ $CFG->prefix='test_';
+ $CFG->enablecompletion=COMPLETION_ENABLED;
+ $SESSION=new stdClass();
+ $USER=(object)array('id'=>314159);
+ }
+
+ function tearDown() {
+ global $DB,$CFG,$SESSION,$USER;
+ $DB=$this->realdb;
+ $CFG=$this->realcfg;
+ $SESSION=$this->realsession;
+ $USER=$this->realuser;
+ }
+
+ function test_is_enabled() {
+ global $CFG;
+
+ // Config alone
+ $CFG->enablecompletion=COMPLETION_DISABLED;
+ $this->assertEqual(COMPLETION_DISABLED,completion_info::is_enabled_for_site());
+ $CFG->enablecompletion=COMPLETION_ENABLED;
+ $this->assertEqual(COMPLETION_ENABLED,completion_info::is_enabled_for_site());
+
+ // Course
+ $course=new stdClass;
+ $c=new completion_info($course);
+ $course->enablecompletion=COMPLETION_DISABLED;
+ $this->assertEqual(COMPLETION_DISABLED,$c->is_enabled());
+ $course->enablecompletion=COMPLETION_ENABLED;
+ $this->assertEqual(COMPLETION_ENABLED,$c->is_enabled());
+ $CFG->enablecompletion=COMPLETION_DISABLED;
+ $this->assertEqual(COMPLETION_DISABLED,$c->is_enabled());
+
+ // Course and CM
+ $cm=new stdClass;
+ $cm->completion=COMPLETION_TRACKING_MANUAL;
+ $this->assertEqual(COMPLETION_DISABLED,$c->is_enabled($cm));
+ $CFG->enablecompletion=COMPLETION_ENABLED;
+ $course->enablecompletion=COMPLETION_DISABLED;
+ $this->assertEqual(COMPLETION_DISABLED,$c->is_enabled($cm));
+ $course->enablecompletion=COMPLETION_ENABLED;
+ $this->assertEqual(COMPLETION_TRACKING_MANUAL,$c->is_enabled($cm));
+ $cm->completion=COMPLETION_TRACKING_NONE;
+ $this->assertEqual(COMPLETION_TRACKING_NONE,$c->is_enabled($cm));
+ $cm->completion=COMPLETION_TRACKING_AUTOMATIC;
+ $this->assertEqual(COMPLETION_TRACKING_AUTOMATIC,$c->is_enabled($cm));
+ }
+
+ function test_update_state() {
+ $c=new completion_cutdown2();
+ $c->completion_info((object)array('id'=>42));
+ $cm=(object)array('id'=>13,'course'=>42);
+
+ // Not enabled, should do nothing
+ $c->expectAt(0,'is_enabled',array($cm));
+ $c->setReturnValueAt(0,'is_enabled',false);
+ $c->update_state($cm);
+
+ // Enabled, but current state is same as possible result, do nothing
+ $current=(object)array('completionstate'=>COMPLETION_COMPLETE);
+ $c->expectAt(1,'is_enabled',array($cm));
+ $c->setReturnValueAt(1,'is_enabled',true);
+
+ $c->expectAt(0,'get_data',array($cm,false,0));
+ $c->setReturnValueAt(0,'get_data',$current);
+ $c->update_state($cm,COMPLETION_COMPLETE);
+
+ // Enabled, but current state is a specific one and new state is just
+ // omplete, so do nothing
+ $current->completionstate=COMPLETION_COMPLETE_PASS;
+ $c->expectAt(2,'is_enabled',array($cm));
+ $c->setReturnValueAt(2,'is_enabled',true);
+ $c->expectAt(1,'get_data',array($cm,false,0));
+ $c->setReturnValueAt(1,'get_data',$current);
+ $c->update_state($cm,COMPLETION_COMPLETE);
+
+ // Manual, change state (no change)
+ $cm->completion=COMPLETION_TRACKING_MANUAL;
+ $current->completionstate=COMPLETION_COMPLETE;
+ $c->expectAt(3,'is_enabled',array($cm));
+ $c->setReturnValueAt(3,'is_enabled',true);
+ $c->expectAt(2,'get_data',array($cm,false,0));
+ $c->setReturnValueAt(2,'get_data',$current);
+ $c->update_state($cm,COMPLETION_COMPLETE);
+
+ // Manual, change state (change)
+ $c->expectAt(4,'is_enabled',array($cm));
+ $c->setReturnValueAt(4,'is_enabled',true);
+ $c->expectAt(3,'get_data',array($cm,false,0));
+ $c->setReturnValueAt(3,'get_data',$current);
+ $c->expectAt(0,'internal_set_data',array($cm,
+ new TimeModifiedExpectation(array('completionstate'=>COMPLETION_INCOMPLETE))));
+ $c->update_state($cm,COMPLETION_INCOMPLETE);
+
+ // Auto, change state
+ $cm->completion=COMPLETION_TRACKING_AUTOMATIC;
+ $c->expectAt(5,'is_enabled',array($cm));
+ $c->setReturnValueAt(5,'is_enabled',true);
+ $c->expectAt(4,'get_data',array($cm,false,0));
+ $c->setReturnValueAt(4,'get_data',$current);
+ $c->expectAt(0,'internal_get_state',array($cm,0,$current));
+ $c->setReturnValueAt(0,'internal_get_state',COMPLETION_COMPLETE_PASS);
+ $c->expectAt(1,'internal_set_data',array($cm,
+ new TimeModifiedExpectation(array('completionstate'=>COMPLETION_COMPLETE_PASS))));
+ $c->update_state($cm,COMPLETION_COMPLETE_PASS);
+
+ $c->tally();
+ }
+
+ function test_internal_get_state() {
+ global $DB;
+
+ $c=new completion_cutdown3();
+ $c->completion_info((object)array('id'=>42));
+ $cm=(object)array('id'=>13,'course'=>42,'completiongradeitemnumber'=>null);
+
+ // If view is required, but they haven't viewed it yet
+ $cm->completionview=COMPLETION_VIEW_REQUIRED;
+ $current=(object)array('viewed'=>COMPLETION_NOT_VIEWED);
+ $this->assertEqual(COMPLETION_INCOMPLETE,$c->internal_get_state($cm,123,$current));
+
+ // OK set view not required
+ $cm->completionview=COMPLETION_VIEW_NOT_REQUIRED;
+
+ // Test not getting module name
+ $cm->modname='label';
+ $this->assertEqual(COMPLETION_COMPLETE,$c->internal_get_state($cm,123,$current));
+
+ // Test getting module name
+ $cm->module=13;
+ unset($cm->modname);
+ $DB->expectOnce('get_field',array('modules','name',array('id'=>13)));
+ $DB->setReturnValue('get_field','label');
+ $this->assertEqual(COMPLETION_COMPLETE,$c->internal_get_state($cm,123,$current));
+
+ // Note: This function is not fully tested (including kind of the main
+ // part) because:
+ // * the grade_item/grade_grade calls are static and can't be mocked
+ // * the plugin_supports call is static and can't be mocked
+
+ $DB->tally();
+ $c->tally();
+ }
+
+ function test_set_module_viewed() {
+ $c=new completion_cutdown();
+ $c->completion_info((object)array('id'=>42));
+ $cm=(object)array('id'=>13,'course'=>42);
+
+ // Not tracking completion, should do nothing
+ $cm->completionview=COMPLETION_VIEW_NOT_REQUIRED;
+ $c->set_module_viewed($cm);
+
+ // Tracking completion but completion is disabled, should do nothing
+ $cm->completionview=COMPLETION_VIEW_REQUIRED;
+ $c->expectAt(0,'is_enabled',array($cm));
+ $c->setReturnValueAt(0,'is_enabled',false);
+ $c->set_module_viewed($cm);
+
+ // Now it's enabled, we expect it to get data. If data already has
+ // viewed, still do nothing
+ $c->expectAt(1,'is_enabled',array($cm));
+ $c->setReturnValueAt(1,'is_enabled',true);
+ $c->expectAt(0,'get_data',array($cm,0));
+ $hasviewed=(object)array('viewed'=>COMPLETION_VIEWED);
+ $c->setReturnValueAt(0,'get_data',$hasviewed);
+ $c->set_module_viewed($cm);
+
+ // OK finally one that hasn't been viewed, now it should set it viewed
+ // and update state
+ $c->expectAt(2,'is_enabled',array($cm));
+ $c->setReturnValueAt(2,'is_enabled',true);
+ $notviewed=(object)array('viewed'=>COMPLETION_NOT_VIEWED);
+ $c->expectAt(1,'get_data',array($cm,1337));
+ $c->setReturnValueAt(1,'get_data',$notviewed);
+ $c->expectOnce('internal_set_data',array($cm,$hasviewed));
+ $c->expectOnce('update_state',array($cm,COMPLETION_COMPLETE,1337));
+ $c->set_module_viewed($cm,1337);
+
+ $c->tally();
+ }
+
+ function test_count_user_data() {
+ global $DB;
+ $cm=(object)array('id'=>42);
+ $DB->setReturnValue('get_field_sql',666);
+ $DB->expectOnce('get_field_sql',array(new IgnoreWhitespaceExpectation("SELECT
+ COUNT(1)
+FROM
+ test_course_modules_completion
+WHERE
+ coursemoduleid=? AND completionstate<>0"),array(42)));
+ $c=new completion_info(null);
+ $this->assertEqual(666,$c->count_user_data($cm));
+
+ $DB->tally();
+ }
+
+ function test_delete_all_state() {
+ global $DB,$SESSION;
+ $course=(object)array('id'=>13);
+ $cm=(object)array('id'=>42,'course'=>13);
+ $c=new completion_info($course);
+ // Check it works ok without data in session
+ $DB->expectAt(0,'delete_records',
+ array('course_modules_completion',array('coursemoduleid'=>42)));
+ $c->delete_all_state($cm);
+
+ // Build up a session to check it deletes the right bits from it
+ // (and not other bits)
+ $SESSION->completioncache=array();
+ $SESSION->completioncache[13]=array();
+ $SESSION->completioncache[13][42]='foo';
+ $SESSION->completioncache[13][43]='foo';
+ $SESSION->completioncache[14]=array();
+ $SESSION->completioncache[14][42]='foo';
+ $DB->expectAt(1,'delete_records',
+ array('course_modules_completion',array('coursemoduleid'=>42)));
+ $c->delete_all_state($cm);
+ $this->assertEqual(array(13=>array(43=>'foo'),14=>array(42=>'foo')),
+ $SESSION->completioncache);
+
+ $DB->tally();
+ }
+
+ function test_reset_all_state() {
+ global $DB;
+ $c=new completion_cutdown();
+ $c->completion_info((object)array('id'=>42));
+
+ $cm=(object)array('id'=>13,'course'=>42);
+
+ $DB->setReturnValue('get_recordset',new fake_recordset(array(
+ (object)array('id'=>1,'userid'=>100),
+ (object)array('id'=>2,'userid'=>101),
+ )));
+ $DB->expectOnce('get_recordset',array('course_modules_completion',
+ array('coursemoduleid'=>13),'','userid'));
+ $c->expectOnce('delete_all_state',array($cm));
+ $c->expectOnce('internal_get_tracked_users',array(false));
+ $c->setReturnValue('internal_get_tracked_users',array(
+ (object)array('id'=>100,'firstname'=>'Woot','lastname'=>'Plugh'),
+ (object)array('id'=>201,'firstname'=>'Vroom','lastname'=>'Xyzzy'),
+ ));
+
+ $c->expectAt(0,'update_state',array($cm,COMPLETION_UNKNOWN,100));
+ $c->expectAt(1,'update_state',array($cm,COMPLETION_UNKNOWN,101));
+ $c->expectAt(2,'update_state',array($cm,COMPLETION_UNKNOWN,201));
+
+ $c->reset_all_state($cm);
+
+ $DB->tally();
+ $c->tally();
+ }
+
+ function test_get_data() {
+ global $DB,$SESSION;
+
+ $c=new completion_info((object)array('id'=>42));
+ $cm=(object)array('id'=>13,'course'=>42);
+
+ // 1. Not current user, record exists
+ $sillyrecord=(object)array('frog'=>'kermit');
+ $DB->expectAt(0,'get_record',array('course_modules_completion',
+ array('coursemoduleid'=>13,'userid'=>123)));
+ $DB->setReturnValueAt(0,'get_record',$sillyrecord);
+ $result=$c->get_data($cm,false,123);
+ $this->assertEqual($sillyrecord,$result);
+ $this->assertTrue(empty($SESSION->completioncache));
+
+ // 2. Not current user, default record, wholecourse (ignored)
+ $DB->expectAt(1,'get_record',array('course_modules_completion',
+ array('coursemoduleid'=>13,'userid'=>123)));
+ $DB->setReturnValueAt(1,'get_record',false);
+ $result=$c->get_data($cm,true,123);
+ $this->assertEqual((object)array(
+ 'id'=>'0','coursemoduleid'=>13,'userid'=>123,'completionstate'=>0,
+ 'viewed'=>0,'timemodified'=>0),$result);
+ $this->assertTrue(empty($SESSION->completioncache));
+
+ // 3. Current user, single record, not from cache
+ $DB->expectAt(2,'get_record',array('course_modules_completion',
+ array('coursemoduleid'=>13,'userid'=>314159)));
+ $DB->setReturnValueAt(2,'get_record',$sillyrecord);
+ $result=$c->get_data($cm);
+ $this->assertEqual($sillyrecord,$result);
+ $this->assertEqual($sillyrecord,$SESSION->completioncache[42][13]);
+ // When checking time(), allow for second overlaps
+ $this->assertTrue(time()-$SESSION->completioncache[42]['updated']<2);
+
+ // 4. Current user, 'whole course', but from cache
+ $result=$c->get_data($cm,true);
+ $this->assertEqual($sillyrecord,$result);
+
+ // 5. Current user, single record, cache expired
+ $SESSION->completioncache[42]['updated']=37; // Quite a long time ago
+ $now=time();
+ $SESSION->completioncache[17]['updated']=$now;
+ $SESSION->completioncache[39]['updated']=72; // Also a long time ago
+ $DB->expectAt(3,'get_record',array('course_modules_completion',
+ array('coursemoduleid'=>13,'userid'=>314159)));
+ $DB->setReturnValueAt(3,'get_record',$sillyrecord);
+ $result=$c->get_data($cm,false);
+ $this->assertEqual($sillyrecord,$result);
+ // Check that updated value is right, then fudge it to make next compare
+ // work
+ $this->assertTrue(time()-$SESSION->completioncache[42]['updated']<2);
+ $SESSION->completioncache[42]['updated']=$now;
+ // Check things got expired from cache
+ $this->assertEqual(array(42=>array(13=>$sillyrecord,'updated'=>$now),
+ 17=>array('updated'=>$now)),$SESSION->completioncache);
+
+ // 6. Current user, 'whole course' and record not in cache
+ unset($SESSION->completioncache);
+
+ // Scenario: Completion data exists for one CMid
+ $basicrecord=(object)array('coursemoduleid'=>13);
+ $DB->setReturnValueAt(0,'get_records_sql',array(
+ 1=>$basicrecord
+ ));
+ $DB->expectAt(0,'get_records_sql',array(new IgnoreWhitespaceExpectation("
+SELECT
+ cmc.*
+FROM
+ test_course_modules cm
+ INNER JOIN test_course_modules_completion cmc ON cmc.coursemoduleid=cm.id
+WHERE
+ cm.course=? AND cmc.userid=?"),array(42,314159)));
+
+ // There are two CMids in total, the one we had data for and another one
+ $modinfo->cms=array((object)array('id'=>13),(object)array('id'=>14));
+ $result=$c->get_data($cm,true,0,$modinfo);
+
+ // Check result
+ $this->assertEqual($basicrecord,$result);
+
+ // Check the cache contents
+ $this->assertTrue(time()-$SESSION->completioncache[42]['updated']<2);
+ $SESSION->completioncache[42]['updated']=$now;
+ $this->assertEqual(array(42=>array(13=>$basicrecord,14=>(object)array(
+ 'id'=>'0','coursemoduleid'=>14,'userid'=>314159,'completionstate'=>0,
+ 'viewed'=>0,'timemodified'=>0),'updated'=>$now)),$SESSION->completioncache);
+
+ $DB->tally();
+ }
+
+ function test_internal_set_data() {
+ global $DB,$SESSION;
+
+ $cm=(object)array('course'=>42,'id'=>13);
+ $c=new completion_info((object)array('id'=>42));
+
+ // 1) Test with new data
+ $data=(object)array('id'=>0,'userid'=>314159);
+ $DB->setReturnValueAt(0,'insert_record',4);
+ $DB->expectAt(0,'insert_record',array('course_modules_completion',$data));
+ $c->internal_set_data($cm,$data);
+ $this->assertEqual(4,$data->id);
+ $this->assertEqual(array(42=>array(13=>$data)),$SESSION->completioncache);
+
+ // 2) Test with existing data and for different user (not cached)
+ unset($SESSION->completioncache);
+ $d2=(object)array('id'=>7,'userid'=>17);
+ $DB->expectAt(0,'update_record',array('course_modules_completion',$d2));
+ $c->internal_set_data($cm,$d2);
+ $this->assertFalse(isset($SESSION->completioncache));
+
+ $DB->tally();
+ }
+
+ function test_get_activities() {
+ global $DB;
+
+ $c=new completion_info((object)array('id'=>42));
+
+ // Try with no activities
+ $DB->expectAt(0,'get_records_select',array('course_modules',
+ 'course=42 AND completion<>'.COMPLETION_TRACKING_NONE));
+ $DB->setReturnValueAt(0,'get_records_select',array());
+ $result=$c->get_activities();
+ $this->assertEqual(array(),$result);
+
+ // Try with an activity (need to fake up modinfo for it as well)
+ $DB->expectAt(1,'get_records_select',array('course_modules',
+ 'course=42 AND completion<>'.COMPLETION_TRACKING_NONE));
+ $DB->setReturnValueAt(1,'get_records_select',array(
+ 13=>(object)array('id'=>13)
+ ));
+ $modinfo=new stdClass;
+ $modinfo->sections=array(array(1,2,3),array(12,13,14));
+ $modinfo->cms[13]=(object)array('modname'=>'frog','name'=>'kermit');
+ $result=$c->get_activities($modinfo);
+ $this->assertEqual(array(13=>(object)array('id'=>13,'modname'=>'frog','name'=>'kermit')),$result);
+
+ $DB->tally();
+ }
+
+ // internal_get_tracked_users() cannot easily be tested because it uses
+ // get_role_users, so skipping that
+
+ function test_get_progress_all() {
+ global $DB;
+
+ $c=new completion_cutdown();
+ $c->completion_info((object)array('id'=>42));
+
+ // 1) Basic usage
+ $c->expectAt(0,'internal_get_tracked_users',array(false,0));
+ $c->setReturnValueAt(0,'internal_get_tracked_users',array(
+ (object)array('id'=>100,'firstname'=>'Woot','lastname'=>'Plugh'),
+ (object)array('id'=>201,'firstname'=>'Vroom','lastname'=>'Xyzzy'),
+ ));
+ $DB->expectAt(0,'get_in_or_equal',array(array(100,201)));
+ $DB->setReturnValueAt(0,'get_in_or_equal',array(' IN (100,201)',array()));
+ $DB->expectAt(0,'get_recordset_sql',array(new IgnoreWhitespaceExpectation("
+SELECT
+ cmc.*
+FROM
+ test_course_modules cm
+ INNER JOIN test_course_modules_completion cmc ON cm.id=cmc.coursemoduleid
+WHERE
+ cm.course=? AND cmc.userid IN (100,201)"),array(42)));
+ $progress1=(object)array('userid'=>100,'coursemoduleid'=>13);
+ $progress2=(object)array('userid'=>201,'coursemoduleid'=>14);
+ $DB->setReturnValueAt(0,'get_recordset_sql',new fake_recordset(array(
+ $progress1,$progress2
+ )));
+
+ $this->assertEqual(array(
+ 100 => (object)array('id'=>100,'firstname'=>'Woot','lastname'=>'Plugh',
+ 'progress'=>array(13=>$progress1)),
+ 201 => (object)array('id'=>201,'firstname'=>'Vroom','lastname'=>'Xyzzy',
+ 'progress'=>array(14=>$progress2)),
+ ),$c->get_progress_all(false));
+
+ // 2) With more than 1,000 results
+ $c->expectAt(1,'internal_get_tracked_users',array(true,3));
+
+ $tracked=array();
+ $ids=array();
+ $progress=array();
+ for($i=100;$i<2000;$i++) {
+ $tracked[]=(object)array('id'=>$i,'firstname'=>'frog','lastname'=>$i);
+ $ids[]=$i;
+ $progress[]=(object)array('userid'=>$i,'coursemoduleid'=>13);
+ $progress[]=(object)array('userid'=>$i,'coursemoduleid'=>14);
+ }
+ $c->setReturnValueAt(1,'internal_get_tracked_users',$tracked);
+
+ $DB->expectAt(1,'get_in_or_equal',array(array_slice($ids,0,1000)));
+ $DB->setReturnValueAt(1,'get_in_or_equal',array(' IN whatever',array()));
+ $DB->expectAt(1,'get_recordset_sql',array(new IgnoreWhitespaceExpectation("
+SELECT
+ cmc.*
+FROM
+ test_course_modules cm
+ INNER JOIN test_course_modules_completion cmc ON cm.id=cmc.coursemoduleid
+WHERE
+ cm.course=? AND cmc.userid IN whatever"),array(42)));
+ $DB->setReturnValueAt(1,'get_recordset_sql',new fake_recordset(array_slice($progress,0,1000)));
+ $DB->expectAt(2,'get_in_or_equal',array(array_slice($ids,1000)));
+ $DB->setReturnValueAt(2,'get_in_or_equal',array(' IN whatever2',array()));
+ $DB->expectAt(2,'get_recordset_sql',array(new IgnoreWhitespaceExpectation("
+SELECT
+ cmc.*
+FROM
+ test_course_modules cm
+ INNER JOIN test_course_modules_completion cmc ON cm.id=cmc.coursemoduleid
+WHERE
+ cm.course=? AND cmc.userid IN whatever2"),array(42)));
+ $DB->setReturnValueAt(2,'get_recordset_sql',new fake_recordset(array_slice($progress,1000)));
+
+ $result=$c->get_progress_all(true,3);
+
+ $resultok=true;
+ $resultok = $resultok && ($ids==array_keys($result));
+ foreach($result as $userid => $data) {
+ $resultok = $resultok && $data->firstname=='frog';
+ $resultok = $resultok && $data->lastname==$userid;
+ $resultok = $resultok && $data->id==$userid;
+ $cms=$data->progress;
+ $resultok= $resultok && (array(13,14)==array_keys($cms));
+ $resultok= $resultok && ((object)array('userid'=>$userid,'coursemoduleid'=>13)==$cms[13]);
+ $resultok= $resultok && ((object)array('userid'=>$userid,'coursemoduleid'=>14)==$cms[14]);
+ }
+ $this->assertTrue($resultok);
+
+ $DB->tally();
+ $c->tally();
+ }
+
+ function test_inform_grade_changed() {
+ $c=new completion_cutdown();
+ $c->completion_info((object)array('id'=>42));
+
+ $cm=(object)array('course'=>42,'id'=>13,'completiongradeitemnumber'=>null);
+ $item=(object)array('itemnumber'=>3);
+ $grade=(object)array('userid'=>31337);
+
+ // Not enabled (should do nothing)
+ $c->setReturnValueAt(0,'is_enabled',false);
+ $c->expectAt(0,'is_enabled',array($cm));
+ $c->inform_grade_changed($cm,$item,$grade,false);
+
+ // Enabled but still no grade completion required, should still do nothing
+ $c->setReturnValueAt(1,'is_enabled',true);
+ $c->expectAt(1,'is_enabled',array($cm));
+ $c->inform_grade_changed($cm,$item,$grade,false);
+
+ // Enabled and completion required but item number is wrong, does nothing
+ $cm->completiongradeitemnumber=7;
+ $c->setReturnValueAt(2,'is_enabled',true);
+ $c->expectAt(2,'is_enabled',array($cm));
+ $c->inform_grade_changed($cm,$item,$grade,false);
+
+ // Enabled and completion required and item number right. It is supposed
+ // to call update_state with the new potential state being obtained from
+ // internal_get_grade_state.
+ $cm->completiongradeitemnumber=3;
+ $c->setReturnValueAt(3,'is_enabled',true);
+ $c->expectAt(3,'is_enabled',array($cm));
+ $c->expectAt(0,'internal_get_grade_state',array($item,$grade));
+ $c->setReturnValueAt(0,'internal_get_grade_state',COMPLETION_COMPLETE_PASS);
+ $c->expectAt(0,'update_state',array($cm,COMPLETION_COMPLETE_PASS,31337));
+ $c->inform_grade_changed($cm,$item,$grade,false);
+
+ // Same as above but marked deleted. It is supposed to call update_state
+ // with new potential state being COMPLETION_INCOMPLETE
+ $c->setReturnValueAt(4,'is_enabled',false);
+ $c->expectAt(4,'is_enabled',array($cm));
+ $c->expectAt(1,'update_state',array($cm,COMPLETION_INCOMPLETE,31337));
+ $c->inform_grade_changed($cm,$item,$grade,false);
+
+ $c->tally();
+ }
+
+ function test_internal_get_grade_state() {
+ $item=new stdClass;
+ $grade=new stdClass;
+
+ $item->gradepass=4;
+ $item->hidden=0;
+ $grade->rawgrade=4.0;
+ $grade->finalgrade=null;
+
+ // Grade has pass mark and is not hidden, user passes
+ $this->assertEqual(
+ COMPLETION_COMPLETE_PASS,
+ completion_info::internal_get_grade_state($item,$grade));
+
+ // Same but user fails
+ $grade->rawgrade=3.9;
+ $this->assertEqual(
+ COMPLETION_COMPLETE_FAIL,
+ completion_info::internal_get_grade_state($item,$grade));
+
+ // User fails on raw grade but passes on final
+ $grade->finalgrade=4.0;
+ $this->assertEqual(
+ COMPLETION_COMPLETE_PASS,
+ completion_info::internal_get_grade_state($item,$grade));
+
+ // Item is hidden
+ $item->hidden=1;
+ $this->assertEqual(
+ COMPLETION_COMPLETE,
+ completion_info::internal_get_grade_state($item,$grade));
+
+ // Item isn't hidden but has no pass mark
+ $item->hidden=0;
+ $item->gradepass=0;
+ $this->assertEqual(
+ COMPLETION_COMPLETE,
+ completion_info::internal_get_grade_state($item,$grade));
+ }
+}
+?>
Index: lang/en_utf8/completion.php
===================================================================
RCS file: lang/en_utf8/completion.php
diff -N lang/en_utf8/completion.php
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ lang/en_utf8/completion.php 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,46 @@
+user, $a->activity: $a->state $a->date';
+$string['progresstrackedroles'] = 'Progress-tracked roles';
+$string['restoringcompletiondata']='Writing completion data';
+$string['saved']='Saved';
+$string['unlockcompletion']='Unlock completion options';
+$string['writingcompletiondata']='Writing completion data';
+?>
Index: stupid.php
===================================================================
RCS file: stupid.php
diff -N stupid.php
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ stupid.php 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,9 @@
+
Index: lib/simpletest/completion.manualtest.txt
===================================================================
RCS file: lib/simpletest/completion.manualtest.txt
diff -N lib/simpletest/completion.manualtest.txt
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ lib/simpletest/completion.manualtest.txt 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,162 @@
+Completion system manual test
+=============================
+
+This text file describes a manual process which can be used to check that the
+completion system is working correctly. This does not exercise every possible
+element of the completion system but it covers most of the basic parts.
+
+Site setup
+----------
+
+1) Make a fresh install of the Moodle version you are testing.
+
+CHECK A: There is no error when installing the completion database tables.
+
+2) Create users 'admin' and 'u1'
+3) Create course 'CF101' (leave default except pick separate groups) and
+ assign 'u1' as student
+
+Course setup
+------------
+
+1) Create a web page resource 'w'
+2) Create a forum 'f1'
+3) Create a forum 'f2'
+4) Create a quiz 'q1' with one question (e.g. true/false question)
+5) Create a quiz 'q2' with one question (can use same question)
+6) In gradebook/reports/categories and items, edit q2's grade (show advanced)
+ to assign a 'grade to pass' of 5.0.
+ [Note: Due to a bug in gradebook at time of writing, you may have to go into
+ admin screens and make sure that 'Student' is included on the list of graded
+ roles, or there will be an error at this step.]
+7) Create 2 groups on the course. Assign u1 to one group.
+
+Completion settings
+-------------------
+
+1) Visit the course setting screen.
+
+CHECK B: The completion controls appear. Completion is enabled.
+
+2) Turn off the setting (disable completion) and save.
+3) Visit the admin page. Find the enablecompletion setting.
+
+CHECK C: The enablecompletion setting appears. Completion is enabled.
+
+4) Turn off this setting and save.
+
+4b) Note: At present I have not found a satisfactory way to set a default
+ for the config option, so if necessary, please manually tick the 'Student'
+ checkbox while on this screen.
+
+5) Visit the course setting screen again.
+
+CHECK D: The completion controls do not appear.
+
+6) Visit the setting screen for 'w'
+
+CHECK E: Completion controls do not appear
+
+7) Go to admin screen and turn completion on again, then return to the 'w' settings
+
+CHECK F: Completion controls still do not appear
+
+8) Go to course settings and turn completion on, then return to 'w' settings
+
+CHECK G: Completion controls appear. Completion is set to manual.
+
+9) Go to 'f1' settings. Set completion to automatic and to 2 discussions/replies.
+10) Go to 'f2' settings. Set completion to automatic and to 'view'
+11) Go to 'q1' and 'q2' settings; set both to automatic and 'grade'. Set them
+ to grade based on the most recent attempt rather than 'highest'.
+
+Completion actions
+------------------
+
+1) Log in as u1 and go to CF101.
+
+CHECK H: A completion tick (unticked) is visible next to 'w'.
+
+2) Click the completion mark a few times.
+
+CHECK I: Completion toggles successfully.
+
+ 2b) Go to u1's profile settings and turn on/off AJAX then repeat toggling the
+ mark. Leave it ticked.
+ CHECK I2: Completion still toggles successfully.
+
+3) Visit 'f1' and post 1 message. Return to course home.
+
+CHECK J: There is no tickmark next to f1. (If examined carefully, the 'not
+ complete' icon should be present.)
+
+4) Visit 'f1' and post a reply to the message. Return to course home.
+
+CHECK K: There is now a tick next to f1.
+
+5) Visit 'f2' and return to the home page.
+
+CHECK L: There is now a tick next to f2.
+
+6) Visit 'q1' and attempt the quiz, getting it wrong and submitting answer.
+ Return to the home page.
+
+CHECK M: There is a black 'completed' tick next to q1.
+
+7) Visit 'q1' again and this time get it right. Return to home page.
+
+CHECK N: There is still a black 'completed' tick next to q1.
+
+8) Visit 'q2' and get it right. Return to home.
+
+CHECK O: There is a green 'completed-passed' tick next to q2.
+
+9) Visit 'q2' and get it wrong. Return to home.
+
+CHECK P: There is a red 'completed-failed' X next to q2.
+
+Completion progress
+-------------------
+
+1) Log in as admin again.
+
+2) From the course admin block, click on the reports link.
+
+CHECK Q: A 'completion progress' link appears.
+
+3) Click on the completion progress link.
+
+CHECK R:
+ A groups dropdown should show the two groups (and 'all').
+ The progress table should include all activities for which completion
+ was set, across the top.
+ The progress table should show u1 down the side.
+ Tick and X icons should match those shown when logged in as u1.
+
+4) Choose a group that does not include u1
+
+CHECK S:
+ An informational ('no users') message should display instead of the progress
+ table.
+
+5) Choose the group that does include u1
+
+CHECK T:
+ The progress table should show u1 again.
+
+Backup/restore
+--------------
+
+1) Backup the course. Choose 'course users', user data for everything except
+ q1 ('no user data'), and default options.
+
+2) Restore to a new course, accepting all defaults.
+
+3) Log in as u1 again and visit the new course.
+
+CHECK U:
+ Completion should appear as it did in the previous version of the course
+ ('w','f1','f2' complete, 'q2' complete-fail) except that q1 should show as
+ incomplete.
+
+
Index: course/report/progress/mod.php
===================================================================
RCS file: course/report/progress/mod.php
diff -N course/report/progress/mod.php
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ course/report/progress/mod.php 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,13 @@
+is_enabled() && has_capability('moodle/course:viewprogress',$context)) {
+ echo '
';
+ echo ''.get_string('completionreport','completion').'';
+ echo '
';
+ }
+?>
Index: course/completion.js
===================================================================
RCS file: course/completion.js
diff -N course/completion.js
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ course/completion.js 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,73 @@
+var completion_strsaved;
+
+function completion_init() {
+ var toggles=YAHOO.util.Dom.getElementsByClassName('togglecompletion', 'form');
+ for(var i=0;i
0.999) {
+ var pos=YAHOO.util.Dom.getXY(form.image);
+ pos[0]+=20; // Icon size + 4px border
+ YAHOO.util.Dom.setStyle(form.saved,'display','block');
+ YAHOO.util.Dom.setXY(form.saved,pos);
+ }
+ setTimeout(function() { completion_update_animation(form,opacity-0.1); },100);
+}
+
+function completion_handle_failure(o) {
+ alert('An error occurred when attempting to connect to our server. The tick mark will not be saved.\n\n('+
+ o.status+' '+o.statusText+')');
+}
+
+function completion_toggle(e) {
+ YAHOO.util.Event.preventDefault(e);
+ YAHOO.util.Connect.asyncRequest('POST','togglecompletion.php',
+ {success:completion_handle_response,failure:completion_handle_failure,scope:this},
+ 'id='+this.cmid+'&completionstate='+this.otherState+'&fromajax=1');
+}
+
+YAHOO.util.Event.onDOMReady(completion_init);
Index: course/togglecompletion.php
===================================================================
RCS file: course/togglecompletion.php
diff -N course/togglecompletion.php
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ course/togglecompletion.php 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,57 @@
+libdir.'/completionlib.php');
+
+// Parameters
+$cmid=required_param('id',PARAM_INT);
+$targetstate=required_param('completionstate',PARAM_INT);
+switch($targetstate) {
+ case COMPLETION_COMPLETE:
+ case COMPLETION_INCOMPLETE:
+ break;
+ default:
+ error('Unsupported completion state');
+}
+$fromajax=optional_param('fromajax',0,PARAM_INT);
+
+function error_or_ajax($message) {
+ global $fromajax;
+ if($fromajax) {
+ print $message;
+ exit;
+ } else {
+ error($message);
+ }
+}
+
+// Get course-modules entry
+if(!($cm=$DB->get_record('course_modules',array('id'=>$cmid)))) {
+ error_or_ajax('Activity ID unknown');
+}
+
+if(!($course=$DB->get_record('course',array('id'=>$cm->course)))) {
+ error_or_ajax('Missing course (database corrupt?)');
+}
+
+// Check user is logged in
+require_login($course);
+
+// Check completion state is manual
+if($cm->completion!=COMPLETION_TRACKING_MANUAL) {
+ error_or_ajax('Activity does not provide manual completion tracking');
+}
+
+// Now change state
+$completion=new completion_info($course);
+$completion->update_state($cm,$targetstate);
+
+// And redirect back to course
+if($fromajax) {
+ print 'OK';
+} else {
+ redirect('view.php?id='.$course->id);
+}
+?>
Index: lib/completionlib.php
===================================================================
RCS file: lib/completionlib.php
diff -N lib/completionlib.php
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ lib/completionlib.php 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,691 @@
+id, ->enablecompletion
+ * @return completion_info
+ */
+ public function completion_info($course) {
+ $this->course=$course;
+ }
+
+ /**
+ * Static function. Determines whether completion is enabled across entire
+ * site.
+ *
+ * @return int COMPLETION_ENABLED (true) if completion is enabled for the site,
+ * COMPLETION_DISABLED (false) if it's complete
+ */
+ public static function is_enabled_for_site() {
+ global $CFG;
+ return $CFG->enablecompletion;
+ }
+
+ /**
+ * Checks whether completion is enabled in a particular course and possibly
+ * activity.
+ *
+ * @param object $cm Course-module object. If not specified, returns the course
+ * completion enable state.
+ * @return COMPLETION_ENABLED or COMPLETION_DISABLED (==0) in the case of
+ * site and course; COMPLETION_TRACKING_MANUAL, _AUTOMATIC or _NONE (==0)
+ * for a course-module.
+ */
+ public function is_enabled($cm=null) {
+ // First check global completion
+ global $CFG;
+ if($CFG->enablecompletion==COMPLETION_DISABLED) {
+ return COMPLETION_DISABLED;
+ }
+
+ // Check course completion
+ if($this->course->enablecompletion==COMPLETION_DISABLED) {
+ return COMPLETION_DISABLED;
+ }
+
+ // If there was no $cm and we got this far, then it's enabled
+ if(!$cm) {
+ return COMPLETION_ENABLED;
+ }
+
+ // Return course-module completion value
+ return $cm->completion;
+ }
+
+ /**
+ * Updates (if necessary) the completion state of activity $cm for the given
+ * user.
+ *
+ * For manual completion, this function is called when completion is toggled
+ * with $possibleresult set to the target state.
+ *
+ * For automatic completion, this function should be called every time a module
+ * does something which might influence a user's completion state. For example,
+ * if a forum provides options for marking itself 'completed' once a user makes
+ * N posts, this function should be called every time a user makes a new post.
+ * [After the post has been saved to the database]. When calling, you do not
+ * need to pass in the new completion state. Instead this function carries out
+ * completion calculation by checking grades and viewed state itself, and
+ * calling the involved module via modulename_get_completion_state() to check
+ * module-specific conditions.
+ *
+ * @param object $cm Course-module
+ * @param int $possibleresult Expected completion result. If the event that
+ * has just occurred (e.g. add post) can only result in making the activity
+ * complete when it wasn't before, use COMPLETION_COMPLETE. If the event that
+ * has just occurred (e.g. delete post) can only result in making the activity
+ * not complete when it was previously complete, use COMPLETION_INCOMPLETE.
+ * Otherwise use COMPLETION_UNKNOWN. Setting this value to something other than
+ * COMPLETION_UNKNOWN significantly improves performance because it will abandon
+ * processing early if the user's completion state already matches the expected
+ * result. For manual events, COMPLETION_COMPLETE or COMPLETION_INCOMPLETE
+ * must be used; these directly set the specified state.
+ * @param int $userid User ID to be updated. Default 0 = current user
+ */
+ public function update_state($cm,$possibleresult=COMPLETION_UNKNOWN,$userid=0) {
+ global $USER,$SESSION;
+ // Do nothing if completion is not enabled for that activity
+ if(!$this->is_enabled($cm)) {
+ return;
+ }
+
+ // Get current value of completion state and do nothing if it's same as
+ // the possible result of this change. If the change is to COMPLETE and the
+ // current value is one of the COMPLETE_xx subtypes, ignore that as well
+ $current=$this->get_data($cm,false,$userid);
+ if($possibleresult==$current->completionstate ||
+ ($possibleresult==COMPLETION_COMPLETE &&
+ ($current->completionstate==COMPLETION_COMPLETE_PASS ||
+ $current->completionstate==COMPLETION_COMPLETE_FAIL))) {
+ return;
+ }
+
+ if($cm->completion==COMPLETION_TRACKING_MANUAL) {
+ // For manual tracking we set the result directly
+ switch($possibleresult) {
+ case COMPLETION_COMPLETE:
+ case COMPLETION_INCOMPLETE:
+ $newstate=$possibleresult;
+ break;
+ default:
+ throw new Exception("Unexpected manual completion state for {$cm->id}: $possibleresult");
+ }
+ } else {
+ // Automatic tracking; get new state
+ $newstate=$this->internal_get_state($cm,$userid,$current);
+ }
+
+ // If changed, update
+ if($newstate!=$current->completionstate) {
+ $current->completionstate=$newstate;
+ $current->timemodified=time();
+ $this->internal_set_data($cm,$current);
+ }
+ }
+
+ /**
+ * Calculates the completion state for an activity and user.
+ *
+ * (Internal function. Not private, so we can unit-test it.)
+ *
+ * @param object $cm Activity
+ * @param int $userid ID of user
+ * @param object $current Previous completion information from database
+ * @return unknown
+ */
+ function internal_get_state($cm,$userid,$current) {
+ // Get user ID
+ global $USER,$DB;
+ if(!$userid) {
+ $userid=$USER->id;
+ }
+
+ // Check viewed
+ if($cm->completionview==COMPLETION_VIEW_REQUIRED &&
+ $current->viewed==COMPLETION_NOT_VIEWED) {
+ return COMPLETION_INCOMPLETE;
+ }
+
+ // Modname hopefully is provided in $cm but just in case it isn't, let's grab it
+ if(!isset($cm->modname)) {
+ $cm->modname=$DB->get_field('modules','name',array('id'=>$cm->module));
+ }
+
+ $newstate=COMPLETION_COMPLETE;
+
+ // Check grade
+ if(!is_null($cm->completiongradeitemnumber)) {
+ $item=grade_item::fetch(array('courseid'=>$cm->course,'itemtype'=>'mod',
+ 'itemmodule'=>$cm->modname,'iteminstance'=>$cm->instance,
+ 'itemnumber'=>$cm->completiongradeitemnumber));
+ if($item) {
+ // Fetch 'grades' (will be one or none)
+ $grades=grade_grade::fetch_users_grades($item,array($userid),false);
+ if(empty($grades)) {
+ // No grade for user
+ return COMPLETION_INCOMPLETE;
+ }
+ if(count($grades)>1) {
+ throw new Exception("Unexpected result: multiple grades for
+ item '{$item->id}', user '{$userid}'");
+ }
+ $newstate=$this->internal_get_grade_state($item,reset($grades));
+ if($newstate==COMPLETION_INCOMPLETE) {
+ return COMPLETION_INCOMPLETE;
+ }
+ } else {
+ throw new Exception("Cannot find grade item for '{$cm->modname}'
+ cm '{$cm->id}' matching number '{$cm->completiongradeitemnumber}'");
+ }
+ }
+
+ if(plugin_supports('mod',$cm->modname,FEATURE_COMPLETION_HAS_RULES)) {
+ $function=$cm->modname.'_get_completion_state';
+ if(!function_exists($function)) {
+ throw new Exception("Module {$cm->modname} claims to support
+ FEATURE_COMPLETION_HAS_RULES but does not have required
+ {$cm->modname}_get_completion_state function");
+ }
+ if(!$function($this->course,$cm,$userid,COMPLETION_AND)) {
+ return COMPLETION_INCOMPLETE;
+ }
+ }
+
+ return $newstate;
+
+ }
+
+
+ /**
+ * Marks a module as viewed.
+ *
+ * Should be called whenever a module is 'viewed' (it is up to the module how to
+ * determine that). Has no effect if viewing is not set as a completion condition.
+ *
+ * @param object $cm Activity
+ * @param int $userid User ID or 0 (default) for current user
+ */
+ public function set_module_viewed($cm,$userid=0) {
+ // Don't do anything if view condition is not turned on
+ if($cm->completionview==COMPLETION_VIEW_NOT_REQUIRED || !$this->is_enabled($cm)) {
+ return;
+ }
+ // Get current completion state
+ $data=$this->get_data($cm,$userid);
+ // If we already viewed it, don't do anything
+ if($data->viewed==COMPLETION_VIEWED) {
+ return;
+ }
+ // OK, change state, save it, and update completion
+ $data->viewed=COMPLETION_VIEWED;
+ $this->internal_set_data($cm,$data);
+ $this->update_state($cm,COMPLETION_COMPLETE,$userid);
+ }
+
+ /**
+ * Determines how much completion data exists for an activity. This is used when
+ * deciding whether completion information should be 'locked' in the module
+ * editing form.
+ *
+ * @param object $cm Activity
+ * @return int The number of users who have completion data stored for this
+ * activity, 0 if none
+ */
+ public function count_user_data($cm) {
+ global $CFG,$DB;
+
+ return $DB->get_field_sql("
+ SELECT
+ COUNT(1)
+ FROM
+ {$CFG->prefix}course_modules_completion
+ WHERE
+ coursemoduleid=? AND completionstate<>0",array($cm->id));
+ }
+
+ /**
+ * Deletes completion state related to an activity for all users.
+ *
+ * Intended for use only when the activity itself is deleted.
+ *
+ * @param object $cm Activity
+ */
+ public function delete_all_state($cm) {
+ global $SESSION,$DB;
+
+ // Delete from database
+ $DB->delete_records('course_modules_completion',array('coursemoduleid'=>$cm->id));
+
+ // Erase cache data for current user if applicable
+ if(isset($SESSION->completioncache) &&
+ array_key_exists($cm->course,$SESSION->completioncache) &&
+ array_key_exists($cm->id,$SESSION->completioncache[$cm->course])) {
+ unset($SESSION->completioncache[$cm->course][$cm->id]);
+ }
+ }
+
+ /**
+ * Recalculates completion state related to an activity for all users.
+ *
+ * Intended for use if completion conditions change. (This should be avoided
+ * as it may cause some things to become incomplete when they were previously
+ * complete, with the effect - for example - of hiding a later activity that
+ * was previously available.)
+ *
+ * @param object $cm Activity
+ */
+ public function reset_all_state($cm) {
+ global $DB;
+ // Get current list of users with completion state
+ $rs=$DB->get_recordset('course_modules_completion',array('coursemoduleid'=>$cm->id),'','userid');
+ $keepusers=array();
+ foreach($rs as $rec) {
+ $keepusers[]=$rec->userid;
+ }
+ $rs->close();
+
+ // Delete all existing state [also clears session cache for current user]
+ $this->delete_all_state($cm);
+
+ // Merge this with list of planned users (according to roles)
+ $trackedusers=$this->internal_get_tracked_users(false);
+ foreach($trackedusers as $trackeduser) {
+ $keepusers[]=$trackeduser->id;
+ }
+ $keepusers=array_unique($keepusers);
+
+ // Recalculate state for each kept user
+ foreach($keepusers as $keepuser) {
+ $this->update_state($cm,COMPLETION_UNKNOWN,$keepuser);
+ }
+ }
+
+ /**
+ * Obtains completion data for a particular activity and user (from the
+ * session cache if available, or by SQL query)
+ *
+ * @param object $cm Activity
+ * @param bool $wholecourse If true (default false) then, when necessary to
+ * fill the cache, retrieves information from the entire course not just for
+ * this one activity
+ * @param int $userid User ID or 0 (default) for current user
+ * @param array $modinfo For unit testing only, supply the value
+ * here. Otherwise the method calls get_fast_modinfo
+ * @return object Completion data (record from course_modules_completion)
+ * @throws Exception In some cases where the requested course-module is not
+ * found on the specified course
+ */
+ public function get_data($cm,$wholecourse=false,$userid=0,$modinfo=null) {
+ // Get user ID
+ global $USER,$CFG,$SESSION,$DB;
+ if(!$userid) {
+ $userid=$USER->id;
+ }
+
+ // Is this the current user?
+ $currentuser=$userid==$USER->id;
+
+ if($currentuser) {
+ // Make sure cache is present
+ if(!isset($SESSION->completioncache)) {
+ $SESSION->completioncache=array();
+ }
+ // Expire any old data from cache
+ foreach($SESSION->completioncache as $courseid=>$activities) {
+ if(empty($activities['updated']) || $activities['updated'] < time()-COMPLETION_CACHE_EXPIRY) {
+ unset($SESSION->completioncache[$courseid]);
+ }
+ }
+ // See if requested data is present, if so use cache to get it
+ if(isset($SESSION->completioncache) &&
+ array_key_exists($this->course->id,$SESSION->completioncache) &&
+ array_key_exists($cm->id,$SESSION->completioncache[$this->course->id])) {
+ return $SESSION->completioncache[$this->course->id][$cm->id];
+ }
+ }
+
+ // Not there, get via SQL
+ if($currentuser && $wholecourse) {
+ // Get whole course data for cache
+ $alldatabycmc=$DB->get_records_sql("
+ SELECT
+ cmc.*
+ FROM
+ {$CFG->prefix}course_modules cm
+ INNER JOIN {$CFG->prefix}course_modules_completion cmc ON cmc.coursemoduleid=cm.id
+ WHERE
+ cm.course=? AND cmc.userid=?",array($this->course->id,$userid));
+
+ // Reindex by cm id
+ $alldata=array();
+ if($alldatabycmc) {
+ foreach($alldatabycmc as $data) {
+ $alldata[$data->coursemoduleid]=$data;
+ }
+ }
+
+ // Get the module info and build up condition info for each one
+ if(empty($modinfo)) {
+ $modinfo=get_fast_modinfo($this->course,$userid);
+ }
+ foreach($modinfo->cms as $othercm) {
+ if(array_key_exists($othercm->id,$alldata)) {
+ $data=$alldata[$othercm->id];
+ } else {
+ // Row not present counts as 'not complete'
+ $data=new StdClass;
+ $data->id=0;
+ $data->coursemoduleid=$othercm->id;
+ $data->userid=$userid;
+ $data->completionstate=0;
+ $data->viewed=0;
+ $data->timemodified=0;
+ }
+ $SESSION->completioncache[$this->course->id][$othercm->id]=$data;
+ }
+ $SESSION->completioncache[$this->course->id]['updated']=time();
+
+ if(!isset($SESSION->completioncache[$this->course->id][$cm->id])) {
+ throw new Exception("Unexpected error: course-module $cm->id could not be found on course $this->course->id");
+ }
+ return $SESSION->completioncache[$this->course->id][$cm->id];
+ } else {
+ // Get single record
+ $data=$DB->get_record('course_modules_completion',array('coursemoduleid'=>$cm->id,'userid'=>$userid));
+ if($data==false) {
+ // Row not present counts as 'not complete'
+ $data=new StdClass;
+ $data->id=0;
+ $data->coursemoduleid=$cm->id;
+ $data->userid=$userid;
+ $data->completionstate=0;
+ $data->viewed=0;
+ $data->timemodified=0;
+ }
+
+ // Put in cache
+ if($currentuser) {
+ $SESSION->completioncache[$this->course->id][$cm->id]=$data;
+ // For single updates, only set date if it was empty before
+ if(empty($SESSION->completioncache[$this->course->id]['updated'])) {
+ $SESSION->completioncache[$this->course->id]['updated']=time();
+ }
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Updates completion data for a particular coursemodule and user (user is
+ * determined from $data).
+ *
+ * (Internal function. Not private, so we can unit-test it.)
+ *
+ * @param object $cm Activity
+ * @param object $data Data about completion for that user
+ */
+ function internal_set_data($cm,$data) {
+ global $USER,$SESSION,$DB;
+ if($data->id) {
+ // Has real (nonzero) id meaning that a database row exists
+ $DB->update_record('course_modules_completion',$data);
+ } else {
+ // Didn't exist before, needs creating
+ $data->id=$DB->insert_record('course_modules_completion',$data);
+ }
+ if($data->userid==$USER->id) {
+ $SESSION->completioncache[$cm->course][$cm->id]=$data;
+ }
+ }
+
+ /**
+ * Obtains a list of activities for which completion is enabled on the
+ * course. The list is ordered by the section order of those activities.
+ * @param array $modinfo For unit testing only, supply the value
+ * here. Otherwise the method calls get_fast_modinfo
+ * @return array Array from $cmid => $cm of all activities with completion enabled,
+ * empty array if none
+ */
+ public function get_activities($modinfo=null) {
+ global $DB;
+
+ // Obtain those activities which have completion turned on
+ $withcompletion=$DB->get_records_select('course_modules','course='.$this->course->id.
+ ' AND completion<>'.COMPLETION_TRACKING_NONE);
+ if(count($withcompletion)==0) {
+ return array();
+ }
+
+ // Use modinfo to get section order and also add in names
+ if(empty($modinfo)) {
+ $modinfo=get_fast_modinfo($this->course);
+ }
+ $result=array();
+ foreach($modinfo->sections as $sectioncms) {
+ foreach($sectioncms as $cmid) {
+ if(array_key_exists($cmid,$withcompletion)) {
+ $result[$cmid]=$withcompletion[$cmid];
+ $result[$cmid]->modname=$modinfo->cms[$cmid]->modname;
+ $result[$cmid]->name=$modinfo->cms[$cmid]->name;
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Gets list of users in a course whose progress is tracked for display on the
+ * progress report.
+ * @param bool $sortfirstname True to sort with firstname
+ * @param int $groupid Optionally restrict to groupid
+ * @return array Array of user objects containing id, firstname, lastname (empty if none)
+ */
+ function internal_get_tracked_users($sortfirstname,$groupid=0) {
+ global $CFG,$DB;
+ if(!empty($CFG->progresstrackedroles)) {
+ $roles=explode(',',$CFG->progresstrackedroles);
+ } else {
+ // This causes it to default to everyone (if there is no student role)
+ $roles=array();
+ }
+ $users=get_role_users($roles,get_context_instance(CONTEXT_COURSE,$this->course->id),true,
+ 'u.id,u.firstname,u.lastname',
+ $sortfirstname ? 'u.firstname ASC' : 'u.lastname ASC',true,$groupid);
+ $users=$users ? $users : array(); // In case it returns false
+ return $users;
+ }
+
+ /**
+ * Obtains progress information across a course for all users on that course, or
+ * for all users in a specific group. Intended for use when displaying progress.
+ *
+ * This includes only users who, in course context, have one of the roles for
+ * which progress is tracked (the progresstrackedroles admin option).
+ *
+ * Users are included (in the first array) even if they do not have
+ * completion progress for any course-module.
+ *
+ * @param bool $sortfirstname If true, sort by first name, otherwise sort by
+ * last name
+ * @param int $groupid Group ID or 0 (default)/false for all groups
+ * @return Array of user objects (like mdl_user id, firstname, lastname)
+ * containing an additional ->progress array of coursemoduleid => completionstate
+ */
+ public function get_progress_all($sortfirstname=false,$groupid=0) {
+ global $CFG,$DB;
+
+ // Get list of applicable users
+ $users=$this->internal_get_tracked_users($sortfirstname,$groupid);
+
+ // Get progress information for these users in groups of 1,000 (if needed)
+ // to avoid making the SQL IN too long
+ $result=array();
+ $userids=array();
+ foreach($users as $user) {
+ $userids[]=$user->id;
+ $result[$user->id]=$user;
+ $result[$user->id]->progress=array();
+ }
+
+ for($i=0;$iget_in_or_equal(array_slice($userids,$i,$blocksize));
+ array_splice($params,0,0,array($this->course->id));
+ $rs=$DB->get_recordset_sql("
+SELECT
+ cmc.*
+FROM
+ {$CFG->prefix}course_modules cm
+ INNER JOIN {$CFG->prefix}course_modules_completion cmc ON cm.id=cmc.coursemoduleid
+WHERE
+ cm.course=? AND cmc.userid $insql
+ ",$params);
+ if(!$rs) {
+ throw new Exception('Failed to obtain completion progress');
+ }
+ foreach($rs as $progress) {
+ $result[$progress->userid]->progress[$progress->coursemoduleid]=$progress;
+ }
+ $rs->close();
+ }
+
+ return $result;
+ }
+
+ public function inform_grade_changed($cm,&$item,&$grade,$deleted) {
+ // Bail out now if completion is not enabled for course-module, grade
+ // is not used to compute completion, or this is a different numbered
+ // grade
+ if(!$this->is_enabled($cm) ||
+ is_null($cm->completiongradeitemnumber) ||
+ $item->itemnumber!=$cm->completiongradeitemnumber) {
+ return;
+ }
+
+ // What is the expected result based on this grade?
+ if($deleted) {
+ // Grade being deleted, so only change could be to make it incomplete
+ $possibleresult=COMPLETION_INCOMPLETE;
+ } else {
+ $possibleresult=$this->internal_get_grade_state($item,$grade);
+ }
+
+ // OK, let's update state based on this
+ $this->update_state($cm,$possibleresult,$grade->userid);
+ }
+
+ /**
+ * Calculates the completion state that would result from a graded item
+ * (where grade-based completion is turned on) based on the actual grade
+ * and settings.
+ *
+ * (Internal function. Not private, so we can unit-test it.)
+ *
+ * @param grade_item &$item
+ * @param grade_grade &$grade
+ * @return int Completion state e.g. COMPLETION_INCOMPLETE
+ */
+ function internal_get_grade_state(&$item,&$grade) {
+ if(!$grade) {
+ return COMPLETION_INCOMPLETE;
+ }
+ // Conditions to show pass/fail:
+ // a) Grade has pass mark (default is 0.00000 which is boolean true so be careful)
+ // b) Grade is visible (neither hidden nor hidden-until)
+ if($item->gradepass && $item->gradepass>0.000009 && !$item->hidden) {
+ // Use final grade if set otherwise raw grade
+ $score=!is_null($grade->finalgrade) ? $grade->finalgrade : $grade->rawgrade;
+
+ // We are displaying and tracking pass/fail
+ if($score>=$item->gradepass) {
+ return COMPLETION_COMPLETE_PASS;
+ } else {
+ return COMPLETION_COMPLETE_FAIL;
+ }
+ } else {
+ // Not displaying pass/fail, but we know grade exists b/c we got here
+ return COMPLETION_COMPLETE;
+ }
+ }
+}
+
+?>
Index: course/report/progress/textrotate.js
===================================================================
RCS file: course/report/progress/textrotate.js
diff -N course/report/progress/textrotate.js
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ course/report/progress/textrotate.js 1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,65 @@
+var SVGNS='http://www.w3.org/2000/svg',XLINKNS='http://www.w3.org/1999/xlink';
+
+function textrotate_make_svg(el)
+{
+ var string=el.firstChild.nodeValue;
+
+ // Create SVG
+ var svg=document.createElementNS(SVGNS,'svg');
+ svg.setAttribute('version','1.1');
+ var width=(el.offsetHeight*9)/8;
+ svg.setAttribute('width',width);
+ svg.setAttribute('height',el.offsetWidth+20);
+
+ var text=document.createElementNS(SVGNS,'text');
+ svg.appendChild(text);
+ text.setAttribute('x',el.offsetWidth);
+ text.setAttribute('y',-el.offsetHeight/4);
+ text.setAttribute('text-anchor','end');
+ text.setAttribute('transform','rotate(90)');
+ text.appendChild(document.createTextNode(string));
+
+ // Is there an icon near the text?
+ var icon=el.parentNode.firstChild;
+ if(icon.nodeName.toLowerCase()=='img') {
+ el.parentNode.removeChild(icon);
+ var image=document.createElementNS(SVGNS,'image');
+ var iconx=el.offsetHeight/4;
+ if(iconx>width-16) iconx=width-16;
+ image.setAttribute('x',iconx);
+ image.setAttribute('y',el.offsetWidth+4);
+ image.setAttribute('width',16);
+ image.setAttribute('height',16);
+ image.setAttributeNS(XLINKNS,'href',icon.src);
+ svg.appendChild(image);
+ }
+
+ // Replace original content with this new SVG
+ el.parentNode.insertBefore(svg,el);
+ el.parentNode.removeChild(el);
+}
+
+function textrotate_init() {
+ var elements=YAHOO.util.Dom.getElementsByClassName('completion-activityname', 'span');
+ for(var i=0;iget_record('course',array('id'=>required_param('course',PARAM_INT)));
+if(!$course) {
+ error('Specified course not found');
+}
+
+// Sort (default lastname, optionally firstname)
+$sort=optional_param('sort','',PARAM_ALPHA);
+$firstnamesort=$sort=='firstname';
+
+// CSV format
+$csv=optional_param('format','',PARAM_ALPHA)==='csv';
+
+function csv_quote($value) {
+ $tl=textlib_get_instance();
+
+ $value=$tl->specialtoascii($value);
+ return '"'.str_replace('"',"'",$value).'"';
+}
+
+require_login($course->id);
+
+// Check basic permission
+$context=get_context_instance(CONTEXT_COURSE,$course->id);
+require_capability('moodle/course:viewprogress',$context);
+
+// Get group mode
+$group=groups_get_course_group($course,true); // Supposed to verify group
+if($group===0 && $course->groupmode==SEPARATEGROUPS) {
+ require_capability('moodle/site:accessallgroups',$context);
+}
+
+// Get data on activities and progress of all users, and give error if we've
+// nothing to display (no users or no activities)
+$reportsurl=$CFG->wwwroot.'/course/report.php?id='.$course->id;
+$completion=new completion_info($course);
+$activities=$completion->get_activities();
+if(count($activities)==0) {
+ print_error('err_noactivities','completion',$reportsurl);
+}
+$progress=$completion->get_progress_all($firstnamesort,$group);
+
+if($csv) {
+ header('Content-Type: text/csv; charset=ISO-8859-1');
+ header('Content-Disposition: attachment; filename=progress.'.
+ preg_replace('/[^a-z0-9-]/','_',strtolower($course->shortname)).'.csv');
+} else {
+ // Use SVG to draw sideways text if supported
+ $svgcleverness=check_browser_version('Firefox',2.0) && !$USER->screenreader;
+
+ // Navigation and header
+ $strreports = get_string("reports");
+ $strcompletion = get_string('completionreport','completion');
+ $navlinks = array();
+ $navlinks[] = array('name' => $strreports, 'link' => "../../report.php?id=$course->id", 'type' => 'misc');
+ $navlinks[] = array('name' => $strcompletion, 'link' => null, 'type' => 'misc');
+ if($svgcleverness) {
+ require_js(array('yui_yahoo','yui_event','yui_dom'));
+ }
+ print_header($strcompletion,$course->fullname,build_navigation($navlinks));
+ if($svgcleverness) {
+ print '';
+ }
+
+ // Handle groups (if enabled)
+ groups_print_course_menu($course,$CFG->wwwroot.'/course/report/progress/?course='.$course->id);
+}
+
+// Okay, let's draw the table of progress info,
+
+// Start of table
+if(!$csv) {
+ print '
'; // ugh
+ if(count($progress)==0) {
+ print ''.get_string('err_nousers','completion').'
';
+ print ''.get_string('continue').'
';
+ print_footer($course);
+ exit;
+ }
+ print '';
+
+ // User heading / sort option
+ print '| ';
+ if($firstnamesort) {
+ print
+ get_string('firstname').' / '.
+ get_string('lastname').'';
+ } else {
+ print ''.
+ get_string('firstname').' / '.
+ get_string('lastname');
+ }
+ print ' | ';
+}
+
+// Activities
+foreach($activities as $activity) {
+ $activity->datepassed = $activity->completionexpected && $activity->completionexpected <= time();
+ $activity->datepassedclass=$activity->datepassed ? 'completion-expired' : '';
+
+ if($activity->completionexpected) {
+ $datetext=userdate($activity->completionexpected,get_string('strftimedate','langconfig'));
+ } else {
+ $datetext='';
+ }
+
+ if($csv) {
+ print ','.csv_quote($activity->name).','.csv_quote($datetext);
+ } else {
+ print ''.
+ ''.
+ ' '.
+ format_string($activity->name).'';
+ if($activity->completionexpected) {
+ print ''.$datetext.' ';
+ }
+ print ' | ';
+ }
+}
+
+if($csv) {
+ print "\n";
+} else {
+ print '
';
+}
+
+// Row for each user
+foreach($progress as $user) {
+ // User name
+ if($csv) {
+ print csv_quote(fullname($user));
+ } else {
+ print '| '.fullname($user).' | ';
+ }
+
+ // Progress for each activity
+ foreach($activities as $activity) {
+
+ // Get progress information and state
+ if(array_key_exists($activity->id,$user->progress)) {
+ $progress=$user->progress[$activity->id];
+ $state=$progress->completionstate;
+ $date=userdate($progress->timemodified);
+ } else {
+ $state=COMPLETION_INCOMPLETE;
+ $date='';
+ }
+
+ // Work out how it corresponds to an icon
+ $completiontype=
+ ($activity->completion==COMPLETION_TRACKING_AUTOMATIC ? 'auto' : 'manual').
+ '-';
+ switch($state) {
+ case COMPLETION_INCOMPLETE : $completiontype.='n'; break;
+ case COMPLETION_COMPLETE : $completiontype.='y'; break;
+ case COMPLETION_COMPLETE_PASS : $completiontype.='pass'; break;
+ case COMPLETION_COMPLETE_FAIL : $completiontype.='fail'; break;
+ }
+
+ $describe=get_string('completion-alt-'.$completiontype,'completion');
+ $a=new StdClass;
+ $a->state=$describe;
+ $a->date=$date;
+ $a->user=fullname($user);
+ $a->activity=$activity->name;
+ $fulldescribe=get_string('progress-title','completion',$a);
+
+ if($csv) {
+ print ','.csv_quote($describe).','.csv_quote($date);
+ } else {
+ print ''.
+ ' | ';
+ }
+ }
+
+ if($csv) {
+ print "\n";
+ } else {
+ print '
';
+ }
+}
+
+if($csv) {
+ exit;
+}
+print '
';
+
+print '';
+
+print_footer($course);
+?>