MDL-15498: Completion system

This commit is contained in:
sam_marshall
2008-07-28 12:31:29 +00:00
parent b3abd2c692
commit 4e781c7b50
51 changed files with 3313 additions and 150 deletions
+5
View File
@@ -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
+35 -4
View File
@@ -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
//
+109 -12
View File
@@ -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;
$newrole = backup_getid($restore->backup_unique_code, 'role', $oldroleid); /// Look for target role
$coursecontext = get_context_instance(CONTEXT_COURSE, $restore->course_id); /// Look for target context
if (!empty($newrole->new_id) && !empty($coursecontext) && !empty($roledata->nameincourse)) {
/// Check the role hasn't any custom name in context
if (!$DB->record_exists('role_names', array('roleid'=>$newrole->new_id, 'contextid'=>$coursecontext->id))) {
$rolename = new object();
$rolename->roleid = $newrole->new_id;
$rolename->contextid = $coursecontext->id;
$rolename->name = $roledata->nameincourse;
$DB->insert_record('role_names', $rolename);
}
$DB->insert_record('role_names', $rolename);
}
}
}
}
}
return true;
}
+73
View File
@@ -0,0 +1,73 @@
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);
+12
View File
@@ -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'));
+84 -2
View File
@@ -1,6 +1,7 @@
<?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
+29
View File
@@ -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);
+167 -2
View File
@@ -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');
}
}
// 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;
}
@@ -171,12 +235,23 @@ class moodleform_mod extends moodleform {
/**
* 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;
@@ -216,6 +291,10 @@ class moodleform_mod extends moodleform {
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) {
@@ -269,8 +348,94 @@ class moodleform_mod extends moodleform {
$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;
+2 -1
View File
@@ -11,7 +11,8 @@
require_login($course->id);
require_capability('moodle/site:viewreports', get_context_instance(CONTEXT_COURSE, $course->id));
$context=get_context_instance(CONTEXT_COURSE, $course->id);
require_capability('moodle/site:viewreports', $context);
$strreports = get_string('reports');
+201
View File
@@ -0,0 +1,201 @@
<?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.'&amp;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.
'&amp;format=csv">'.get_string('csvdownload','completion').'</a></li></ul>';
print_footer($course);
?>
+13
View File
@@ -0,0 +1,13 @@
<?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>';
}
?>
+65
View File
@@ -0,0 +1,65 @@
var SVGNS='http://www.w3.org/2000/svg',XLINKNS='http://www.w3.org/1999/xlink';
function textrotate_make_svg(el)
{
var string=el.firstChild.nodeValue;
// Create SVG
var svg=document.createElementNS(SVGNS,'svg');
svg.setAttribute('version','1.1');
var width=(el.offsetHeight*9)/8;
svg.setAttribute('width',width);
svg.setAttribute('height',el.offsetWidth+20);
var text=document.createElementNS(SVGNS,'text');
svg.appendChild(text);
text.setAttribute('x',el.offsetWidth);
text.setAttribute('y',-el.offsetHeight/4);
text.setAttribute('text-anchor','end');
text.setAttribute('transform','rotate(90)');
text.appendChild(document.createTextNode(string));
// Is there an icon near the text?
var icon=el.parentNode.firstChild;
if(icon.nodeName.toLowerCase()=='img') {
el.parentNode.removeChild(icon);
var image=document.createElementNS(SVGNS,'image');
var iconx=el.offsetHeight/4;
if(iconx>width-16) iconx=width-16;
image.setAttribute('x',iconx);
image.setAttribute('y',el.offsetWidth+4);
image.setAttribute('width',16);
image.setAttribute('height',16);
image.setAttributeNS(XLINKNS,'href',icon.src);
svg.appendChild(image);
}
// Replace original content with this new SVG
el.parentNode.insertBefore(svg,el);
el.parentNode.removeChild(el);
}
function textrotate_init() {
var elements=YAHOO.util.Dom.getElementsByClassName('completion-activityname', 'span');
for(var i=0;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);
+57
View File
@@ -0,0 +1,57 @@
<?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);
}
?>
+9
View File
@@ -195,6 +195,15 @@
$PAGE->print_header(get_string('course').': %fullname%', NULL, '', $bodytags);
$completion=new completion_info($course);
if($completion->is_enabled() && ajaxenabled()) {
require_js(array('yui_yahoo','yui_event','yui_connection','yui_dom'));
// Need to do this after the header because it requires the YUI stuff
// to be loaded already
print '<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">';
+48
View File
@@ -0,0 +1,48 @@
<?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';
?>
+11 -2
View File
@@ -40,6 +40,15 @@ $string['cannotupdaterate'] = 'Could not update an old rating ($a[0] = $a[1])';
$string['cannotinsertrate'] = 'Could not insert a new rating ($a[0] = $a[1])';
$string['cannottrack'] = 'Could not stop tracking that forum';
$string['cleanreadtime'] = 'Mark old posts as read hour';
$string['completiondiscussions'] = 'User must create discussions:';
$string['completiondiscussionshelp'] = 'requiring discussions to complete';
$string['completiondiscussionsgroup'] = 'Require discussions';
$string['completionposts'] = 'User must post discussions or replies:';
$string['completionpostshelp'] = 'requiring discussions or replies to complete';
$string['completionpostsgroup'] = 'Require posts';
$string['completionreplies'] = 'User must post replies:';
$string['completionreplieshelp'] = 'requiring replies to complete';
$string['completionrepliesgroup'] = 'Require replies';
$string['configcleanreadtime'] = 'The hour of the day to clean old posts from the \'read\' table.';
$string['configdisplaymode'] = 'The default display mode for discussions if one isn\'t set.';
$string['configenablerssfeeds'] = 'This switch will enable the possibility of RSS feeds for all forums. You will still need to turn feeds on manually in the settings for each forum.';
@@ -87,11 +96,11 @@ $string['editedby'] = 'Edited by $a->name - original submission $a->date';
$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';
@@ -0,0 +1,26 @@
<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>
@@ -0,0 +1,19 @@
<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>
@@ -0,0 +1,46 @@
<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>
@@ -0,0 +1,47 @@
<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>
@@ -0,0 +1,14 @@
<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>
+29
View File
@@ -0,0 +1,29 @@
<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>
+65 -68
View File
@@ -201,7 +201,7 @@ function get_db_directories() {
$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) {
@@ -1883,7 +1883,7 @@ class admin_setting_configtext extends admin_setting {
$data = 0;
}
// $data is a string
$validated = $this->validate($data);
$validated = $this->validate($data);
if ($validated !== true) {
return $validated;
}
@@ -1944,7 +1944,7 @@ class admin_setting_configtextarea extends admin_setting_configtext {
$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>',
@@ -2221,7 +2221,7 @@ class admin_setting_configmulticheckbox extends admin_setting {
}
}
}
$options = array();
$defaults = array();
foreach($this->choices as $key=>$description) {
@@ -2258,7 +2258,7 @@ class admin_setting_configmulticheckbox extends admin_setting {
$return .= '</div>';
return format_admin_setting($this, $this->visiblename, $return, $this->description, false, '', $defaultinfo, $query);
}
}
@@ -2353,7 +2353,7 @@ class admin_setting_configselect extends admin_setting {
if (strpos($textlib->strtolower($value), $query) !== false) {
return true;
}
}
}
return false;
}
@@ -2800,7 +2800,7 @@ class admin_setting_sitesettext extends admin_setting_configtext {
function write_setting($data) {
global $DB;
$data = trim($data);
$validated = $this->validate($data);
$validated = $this->validate($data);
if ($validated !== true) {
return $validated;
}
@@ -3388,12 +3388,22 @@ class admin_setting_special_calendar_weekend extends admin_setting {
/**
* 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() {
@@ -3418,18 +3428,33 @@ class admin_setting_special_gradebookroles extends admin_setting_configmultichec
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;
@@ -3444,45 +3469,17 @@ class admin_setting_regradingcheckbox extends admin_setting_configcheckbox {
}
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');
}
}
@@ -3589,7 +3586,7 @@ class admin_setting_gradecat_combo extends admin_setting {
$defaultinfo[] = get_string('advanced');
}
$defaultinfo = implode(', ', $defaultinfo);
} else {
$defaultinfo = NULL;
}
@@ -4792,7 +4789,7 @@ function db_replace($search, $replace) {
/**
* 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() {
@@ -4813,7 +4810,7 @@ function print_plugin_tables() {
'scorm',
'survey',
'wiki');
$plugins_standard['blocks'] = array('activity_modules',
'admin',
'admin_bookmarks',
@@ -4845,7 +4842,7 @@ function print_plugin_tables() {
'tag_flickr',
'tag_youtube',
'tags');
$plugins_standard['filter'] = array('activitynames',
'algebra',
'censor',
@@ -4872,14 +4869,14 @@ function print_plugin_tables() {
$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) {
@@ -4895,8 +4892,8 @@ function print_plugin_tables() {
$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';
@@ -4906,15 +4903,15 @@ function print_plugin_tables() {
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;
@@ -4930,11 +4927,11 @@ function print_plugin_tables() {
// 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';
@@ -4949,15 +4946,15 @@ function print_plugin_tables() {
. "<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;
}
+710
View File
@@ -0,0 +1,710 @@
<?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);
}
}
?>
+14 -1
View File
@@ -539,6 +539,19 @@ $moodle_capabilities = array(
)
),
'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',
@@ -830,7 +843,7 @@ $moodle_capabilities = array(
'admin' => CAP_ALLOW
)
),
//capabilities designed for the new message system configuration
'moodle/user:editmessageprofile' => array(
+45 -10
View File
@@ -73,7 +73,9 @@
<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"/>
@@ -131,7 +133,7 @@
<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"/>
@@ -146,7 +148,14 @@
<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"/>
@@ -160,7 +169,33 @@
<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"/>
@@ -1680,11 +1715,11 @@
<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"/>
@@ -1790,4 +1825,4 @@
</SENTENCES>
</STATEMENT>
</STATEMENTS>
</XMLDB>
</XMLDB>
+77 -1
View File
@@ -191,7 +191,7 @@ function xmldb_main_upgrade($oldversion=0) {
$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!
@@ -347,6 +347,82 @@ function xmldb_main_upgrade($oldversion=0) {
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:
+54
View File
@@ -735,5 +735,59 @@ class grade_grade extends grade_object {
$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);
}
}
?>
+15
View File
@@ -173,6 +173,7 @@ abstract class grade_object {
global $DB;
if ($datas = $DB->get_records_select($table, $wheresql, $params)) {
$result = array();
foreach($datas as $data) {
$instance = new $classname();
@@ -182,6 +183,7 @@ abstract class grade_object {
return $result;
} else {
return false;
}
}
@@ -215,6 +217,7 @@ abstract class grade_object {
$DB->insert_record($this->table.'_history', $data);
}
$this->notify_changed(false);
return true;
}
@@ -244,6 +247,7 @@ abstract class grade_object {
$data->userlogged = $USER->id;
$DB->insert_record($this->table.'_history', $data);
}
$this->notify_changed(true);
return true;
} else {
@@ -306,6 +310,7 @@ abstract class grade_object {
$DB->insert_record($this->table.'_history', $data);
}
$this->notify_changed(false);
return $this->id;
}
@@ -344,5 +349,15 @@ abstract class grade_object {
}
}
}
/**
* 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) {
}
}
?>
+55 -13
View File
@@ -3345,7 +3345,7 @@ function delete_course($courseorid, $showfeedback = true) {
$courseid = $courseorid;
if (!$course = $DB->get_record('course', array('id'=>$courseid))) {
return false;
}
}
}
// frontpage course can not be deleted!!
@@ -5938,6 +5938,48 @@ function get_list_of_plugins($plugin='mod', $exclude='', $basedir='') {
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.
*
@@ -5949,13 +5991,13 @@ function check_php_version($version='5.2.0') {
}
/**
* 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) {
@@ -5966,8 +6008,8 @@ function check_php_version($version='5.2.0') {
if (preg_match("/$brand/i", $_SERVER['HTTP_USER_AGENT'])) {
return true;
}
return false;
return false;
}
/**
@@ -6572,7 +6614,7 @@ function shorten_text($text, $ideal=30, $exact = 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;
@@ -6582,23 +6624,23 @@ function shorten_text($text, $ideal=30, $exact = false) {
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;
}
+167
View File
@@ -0,0 +1,167 @@
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.
+676
View File
@@ -0,0 +1,676 @@
<?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));
}
}
?>
+4 -1
View File
@@ -23,7 +23,10 @@
<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"/>
+32 -1
View File
@@ -21,6 +21,8 @@ function xmldb_forum_upgrade($oldversion=0) {
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
@@ -75,7 +77,7 @@ function xmldb_forum_upgrade($oldversion=0) {
upgrade_mod_savepoint($result, 2007101512, 'forum');
}
if ($result and $oldversion < 2008072401) {
$eventdata = new object();
$eventdata->modulename = 'forum';
@@ -85,6 +87,35 @@ function xmldb_forum_upgrade($oldversion=0) {
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;
+120 -15
View File
@@ -29,7 +29,7 @@ define ('FORUM_AGGREGATE_SUM', 5);
/// STANDARD FUNCTIONS ///////////////////////////////////////////////////////////
/**
/**
* Code to be executed when a module is installed
* now is just used to register the module as message provider
*/
@@ -209,6 +209,83 @@ function forum_delete_instance($id) {
}
/**
* 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
@@ -3981,10 +4058,14 @@ function forum_add_discussion($discussion,&$message) {
/**
*
* 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;
@@ -3992,10 +4073,7 @@ function forum_delete_discussion($discussion, $fulldelete=false) {
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;
}
}
@@ -4007,26 +4085,45 @@ function forum_delete_discussion($discussion, $fulldelete=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);
@@ -4040,6 +4137,14 @@ function forum_delete_post($post, $children=false) {
// 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;
@@ -4345,8 +4450,8 @@ function forum_user_has_posted($forumid, $did, $userid) {
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));
}
}
/**
+74 -1
View File
@@ -13,7 +13,7 @@ class mod_forum_mod_form extends moodleform_mod {
$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);
}
@@ -178,7 +178,80 @@ class mod_forum_mod_form extends moodleform_mod {
$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;
}
}
?>
+19 -4
View File
@@ -280,14 +280,15 @@
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
@@ -402,7 +403,7 @@
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
@@ -554,6 +555,13 @@
}
add_to_log($course->id, "forum", "add post",
"$discussionurl&amp;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);
@@ -605,6 +613,13 @@
$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 {
+1 -1
View File
@@ -5,7 +5,7 @@
// This fragment is called by /admin/index.php
////////////////////////////////////////////////////////////////////////////////
$module->version = 2008072401;
$module->version = 2008072800;
$module->requires = 2008072401; // Requires this Moodle version
$module->cron = 60;
+2
View File
@@ -268,6 +268,8 @@
break;
}
$completion=new completion_info($course);
$completion->set_module_viewed($cm);
print_footer($course);
?>
+13 -1
View File
@@ -58,7 +58,7 @@ define("QUIZ_MAX_EVENT_LENGTH", 5*24*60*60); // 5 days maximum
/// FUNCTIONS ///////////////////////////////////////////////////////////////////
/**
/**
* Code to be executed when a module is installed
* now is just used to register the module as message provider
*/
@@ -1210,6 +1210,18 @@ function quiz_num_attempt_summary($quiz, $cm, $returnzero = false, $currentgroup
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
*/
+6
View File
@@ -373,6 +373,12 @@
// 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 =================================================================
Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

+62 -9
View File
@@ -91,11 +91,11 @@ h6.main {
}
#redirect #message {
}
#redirect #continue {
}
/* .clearfix {display: inline-table;} */
@@ -1803,6 +1803,29 @@ border-width:0px;
#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;
@@ -1845,7 +1868,7 @@ border-width:0px;
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;
@@ -1896,7 +1919,7 @@ border-width:0px;
.weeks .right,
.topics .right {
float: right;
}
}
.section .activity img.activityicon {
vertical-align:middle;
@@ -2277,7 +2300,7 @@ div.allcoursegrades {
}
#grade-aggregation-help dt {
margin-top: 15px;
margin-top: 15px;
}
#grade-aggregation-help dd.example {
@@ -2684,6 +2707,36 @@ body.notes .notesgroup {
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
***/
@@ -2778,7 +2831,7 @@ body.notes .notesgroup {
}
.tabrow0 .here a:link,
.tabrow0 .here a:link,
.tabrow0 .here a:visited,
.tabrow0 .here a.nolink {
position:relative;
@@ -4035,17 +4088,17 @@ table.quizreviewsummary td.cell {
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;
+1 -1
View File
@@ -6,7 +6,7 @@
// This is compared against the values stored in the database to determine
// whether upgrades should be performed (see lib/db/*.php)
$version = 2008072600; // YYYYMMDD = date of the last version bump
$version = 2008072800; // YYYYMMDD = date of the last version bump
// XX = daily increments
$release = '2.0 dev (Build: 20080728)'; // Human-friendly version name