+ * 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,f