| ' . get_string('directory') . " | \n" . '' . get_string('name') . " | \n" . '' . get_string('status') . " | \n? | \n" . "$plugin_name | \n" . "" . ${'str' . $standard} . " $strmissingfromdisk | \n\n"; - $row++; + $row++; } } $html .= '
|---|
+ * 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 28 Jul 2008 12:05:04 -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 '
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; } @@ -5164,6 +5208,9 @@ case "ENROLPERIOD": $this->info->course_enrolperiod = $this->getContents(); break; + case "ENABLECOMPLETION": + $this->info->course_enablecompletion = $this->getContents(); + break; } } if ($this->tree[4] == "CATEGORY") { @@ -5539,7 +5586,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); } } @@ -5577,6 +5632,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; } @@ -5672,6 +5739,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) @@ -8338,22 +8435,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 28 Jul 2008 12:05:01 -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.39 diff -u -r1.39 forum.php --- lang/en_utf8/forum.php 25 Jul 2008 12:30:08 -0000 1.39 +++ lang/en_utf8/forum.php 28 Jul 2008 12:05:04 -0000 @@ -40,6 +40,15 @@ $string['cannotinsertrate'] = 'Could not insert a new rating ($a[0] = $a[1])'; $string['cannottrack'] = 'Could not stop tracking that forum'; $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.'; @@ -87,11 +96,11 @@ $string['editing'] = 'Editing'; $string['emptymessage'] = 'Something was wrong with your post. Perhaps you left it blank, or the attachment was too big. Your changes have NOT been saved.'; $string['everyonecanchoose'] = 'Everyone can choose to be subscribed'; -$string['everyonecannowchoose'] = 'Everyone can now choose to be subscribed'; +$string['everyonecannowchoose'] = 'Everyone can now choose to be subscribed'; $string['everyoneisnowsubscribed'] = 'Everyone is now subscribed to this forum'; $string['everyoneissubscribed'] = 'Everyone is subscribed to this forum'; $string['existingsubscribers'] = 'Existing subscribers'; -$string['forcessubscribe'] = 'This forum forces everyone to be subscribed'; +$string['forcessubscribe'] = 'This forum forces everyone to be subscribed'; $string['forcesubscribe'] = 'Force everyone to be subscribed'; $string['forcesubscribeq'] = 'Force everyone to be subscribed?'; $string['forum'] = 'Forum'; Index: version.php =================================================================== RCS file: /cvsroot/moodle/moodle/version.php,v retrieving revision 1.737 diff -u -r1.737 version.php --- version.php 28 Jul 2008 01:37:25 -0000 1.737 +++ version.php 28 Jul 2008 12:05:00 -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 = 2008072600; // YYYYMMDD = date of the last version bump + $version = 2008072800; // YYYYMMDD = date of the last version bump // XX = daily increments $release = '2.0 dev (Build: 20080728)'; // 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 28 Jul 2008 12:05:00 -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 +