$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
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
$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
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));
}
return $status;
}
-
+
//Print users to xml
//Only users previously calculated in backup_ids will output
//
$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;
//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
}
}
}
+
+ // 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 "<p>Can't find new ID for cm $data->coursemoduleid.</p>";
+ }
+ $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 "<p>Failed to insert completion data record.</p>";
+ }
+ $status=false;
+ continue;
+ }
+ }
+ }
+
} else {
$status = false;
}
case "ENROLPERIOD":
$this->info->course_enrolperiod = $this->getContents();
break;
+ case "ENABLECOMPLETION":
+ $this->info->course_enablecompletion = $this->getContents();
+ break;
}
}
if ($this->tree[4] == "CATEGORY") {
$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);
}
}
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;
}
}
} /// 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)
}
/// 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;
-
- $DB->insert_record('role_names', $rolename);
- }
+ $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);
}
}
}
}
+ }
return true;
}
--- /dev/null
+var completion_strsaved;
+
+function completion_init() {
+ var toggles=YAHOO.util.Dom.getElementsByClassName('togglecompletion', 'form');
+ for(var i=0;i<toggles.length;i++) {
+ completion_init_toggle(toggles[i]);
+ }
+}
+
+function completion_init_toggle(form) {
+ // Store all necessary references for easy access
+ var inputs=form.getElementsByTagName('input');
+ form.cmid=inputs[0].value;
+ form.otherState=inputs[1].value;
+ form.image=inputs[2];
+
+ // Create and position 'Saved' text
+ var saved=document.createElement('div');
+ YAHOO.util.Dom.addClass(saved,'completion-saved-display');
+ YAHOO.util.Dom.setStyle(saved,'display','none');
+ saved.appendChild(document.createTextNode(completion_strsaved));
+ form.appendChild(saved);
+ form.saved=saved;
+
+ // Add event handler
+ YAHOO.util.Event.addListener(form, "submit", completion_toggle);
+}
+
+function completion_handle_response(o) {
+ if(o.responseText!='OK') {
+ alert('An error occurred when attempting to save your tick mark.\n\n('+o.responseText+'.)');
+ return;
+ }
+ // Change image
+ if(this.otherState==1) {
+ this.image.src=this.image.src.replace(/n\.gif$/,'y.gif');
+ this.otherState=0;
+ } else {
+ this.image.src=this.image.src.replace(/y\.gif$/,'n.gif');
+ this.otherState=1;
+ }
+ // Start animation
+ completion_update_animation(this,1.0);
+}
+
+function completion_update_animation(form,opacity) {
+ if(opacity<0.001) {
+ YAHOO.util.Dom.setStyle(form.saved,'display','none');
+ return;
+ }
+ YAHOO.util.Dom.setStyle(form.saved,'opacity',opacity);
+ if(opacity>0.999) {
+ var pos=YAHOO.util.Dom.getXY(form.image);
+ pos[0]+=20; // Icon size + 4px border
+ YAHOO.util.Dom.setStyle(form.saved,'display','block');
+ YAHOO.util.Dom.setXY(form.saved,pos);
+ }
+ setTimeout(function() { completion_update_animation(form,opacity-0.1); },100);
+}
+
+function completion_handle_failure(o) {
+ alert('An error occurred when attempting to connect to our server. The tick mark will not be saved.\n\n('+
+ o.status+' '+o.statusText+')');
+}
+
+function completion_toggle(e) {
+ YAHOO.util.Event.preventDefault(e);
+ YAHOO.util.Connect.asyncRequest('POST','togglecompletion.php',
+ {success:completion_handle_response,failure:completion_handle_failure,scope:this},
+ 'id='+this.cmid+'&completionstate='+this.otherState+'&fromajax=1');
+}
+
+YAHOO.util.Event.onDOMReady(completion_init);
$languages += get_list_of_languages();
$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'));
<?php // $Id$
// Library of useful functions
+require_once($CFG->libdir.'/completionlib.php');
define('COURSE_MAX_LOG_DISPLAY', 150); // days
define('COURSE_MAX_LOGS_PER_PAGE', 1000); // records
/**
* 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;
$extra = '';
if (!empty($modinfo->cms[$modnumber]->extra)) {
- $extra = $modinfo->cms[$modnumber]->extra;
+ $extra = $modinfo->cms[$modnumber]->extra;
}
if ($mod->modname == "label") {
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 "
+<form class='togglecompletion' method='post' action='togglecompletion.php'><div>
+<input type='hidden' name='id' value='{$mod->id}' />
+<input type='hidden' name='completionstate' value='$newstate' />
+<input type='image' src='$imgsrc' alt='$imgalt' title='$imgtitle' />
+</div></form>";
+ } else {
+ // In auto mode, or when editing, the icon is just an image
+ echo "
+<span class='autocompletion'><img src='$imgsrc' alt='$imgalt' title='$imgalt' /></span>";
+ }
+ }
+ }
+
echo "</li>\n";
}
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
$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))) {
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)) {
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);
* List of modform features
*/
var $_features;
+
+ /**
+ * @var array Custom completion-rule elements, if enabled
+ */
+ var $_customcompletionelements;
function moodleform_mod($instance, $section, $cm) {
$this->_instance = $instance;
$mform->removeElement('groupingid');
}
}
+
+ // Completion: If necessary, freeze fields
+ $completion=new completion_info($COURSE);
+ if($completion->is_enabled()) {
+ // If anybody has completed the activity, these options will be 'locked'
+ $completedcount = empty($this->_cm)
+ ? 0
+ : $completion->count_user_data($this->_cm);
+
+ $freeze=false;
+ if(!$completedcount) {
+ if($mform->elementExists('unlockcompletion')) {
+ $mform->removeElement('unlockcompletion');
+ }
+ } else {
+ // Has the element been unlocked?
+ if($mform->exportValue('unlockcompletion')) {
+ // Yes, add in warning text and set the hidden variable
+ $mform->insertElementBefore(
+ $mform->createElement('static','completedunlocked',
+ get_string('completedunlocked','completion'),
+ get_string('completedunlockedtext','completion')),
+ 'unlockcompletion');
+ $mform->removeElement('unlockcompletion');
+ $mform->getElement('completionunlocked')->setValue(1);
+ } else {
+ // No, add in the warning text with the count (now we know
+ // it) before the unlock button
+ $mform->insertElementBefore(
+ $mform->createElement('static','completedwarning',
+ get_string('completedwarning','completion'),
+ get_string('completedwarningtext','completion',$completedcount)),
+ 'unlockcompletion');
+ $mform->setHelpButton('completedwarning', array('completionlocked', get_string('help_completionlocked', 'completion'), 'completion'));
+
+ $freeze=true;
+ }
+ }
+
+ if($freeze) {
+ $mform->freeze('completion');
+ if($mform->elementExists('completionview')) {
+ $mform->freeze('completionview'); // don't use hardFreeze or checkbox value gets lost
+ }
+ if($mform->elementExists('completionusegrade')) {
+ $mform->freeze('completionusegrade');
+ }
+ $mform->freeze($this->_customcompletionelements);
+ }
+ }
}
// form verification
$errors['cmidnumber'] = get_string('idnumbertaken');
}
}
+
+ // Completion: Don't let them choose automatic completion without turning
+ // on some conditions
+ if(array_key_exists('completion',$data) && $data['completion']==COMPLETION_TRACKING_AUTOMATIC) {
+ if(empty($data['completionview']) && empty($data['completionusegrade']) &&
+ !$this->completion_rule_enabled($data)) {
+ $errors['completion']=get_string('badautocompletion','completion');
+ }
+ }
return $errors;
}
/**
* Adds all the standard elements to a form to edit the settings for an activity module.
*
- * @param mixed array or object describing supported features - groups, groupings, groupmembersonly, etc.
+ * @param mixed $features array or object describing supported features - groups, groupings, groupmembersonly, etc.
+ * @param string $modname Name of module e.g. 'label'
*/
- function standard_coursemodule_elements($features=null){
+ function standard_coursemodule_elements($features=null,$modname=null){
global $COURSE, $CFG, $DB;
$mform =& $this->_form;
+ // Guess module name if not supplied
+ if(!$modname) {
+ $matches=array();
+ if(!preg_match('/^mod_([^_]+)_mod_form$/',$this->_formname,$matches)) {
+ debugging('Use $modname parameter or rename form to mod_xx_mod_form, where xx is name of your module');
+ error('Unknown module name for form');
+ }
+ $modname=$matches[1];
+ }
+
// deal with legacy $supportgroups param
if ($features === true or $features === false) {
$groupmode = $features;
if (!isset($this->_features->idnumber)) {
$this->_features->idnumber = true;
}
+
+ if(!isset($this->_features->defaultcompletion)) {
+ $this->_features->defaultcompletion = true;
+ }
$outcomesused = false;
if (!empty($CFG->enableoutcomes) and $this->_features->outcomes) {
$mform->addElement('select', 'gradecat', get_string('gradecategory', 'grades'), $categories);
}
+ // Conditional activities: completion tracking section
+ require_once($CFG->libdir.'/completionlib.php');
+ $completion=new completion_info($COURSE);
+ if($completion->is_enabled()) {
+ $mform->addElement('header', '', get_string('activitycompletion', 'completion'));
+
+ // Unlock button for if people have completed it (will
+ // be removed in definition_after_data if they haven't)
+ $mform->addElement('submit','unlockcompletion',get_string('unlockcompletion','completion'));
+ $mform->registerNoSubmitButton('unlockcompletion');
+ $mform->addElement('hidden','completionunlocked',0);
+
+ $mform->addElement('select', 'completion', get_string('completion','completion'),
+ array(COMPLETION_TRACKING_NONE=>get_string('completion_none','completion'),
+ COMPLETION_TRACKING_MANUAL=>get_string('completion_manual','completion')));
+ $mform->setHelpButton('completion', array('completion', get_string('help_completion', 'completion'), 'completion'));
+ $mform->setDefault('completion',$this->_features->defaultcompletion
+ ? COMPLETION_TRACKING_MANUAL
+ : COMPLETION_TRACKING_NONE);
+
+ // Automatic completion once you view it
+ $gotcompletionoptions=false;
+ if(plugin_supports('mod',$modname,FEATURE_COMPLETION_TRACKS_VIEWS)) {
+ $mform->addElement('checkbox', 'completionview', get_string('completionview','completion'),
+ get_string('completionview_text','completion'));
+ $mform->setHelpButton('completionview', array('completionview', get_string('help_completionview', 'completion'), 'completion'));
+ $mform->disabledIf('completionview','completion','ne',COMPLETION_TRACKING_AUTOMATIC);
+ $gotcompletionoptions=true;
+ }
+
+ // Automatic completion once it's graded
+ if(plugin_supports('mod',$modname,FEATURE_GRADE_HAS_GRADE)) {
+ $mform->addElement('checkbox', 'completionusegrade', get_string('completionusegrade','completion'),
+ get_string('completionusegrade_text','completion'));
+ $mform->setHelpButton('completionusegrade', array('completionusegrade', get_string('help_completionusegrade', 'completion'), 'completion'));
+ $mform->disabledIf('completionusegrade','completion','ne',COMPLETION_TRACKING_AUTOMATIC);
+ $gotcompletionoptions=true;
+ }
+
+ // Automatic completion according to module-specific rules
+ $this->_customcompletionelements = $this->add_completion_rules();
+ foreach($this->_customcompletionelements as $element) {
+ $mform->disabledIf($element,'completion','ne',COMPLETION_TRACKING_AUTOMATIC);
+ }
+
+ $gotcompletionoptions = $gotcompletionoptions ||
+ count($this->_customcompletionelements)>0;
+
+ // Automatic option only appears if possible
+ if($gotcompletionoptions) {
+ $mform->getElement('completion')->addOption(
+ get_string('completion_automatic','completion'),
+ COMPLETION_TRACKING_AUTOMATIC);
+ }
+
+ // Completion expected at particular date? (For progress tracking)
+ $mform->addElement('date_selector', 'completionexpected', get_string('completionexpected','completion'), array('optional'=>true));
+ $mform->setHelpButton('completionexpected', array('completionexpected', get_string('help_completionexpected', 'completion'), 'completion'));
+ $mform->disabledIf('completionexpected','completion','eq',COMPLETION_TRACKING_NONE);
+ }
+
$this->standard_hidden_coursemodule_elements();
}
+
+ /**
+ * Can be overridden to add custom completion rules if the module wishes
+ * them. If overriding this, you should also override completion_rule_enabled.
+ * <p>
+ * 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;
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');
--- /dev/null
+<?php
+
+require_once('../../../config.php');
+global $DB;
+
+// Get course
+$course=$DB->get_record('course',array('id'=>required_param('course',PARAM_INT)));
+if(!$course) {
+ error('Specified course not found');
+}
+
+// Sort (default lastname, optionally firstname)
+$sort=optional_param('sort','',PARAM_ALPHA);
+$firstnamesort=$sort=='firstname';
+
+// CSV format
+$csv=optional_param('format','',PARAM_ALPHA)==='csv';
+
+function csv_quote($value) {
+ $tl=textlib_get_instance();
+
+ $value=$tl->specialtoascii($value);
+ return '"'.str_replace('"',"'",$value).'"';
+}
+
+require_login($course->id);
+
+// Check basic permission
+$context=get_context_instance(CONTEXT_COURSE,$course->id);
+require_capability('moodle/course:viewprogress',$context);
+
+// Get group mode
+$group=groups_get_course_group($course,true); // Supposed to verify group
+if($group===0 && $course->groupmode==SEPARATEGROUPS) {
+ require_capability('moodle/site:accessallgroups',$context);
+}
+
+// Get data on activities and progress of all users, and give error if we've
+// nothing to display (no users or no activities)
+$reportsurl=$CFG->wwwroot.'/course/report.php?id='.$course->id;
+$completion=new completion_info($course);
+$activities=$completion->get_activities();
+if(count($activities)==0) {
+ print_error('err_noactivities','completion',$reportsurl);
+}
+$progress=$completion->get_progress_all($firstnamesort,$group);
+
+if($csv) {
+ header('Content-Type: text/csv; charset=ISO-8859-1');
+ header('Content-Disposition: attachment; filename=progress.'.
+ preg_replace('/[^a-z0-9-]/','_',strtolower($course->shortname)).'.csv');
+} else {
+ // Use SVG to draw sideways text if supported
+ $svgcleverness=check_browser_version('Firefox',2.0) && !$USER->screenreader;
+
+ // Navigation and header
+ $strreports = get_string("reports");
+ $strcompletion = get_string('completionreport','completion');
+ $navlinks = array();
+ $navlinks[] = array('name' => $strreports, 'link' => "../../report.php?id=$course->id", 'type' => 'misc');
+ $navlinks[] = array('name' => $strcompletion, 'link' => null, 'type' => 'misc');
+ if($svgcleverness) {
+ require_js(array('yui_yahoo','yui_event','yui_dom'));
+ }
+ print_header($strcompletion,$course->fullname,build_navigation($navlinks));
+ if($svgcleverness) {
+ print '<script type="text/javascript" src="textrotate.js"></script>';
+ }
+
+ // Handle groups (if enabled)
+ groups_print_course_menu($course,$CFG->wwwroot.'/course/report/progress/?course='.$course->id);
+}
+
+// Okay, let's draw the table of progress info,
+
+// Start of table
+if(!$csv) {
+ print '<br class="clearer"/>'; // ugh
+ if(count($progress)==0) {
+ print '<p class="nousers">'.get_string('err_nousers','completion').'</p>';
+ print '<p><a href="'.$reportsurl.'">'.get_string('continue').'</a></p>';
+ print_footer($course);
+ exit;
+ }
+ print '<table id="completion-progress" class="generaltable flexible boxaligncenter" style="text-align:left"><tr style="vertical-align:top">';
+
+ // User heading / sort option
+ print '<th scope="col" class="completion-sortchoice">';
+ if($firstnamesort) {
+ print
+ get_string('firstname').' / <a href="./?course='.$course->id.'">'.
+ get_string('lastname').'</a>';
+ } else {
+ print '<a href="./?course='.$course->id.'&sort=firstname">'.
+ get_string('firstname').'</a> / '.
+ get_string('lastname');
+ }
+ print '</th>';
+}
+
+// Activities
+foreach($activities as $activity) {
+ $activity->datepassed = $activity->completionexpected && $activity->completionexpected <= time();
+ $activity->datepassedclass=$activity->datepassed ? 'completion-expired' : '';
+
+ if($activity->completionexpected) {
+ $datetext=userdate($activity->completionexpected,get_string('strftimedate','langconfig'));
+ } else {
+ $datetext='';
+ }
+
+ if($csv) {
+ print ','.csv_quote($activity->name).','.csv_quote($datetext);
+ } else {
+ print '<th scope="col" class="'.$activity->datepassedclass.'">'.
+ '<a href="'.$CFG->wwwroot.'/mod/'.$activity->modname.
+ '/view.php?id='.$activity->id.'">'.
+ '<img src="'.$CFG->pixpath.'/mod/'.$activity->modname.'/icon.gif" alt="'.
+ get_string('modulename',$activity->modname).'" /> <span class="completion-activityname">'.
+ format_string($activity->name).'</span></a>';
+ if($activity->completionexpected) {
+ print '<div class="completion-expected"><span>'.$datetext.'</span></div>';
+ }
+ print '</th>';
+ }
+}
+
+if($csv) {
+ print "\n";
+} else {
+ print '</tr>';
+}
+
+// Row for each user
+foreach($progress as $user) {
+ // User name
+ if($csv) {
+ print csv_quote(fullname($user));
+ } else {
+ print '<tr><th scope="row"><a href="'.$CFG->wwwroot.'/user/view.php?id='.
+ $user->id.'&course='.$course->id.'">'.fullname($user).'</a></th>';
+ }
+
+ // Progress for each activity
+ foreach($activities as $activity) {
+
+ // Get progress information and state
+ if(array_key_exists($activity->id,$user->progress)) {
+ $progress=$user->progress[$activity->id];
+ $state=$progress->completionstate;
+ $date=userdate($progress->timemodified);
+ } else {
+ $state=COMPLETION_INCOMPLETE;
+ $date='';
+ }
+
+ // Work out how it corresponds to an icon
+ $completiontype=
+ ($activity->completion==COMPLETION_TRACKING_AUTOMATIC ? 'auto' : 'manual').
+ '-';
+ switch($state) {
+ case COMPLETION_INCOMPLETE : $completiontype.='n'; break;
+ case COMPLETION_COMPLETE : $completiontype.='y'; break;
+ case COMPLETION_COMPLETE_PASS : $completiontype.='pass'; break;
+ case COMPLETION_COMPLETE_FAIL : $completiontype.='fail'; break;
+ }
+
+ $describe=get_string('completion-alt-'.$completiontype,'completion');
+ $a=new StdClass;
+ $a->state=$describe;
+ $a->date=$date;
+ $a->user=fullname($user);
+ $a->activity=$activity->name;
+ $fulldescribe=get_string('progress-title','completion',$a);
+
+ if($csv) {
+ print ','.csv_quote($describe).','.csv_quote($date);
+ } else {
+ print '<td class="completion-progresscell '.$activity->datepassedclass.'">'.
+ '<img src="'.$CFG->pixpath.'/i/completion-'.$completiontype.
+ '.gif" alt="'.$describe.'" title="'.$fulldescribe.'" /></td>';
+ }
+ }
+
+ if($csv) {
+ print "\n";
+ } else {
+ print '</tr>';
+ }
+}
+
+if($csv) {
+ exit;
+}
+print '</table>';
+
+print '<ul class="progress-actions"><li><a href="index.php?course='.$course->id.
+ '&format=csv">'.get_string('csvdownload','completion').'</a></li></ul>';
+
+print_footer($course);
+?>
--- /dev/null
+<?php //$Id$
+
+ if (!defined('MOODLE_INTERNAL')) {
+ die('Direct access to this script is forbidden.'); // It must be included from a Moodle page
+ }
+
+ $completion=new completion_info($course);
+ if ($completion->is_enabled() && has_capability('moodle/course:viewprogress',$context)) {
+ echo '<p>';
+ echo '<a href="'.$CFG->wwwroot.'/course/report/progress/?course='.$course->id.'">'.get_string('completionreport','completion').'</a>';
+ echo '</p>';
+ }
+?>
--- /dev/null
+var SVGNS='http://www.w3.org/2000/svg',XLINKNS='http://www.w3.org/1999/xlink';
+
+function textrotate_make_svg(el)
+{
+ var string=el.firstChild.nodeValue;
+
+ // Create SVG
+ var svg=document.createElementNS(SVGNS,'svg');
+ svg.setAttribute('version','1.1');
+ var width=(el.offsetHeight*9)/8;
+ svg.setAttribute('width',width);
+ svg.setAttribute('height',el.offsetWidth+20);
+
+ var text=document.createElementNS(SVGNS,'text');
+ svg.appendChild(text);
+ text.setAttribute('x',el.offsetWidth);
+ text.setAttribute('y',-el.offsetHeight/4);
+ text.setAttribute('text-anchor','end');
+ text.setAttribute('transform','rotate(90)');
+ text.appendChild(document.createTextNode(string));
+
+ // Is there an icon near the text?
+ var icon=el.parentNode.firstChild;
+ if(icon.nodeName.toLowerCase()=='img') {
+ el.parentNode.removeChild(icon);
+ var image=document.createElementNS(SVGNS,'image');
+ var iconx=el.offsetHeight/4;
+ if(iconx>width-16) iconx=width-16;
+ image.setAttribute('x',iconx);
+ image.setAttribute('y',el.offsetWidth+4);
+ image.setAttribute('width',16);
+ image.setAttribute('height',16);
+ image.setAttributeNS(XLINKNS,'href',icon.src);
+ svg.appendChild(image);
+ }
+
+ // Replace original content with this new SVG
+ el.parentNode.insertBefore(svg,el);
+ el.parentNode.removeChild(el);
+}
+
+function textrotate_init() {
+ var elements=YAHOO.util.Dom.getElementsByClassName('completion-activityname', 'span');
+ for(var i=0;i<elements.length;i++)
+ {
+ var el=elements[i];
+ el.parentNode.parentNode.parentNode.style.verticalAlign='bottom';
+ textrotate_make_svg(el);
+ }
+
+ elements=YAHOO.util.Dom.getElementsByClassName('completion-expected', 'div');
+ for(var i=0;i<elements.length;i++)
+ {
+ var el=elements[i];
+ el.style.display='inline';
+ //el.style.fontSize='0.6em';
+ var parent=el.parentNode;
+ parent.removeChild(el);
+ parent.insertBefore(el,parent.firstChild);
+ textrotate_make_svg(el.firstChild);
+ }
+}
+
+YAHOO.util.Event.onDOMReady(textrotate_init);
+
--- /dev/null
+<?php
+// Toggles the manual completion flag for a particular activity and the current
+// user.
+
+require_once('../config.php');
+require_once($CFG->libdir.'/completionlib.php');
+
+// Parameters
+$cmid=required_param('id',PARAM_INT);
+$targetstate=required_param('completionstate',PARAM_INT);
+switch($targetstate) {
+ case COMPLETION_COMPLETE:
+ case COMPLETION_INCOMPLETE:
+ break;
+ default:
+ error('Unsupported completion state');
+}
+$fromajax=optional_param('fromajax',0,PARAM_INT);
+
+function error_or_ajax($message) {
+ global $fromajax;
+ if($fromajax) {
+ print $message;
+ exit;
+ } else {
+ error($message);
+ }
+}
+
+// Get course-modules entry
+if(!($cm=$DB->get_record('course_modules',array('id'=>$cmid)))) {
+ error_or_ajax('Activity ID unknown');
+}
+
+if(!($course=$DB->get_record('course',array('id'=>$cm->course)))) {
+ error_or_ajax('Missing course (database corrupt?)');
+}
+
+// Check user is logged in
+require_login($course);
+
+// Check completion state is manual
+if($cm->completion!=COMPLETION_TRACKING_MANUAL) {
+ error_or_ajax('Activity does not provide manual completion tracking');
+}
+
+// Now change state
+$completion=new completion_info($course);
+$completion->update_state($cm,$targetstate);
+
+// And redirect back to course
+if($fromajax) {
+ print 'OK';
+} else {
+ redirect('view.php?id='.$course->id);
+}
+?>
$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 '<script type="text/javascript" src="completion.js"></script>'.
+ '<script type="text/javascript">completion_strsaved="'.get_string('saved','completion').'"</script>';
+ }
+
// Course wrapper start.
echo '<div class="course-content">';
--- /dev/null
+<?php
+$string['activitycompletion']='Activity completion';
+$string['badautocompletion']='When you select automatic completion, you must also enable at least one requirement (below).';
+$string['completedunlocked']='Completion options unlocked';
+$string['completedunlockedtext']='When you save changes, completion state for all users will be erased. If you change your mind about this, do not save the form.';
+$string['completedwarning']='Completion options locked';
+$string['completedwarningtext']='One or more users ($a) has already marked this activity completed. Changing completion options will erase their completion state and may cause confusion. The options have been locked and we recommend that you do not unlock them unless absolutely necessary.';
+$string['completion']='Completion tracking';
+$string['completion-alt-auto-enabled']='The system marks this item complete according to conditions';
+$string['completion-alt-auto-fail']='Completed (did not achieve pass grade)';
+$string['completion-alt-auto-n']='Not completed';
+$string['completion-alt-auto-pass']='Completed (achieved pass grade)';
+$string['completion-alt-auto-y']='Completed';
+$string['completion-alt-manual-enabled']='Users can manually mark this item complete';
+$string['completion-alt-manual-n']='Not completed';
+$string['completion-alt-manual-y']='Completed';
+$string['completion-title-manual-n']='Mark as complete';
+$string['completion-title-manual-y']='Mark as not complete';
+$string['completion_automatic']='Show activity as complete when conditions are met';
+$string['completion_manual']='Users can manually mark the activity as completed';
+$string['completion_none']='Do not indicate activity completion';
+$string['completionenabled']='Enabled, control via activity settings';
+$string['completionexpected']='Expect completed on';
+$string['completiondisabled']='Disabled, not shown in activity settings';
+$string['completionreport']='Completion progress report';
+$string['completionusegrade']='Require grade';
+$string['completionusegrade_text']='User must receive a grade';
+$string['completionview']='Require view';
+$string['completionview_text']='User must view activity';
+$string['configenablecompletion'] = 'When enabled, this lets you turn on completion tracking (progress) features at course level.';
+$string['configprogresstrackedroles'] = 'Roles that are displayed in the progress-tracking screen. (Usually includes just students and equivalent roles.)';
+$string['csvdownload']='Download in spreadsheet format (.csv)';
+$string['enablecompletion'] = 'Enable completion tracking';
+$string['err_noactivities']='Completion information is not enabled for any activity, so none can be displayed. You can enable completion information by editing the settings for an activity.';
+$string['err_nousers']='There are no users on this course or group for whom completion information is displayed. (By default, completion information is displayed only for students, so if there are no students, you will see this error. Administrators can alter this option via the admin screens.)';
+$string['err_system']='An internal error occurred in the completion system. (System administrators can enable debugging information to see more detail.)';
+$string['help_completion']='completion tracking';
+$string['help_completionexpected']='the date completion is expected';
+$string['help_completionlocked']='locked completion options';
+$string['help_completionview']='requiring view to complete';
+$string['progress']='Student progress';
+$string['progress-title']='$a->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';
+?>
$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.';
$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';
--- /dev/null
+<h1>Completion tracking</h1>
+
+<p>
+If this option is turned on, the system will track whether students have
+completed the activity. Activity completion is shown to students beside each
+activity, and can also be viewed (for all their students) by teachers.
+</p>
+
+<p>
+There are three values for this option:
+</p>
+
+<ul>
+<li>Off. Completion information is not tracked for this activity.</li>
+<li>Manual. Students can tick a box beside the activity to indicate
+ that they have completed it.</li>
+<li>Automatic. Based on conditions set on this screen (immediately below the
+ dropdown), the activity will automatically be marked completed.</li>
+</ul>
+
+<p>
+Some types of activity do not support automatic conditions, so you can only
+choose Off or Manual.
+</p>
+
+
--- /dev/null
+<h1>Expect completed on</h1>
+
+<p>
+This optional field lets you associate a date with the activity. Dates do not
+affect the behaviour of the system (for example, you can still complete an
+activity after the date passes) and are not shown to students.
+They are displayed only when viewing the progress report.
+</p>
+
+<p>
+Teachers can use the dates on the progress report to help determine whether
+or not certain students might be falling behind. They could then decide to
+contact students and offer assistance.
+</p>
+
+<p>
+There is no need to complete this field unless you want a date to show in the
+progress report.
+</p>
\ No newline at end of file
--- /dev/null
+<h1>Locked completion options</h1>
+
+<p>
+If at least one person has completed an activity, completion options are
+'locked'. This is because changing these options may result in
+unexpected behaviour.
+</p>
+
+<h2>Potential confusion</h2>
+
+<p>
+If somebody has ticked an activity as
+manually completed, and you then set it to automatic completion, the activity will
+become unticked - very confusing for the student who had already ticked it!
+</p>
+
+<p>
+It is best not to unlock options unless you are sure it won't cause problems
+- for example, if you know that students don't have access to the course yet,
+so it will only be staff who have marked the activity completed when testing.
+</p>
+
+<h2>What happens when you unlock</h2>
+
+<p>
+Once you unlock options and then click
+'Save changes', all completion information for the activity will be deleted and,
+if possible, regenerated according to the new settings.
+</p>
+
+<ul>
+<li>If the new completion option is manual, everyone will be set to 'not completed'
+regardless of any previous setting.</li>
+<li>If it is automatic then, depending on the options chosen, the system may or may
+not be able to construct a correct current value for everyone.<ul>
+<li>The 'viewed' requirement will not work - even if a student has viewed the
+activity before, it will not be marked completed until they view it again.</li>
+<li>Most other options will be recalculated successfully.</li>
+</ul></li>
+</ul>
+
+<p>
+If you change completion options while a student is logged in, they may not see
+the changes for some minutes.
+</p>
+
--- /dev/null
+<h1>Require grade</h1>
+
+<p>
+When this option is turned on, students have to get a grade on the activity in
+order to complete it. For example, a quiz would be
+marked completed as soon as the user submits it.
+</p>
+
+<p>
+It does not matter how well the student did. Getting any grade will mark
+the activity completed.
+</p>
+
+<h2>Distinguishing between pass and fail</h2>
+
+<p>
+It is possible to distinguish between 'pass' and 'fail' grades so that the
+activity becomes 'completed, passed' or 'completed, not passed' instead of just
+'completed'. These results show a different icon and alternative text.
+</p>
+
+<p>
+To set this up, you need to specify the pass value for this activity's
+individual grade:
+</p>
+
+<ol>
+<li>Go to the course gradebook by clicking the 'Grades' link on the course administration
+block.</li>
+<li>From the 'Choose an action' dropdown, pick 'Categories and items'.</li>
+<li>Click the Edit icon next to the grade item for this activity.</li>
+<li>Turn on 'Show Advanced'.</li>
+<li>Type a grade value (e.g. 5.0) in the 'Grade to pass' box.</li>
+</ol>
+
+<p>
+Once you have done this, anybody submitting the quiz will receive either the
+pass or fail completion icon. If the quiz can be taken multiple times, the
+completion icon will automatically update whenever the grade does.
+</p>
+
+<p>
+There is one limitation: this only works if grades are immediately visible to
+students. The grade must be neither permanently hidden, nor hidden until a certain
+date. If a grade is hidden then only the standard 'completed' state will be
+displayed - even once the hidden date has passed.
+</p>
\ No newline at end of file
--- /dev/null
+<h1>Require view</h1>
+
+<p>
+When this option is turned on, students have to view the activity in order
+to complete it.
+</p>
+
+<ul>
+<li>In most cases, clicking the link is enough to 'view' the activity.</li>
+<li>You should usually not turn on the 'view' condition if you have other
+requirements - this makes extra work for the server and it's unlikely that a
+student could meet any other conditions without viewing the activity.</li>
+</ul>
+
--- /dev/null
+<h1>Forum completion</h1>
+
+<p>
+The forum provides three special options which you can require in order that
+it counts as completed.
+</p>
+
+<ul>
+<li>User must post discussions or replies: the student must post a certain
+number of times to the forum, and it doesn't matter whether those posts are
+new discussions or replies to existing discussions.</li>
+<li>User must create discussions: the student most post a certain number of
+new discussions.</li>
+<li>User must post replies: the student must post a certain number of replies
+in existing discussions.</li>
+</ul>
+
+<p>
+You can tick more than one option if necessary; this means both conditions
+must be met before the forum counts as completed. For example, if you
+require 2 discussions and 10 'discussions or replies', then any combination
+of 10 discussions/replies will be sufficient provided that it includes at least
+2 discussions. (10 new discussions; 2 new discussions and 8 replies; or
+somewhere in between.)
+</p>
+
+
+
+
$dbdirs[] = $CFG->dirroot.'/'.$CFG->admin.'/report/'.$plugin.'/db';
}
}
-
+
/// Now quiz report plugins (mod/quiz/report/xxx/db)
if ($plugins = get_list_of_plugins('mod/quiz/report', 'db')) {
foreach ($plugins as $plugin) {
$data = 0;
}
// $data is a string
- $validated = $this->validate($data);
+ $validated = $this->validate($data);
if ($validated !== true) {
return $validated;
}
$defaultinfo = $default;
if (!is_null($default) and $default !== '') {
$defaultinfo = "\n".$default;
- }
+ }
return format_admin_setting($this, $this->visiblename,
'<div class="form-textarea form-textarea-advanced" ><textarea rows="'. $this->rows .'" cols="'. $this->cols .'" id="'. $this->get_id() .'" name="'. $this->get_full_name() .'">'. s($data) .'</textarea></div>',
}
}
}
-
+
$options = array();
$defaults = array();
foreach($this->choices as $key=>$description) {
$return .= '</div>';
return format_admin_setting($this, $this->visiblename, $return, $this->description, false, '', $defaultinfo, $query);
-
+
}
}
if (strpos($textlib->strtolower($value), $query) !== false) {
return true;
}
- }
+ }
return false;
}
function write_setting($data) {
global $DB;
$data = trim($data);
- $validated = $this->validate($data);
+ $validated = $this->validate($data);
if ($validated !== true) {
return $validated;
}
/**
- * Graded roles in gradebook
+ * Admin setting that allows a user to pick appropriate roles for something.
*/
-class admin_setting_special_gradebookroles extends admin_setting_configmulticheckbox {
- function admin_setting_special_gradebookroles() {
- parent::admin_setting_configmulticheckbox('gradebookroles', get_string('gradebookroles', 'admin'),
- get_string('configgradebookroles', 'admin'), NULL, NULL);
+class admin_setting_pickroles extends admin_setting_configmulticheckbox {
+ private $types;
+
+ /**
+ * @param string $name Name of config variable
+ * @param string $visiblename Display name
+ * @param string $description Description
+ * @param array $types Array of capabilities (usually moodle/legacy:something)
+ * which identify roles that will be enabled by default. Default is the
+ * student role
+ */
+ function admin_setting_pickroles($name, $visiblename, $description,$types=array('moodle/legacy:student')) {
+ parent::admin_setting_configmulticheckbox($name, $visiblename, $description, NULL, NULL);
+ $this->types=$types;
}
function load_choices() {
function get_defaultsetting() {
global $CFG;
if (empty($CFG->rolesactive)) {
- return NULL;
+ return array(0);
}
$result = array();
- if ($studentroles = get_roles_with_capability('moodle/legacy:student', CAP_ALLOW)) {
- foreach ($studentroles as $studentrole) {
- $result[$studentrole->id] = '1';
+ foreach($this->types as $capability) {
+ if ($caproles = get_roles_with_capability($capability, CAP_ALLOW)) {
+ foreach ($caproles as $caprole) {
+ if(!in_array($caprole->id,$result)) {
+ $result[] = $caprole->id;
+ }
+ }
}
}
return $result;
}
}
+/**
+ * Graded roles in gradebook
+ */
+class admin_setting_special_gradebookroles extends admin_setting_pickroles {
+ function admin_setting_special_gradebookroles() {
+ parent::admin_setting_pickroles('gradebookroles', get_string('gradebookroles', 'admin'),
+ get_string('configgradebookroles', 'admin'));
+ }
+}
+
+
class admin_setting_regradingcheckbox extends admin_setting_configcheckbox {
function write_setting($data) {
global $CFG, $DB;
}
return $return;
- }
+ }
}
/**
* Which roles to show on course decription page
*/
-class admin_setting_special_coursemanager extends admin_setting_configmulticheckbox {
+class admin_setting_special_coursemanager extends admin_setting_pickroles {
function admin_setting_special_coursemanager() {
- parent::admin_setting_configmulticheckbox('coursemanager', get_string('coursemanager', 'admin'),
- get_string('configcoursemanager', 'admin'), NULL, NULL);
- }
-
- function load_choices() {
- global $DB;
- if (is_array($this->choices)) {
- return true;
- }
- if ($roles = $DB->get_records('role',null,'sortorder')) {
- $this->choices = array();
- foreach($roles as $role) {
- $this->choices[$role->id] = format_string($role->name);
- }
- return true;
- }
- return false;
- }
-
- function get_defaultsetting() {
- global $CFG;
- if (empty($CFG->rolesactive)) {
- return NULL;
- }
- $result = array();
- if ($teacherroles = get_roles_with_capability('moodle/legacy:editingteacher', CAP_ALLOW)) {
- foreach ($teacherroles as $teacherrole) {
- $result[$teacherrole->id] = '1';
- }
- }
- return $result;
+ parent::admin_setting_pickroles('coursemanager', get_string('coursemanager', 'admin'),
+ get_string('configcoursemanager', 'admin'),
+ 'moodle/legacy:editingteacher');
}
}
$defaultinfo[] = get_string('advanced');
}
$defaultinfo = implode(', ', $defaultinfo);
-
+
} else {
$defaultinfo = NULL;
}
/**
* Prints tables of detected plugins, one table per plugin type,
- * and prints whether they are part of the standard Moodle
+ * and prints whether they are part of the standard Moodle
* distribution or not.
*/
function print_plugin_tables() {
'scorm',
'survey',
'wiki');
-
+
$plugins_standard['blocks'] = array('activity_modules',
'admin',
'admin_bookmarks',
'tag_flickr',
'tag_youtube',
'tags');
-
+
$plugins_standard['filter'] = array('activitynames',
'algebra',
'censor',
$plugins_ondisk['mod'] = get_list_of_plugins('mod', 'db');
$plugins_ondisk['blocks'] = get_list_of_plugins('blocks', 'db');
$plugins_ondisk['filter'] = get_list_of_plugins('filter', 'db');
-
+
$strstandard = get_string('standard');
$strnonstandard = get_string('nonstandard');
$strmissingfromdisk = '(' . get_string('missingfromdisk') . ')';
$strabouttobeinstalled = '(' . get_string('abouttobeinstalled') . ')';
$html = '';
-
+
$html .= '<table class="generaltable plugincheckwrapper" cellspacing="4" cellpadding="1"><tr valign="top">';
foreach ($plugins_ondisk as $cat => $list_ondisk) {
$html .= '<tr class="r0"><th class="header c0">' . get_string('directory') . "</th>\n"
. '<th class="header c1">' . get_string('name') . "</th>\n"
. '<th class="header c2">' . get_string('status') . "</th>\n</tr>\n";
-
- $row = 1;
+
+ $row = 1;
foreach ($list_ondisk as $k => $plugin) {
$status = 'ok';
if (!in_array($plugin, $plugins_standard[$cat])) {
$standard = 'nonstandard';
$status = 'warning';
- }
-
+ }
+
// Get real name and full path of plugin
$plugin_name = "[[$plugin]]";
-
+
$plugin_path = "$cat/$plugin";
-
+
$plugin_name = get_plugin_name($plugin, $cat);
-
+
// Determine if the plugin is about to be installed
if ($cat != 'filter' && !in_array($plugin, $plugins_installed[$cat])) {
$note = $strabouttobeinstalled;
// If the plugin was both on disk and in the db, unset the value from the installed plugins list
if ($key = array_search($plugin, $plugins_installed[$cat])) {
unset($plugins_installed[$cat][$key]);
- }
- }
+ }
+ }
// If there are plugins left in the plugins_installed list, it means they are missing from disk
- foreach ($plugins_installed[$cat] as $k => $missing_plugin) {
+ foreach ($plugins_installed[$cat] as $k => $missing_plugin) {
// Make sure the plugin really is missing from disk
if (!in_array($missing_plugin, $plugins_ondisk[$cat])) {
$standard = 'standard';
. "<td class=\"cell c0\">?</td>\n"
. "<td class=\"cell c1\">$plugin_name</td>\n"
. "<td class=\"$standard $status cell c2\">" . ${'str' . $standard} . " $strmissingfromdisk</td>\n</tr>\n";
- $row++;
+ $row++;
}
}
$html .= '</table></td>';
}
-
+
$html .= '</tr></table><br />';
-
+
echo $html;
}
--- /dev/null
+<?php
+// Contains a class used for tracking whether activities have been completed
+// by students ('completion')
+
+// Completion top-level options (admin setting enablecompletion)
+
+/** The completion system is enabled in this site/course */
+define('COMPLETION_ENABLED',1);
+/** The completion system is not enabled in this site/course */
+define('COMPLETION_DISABLED',0);
+
+// Completion tracking options per-activity (course_modules/completion)
+
+/** Completion tracking is disabled for this activity */
+define('COMPLETION_TRACKING_NONE',0);
+/** Manual completion tracking (user ticks box) is enabled for this activity */
+define('COMPLETION_TRACKING_MANUAL',1);
+/** Automatic completion tracking (system ticks box) is enabled for this activity */
+define('COMPLETION_TRACKING_AUTOMATIC',2);
+
+// Completion state values (course_modules_completion/completionstate)
+
+/** The user has not completed this activity. */
+define('COMPLETION_INCOMPLETE',0);
+/** The user has completed this activity. It is not specified whether they have
+ * passed or failed it. */
+define('COMPLETION_COMPLETE',1);
+/** The user has completed this activity with a grade above the pass mark. */
+define('COMPLETION_COMPLETE_PASS',2);
+/** The user has completed this activity but their grade is less than the pass mark */
+define('COMPLETION_COMPLETE_FAIL',3);
+
+// Completion effect changes (used only in update_state)
+
+/** The effect of this change to completion status is unknown. */
+define('COMPLETION_UNKNOWN',-1);
+/** The user's grade has changed, so their new state might be
+ * COMPLETION_COMPLETE_PASS or COMPLETION_COMPLETE_FAIL. */
+// TODO Is this useful?
+define('COMPLETION_GRADECHANGE',-2);
+
+// Whether view is required to create an activity (course_modules/completionview)
+
+/** User must view this activity */
+define('COMPLETION_VIEW_REQUIRED',1);
+/** User does not need to view this activity */
+define('COMPLETION_VIEW_NOT_REQUIRED',0);
+
+// Completion viewed state (course_modules_completion/viewed)
+
+/** User has viewed this activity */
+define('COMPLETION_VIEWED',1);
+/** User has not viewed this activity */
+define('COMPLETION_NOT_VIEWED',0);
+
+// Completion cacheing
+
+/** Cache expiry time in seconds (10 minutes) */
+define('COMPLETION_CACHE_EXPIRY',10*60);
+
+// Combining completion condition. This is also the value you should return
+// if you don't have any applicable conditions.
+/** Completion details should be ORed together and you should return false if
+ none apply */
+define('COMPLETION_OR',false);
+/** Completion details should be ANDed together and you should return true if
+ none apply */
+define('COMPLETION_AND',true);
+
+/**
+ * Class represents completion information for a course.
+ * (Does not contain any data, so you can safely construct it multiple times
+ * without causing any problems.)
+ */
+class completion_info {
+ private $course;
+
+ /**
+ * Constructs with course details.
+ *
+ * @param object $course Moodle course object. Must have at least ->id, ->enablecompletion
+ * @return completion_info
+ */
+ public function completion_info($course) {
+ $this->course=$course;
+ }
+
+ /**
+ * Static function. Determines whether completion is enabled across entire
+ * site.
+ *
+ * @return int COMPLETION_ENABLED (true) if completion is enabled for the site,
+ * COMPLETION_DISABLED (false) if it's complete
+ */
+ public static function is_enabled_for_site() {
+ global $CFG;
+ return $CFG->enablecompletion;
+ }
+
+ /**
+ * Checks whether completion is enabled in a particular course and possibly
+ * activity.
+ *
+ * @param object $cm Course-module object. If not specified, returns the course
+ * completion enable state.
+ * @return COMPLETION_ENABLED or COMPLETION_DISABLED (==0) in the case of
+ * site and course; COMPLETION_TRACKING_MANUAL, _AUTOMATIC or _NONE (==0)
+ * for a course-module.
+ */
+ public function is_enabled($cm=null) {
+ // First check global completion
+ global $CFG;
+ if($CFG->enablecompletion==COMPLETION_DISABLED) {
+ return COMPLETION_DISABLED;
+ }
+
+ // Check course completion
+ if($this->course->enablecompletion==COMPLETION_DISABLED) {
+ return COMPLETION_DISABLED;
+ }
+
+ // If there was no $cm and we got this far, then it's enabled
+ if(!$cm) {
+ return COMPLETION_ENABLED;
+ }
+
+ // Return course-module completion value
+ return $cm->completion;
+ }
+
+ /**
+ * Updates (if necessary) the completion state of activity $cm for the given
+ * user.
+ * <p>
+ * For manual completion, this function is called when completion is toggled
+ * with $possibleresult set to the target state.
+ * <p>
+ * For automatic completion, this function should be called every time a module
+ * does something which might influence a user's completion state. For example,
+ * if a forum provides options for marking itself 'completed' once a user makes
+ * N posts, this function should be called every time a user makes a new post.
+ * [After the post has been saved to the database]. When calling, you do not
+ * need to pass in the new completion state. Instead this function carries out
+ * completion calculation by checking grades and viewed state itself, and
+ * calling the involved module via modulename_get_completion_state() to check
+ * module-specific conditions.
+ *
+ * @param object $cm Course-module
+ * @param int $possibleresult Expected completion result. If the event that
+ * has just occurred (e.g. add post) can only result in making the activity
+ * complete when it wasn't before, use COMPLETION_COMPLETE. If the event that
+ * has just occurred (e.g. delete post) can only result in making the activity
+ * not complete when it was previously complete, use COMPLETION_INCOMPLETE.
+ * Otherwise use COMPLETION_UNKNOWN. Setting this value to something other than
+ * COMPLETION_UNKNOWN significantly improves performance because it will abandon
+ * processing early if the user's completion state already matches the expected
+ * result. For manual events, COMPLETION_COMPLETE or COMPLETION_INCOMPLETE
+ * must be used; these directly set the specified state.
+ * @param int $userid User ID to be updated. Default 0 = current user
+ */
+ public function update_state($cm,$possibleresult=COMPLETION_UNKNOWN,$userid=0) {
+ global $USER,$SESSION;
+ // Do nothing if completion is not enabled for that activity
+ if(!$this->is_enabled($cm)) {
+ return;
+ }
+
+ // Get current value of completion state and do nothing if it's same as
+ // the possible result of this change. If the change is to COMPLETE and the
+ // current value is one of the COMPLETE_xx subtypes, ignore that as well
+ $current=$this->get_data($cm,false,$userid);
+ if($possibleresult==$current->completionstate ||
+ ($possibleresult==COMPLETION_COMPLETE &&
+ ($current->completionstate==COMPLETION_COMPLETE_PASS ||
+ $current->completionstate==COMPLETION_COMPLETE_FAIL))) {
+ return;
+ }
+
+ if($cm->completion==COMPLETION_TRACKING_MANUAL) {
+ // For manual tracking we set the result directly
+ switch($possibleresult) {
+ case COMPLETION_COMPLETE:
+ case COMPLETION_INCOMPLETE:
+ $newstate=$possibleresult;
+ break;
+ default:
+ $this->internal_systemerror("Unexpected manual completion state for {$cm->id}: $possibleresult");
+ }
+ } else {
+ // Automatic tracking; get new state
+ $newstate=$this->internal_get_state($cm,$userid,$current);
+ }
+
+ // If changed, update
+ if($newstate!=$current->completionstate) {
+ $current->completionstate=$newstate;
+ $current->timemodified=time();
+ $this->internal_set_data($cm,$current);
+ }
+ }
+
+ /**
+ * Calculates the completion state for an activity and user.
+ * <p>
+ * (Internal function. Not private, so we can unit-test it.)
+ *
+ * @param object $cm Activity
+ * @param int $userid ID of user
+ * @param object $current Previous completion information from database
+ * @return unknown
+ */
+ function internal_get_state($cm,$userid,$current) {
+ // Get user ID
+ global $USER,$DB;
+ if(!$userid) {
+ $userid=$USER->id;
+ }
+
+ // Check viewed
+ if($cm->completionview==COMPLETION_VIEW_REQUIRED &&
+ $current->viewed==COMPLETION_NOT_VIEWED) {
+ return COMPLETION_INCOMPLETE;
+ }
+
+ // Modname hopefully is provided in $cm but just in case it isn't, let's grab it
+ if(!isset($cm->modname)) {
+ $cm->modname=$DB->get_field('modules','name',array('id'=>$cm->module));
+ }
+
+ $newstate=COMPLETION_COMPLETE;
+
+ // Check grade
+ if(!is_null($cm->completiongradeitemnumber)) {
+ $item=grade_item::fetch(array('courseid'=>$cm->course,'itemtype'=>'mod',
+ 'itemmodule'=>$cm->modname,'iteminstance'=>$cm->instance,
+ 'itemnumber'=>$cm->completiongradeitemnumber));
+ if($item) {
+ // Fetch 'grades' (will be one or none)
+ $grades=grade_grade::fetch_users_grades($item,array($userid),false);
+ if(empty($grades)) {
+ // No grade for user
+ return COMPLETION_INCOMPLETE;
+ }
+ if(count($grades)>1) {
+ $this->internal_systemerror("Unexpected result: multiple grades for
+ item '{$item->id}', user '{$userid}'");
+ }
+ $newstate=$this->internal_get_grade_state($item,reset($grades));
+ if($newstate==COMPLETION_INCOMPLETE) {
+ return COMPLETION_INCOMPLETE;
+ }
+ } else {
+ $this->internal_systemerror("Cannot find grade item for '{$cm->modname}'
+ cm '{$cm->id}' matching number '{$cm->completiongradeitemnumber}'");
+ }
+ }
+
+ if(plugin_supports('mod',$cm->modname,FEATURE_COMPLETION_HAS_RULES)) {
+ $function=$cm->modname.'_get_completion_state';
+ if(!function_exists($function)) {
+ $this->internal_systemerror("Module {$cm->modname} claims to support
+ FEATURE_COMPLETION_HAS_RULES but does not have required
+ {$cm->modname}_get_completion_state function");
+ }
+ if(!$function($this->course,$cm,$userid,COMPLETION_AND)) {
+ return COMPLETION_INCOMPLETE;
+ }
+ }
+
+ return $newstate;
+
+ }
+
+
+ /**
+ * Marks a module as viewed.
+ * <p>
+ * Should be called whenever a module is 'viewed' (it is up to the module how to
+ * determine that). Has no effect if viewing is not set as a completion condition.
+ *
+ * @param object $cm Activity
+ * @param int $userid User ID or 0 (default) for current user
+ */
+ public function set_module_viewed($cm,$userid=0) {
+ // Don't do anything if view condition is not turned on
+ if($cm->completionview==COMPLETION_VIEW_NOT_REQUIRED || !$this->is_enabled($cm)) {
+ return;
+ }
+ // Get current completion state
+ $data=$this->get_data($cm,$userid);
+ // If we already viewed it, don't do anything
+ if($data->viewed==COMPLETION_VIEWED) {
+ return;
+ }
+ // OK, change state, save it, and update completion
+ $data->viewed=COMPLETION_VIEWED;
+ $this->internal_set_data($cm,$data);
+ $this->update_state($cm,COMPLETION_COMPLETE,$userid);
+ }
+
+ /**
+ * Determines how much completion data exists for an activity. This is used when
+ * deciding whether completion information should be 'locked' in the module
+ * editing form.
+ *
+ * @param object $cm Activity
+ * @return int The number of users who have completion data stored for this
+ * activity, 0 if none
+ */
+ public function count_user_data($cm) {
+ global $CFG,$DB;
+
+ return $DB->get_field_sql("
+ SELECT
+ COUNT(1)
+ FROM
+ {$CFG->prefix}course_modules_completion
+ WHERE
+ coursemoduleid=? AND completionstate<>0",array($cm->id));
+ }
+
+ /**
+ * Deletes completion state related to an activity for all users.
+ * <p>
+ * Intended for use only when the activity itself is deleted.
+ *
+ * @param object $cm Activity
+ */
+ public function delete_all_state($cm) {
+ global $SESSION,$DB;
+
+ // Delete from database
+ $DB->delete_records('course_modules_completion',array('coursemoduleid'=>$cm->id));
+
+ // Erase cache data for current user if applicable
+ if(isset($SESSION->completioncache) &&
+ array_key_exists($cm->course,$SESSION->completioncache) &&
+ array_key_exists($cm->id,$SESSION->completioncache[$cm->course])) {
+ unset($SESSION->completioncache[$cm->course][$cm->id]);
+ }
+ }
+
+ /**
+ * Recalculates completion state related to an activity for all users.
+ * <p>
+ * Intended for use if completion conditions change. (This should be avoided
+ * as it may cause some things to become incomplete when they were previously
+ * complete, with the effect - for example - of hiding a later activity that
+ * was previously available.)
+ *
+ * @param object $cm Activity
+ */
+ public function reset_all_state($cm) {
+ global $DB;
+ // Get current list of users with completion state
+ $rs=$DB->get_recordset('course_modules_completion',array('coursemoduleid'=>$cm->id),'','userid');
+ $keepusers=array();
+ foreach($rs as $rec) {
+ $keepusers[]=$rec->userid;
+ }
+ $rs->close();
+
+ // Delete all existing state [also clears session cache for current user]
+ $this->delete_all_state($cm);
+
+ // Merge this with list of planned users (according to roles)
+ $trackedusers=$this->internal_get_tracked_users(false);
+ foreach($trackedusers as $trackeduser) {
+ $keepusers[]=$trackeduser->id;
+ }
+ $keepusers=array_unique($keepusers);
+
+ // Recalculate state for each kept user
+ foreach($keepusers as $keepuser) {
+ $this->update_state($cm,COMPLETION_UNKNOWN,$keepuser);
+ }
+ }
+
+ /**
+ * Obtains completion data for a particular activity and user (from the
+ * session cache if available, or by SQL query)
+ *
+ * @param object $cm Activity
+ * @param bool $wholecourse If true (default false) then, when necessary to
+ * fill the cache, retrieves information from the entire course not just for
+ * this one activity
+ * @param int $userid User ID or 0 (default) for current user
+ * @param array $modinfo For unit testing only, supply the value
+ * here. Otherwise the method calls get_fast_modinfo
+ * @return object Completion data (record from course_modules_completion)
+ * @throws Exception In some cases where the requested course-module is not
+ * found on the specified course
+ */
+ public function get_data($cm,$wholecourse=false,$userid=0,$modinfo=null) {
+ // Get user ID
+ global $USER,$CFG,$SESSION,$DB;
+ if(!$userid) {
+ $userid=$USER->id;
+ }
+
+ // Is this the current user?
+ $currentuser=$userid==$USER->id;
+
+ if($currentuser) {
+ // Make sure cache is present
+ if(!isset($SESSION->completioncache)) {
+ $SESSION->completioncache=array();
+ }
+ // Expire any old data from cache
+ foreach($SESSION->completioncache as $courseid=>$activities) {
+ if(empty($activities['updated']) || $activities['updated'] < time()-COMPLETION_CACHE_EXPIRY) {
+ unset($SESSION->completioncache[$courseid]);
+ }
+ }
+ // See if requested data is present, if so use cache to get it
+ if(isset($SESSION->completioncache) &&
+ array_key_exists($this->course->id,$SESSION->completioncache) &&
+ array_key_exists($cm->id,$SESSION->completioncache[$this->course->id])) {
+ return $SESSION->completioncache[$this->course->id][$cm->id];
+ }
+ }
+
+ // Not there, get via SQL
+ if($currentuser && $wholecourse) {
+ // Get whole course data for cache
+ $alldatabycmc=$DB->get_records_sql("
+ SELECT
+ cmc.*
+ FROM
+ {$CFG->prefix}course_modules cm
+ INNER JOIN {$CFG->prefix}course_modules_completion cmc ON cmc.coursemoduleid=cm.id
+ WHERE
+ cm.course=? AND cmc.userid=?",array($this->course->id,$userid));
+
+ // Reindex by cm id
+ $alldata=array();
+ if($alldatabycmc) {
+ foreach($alldatabycmc as $data) {
+ $alldata[$data->coursemoduleid]=$data;
+ }
+ }
+
+ // Get the module info and build up condition info for each one
+ if(empty($modinfo)) {
+ $modinfo=get_fast_modinfo($this->course,$userid);
+ }
+ foreach($modinfo->cms as $othercm) {
+ if(array_key_exists($othercm->id,$alldata)) {
+ $data=$alldata[$othercm->id];
+ } else {
+ // Row not present counts as 'not complete'
+ $data=new StdClass;
+ $data->id=0;
+ $data->coursemoduleid=$othercm->id;
+ $data->userid=$userid;
+ $data->completionstate=0;
+ $data->viewed=0;
+ $data->timemodified=0;
+ }
+ $SESSION->completioncache[$this->course->id][$othercm->id]=$data;
+ }
+ $SESSION->completioncache[$this->course->id]['updated']=time();
+
+ if(!isset($SESSION->completioncache[$this->course->id][$cm->id])) {
+ $this->internal_systemerror("Unexpected error: course-module {$cm->id} could not be found on course {$this->course->id}");
+ }
+ return $SESSION->completioncache[$this->course->id][$cm->id];
+ } else {
+ // Get single record
+ $data=$DB->get_record('course_modules_completion',array('coursemoduleid'=>$cm->id,'userid'=>$userid));
+ if($data==false) {
+ // Row not present counts as 'not complete'
+ $data=new StdClass;
+ $data->id=0;
+ $data->coursemoduleid=$cm->id;
+ $data->userid=$userid;
+ $data->completionstate=0;
+ $data->viewed=0;
+ $data->timemodified=0;
+ }
+
+ // Put in cache
+ if($currentuser) {
+ $SESSION->completioncache[$this->course->id][$cm->id]=$data;
+ // For single updates, only set date if it was empty before
+ if(empty($SESSION->completioncache[$this->course->id]['updated'])) {
+ $SESSION->completioncache[$this->course->id]['updated']=time();
+ }
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Updates completion data for a particular coursemodule and user (user is
+ * determined from $data).
+ * <p>
+ * (Internal function. Not private, so we can unit-test it.)
+ *
+ * @param object $cm Activity
+ * @param object $data Data about completion for that user
+ */
+ function internal_set_data($cm,$data) {
+ global $USER,$SESSION,$DB;
+ if($data->id) {
+ // Has real (nonzero) id meaning that a database row exists
+ $DB->update_record('course_modules_completion',$data);
+ } else {
+ // Didn't exist before, needs creating
+ $data->id=$DB->insert_record('course_modules_completion',$data);
+ }
+ if($data->userid==$USER->id) {
+ $SESSION->completioncache[$cm->course][$cm->id]=$data;
+ }
+ }
+
+ /**
+ * Obtains a list of activities for which completion is enabled on the
+ * course. The list is ordered by the section order of those activities.
+ * @param array $modinfo For unit testing only, supply the value
+ * here. Otherwise the method calls get_fast_modinfo
+ * @return array Array from $cmid => $cm of all activities with completion enabled,
+ * empty array if none
+ */
+ public function get_activities($modinfo=null) {
+ global $DB;
+
+ // Obtain those activities which have completion turned on
+ $withcompletion=$DB->get_records_select('course_modules','course='.$this->course->id.
+ ' AND completion<>'.COMPLETION_TRACKING_NONE);
+ if(count($withcompletion)==0) {
+ return array();
+ }
+
+ // Use modinfo to get section order and also add in names
+ if(empty($modinfo)) {
+ $modinfo=get_fast_modinfo($this->course);
+ }
+ $result=array();
+ foreach($modinfo->sections as $sectioncms) {
+ foreach($sectioncms as $cmid) {
+ if(array_key_exists($cmid,$withcompletion)) {
+ $result[$cmid]=$withcompletion[$cmid];
+ $result[$cmid]->modname=$modinfo->cms[$cmid]->modname;
+ $result[$cmid]->name=$modinfo->cms[$cmid]->name;
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Gets list of users in a course whose progress is tracked for display on the
+ * progress report.
+ * @param bool $sortfirstname True to sort with firstname
+ * @param int $groupid Optionally restrict to groupid
+ * @return array Array of user objects containing id, firstname, lastname (empty if none)
+ */
+ function internal_get_tracked_users($sortfirstname,$groupid=0) {
+ global $CFG,$DB;
+ if(!empty($CFG->progresstrackedroles)) {
+ $roles=explode(',',$CFG->progresstrackedroles);
+ } else {
+ // This causes it to default to everyone (if there is no student role)
+ $roles=array();
+ }
+ $users=get_role_users($roles,get_context_instance(CONTEXT_COURSE,$this->course->id),true,
+ 'u.id,u.firstname,u.lastname',
+ $sortfirstname ? 'u.firstname ASC' : 'u.lastname ASC',true,$groupid);
+ $users=$users ? $users : array(); // In case it returns false
+ return $users;
+ }
+
+ /**
+ * Obtains progress information across a course for all users on that course, or
+ * for all users in a specific group. Intended for use when displaying progress.
+ * <p>
+ * This includes only users who, in course context, have one of the roles for
+ * which progress is tracked (the progresstrackedroles admin option).
+ * <p>
+ * Users are included (in the first array) even if they do not have
+ * completion progress for any course-module.
+ *
+ * @param bool $sortfirstname If true, sort by first name, otherwise sort by
+ * last name
+ * @param int $groupid Group ID or 0 (default)/false for all groups
+ * @return Array of user objects (like mdl_user id, firstname, lastname)
+ * containing an additional ->progress array of coursemoduleid => completionstate
+ */
+ public function get_progress_all($sortfirstname=false,$groupid=0) {
+ global $CFG,$DB;
+
+ // Get list of applicable users
+ $users=$this->internal_get_tracked_users($sortfirstname,$groupid);
+
+ // Get progress information for these users in groups of 1,000 (if needed)
+ // to avoid making the SQL IN too long
+ $result=array();
+ $userids=array();
+ foreach($users as $user) {
+ $userids[]=$user->id;
+ $result[$user->id]=$user;
+ $result[$user->id]->progress=array();
+ }
+
+ for($i=0;$i<count($userids);$i+=1000) {
+ $blocksize=count($userids)-$i < 1000 ? count($userids)-$i : 1000;
+
+ list($insql,$params)=$DB->get_in_or_equal(array_slice($userids,$i,$blocksize));
+ array_splice($params,0,0,array($this->course->id));
+ $rs=$DB->get_recordset_sql("
+SELECT
+ cmc.*
+FROM
+ {$CFG->prefix}course_modules cm
+ INNER JOIN {$CFG->prefix}course_modules_completion cmc ON cm.id=cmc.coursemoduleid
+WHERE
+ cm.course=? AND cmc.userid $insql
+ ",$params);
+ if(!$rs) {
+ $this->internal_systemerror('Failed to obtain completion progress');
+ }
+ foreach($rs as $progress) {
+ $result[$progress->userid]->progress[$progress->coursemoduleid]=$progress;
+ }
+ $rs->close();
+ }
+
+ return $result;
+ }
+
+ public function inform_grade_changed($cm,&$item,&$grade,$deleted) {
+ // Bail out now if completion is not enabled for course-module, grade
+ // is not used to compute completion, or this is a different numbered
+ // grade
+ if(!$this->is_enabled($cm) ||
+ is_null($cm->completiongradeitemnumber) ||
+ $item->itemnumber!=$cm->completiongradeitemnumber) {
+ return;
+ }
+
+ // What is the expected result based on this grade?
+ if($deleted) {
+ // Grade being deleted, so only change could be to make it incomplete
+ $possibleresult=COMPLETION_INCOMPLETE;
+ } else {
+ $possibleresult=$this->internal_get_grade_state($item,$grade);
+ }
+
+ // OK, let's update state based on this
+ $this->update_state($cm,$possibleresult,$grade->userid);
+ }
+
+ /**
+ * Calculates the completion state that would result from a graded item
+ * (where grade-based completion is turned on) based on the actual grade
+ * and settings.
+ * <p>
+ * (Internal function. Not private, so we can unit-test it.)
+ *
+ * @param grade_item &$item
+ * @param grade_grade &$grade
+ * @return int Completion state e.g. COMPLETION_INCOMPLETE
+ */
+ function internal_get_grade_state(&$item,&$grade) {
+ if(!$grade) {
+ return COMPLETION_INCOMPLETE;
+ }
+ // Conditions to show pass/fail:
+ // a) Grade has pass mark (default is 0.00000 which is boolean true so be careful)
+ // b) Grade is visible (neither hidden nor hidden-until)
+ if($item->gradepass && $item->gradepass>0.000009 && !$item->hidden) {
+ // Use final grade if set otherwise raw grade
+ $score=!is_null($grade->finalgrade) ? $grade->finalgrade : $grade->rawgrade;
+
+ // We are displaying and tracking pass/fail
+ if($score>=$item->gradepass) {
+ return COMPLETION_COMPLETE_PASS;
+ } else {
+ return COMPLETION_COMPLETE_FAIL;
+ }
+ } else {
+ // Not displaying pass/fail, but we know grade exists b/c we got here
+ return COMPLETION_COMPLETE;
+ }
+ }
+
+ /**
+ * This temporary function is intended to be replaced once a Moodle exception
+ * system is agreed. Code that used to call this function should instead
+ * throw an exception, so this function should be deleted. The function is
+ * only used internally.
+ *
+ * This is to be used only for system errors (things that shouldn't happen)
+ * and not user-level errors.
+ *
+ * @param string $error Error string (will not be displayed to user unless
+ * debugging is enabled)
+ */
+ function internal_systemerror($error) {
+ global $CFG;
+ debugging($error,DEBUG_ALL);
+ print_error('err_system','completion',$CFG->wwwroot.'/course/view.php?id='.$this->course->id);
+ }
+}
+
+
+?>
)
),
+ 'moodle/course:viewprogress' => array(
+
+ 'riskbitmask' => RISK_PERSONAL,
+
+ 'captype' => 'read',
+ 'contextlevel' => CONTEXT_COURSE,
+ 'legacy' => array(
+ 'teacher' => CAP_ALLOW,
+ 'editingteacher' => CAP_ALLOW,
+ 'admin' => CAP_ALLOW
+ )
+ ),
+
'moodle/course:viewhiddencourses' => array(
'captype' => 'read',
'admin' => CAP_ALLOW
)
),
-
+
//capabilities designed for the new message system configuration
'moodle/user:editmessageprofile' => array(
<FIELD NAME="enrolstartdate" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" PREVIOUS="enrollable" NEXT="enrolenddate"/>
<FIELD NAME="enrolenddate" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" PREVIOUS="enrolstartdate" NEXT="enrol"/>
<FIELD NAME="enrol" TYPE="char" LENGTH="20" NOTNULL="true" SEQUENCE="false" ENUM="false" PREVIOUS="enrolenddate" NEXT="defaultrole"/>
- <FIELD NAME="defaultrole" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" COMMENT="The default role given to participants who self-enrol" PREVIOUS="enrol"/>
+ <FIELD NAME="defaultrole" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" COMMENT="The default role given to participants who self-enrol" PREVIOUS="enrol" NEXT="enablecompletion"/>
+ <FIELD NAME="enablecompletion" TYPE="int" LENGTH="1" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" COMMENT="1 = allow use of 'completion' progress-tracking on this course.
+0 = disable completion tracking on this course." PREVIOUS="defaultrole"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<INDEX NAME="child_course" UNIQUE="false" FIELDS="child_course" PREVIOUS="parent_course"/>
</INDEXES>
</TABLE>
- <TABLE NAME="course_modules" COMMENT="course_modules table retrofitted from MySQL" PREVIOUS="course_meta" NEXT="course_sections">
+ <TABLE NAME="course_modules" COMMENT="course_modules table retrofitted from MySQL" PREVIOUS="course_meta" NEXT="course_modules_completion">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" ENUM="false" NEXT="course"/>
<FIELD NAME="course" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" PREVIOUS="id" NEXT="module"/>
<FIELD NAME="visibleold" TYPE="int" LENGTH="1" NOTNULL="true" UNSIGNED="false" DEFAULT="1" SEQUENCE="false" ENUM="false" PREVIOUS="visible" NEXT="groupmode"/>
<FIELD NAME="groupmode" TYPE="int" LENGTH="4" NOTNULL="true" UNSIGNED="false" DEFAULT="0" SEQUENCE="false" ENUM="false" PREVIOUS="visibleold" NEXT="groupingid"/>
<FIELD NAME="groupingid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" PREVIOUS="groupmode" NEXT="groupmembersonly"/>
- <FIELD NAME="groupmembersonly" TYPE="int" LENGTH="4" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" COMMENT="only members of any group are allowed to access the activity" PREVIOUS="groupingid"/>
+ <FIELD NAME="groupmembersonly" TYPE="int" LENGTH="4" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" COMMENT="only members of any group are allowed to access the activity" PREVIOUS="groupingid" NEXT="completion"/>
+ <FIELD NAME="completion" TYPE="int" LENGTH="1" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" COMMENT="Whether the completion-tracking facilities are enabled for this activity.
+0 = not enabled (database default)
+1 = manual tracking, user can tick this activity off (UI default for most activity types)
+2 = automatic tracking, system should mark completion according to rules specified in course_moduleS_completion" PREVIOUS="groupmembersonly" NEXT="completiongradeitemnumber"/>
+ <FIELD NAME="completiongradeitemnumber" TYPE="int" LENGTH="10" NOTNULL="false" UNSIGNED="true" SEQUENCE="false" ENUM="false" COMMENT="Grade-item number used to track automatic completion, if applicable." PREVIOUS="completion" NEXT="completionview"/>
+ <FIELD NAME="completionview" TYPE="int" LENGTH="1" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" COMMENT="Controls whether a page view is part of the automatic completion requirements for this activity. 0 = view not required 1 = view required" PREVIOUS="completiongradeitemnumber" NEXT="completionexpected"/>
+ <FIELD NAME="completionexpected" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" COMMENT="Date at which students are expected to complete this activity. This field is used when displaying student progress." PREVIOUS="completionview"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="groupingid"/>
<INDEX NAME="idnumber-course" UNIQUE="false" FIELDS="idnumber, course" COMMENT="non unique index (although programatically we are guarantying some sort of uniqueness both under this table and the grade_items one). TODO: We need a central store of module idnumbers in the future." PREVIOUS="instance"/>
</INDEXES>
</TABLE>
- <TABLE NAME="course_sections" COMMENT="to define the sections for each course" PREVIOUS="course_modules" NEXT="course_request">
+ <TABLE NAME="course_modules_completion" COMMENT="Stores the completion state (completed or not completed, etc) of each user on each activity." PREVIOUS="course_modules" NEXT="course_sections">
+ <FIELDS>
+ <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" ENUM="false" NEXT="coursemoduleid"/>
+ <FIELD NAME="coursemoduleid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" ENUM="false" COMMENT="Activity that has been completed (or not)." PREVIOUS="id" NEXT="userid"/>
+ <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" ENUM="false" COMMENT="ID of user who has (or hasn't) completed the activity." PREVIOUS="coursemoduleid" NEXT="completionstate"/>
+ <FIELD NAME="completionstate" TYPE="int" LENGTH="1" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" ENUM="false" COMMENT="Whether or not the user has completed the activity. Available states:
+0 = not completed [if there's no row in this table, that also counts as 0]
+1 = completed
+2 = completed, show passed
+3 = completed, show failed" PREVIOUS="userid" NEXT="viewed"/>
+
+ <FIELD NAME="viewed" TYPE="int" LENGTH="1" NOTNULL="false" UNSIGNED="true" SEQUENCE="false" ENUM="false" COMMENT="Tracks whether or not this activity has been viewed.
+NULL = we are not tracking viewed for this activity
+0 = not viewed
+1 = viewed" PREVIOUS="completionstate" NEXT="timemodified"/>
+ <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" ENUM="false" COMMENT="Time at which the completion state last changed." PREVIOUS="viewed"/>
+ </FIELDS>
+ <KEYS>
+ <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+ </KEYS>
+ <INDEXES>
+ <INDEX NAME="coursemoduleid" UNIQUE="false" FIELDS="coursemoduleid" COMMENT="For quick access via course-module (e.g. when displaying course module settings page and we need to determine whether anyone has completed it)." NEXT="userid"/>
+ <INDEX NAME="userid" UNIQUE="false" FIELDS="userid" COMMENT="Index on user ID. Used when obtaining completion information for normal course page view." PREVIOUS="coursemoduleid"/>
+
+ </INDEXES>
+ </TABLE>
+ <TABLE NAME="course_sections" COMMENT="to define the sections for each course" PREVIOUS="course_modules_completion" NEXT="course_request">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" ENUM="false" NEXT="course"/>
<FIELD NAME="course" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" PREVIOUS="id" NEXT="section"/>
<FIELD NAME="data5" TYPE="text" LENGTH="big" NOTNULL="false" SEQUENCE="false" ENUM="false" PREVIOUS="data4" NEXT="timecreated"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" PREVIOUS="data5" NEXT="timemodified"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" PREVIOUS="timecreated"/>
- </FIELDS>
- <KEYS>
- <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
- </KEYS>
- </TABLE>
+ </FIELDS>
+ <KEYS>
+ <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+ </KEYS>
+ </TABLE>
<TABLE NAME="portfolio_instance" COMMENT="base table (not including config data) for instances of portfolio plugins." PREVIOUS="repository" NEXT="portfolio_instance_config">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" ENUM="false" NEXT="plugin"/>
</SENTENCES>
</STATEMENT>
</STATEMENTS>
-</XMLDB>
+</XMLDB>
\ No newline at end of file
$result = $DB->delete_records_select('role_names', $DB->sql_isempty('role_names', 'name', false, false));
upgrade_main_savepoint($result, 2008070300);
}
-
+
if ($result && $oldversion < 2008070700) {
if (isset($CFG->defaultuserroleid) and isset($CFG->guestroleid) and $CFG->defaultuserroleid == $CFG->guestroleid) {
// guest can not be selected in defaultuserroleid!
upgrade_main_savepoint($result, 2008072400);
}
+ if ($result && $oldversion < 2008072800) {
+
+ /// Define field enablecompletion to be added to course
+ $table = new xmldb_table('course');
+ $field = new xmldb_field('enablecompletion');
+ $field->set_attributes(XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, '0', 'defaultrole');
+
+ /// Launch add field enablecompletion
+ if(!$dbman->field_exists($table,$field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ /// Define field completion to be added to course_modules
+ $table = new xmldb_table('course_modules');
+ $field = new xmldb_field('completion');
+ $field->set_attributes(XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, '0', 'groupmembersonly');
+
+ /// Launch add field completion
+ if(!$dbman->field_exists($table,$field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ /// Define field completiongradeitemnumber to be added to course_modules
+ $field = new xmldb_field('completiongradeitemnumber');
+ $field->set_attributes(XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null, null, null, 'completion');
+
+ /// Launch add field completiongradeitemnumber
+ if(!$dbman->field_exists($table,$field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ /// Define field completionview to be added to course_modules
+ $field = new xmldb_field('completionview');
+ $field->set_attributes(XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, '0', 'completiongradeitemnumber');
+
+ /// Launch add field completionview
+ if(!$dbman->field_exists($table,$field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ /// Define field completionexpected to be added to course_modules
+ $field = new xmldb_field('completionexpected');
+ $field->set_attributes(XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, '0', 'completionview');
+
+ /// Launch add field completionexpected
+ if(!$dbman->field_exists($table,$field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ /// Define table course_modules_completion to be created
+ $table = new xmldb_table('course_modules_completion');
+ if(!$dbman->table_exists($table)) {
+
+ /// Adding fields to table course_modules_completion
+ $table->addFieldInfo('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null, null, null);
+ $table->addFieldInfo('coursemoduleid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null);
+ $table->addFieldInfo('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null);
+ $table->addFieldInfo('completionstate', XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null);
+ $table->addFieldInfo('viewed', XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, null, null, null, null, null);
+ $table->addFieldInfo('timemodified', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null);
+
+ /// Adding keys to table course_modules_completion
+ $table->addKeyInfo('primary', XMLDB_KEY_PRIMARY, array('id'));
+
+ /// Adding indexes to table course_modules_completion
+ $table->addIndexInfo('coursemoduleid', XMLDB_INDEX_NOTUNIQUE, array('coursemoduleid'));
+ $table->addIndexInfo('userid', XMLDB_INDEX_NOTUNIQUE, array('userid'));
+
+ /// Launch create table for course_modules_completion
+ $dbman->create_table($table);
+ }
+
+ /// Main savepoint reached
+ upgrade_main_savepoint($result, 2008072800);
+ }
+
/*
* TODO:
$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);
+ }
}
?>
global $DB;
if ($datas = $DB->get_records_select($table, $wheresql, $params)) {
+
$result = array();
foreach($datas as $data) {
$instance = new $classname();
return $result;
} else {
+
return false;
}
}
$DB->insert_record($this->table.'_history', $data);
}
+ $this->notify_changed(false);
return true;
}
$data->userlogged = $USER->id;
$DB->insert_record($this->table.'_history', $data);
}
+ $this->notify_changed(true);
return true;
} else {
$DB->insert_record($this->table.'_history', $data);
}
+ $this->notify_changed(false);
return $this->id;
}
}
}
}
+
+ /**
+ * 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) {
+ }
}
?>
$courseid = $courseorid;
if (!$course = $DB->get_record('course', array('id'=>$courseid))) {
return false;
- }
+ }
}
// frontpage course can not be deleted!!
return $plugins;
}
+// Feature constants
+/** True if module can provide a grade */
+define('FEATURE_GRADE_HAS_GRADE','grade_has_grade');
+/** True if module has code to track whether somebody viewed it */
+define('FEATURE_COMPLETION_TRACKS_VIEWS','completion_tracks_views');
+/** True if module has custom completion rules */
+define('FEATURE_COMPLETION_HAS_RULES','completion_has_rules');
+
+/**
+ * Checks whether a plugin supports a specified feature.
+ *
+ * @param string $type Plugin type e.g. 'mod'
+ * @param string $name Plugin name
+ * @param string $feature Feature code (FEATURE_xx constant)
+ * @return Feature result (false if not supported, usually true but may have
+ * other feature-specific value otherwise)
+ */
+function plugin_supports($type,$name,$feature) {
+ global $CFG;
+ switch($type) {
+ case 'mod' :
+ $file=$CFG->dirroot.'/mod/'.$name.'/lib.php';
+ $function=$name.'_supports';
+ break;
+ default:
+ throw new Exception('Unsupported plugin type ('.$type.')');
+ }
+
+ // Load library and look for function
+ require_once($file);
+ if(function_exists($function)) {
+ // Function exists, so just return function result
+ return $function($feature);
+ } else {
+ switch($feature) {
+ // If some features can also be checked in other ways
+ // for legacy support, this could be added here
+ default: return false;
+ }
+ }
+}
+
/**
* Returns true if the current version of PHP is greater that the specified one.
*
}
/**
- * Checks to see if is the browser operating system matches the specified
+ * Checks to see if is the browser operating system matches the specified
* brand.
- *
+ *
* Known brand: 'Windows','Linux','Macintosh','SGI','SunOS','HP-UX'
*
* @uses $_SERVER
- * @param string $brand The operating system identifier being tested
+ * @param string $brand The operating system identifier being tested
* @return bool true if the given brand below to the detected operating system
*/
function check_browser_operating_system($brand) {
if (preg_match("/$brand/i", $_SERVER['HTTP_USER_AGENT'])) {
return true;
}
-
- return false;
+
+ return false;
}
/**
// if the words shouldn't be cut in the middle...
if (!$exact) {
// ...search the last occurance of a space...
- for ($k=strlen($truncate);$k>0;$k--) {
+ for ($k=strlen($truncate);$k>0;$k--) {
if (!empty($truncate[$k]) && ($char = $truncate[$k])) {
if ($char == '.' or $char == ' ') {
$breakpos = $k+1;
break; // character boundary.
}
}
- }
+ }
- if (isset($breakpos)) {
+ if (isset($breakpos)) {
// ...and cut the text in this position
$truncate = substr($truncate, 0, $breakpos);
- }
- }
+ }
+ }
// add the defined ending to the text
- $truncate .= $ending;
+ $truncate .= $ending;
// close all unclosed html-tags
foreach ($open_tags as $tag) {
$truncate .= '</' . $tag . '>';
}
- return $truncate;
+ return $truncate;
}
--- /dev/null
+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' (as part of install) and 'u1'
+3) Set the server debug to 'Developer' so that you spot any warnings etc.
+4) 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
+------------------
+
+Note: Icons are subject to change, so references to a 'tick' etc might not be
+correct.
+
+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.
+
+
--- /dev/null
+<?php
+if (!defined('MOODLE_INTERNAL')) {
+ die('Direct access to this script is forbidden.');
+}
+require_once($CFG->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));
+ }
+}
+?>
\ No newline at end of file
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" PREVIOUS="rssarticles" NEXT="warnafter"/>
<FIELD NAME="warnafter" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" PREVIOUS="timemodified" NEXT="blockafter"/>
<FIELD NAME="blockafter" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" PREVIOUS="warnafter" NEXT="blockperiod"/>
- <FIELD NAME="blockperiod" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" PREVIOUS="blockafter"/>
+ <FIELD NAME="blockperiod" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" PREVIOUS="blockafter" NEXT="completiondiscussions"/>
+ <FIELD NAME="completiondiscussions" TYPE="int" LENGTH="9" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" COMMENT="Nonzero if a certain number of posts are required to mark this forum completed for a user." PREVIOUS="blockperiod" NEXT="completionreplies"/>
+ <FIELD NAME="completionreplies" TYPE="int" LENGTH="9" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" COMMENT="Nonzero if a certain number of replies are required to mark this forum complete for a user." PREVIOUS="completiondiscussions" NEXT="completionposts"/>
+ <FIELD NAME="completionposts" TYPE="int" LENGTH="9" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" COMMENT="Nonzero if a certain number of posts or replies (total) are required to mark this forum complete for a user." PREVIOUS="completionreplies"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
global $CFG, $THEME, $DB;
+ $dbman = $DB->get_manager(); // loads ddl manager and xmldb classes
+
$result = true;
/// And upgrade begins here. For each one, you'll need one
upgrade_mod_savepoint($result, 2007101512, 'forum');
}
-
+
if ($result and $oldversion < 2008072401) {
$eventdata = new object();
$eventdata->modulename = 'forum';
upgrade_mod_savepoint($result, 2008072401, 'forum');
}
+ if ($result && $oldversion < 2008072800) {
+ /// Define field completiondiscussions to be added to forum
+ $table = new XMLDBTable('forum');
+ $field = new XMLDBField('completiondiscussions');
+ $field->setAttributes(XMLDB_TYPE_INTEGER, '9', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, '0', 'draft');
+
+ /// Launch add field completiondiscussions
+ if(!$dbman->field_exists($table,$field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ $field = new XMLDBField('completionreplies');
+ $field->setAttributes(XMLDB_TYPE_INTEGER, '9', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, '0', 'completiondiscussions');
+
+ /// Launch add field completionreplies
+ if(!$dbman->field_exists($table,$field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ /// Define field completionposts to be added to forum
+ $field = new XMLDBField('completionposts');
+ $field->setAttributes(XMLDB_TYPE_INTEGER, '9', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, '0', 'completionreplies');
+
+ /// Launch add field completionposts
+ if(!$dbman->field_exists($table,$field)) {
+ $dbman->add_field($table, $field);
+ }
+ upgrade_mod_savepoint($result, 2008072800, 'forum');
+ }
return $result;
/// STANDARD FUNCTIONS ///////////////////////////////////////////////////////////
-/**
+/**
* Code to be executed when a module is installed
* now is just used to register the module as message provider
*/
}
+/**
+ * Indicates API features that the forum supports.
+ *
+ * @param string $feature
+ * @return mixed True if yes (some features may use other values)
+ */
+function forum_supports($feature) {
+ switch($feature) {
+ case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
+ case FEATURE_COMPLETION_HAS_RULES: return true;
+ default: return false;
+ }
+}
+
+
+/**
+ * Obtains the automatic completion state for this forum based on any conditions
+ * in forum settings.
+ *
+ * @param object $course Course
+ * @param object $cm Course-module
+ * @param int $userid User ID
+ * @param bool $type Type of comparison (or/and; can be used as return value if no conditions)
+ * @return bool True if completed, false if not. (If no conditions, then return
+ * value depends on comparison type)
+ */
+function forum_get_completion_state($course,$cm,$userid,$type) {
+ global $CFG,$DB;
+
+ // Get forum details
+ if(!($forum=$DB->get_record('forum',array('id'=>$cm->instance)))) {
+ throw new Exception("Can't find forum {$cm->instance}");
+ }
+
+ $result=$type; // Default return value
+
+ $postcountparams=array('userid'=>$userid,'forumid'=>$forum->id);
+ $postcountsql="
+SELECT
+ COUNT(1)
+FROM
+ {$CFG->prefix}forum_posts fp
+ INNER JOIN {$CFG->prefix}forum_discussions fd ON fp.discussion=fd.id
+WHERE
+ fp.userid=:userid AND fd.forum=:forumid";
+
+ if($forum->completiondiscussions) {
+ $value = $forum->completiondiscussions <=
+ $DB->count_records('forum_discussions',array('forum'=>$forum->id,'userid'=>$userid));
+ if($type==COMPLETION_AND) {
+ $result=$result && $value;
+ } else {
+ $result=$result || $value;
+ }
+ }
+ if($forum->completionreplies) {
+ $value = $forum->completionreplies <=
+ $DB->get_field_sql( $postcountsql.' AND fp.parent<>0',$postcountparams);
+ if($type==COMPLETION_AND) {
+ $result=$result && $value;
+ } else {
+ $result=$result || $value;
+ }
+ }
+ if($forum->completionposts) {
+ $value = $forum->completionposts <= $DB->get_field_sql($postcountsql,$postcountparams);
+ if($type==COMPLETION_AND) {
+ $result=$result && $value;
+ } else {
+ $result=$result || $value;
+ }
+ }
+
+ return $result;
+}
+
+
/**
* Function to be run periodically according to the moodle cron
* Finds all posts that have yet to be mailed out, and mails them
/**
- *
+ * Deletes a discussion and handles all associated cleanup.
+ * @param object $discussion Discussion to delete
+ * @param bool $fulldelete True when deleting entire forum
+ * @param object $course Course (required if fulldelete is false)
+ * @param object $cm Course-module (required if fulldelete is false)
+ * @param object $forum Forum (required if fulldelete is false)
*/
-function forum_delete_discussion($discussion, $fulldelete=false) {
-// $discussion is a discussion record object
+function forum_delete_discussion($discussion, $fulldelete=false,$course=null,$cm=null,$forum=null) {
global $DB;
$result = true;
foreach ($posts as $post) {
$post->course = $discussion->course;
$post->forum = $discussion->forum;
- if (! $DB->delete_records("forum_ratings", array("post" => "$post->id"))) {
- $result = false;
- }
- if (! forum_delete_post($post, $fulldelete)) {
+ if (! forum_delete_post($post, 'ignore',$course, $cm, $forum, $fulldelete)) {
$result = false;
}
}
$result = false;
}
+ // Update completion state if we are tracking completion based on number of posts
+ $completion=new completion_info($course);
+ if(!$fulldelete && // But don't bother when deleting whole thing
+ $completion->is_enabled($cm)==COMPLETION_TRACKING_AUTOMATIC &&
+ ($forum->completiondiscussions || $forum->completionreplies || $forum->completionposts)) {
+ $completion->update_state($cm,COMPLETION_INCOMPLETE,$discussion->userid);
+ }
+
return $result;
}
/**
- *
+ * Deletes a single forum post.
+ * @param object $post Forum post object
+ * @param mixed $children Whether to delete children. If false, returns false
+ * if there are any children (without deleting the post). If true,
+ * recursively deletes all children. If set to special value 'ignore', deletes
+ * post regardless of children (this is for use only when deleting all posts
+ * in a disussion).
+ * @param object $course Course
+ * @param object $cm Course-module
+ * @param object $forum Forum
+ * @param bool $skipcompletion True to skip updating completion state if it
+ * would otherwise be updated, i.e. when deleting entire forum anyway.
*/
-function forum_delete_post($post, $children=false) {
+function forum_delete_post($post, $children, $course, $cm, $forum, $skipcompletion=false) {
global $DB;
- if ($childposts = $DB->get_records('forum_posts', array('parent' => $post->id))) {
+ if ($children!='ignore' && ($childposts = $DB->get_records('forum_posts', array('parent'=>$post->id)))) {
if ($children) {
foreach ($childposts as $childpost) {
- forum_delete_post($childpost, true);
+ forum_delete_post($childpost, true, $course, $cm, $forum, $skipcompletion);
}
} else {
return false;
}
}
if ($DB->delete_records("forum_posts", array("id" => $post->id))) {
- $DB->delete_records("forum_ratings", array("post" => $post->id)); // Just in case
+ $DB->delete_records("forum_ratings", array("post" => $post->id));
forum_tp_delete_read_records(-1, $post->id);
// Just in case we are deleting the last post
forum_discussion_update_last_post($post->discussion);
+ // Update completion state if we are tracking completion based on number of posts
+ $completion=new completion_info($course);
+ if(!$skipcompletion && // But don't bother when deleting whole thing
+ $completion->is_enabled($cm)==COMPLETION_TRACKING_AUTOMATIC &&
+ ($forum->completiondiscussions || $forum->completionreplies || $forum->completionposts)) {
+ $completion->update_state($cm,COMPLETION_INCOMPLETE,$post->userid);
+ }
+
return true;
}
return false;
WHERE p.userid = :userid AND d.forum = :forumid";
return $DB->record_exists_sql($sql, array('forumid'=>$forumid,'userid'=>$userid));
} else {
- return $DB->record_exists('forum_posts', array('discussion'=>$did,'userid'=>$userid));
- }
+ return $DB->record_exists('forum_posts', array('discussion'=>$did,'userid'=>$userid));
+}
}
/**
$mform->addElement('text', 'name', get_string('forumname', 'forum'), array('size'=>'64'));
if (!empty($CFG->formatstringstriptags)) {
- $mform->setType('name', PARAM_TEXT);
+ $mform->setType('name', PARAM_TEXT);
} else {
$mform->setType('name', PARAM_CLEAN);
}
$default_values['ratingtime']=
($default_values['assesstimestart'] && $default_values['assesstimefinish']) ? 1 : 0;
}
+
+ // Set up the completion checkboxes which aren't part of standard data.
+ // We also make the default value (if you turn on the checkbox) for those
+ // numbers to be 1, this will not apply unless checkbox is ticked.
+ $default_values['completiondiscussionsenabled']=
+ !empty($default_values['completiondiscussions']) ? 1 : 0;
+ if(empty($default_values['completiondiscussions'])) {
+ $default_values['completiondiscussions']=1;
+ }
+ $default_values['completionrepliesenabled']=
+ !empty($default_values['completionreplies']) ? 1 : 0;
+ if(empty($default_values['completionreplies'])) {
+ $default_values['completionreplies']=1;
+ }
+ $default_values['completionpostsenabled']=
+ !empty($default_values['completionposts']) ? 1 : 0;
+ if(empty($default_values['completionposts'])) {
+ $default_values['completionposts']=1;
+ }
}
+ function add_completion_rules() {
+ $mform =& $this->_form;
+
+ $group=array();
+ $group[] =& $mform->createElement('checkbox', 'completionpostsenabled', '', get_string('completionposts','forum'));
+ $group[] =& $mform->createElement('text', 'completionposts', '', array('size'=>3));
+ $mform->setType('completionposts',PARAM_INT);
+ $mform->addGroup($group, 'completionpostsgroup', get_string('completionpostsgroup','forum'), array(' '), false);
+ $mform->setHelpButton('completionpostsgroup', array('completion', get_string('completionpostshelp', 'forum'), 'forum'));
+ $mform->disabledIf('completionposts','completionpostsenabled','notchecked');
+
+ $group=array();
+ $group[] =& $mform->createElement('checkbox', 'completiondiscussionsenabled', '', get_string('completiondiscussions','forum'));
+ $group[] =& $mform->createElement('text', 'completiondiscussions', '', array('size'=>3));
+ $mform->setType('completiondiscussions',PARAM_INT);
+ $mform->addGroup($group, 'completiondiscussionsgroup', get_string('completiondiscussionsgroup','forum'), array(' '), false);
+ $mform->setHelpButton('completiondiscussionsgroup', array('completion', get_string('completiondiscussionshelp', 'forum'), 'forum'));
+ $mform->disabledIf('completiondiscussions','completiondiscussionsenabled','notchecked');
+
+ $group=array();
+ $group[] =& $mform->createElement('checkbox', 'completionrepliesenabled', '', get_string('completionreplies','forum'));
+ $group[] =& $mform->createElement('text', 'completionreplies', '', array('size'=>3));
+ $mform->setType('completionreplies',PARAM_INT);
+ $mform->addGroup($group, 'completionrepliesgroup', get_string('completionrepliesgroup','forum'), array(' '), false);
+ $mform->setHelpButton('completionrepliesgroup', array('completion', get_string('completionreplieshelp', 'forum'), 'forum'));
+ $mform->disabledIf('completionreplies','completionrepliesenabled','notchecked');
+
+ return array('completiondiscussionsgroup','completionrepliesgroup','completionpostsgroup');
+ }
+
+ function completion_rule_enabled($data) {
+ return (!empty($data['completiondiscussionsenabled']) && $data['completiondiscussions']!=0) ||
+ (!empty($data['completionrepliesenabled']) && $data['completionreplies']!=0) ||
+ (!empty($data['completionpostsenabled']) && $data['completionposts']!=0);
+ }
+
+ function get_data($slashed=true) {
+ $data=parent::get_data($slashed);
+ if(!$data) {
+ return false;
+ }
+ // Turn off completion settings if the checkboxes aren't ticked
+ $autocompletion=!empty($data->completion) && $data->completion==COMPLETION_TRACKING_AUTOMATIC;
+ if(empty($data->completiondiscussionsenabled) || !$autocompletion) {
+ $data->completiondiscussions=0;
+ }
+ if(empty($data->completionrepliesenabled) || !$autocompletion) {
+ $data->completionreplies=0;
+ }
+ if(empty($data->completionpostsenabled) || !$autocompletion) {
+ $data->completionposts=0;
+ }
+ return $data;
+ }
}
?>
notice("Sorry, but you are not allowed to delete that discussion!",
forum_go_back_to("discuss.php?d=$post->discussion"));
}
- forum_delete_discussion($discussion);
+ forum_delete_discussion($discussion,false,$course,$cm,$forum);
add_to_log($discussion->course, "forum", "delete discussion",
"view.php?id=$cm->id", "$forum->id", $cm->id);
-
+
redirect("view.php?f=$discussion->forum");
- } else if (forum_delete_post($post, has_capability('mod/forum:deleteanypost', $modcontext))) {
+ } else if (forum_delete_post($post, has_capability('mod/forum:deleteanypost', $modcontext),
+ $course, $cm, $forum)) {
if ($forum->type == 'single') {
// Single discussion forums are an exception. We show
add_to_log($discussion->course, "forum", "prune post",
"discuss.php?d=$newid", "$post->id", $cm->id);
-
+
redirect(forum_go_back_to("discuss.php?d=$newid"));
} else { // User just asked to prune something
}
add_to_log($course->id, "forum", "add post",
"$discussionurl&parent=$fromform->id", "$fromform->id", $cm->id);
+
+ // Update completion state
+ $completion=new completion_info($course);
+ if($completion->is_enabled($cm) &&
+ ($forum->completionreplies || $forum->completionposts)) {
+ $completion->update_state($cm,COMPLETION_COMPLETE);
+ }
redirect(forum_go_back_to("$discussionurl#p$fromform->id"), $message.$subscribemessage, $timemessage);
$timemessage = 4;
}
+ // Update completion status
+ $completion=new completion_info($course);
+ if($completion->is_enabled($cm) &&
+ ($forum->completiondiscussions || $forum->completionposts)) {
+ $completion->update_state($cm,COMPLETION_COMPLETE);
+ }
+
redirect(forum_go_back_to("view.php?f=$fromform->forum"), $message.$subscribemessage, $timemessage);
} else {
// This fragment is called by /admin/index.php
////////////////////////////////////////////////////////////////////////////////
-$module->version = 2008072401;
+$module->version = 2008072800;
$module->requires = 2008072401; // Requires this Moodle version
$module->cron = 60;
break;
}
+ $completion=new completion_info($course);
+ $completion->set_module_viewed($cm);
print_footer($course);
?>
/// FUNCTIONS ///////////////////////////////////////////////////////////////////
-/**
+/**
* Code to be executed when a module is installed
* now is just used to register the module as message provider
*/
return '';
}
+/**
+ * @param string $feature FEATURE_xx constant for requested feature
+ * @return bool True if quiz supports feature
+ */
+function quiz_supports($feature) {
+ switch($feature) {
+ case FEATURE_GRADE_HAS_GRADE: return true;
+ case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
+ default: return false;
+ }
+}
+
/**
* Returns all other caps used in module
*/
// Should we not be seeing if we need to print right-hand-side blocks?
finish_page($course);
+
+ // Mark module as viewed (note, we do this here and not in finish_page,
+ // otherwise the 'not enrolled' error conditions would result in marking
+ // 'viewed', I think it's better if they don't.)
+ $completion=new completion_info($course);
+ $completion->set_module_viewed(cm);
// Utility functions =================================================================
}
#redirect #message {
-
+
}
#redirect #continue {
-
+
}
/* .clearfix {display: inline-table;} */
#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;
padding: 0;
}
.weeks-format .block_calendar_month .minicalendar th,
-.topics-format .block_calendar_month .minicalendar th,
+.topics-format .block_calendar_month .minicalendar th,
.weeks-format .block_calendar_month .minicalendar td,
.topics-format .block_calendar_month .minicalendar td {
padding: 0.1em 0 0.1em 1px;
.weeks .right,
.topics .right {
float: right;
-}
+}
.section .activity img.activityicon {
vertical-align:middle;
}
#grade-aggregation-help dt {
- margin-top: 15px;
+ margin-top: 15px;
}
#grade-aggregation-help dd.example {
border-bottom: 1px solid #555;
}
+/***
+ *** 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
***/
}
-.tabrow0 .here a:link,
+.tabrow0 .here a:link,
.tabrow0 .here a:visited,
.tabrow0 .here a.nolink {
position:relative;
padding: 4px;
}
-#mod-quiz-grading table#grading .header .commands
+#mod-quiz-grading table#grading .header .commands
{
display: inline;
}
-#mod-quiz-grading table#grading .picture
+#mod-quiz-grading table#grading .picture
{
width: 40px;
}
-#mod-quiz-grading table#grading td
+#mod-quiz-grading table#grading td
{
border-left-width: 1px;
border-right-width: 1px;
// 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