Commit 4e781c7b authored by sam_marshall's avatar sam_marshall
Browse files

MDL-15498: Completion system

parent b3abd2c6
......@@ -13,6 +13,11 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
$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
......
......@@ -716,7 +716,8 @@
fwrite ($bf,full_tag("ENROLSTARTDATE",3,false,$course->enrolstartdate));
fwrite ($bf,full_tag("ENROLENDDATE",3,false,$course->enrolenddate));
fwrite ($bf,full_tag("ENROLPERIOD",3,false,$course->enrolperiod));
fwrite ($bf,full_tag("ENABLECOMPLETION",3,false,$course->enablecompletion));
/// write local course overrides here?
write_role_overrides_xml($bf, $context, 3);
/// write role_assign code here
......@@ -1162,6 +1163,11 @@
$status = true;
$first_record = true;
$course=$DB->get_record('course',array('id'=>$preferences->backup_course));
if(!$course) {
return false;
}
//Now print the mods in section
//Extracts mod id from sequence
......@@ -1221,11 +1227,36 @@
fwrite ($bf,full_tag("GROUPINGID",6,false,$course_module->groupingid));
fwrite ($bf,full_tag("GROUPMEMBERSONLY",6,false,$course_module->groupmembersonly));
fwrite ($bf,full_tag("IDNUMBER",6,false,$course_module->idnumber));
fwrite ($bf,full_tag("COMPLETION",6,false,$course_module->completion));
fwrite ($bf,full_tag("COMPLETIONGRADEITEMNUMBER",6,false,$course_module->completiongradeitemnumber));
fwrite ($bf,full_tag("COMPLETIONVIEW",6,false,$course_module->completionview));
fwrite ($bf,full_tag("COMPLETIONEXPECTED",6,false,$course_module->completionexpected));
// get all the role_capabilities overrides in this mod
write_role_overrides_xml($bf, $context, 6);
/// write role_assign code here
write_role_assignments_xml($bf, $preferences, $context, 6);
/// write role_assign code here
write_role_assignments_xml($bf, $preferences, $context, 6);
// write completion data if enabled and user data enabled
require_once($CFG->libdir.'/completionlib.php');
$completion=new completion_info($course);
if($completion->is_enabled($course_module) &&
backup_userdata_selected($preferences,$moduletype,$course_module->instance)) {
fwrite ($bf,start_tag("COMPLETIONDATA",6,true));
// Get all completion records for this module and loop
$data=$DB->get_records('course_modules_completion',array('coursemoduleid'=>$course_module->id));
$data=$data ? $data : array();
foreach($data as $completion) {
// Write completion record
fwrite ($bf,start_tag("COMPLETION",7,true));
fwrite ($bf,full_tag("USERID",8,false,$completion->userid));
fwrite ($bf,full_tag("COMPLETIONSTATE",8,false,$completion->completionstate));
fwrite ($bf,full_tag("VIEWED",8,false,$completion->viewed));
fwrite ($bf,full_tag("TIMEMODIFIED",8,false,$completion->timemodified));
fwrite ($bf,end_tag("COMPLETION",7,true));
}
fwrite ($bf,end_tag("COMPLETIONDATA",6,true));
}
fwrite ($bf,end_tag("MOD",5,true));
}
......@@ -1240,7 +1271,7 @@
return $status;
}
//Print users to xml
//Only users previously calculated in backup_ids will output
//
......
......@@ -752,6 +752,7 @@ define('RESTORE_GROUPS_GROUPINGS', 3);
$course->enrolenddate += $restore->course_startdateoffset;
}
$course->enrolperiod = $course_header->course_enrolperiod;
$course->enablecompletion = isset($course_header->course_enablecompletion) ? $course_header->course_enablecompletion : 0;
//Put as last course in category
$course->sortorder = $category->sortorder + MAX_COURSES_IN_CATEGORY - 1;
......@@ -1119,11 +1120,17 @@ define('RESTORE_GROUPS_GROUPINGS', 3);
//print_object($course_module); //Debug
//Save it to db
if ($mod->idnumber) {
$mod->idnumber=backup_todb($mod->idnumber);
if (grade_verify_idnumber($mod->idnumber, $restore->course_id)) {
$course_module->idnumber = $mod->idnumber;
}
}
$course_module->completion=$mod->completion;
$course_module->completiongradeitemnumber=backup_todb($mod->completiongradeitemnumber);
$course_module->completionview=$mod->completionview;
$course_module->completionexpected=$mod->completionexpected;
$newidmod = $DB->insert_record("course_modules", $course_module);
if ($newidmod) {
//save old and new module id
......@@ -1162,6 +1169,43 @@ define('RESTORE_GROUPS_GROUPINGS', 3);
}
}
}
// 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;
}
......@@ -5164,6 +5208,9 @@ define('RESTORE_GROUPS_GROUPINGS', 3);
case "ENROLPERIOD":
$this->info->course_enrolperiod = $this->getContents();
break;
case "ENABLECOMPLETION":
$this->info->course_enablecompletion = $this->getContents();
break;
}
}
if ($this->tree[4] == "CATEGORY") {
......@@ -5539,7 +5586,15 @@ define('RESTORE_GROUPS_GROUPINGS', 3);
$this->info->tempmod->groupmembersonly;
$this->info->tempsection->mods[$this->info->tempmod->id]->idnumber =
$this->info->tempmod->idnumber;
$this->info->tempsection->mods[$this->info->tempmod->id]->completion =
isset($this->info->tempmod->completion) ? $this->info->tempmod->completion : 0;
$this->info->tempsection->mods[$this->info->tempmod->id]->completiongradeitemnumber =
isset($this->info->tempmod->completiongradeitemnumber) ? $this->info->tempmod->completiongradeitemnumber : null;
$this->info->tempsection->mods[$this->info->tempmod->id]->completionview =
isset($this->info->tempmod->completionview) ? $this->info->tempmod->completionview : 0;
$this->info->tempsection->mods[$this->info->tempmod->id]->completionexpected =
isset($this->info->tempmod->completionexpected) ? $this->info->tempmod->completionexpected : 0;
unset($this->info->tempmod);
}
}
......@@ -5577,6 +5632,18 @@ define('RESTORE_GROUPS_GROUPINGS', 3);
case "IDNUMBER":
$this->info->tempmod->idnumber = $this->getContents();
break;
case "COMPLETION":
$this->info->tempmod->completion = $this->getContents();
break;
case "COMPLETIONGRADEITEMNUMBER":
$this->info->tempmod->completiongradeitemnumber = $this->getContents();
break;
case "COMPLETIONVIEW":
$this->info->tempmod->completionview = $this->getContents();
break;
case "COMPLETIONEXPECTED":
$this->info->tempmod->completionexpected = $this->getContents();
break;
default:
break;
}
......@@ -5672,6 +5739,36 @@ define('RESTORE_GROUPS_GROUPINGS', 3);
}
} /// ends role_overrides
if (isset($this->tree[7]) && $this->tree[7] == "COMPLETIONDATA") {
if($this->level == 8) {
switch($tagName) {
case 'COMPLETION':
// Got all data to make completion entry...
$this->info->tempcompletion->coursemoduleid=$this->info->tempmod->id;
$this->info->completiondata[]=$this->info->tempcompletion;
unset($this->info->tempcompletion);
$this->info->tempcompletion=new stdClass;
break;
}
}
if($this->level == 9) {
switch($tagName) {
case 'USERID' :
$this->info->tempcompletion->userid=$this->getContents();
break;
case 'COMPLETIONSTATE' :
$this->info->tempcompletion->completionstate=$this->getContents();
break;
case 'VIEWED' :
$this->info->tempcompletion->viewed=$this->getContents();
break;
case 'TIMEMODIFIED' :
$this->info->tempcompletion->timemodified=$this->getContents();
break;
}
}
}
}
//Stop parsing if todo = SECTIONS and tagName = SECTIONS (en of the tag, of course)
......@@ -8338,22 +8435,22 @@ define('RESTORE_GROUPS_GROUPINGS', 3);
}
/// 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;
}
......
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);
......@@ -350,6 +350,18 @@ class course_edit_form extends moodleform {
$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
......@@ -1276,7 +1277,7 @@ function set_section_visible($courseid, $sectionnumber, $visibility) {
/**
* 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;
......@@ -1365,7 +1366,7 @@ function print_section($course, $section, $mods, $modnamesused, $absolute=false,
$extra = '';
if (!empty($modinfo->cms[$modnumber]->extra)) {
$extra = $modinfo->cms[$modnumber]->extra;
$extra = $modinfo->cms[$modnumber]->extra;
}
if ($mod->modname == "label") {
......@@ -1453,6 +1454,66 @@ function print_section($course, $section, $mods, $modnamesused, $absolute=false,
echo '&nbsp;&nbsp;';
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";
}
......@@ -2228,6 +2289,27 @@ function set_coursemodule_idnumber($id, $idnumber) {
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
......
......@@ -118,6 +118,10 @@
$form->instance = $cm->instance;
$form->return = $return;
$form->update = $update;
$form->completion = $cm->completion;
$form->completionview = $cm->completionview;
$form->completionexpected = $cm->completionexpected;
$form->completionusegrade = is_null($cm->completiongradeitemnumber) ? 0 : 1;
if ($items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$form->modulename,
'iteminstance'=>$form->instance, 'courseid'=>$COURSE->id))) {
......@@ -241,6 +245,19 @@
if (!isset($fromform->name)) { //label
$fromform->name = $fromform->modulename;
}
if (!isset($fromform->completion)) {
$fromform->completion=COMPLETION_DISABLED;
}
if (!isset($fromform->completionview)) {
$fromform->completionview=COMPLETION_VIEW_NOT_REQUIRED;
}
// Convert the 'use grade' checkbox into a grade-item number: 0 if
// checked, null if not
$fromform->completiongradeitemnumber =
isset($fromform->completionusegrade) && $fromform->completionusegrade
? 0 : null;
if (!empty($fromform->update)) {
......@@ -262,6 +279,18 @@
set_coursemodule_groupingid($fromform->coursemodule, $fromform->groupingid);
set_coursemodule_groupmembersonly($fromform->coursemodule, $fromform->groupmembersonly);
// Handle completion settings. If necessary, wipe existing completion
// data first.
if(!empty($fromform->completionunlocked)) {
$completion=new completion_info($course);
$completion->reset_all_state($cm);
}
set_coursemodule_completion($fromform->coursemodule, $fromform->completion);
set_coursemodule_completionview($fromform->coursemodule, $fromform->completionview);
set_coursemodule_completionexpected($fromform->coursemodule, $fromform->completionexpected);
set_coursemodule_completiongradeitemnumber(
$fromform->coursemodule,$fromform->completiongradeitemnumber);
if (isset($fromform->cmidnumber)) { //label
// set cm idnumber
set_coursemodule_idnumber($fromform->coursemodule, $fromform->cmidnumber);
......
......@@ -32,6 +32,11 @@ class moodleform_mod extends moodleform {
* 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;
......@@ -117,6 +122,56 @@ class moodleform_mod extends moodleform {
$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
......@@ -149,6 +204,15 @@ class moodleform_mod extends moodleform {
$errors['cmidnumber'] = get_string('idnumbertaken');
}