From: sam_marshall Date: Wed, 17 Dec 2008 16:37:35 +0000 (+0000) Subject: MDL-15499: Conditional availability of activities X-Git-Url: http://git.mjollnir.org/gw?a=commitdiff_plain;h=82bd6a5ea9273bb20d798185f9104046651ad656;p=moodle.git MDL-15499: Conditional availability of activities --- diff --git a/admin/settings/development.php b/admin/settings/development.php index c246d3980e..ba722ae21c 100644 --- a/admin/settings/development.php +++ b/admin/settings/development.php @@ -14,11 +14,6 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page $item->set_updatedcallback('reset_text_filters_cache'); $temp->add($item); - // Completion system - require_once($CFG->libdir.'/completionlib.php'); - $temp->add(new admin_setting_configcheckbox('enablecompletion', get_string('enablecompletion','completion'), get_string('configenablecompletion','completion'), COMPLETION_DISABLED)); - $temp->add(new admin_setting_pickroles('progresstrackedroles', get_string('progresstrackedroles','completion'), get_string('configprogresstrackedroles', 'completion'), array('moodle/legacy:student'))); - $ADMIN->add('experimental', $temp); // DB transfer related pages diff --git a/admin/settings/subsystems.php b/admin/settings/subsystems.php index b8610761ad..28d9518919 100644 --- a/admin/settings/subsystems.php +++ b/admin/settings/subsystems.php @@ -29,4 +29,15 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page $options = array('off'=>get_string('off', 'mnet'), 'strict'=>get_string('on', 'mnet')); $optionalsubsystems->add(new admin_setting_configselect('mnet_dispatcher_mode', get_string('net', 'mnet'), get_string('configmnet', 'mnet'), 'off', $options)); + // Conditional activities: completion and availability + $optionalsubsystems->add(new admin_setting_configcheckbox('enablecompletion', + get_string('enablecompletion','completion'), + get_string('configenablecompletion','completion'), 0)); + $optionalsubsystems->add(new admin_setting_pickroles('progresstrackedroles', + get_string('progresstrackedroles','completion'), + get_string('configprogresstrackedroles', 'completion'), + array('moodle/legacy:student'))); + $optionalsubsystems->add(new admin_setting_configcheckbox('enableavailability', + get_string('enableavailability','condition'), + get_string('configenableavailability','condition'), 0)); } diff --git a/backup/backuplib.php b/backup/backuplib.php index f167b243a1..e03e4d3306 100644 --- a/backup/backuplib.php +++ b/backup/backuplib.php @@ -1227,6 +1227,10 @@ 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)); + fwrite ($bf,full_tag("AVAILABLEFROM",6,false,$course_module->availablefrom)); + fwrite ($bf,full_tag("AVAILABLEUNTIL",6,false,$course_module->availableuntil)); + fwrite ($bf,full_tag("SHOWAVAILABILITY",6,false,$course_module->showavailability)); + // get all the role_capabilities overrides in this mod write_role_overrides_xml($bf, $context, 6); /// write role_assign code here @@ -1254,6 +1258,27 @@ fwrite ($bf,end_tag("COMPLETIONDATA",6,true)); } + // Write availability data if enabled + require_once($CFG->libdir.'/conditionlib.php'); + if(!empty($CFG->enableavailability)) { + fwrite ($bf,start_tag("AVAILABILITYDATA",6,true)); + // Get all availability restrictions for this activity + $data=$DB->get_records('course_modules_availability', + array('coursemoduleid'=>$course_module->id)); + $data=$data ? $data : array(); + foreach($data as $availability) { + // Write availability record + fwrite ($bf,start_tag("AVAILABILITY",7,true)); + fwrite ($bf,full_tag("SOURCECMID",8,false,$availability->sourcecmid)); + fwrite ($bf,full_tag("REQUIREDCOMPLETION",8,false,$availability->requiredcompletion)); + fwrite ($bf,full_tag("GRADEITEMID",8,false,$availability->gradeitemid)); + fwrite ($bf,full_tag("GRADEMIN",8,false,$availability->grademin)); + fwrite ($bf,full_tag("GRADEMAX",8,false,$availability->grademax)); + fwrite ($bf,end_tag("AVAILABILITY",7,true)); + } + fwrite ($bf,end_tag("AVAILABILITYDATA",6,true)); + } + fwrite ($bf,end_tag("MOD",5,true)); } //check for next diff --git a/backup/restorelib.php b/backup/restorelib.php index 4899a94774..40cfed76ce 100644 --- a/backup/restorelib.php +++ b/backup/restorelib.php @@ -1127,6 +1127,10 @@ define('RESTORE_GROUPS_GROUPINGS', 3); $course_module->completionview=$mod->completionview; $course_module->completionexpected=$mod->completionexpected; + $course_module->availablefrom=$mod->availablefrom+$restore->course_startdateoffset; + $course_module->availableuntil=$mod->availableuntil+$restore->course_startdateoffset; + $course_module->showavailability=$mod->showavailability; + $newidmod = $DB->insert_record("course_modules", $course_module); if ($newidmod) { //save old and new module id @@ -1221,6 +1225,61 @@ define('RESTORE_GROUPS_GROUPINGS', 3); } } + // Store availability information + if($status && !empty($info->availabilitydata) && count($info->availabilitydata)>0) { + + foreach($info->availabilitydata as $data) { + // Convert cmid + $newcmid=backup_getid($restore->backup_unique_code, 'course_modules', $data->coursemoduleid); + if($newcmid) { + $data->coursemoduleid=$newcmid->new_id; + } else { + if (!defined('RESTORE_SILENTLY')) { + echo "

Can't find new ID for cm $data->coursemoduleid, ignoring availability condition.

"; + } + $status=false; + continue; + } + + // Convert source cmid + if($data->sourcecmid) { + $newcmid=backup_getid($restore->backup_unique_code, 'course_modules', $data->sourcecmid); + if($newcmid) { + $data->sourcecmid=$newcmid->new_id; + } else { + if (!defined('RESTORE_SILENTLY')) { + echo "

Can't find new ID for source cm $data->sourcecmid, ignoring availability condition.

"; + } + $status=false; + continue; + } + } + + // Convert grade id + if($data->gradeitemid) { + $newgradeid=backup_getid($restore->backup_unique_code, + 'grade_items',$data->gradeitemid); + if($newgradeid) { + $data->gradeitemid=$newgradeid->new_id; + } else { + if (!defined('RESTORE_SILENTLY')) { + echo "

Can't find new ID for grade item $data->gradeitemid, ignoring availability condition.

"; + } + $status=false; + continue; + } + } + + // Add record + if(!$DB->insert_record('course_modules_availability',$data)) { + if (!defined('RESTORE_SILENTLY')) { + echo "

Failed to insert availability data record.

"; + } + $status=false; + continue; + } + } + } } else { $status = false; } @@ -5618,6 +5677,12 @@ define('RESTORE_GROUPS_GROUPINGS', 3); 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; + $this->info->tempsection->mods[$this->info->tempmod->id]->availablefrom = + isset($this->info->tempmod->availablefrom) ? $this->info->tempmod->availablefrom : 0; + $this->info->tempsection->mods[$this->info->tempmod->id]->availableuntil = + isset($this->info->tempmod->availableuntil) ? $this->info->tempmod->availableuntil : 0; + $this->info->tempsection->mods[$this->info->tempmod->id]->showavailability = + isset($this->info->tempmod->showavailability) ? $this->info->tempmod->showavailability : 0; unset($this->info->tempmod); } @@ -5669,6 +5734,15 @@ define('RESTORE_GROUPS_GROUPINGS', 3); case "COMPLETIONEXPECTED": $this->info->tempmod->completionexpected = $this->getContents(); break; + case "AVAILABLEFROM": + $this->info->tempmod->availablefrom = $this->getContents(); + break; + case "AVAILABLEUNTIL": + $this->info->tempmod->availableuntil = $this->getContents(); + break; + case "SHOWAVAILABILITY": + $this->info->tempmod->showavailability = $this->getContents(); + break; default: break; } @@ -5794,6 +5868,40 @@ define('RESTORE_GROUPS_GROUPINGS', 3); } } } + + if (isset($this->tree[7]) && $this->tree[7] == "AVAILABILITYDATA") { + if($this->level == 8) { + switch($tagName) { + case 'AVAILABILITY': + // Got all data to make completion entry... + $this->info->tempavailability->coursemoduleid=$this->info->tempmod->id; + $this->info->availabilitydata[]=$this->info->tempavailability; + unset($this->info->tempavailability); + $this->info->tempavailability=new stdClass; + break; + } + } + + if($this->level == 9) { + switch($tagName) { + case 'SOURCECMID' : + $this->info->tempavailability->sourcecmid=$this->getContents(); + break; + case 'REQUIREDCOMPLETION' : + $this->info->tempavailability->requiredcompletion=$this->getContents(); + break; + case 'GRADEITEMID' : + $this->info->tempavailability->gradeitemid=$this->getContents(); + break; + case 'GRADEMIN' : + $this->info->tempavailability->grademin=$this->getContents(); + break; + case 'GRADEMAX' : + $this->info->tempavailability->grademax=$this->getContents(); + break; + } + } + } } //Stop parsing if todo = SECTIONS and tagName = SECTIONS (en of the tag, of course) diff --git a/course/lib.php b/course/lib.php index 22b4036411..f897235c30 100644 --- a/course/lib.php +++ b/course/lib.php @@ -982,6 +982,9 @@ function get_array_of_activities($courseid) { // groupmembersonly - is this instance visible to group members only // extra - contains extra string to include in any link global $CFG, $DB; + if(!empty($CFG->enableavailability)) { + require_once($CFG->libdir.'/conditionlib.php'); + } $mod = array(); @@ -1005,7 +1008,17 @@ function get_array_of_activities($courseid) { $mod[$seq]->groupmode = $rawmods[$seq]->groupmode; $mod[$seq]->groupingid = $rawmods[$seq]->groupingid; $mod[$seq]->groupmembersonly = $rawmods[$seq]->groupmembersonly; + $mod[$seq]->indent = $rawmods[$seq]->indent; + $mod[$seq]->completion = $rawmods[$seq]->completion; $mod[$seq]->extra = ""; + if(!empty($CFG->enableavailability)) { + condition_info::fill_availability_conditions($rawmods[$seq]); + $mod[$seq]->availablefrom = $rawmods[$seq]->availablefrom; + $mod[$seq]->availableuntil = $rawmods[$seq]->availableuntil; + $mod[$seq]->showavailability = $rawmods[$seq]->showavailability; + $mod[$seq]->conditionscompletion = $rawmods[$seq]->conditionscompletion; + $mod[$seq]->conditionsgrade = $rawmods[$seq]->conditionsgrade; + } $modname = $mod[$seq]->mod; $functionname = $modname."_get_coursemodule_info"; @@ -1048,6 +1061,9 @@ function get_array_of_activities($courseid) { */ function &get_fast_modinfo(&$course, $userid=0) { global $CFG, $USER, $DB; + if(!empty($CFG->enableavailability)) { + require_once($CFG->libdir.'/conditionlib.php'); + } static $cache = array(); @@ -1132,9 +1148,27 @@ function &get_fast_modinfo(&$course, $userid=0) { $cm->groupmode = $mod->groupmode; $cm->groupingid = $mod->groupingid; $cm->groupmembersonly = $mod->groupmembersonly; + $cm->indent = $mod->indent; + $cm->completion = $mod->completion; $cm->extra = isset($mod->extra) ? urldecode($mod->extra) : ''; $cm->icon = isset($mod->icon) ? $mod->icon : ''; $cm->uservisible = true; + if(!empty($CFG->enableavailability)) { + // We must have completion information from modinfo. If it's not + // there, cache needs rebuilding + if(!isset($mod->availablefrom)) { + debugging('enableavailability option was changed; rebuilding '. + 'cache for course '.$course->id); + rebuild_course_cache($course->id,true); + // Re-enter this routine to do it all properly + return get_fast_modinfo($course,$userid); + } + $cm->availablefrom = $mod->availablefrom; + $cm->availableuntil = $mod->availableuntil; + $cm->showavailability = $mod->showavailability; + $cm->conditionscompletion = $mod->conditionscompletion; + $cm->conditionsgrade = $mod->conditionsgrade; + } // preload long names plurals and also check module is installed properly if (!isset($modlurals[$cm->modname])) { @@ -1145,7 +1179,29 @@ function &get_fast_modinfo(&$course, $userid=0) { } $cm->modplural = $modlurals[$cm->modname]; - if (!$cm->visible and !has_capability('moodle/course:viewhiddenactivities', $contexts[$cm->id], $userid)) { + if(!empty($CFG->enableavailability)) { + // Unfortunately the next call really wants to call + // get_fast_modinfo, but that would be recursive, so we fake up a + // modinfo for it already + if(empty($minimalmodinfo)) { + $minimalmodinfo=new stdClass(); + $minimalmodinfo->cms=array(); + foreach($info as $mod) { + $minimalcm=new stdClass(); + $minimalcm->id=$mod->cm; + $minimalcm->name=urldecode($mod->name); + $minimalmodinfo->cms[$minimalcm->id]=$minimalcm; + } + } + + // Get availability information + $ci = new condition_info($cm); + $cm->available=$ci->is_available($cm->availableinfo,true,$userid, + $minimalmodinfo); + } else { + $cm->available=true; + } + if ((!$cm->visible or !$cm->available) and !has_capability('moodle/course:viewhiddenactivities', $contexts[$cm->id], $userid)) { $cm->uservisible = false; } else if (!empty($CFG->enablegroupings) and !empty($cm->groupmembersonly) @@ -1183,7 +1239,7 @@ function &get_fast_modinfo(&$course, $userid=0) { * Returns a number of useful structures for course displays */ function get_all_mods($courseid, &$mods, &$modnames, &$modnamesplural, &$modnamesused) { - global $DB; + global $DB,$COURSE; $mods = array(); // course modules indexed by id $modnames = array(); // all course module names (except resource!) @@ -1202,7 +1258,10 @@ function get_all_mods($courseid, &$mods, &$modnames, &$modnamesplural, &$modname print_error("nomodules", 'debug'); } - if ($rawmods = get_course_mods($courseid)) { + $course = ($courseid==$COURSE->id) ? $COURSE : $DB->get_record('course',array('id'=>$courseid)); + $modinfo = get_fast_modinfo($course); + + if ($rawmods=$modinfo->cms) { foreach($rawmods as $mod) { // Index the mods if (empty($modnames[$mod->modname])) { continue; @@ -1336,7 +1395,8 @@ function print_section($course, $section, $mods, $modnamesused, $absolute=false, } if (isset($modinfo->cms[$modnumber])) { - if (!$modinfo->cms[$modnumber]->uservisible) { + if (!$modinfo->cms[$modnumber]->uservisible && + empty($modinfo->cms[$modnumber]->showavailability)) { // visibility shortcut continue; } @@ -1345,7 +1405,8 @@ function print_section($course, $section, $mods, $modnamesused, $absolute=false, // module not installed continue; } - if (!coursemodule_visible_for_user($mod)) { + if (!coursemodule_visible_for_user($mod) && + empty($mod->showavailability)) { // full visibility check continue; } @@ -1366,11 +1427,11 @@ 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") { - if (!$mod->visible) { + if (!$mod->visible || !$mod->uservisible) { echo "
"; } echo format_text($extra, FORMAT_HTML, $labelformatoptions); @@ -1416,17 +1477,27 @@ function print_section($course, $section, $mods, $modnamesused, $absolute=false, $altname = get_accesshide(' '.$altname); } - $linkcss = $mod->visible ? "" : " class=\"dimmed\" "; - echo ''. - ' '. - $instancename.$altname.''; - - if (!empty($CFG->enablegroupings) && !empty($mod->groupingid) && has_capability('moodle/course:managegroups', get_context_instance(CONTEXT_COURSE, $course->id))) { - if (!isset($groupings)) { - $groupings = groups_get_all_groupings($course->id); + // We may be displaying this just in order to show information + // about visibility, without the actual link + if($mod->uservisible) { + // Display normal module link + $linkcss = $mod->visible ? "" : " class=\"dimmed\" "; + echo ''. + ' '. + $instancename.$altname.''; + + if (!empty($CFG->enablegroupings) && !empty($mod->groupingid) && has_capability('moodle/course:managegroups', get_context_instance(CONTEXT_COURSE, $course->id))) { + if (!isset($groupings)) { + $groupings = groups_get_all_groupings($course->id); + } + echo " (".format_string($groupings[$mod->groupingid]->name).')'; } - echo " (".format_string($groupings[$mod->groupingid]->name).')'; + } else { + // Display greyed-out text of link + echo ''. + ' '. + $instancename.$altname.''; } } if ($usetracking && $mod->modname == 'forum') { @@ -1460,7 +1531,8 @@ function print_section($course, $section, $mods, $modnamesused, $absolute=false, $completion=$hidecompletion ? COMPLETION_TRACKING_NONE : $completioninfo->is_enabled($mod); - if($completion!=COMPLETION_TRACKING_NONE && isloggedin() && !isguestuser()) { + if($completion!=COMPLETION_TRACKING_NONE && isloggedin() && + !isguestuser() && $mod->uservisible) { $completiondata=$completioninfo->get_data($mod,true); $completionicon=''; if($isediting) { @@ -1524,6 +1596,21 @@ function print_section($course, $section, $mods, $modnamesused, $absolute=false, } } + // Show availability information (for someone who isn't allowed to + // see the activity itself, or for staff) + if(!$mod->uservisible) { + echo '
'.$mod->availableinfo.'
'; + } else if($isediting && !empty($CFG->enableavailability)) { + $ci = new condition_info($mod); + $fullinfo=$ci->get_full_information(); + if($fullinfo) { + echo '
'.get_string($mod->showavailability + ? 'userrestriction_visible' + : 'userrestriction_hidden','condition', + $fullinfo).'
'; + } + } + echo "\n"; } diff --git a/course/mod.php b/course/mod.php index 399a3dc375..77044a02de 100644 --- a/course/mod.php +++ b/course/mod.php @@ -172,6 +172,8 @@ print_error('cannotupdatelevel'); } + rebuild_course_cache($cm->course); + if (SITEID == $cm->course) { redirect($CFG->wwwroot); } else { diff --git a/course/modedit.php b/course/modedit.php index 69a2a7ea2c..eea05a9fa4 100644 --- a/course/modedit.php +++ b/course/modedit.php @@ -6,6 +6,7 @@ require_once("lib.php"); require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->libdir.'/completionlib.php'); + require_once($CFG->libdir.'/conditionlib.php'); $add = optional_param('add', 0, PARAM_ALPHA); $update = optional_param('update', 0, PARAM_INT); @@ -123,6 +124,11 @@ $form->completionview = $cm->completionview; $form->completionexpected = $cm->completionexpected; $form->completionusegrade = is_null($cm->completiongradeitemnumber) ? 0 : 1; + if(!empty($CFG->enableavailability)) { + $form->availablefrom = $cm->availablefrom; + $form->availableuntil = $cm->availableuntil; + $form->showavailability = $cm->showavailability; + } if ($items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$form->modulename, 'iteminstance'=>$form->instance, 'courseid'=>$course->id))) { @@ -285,6 +291,12 @@ $cm->completionview = $fromform->completionview; $cm->completionexpected = $fromform->completionexpected; } + if(!empty($CFG->enableavailability)) { + $cm->availablefrom = $fromform->availablefrom; + $cm->availableuntil = $fromform->availableuntil; + $cm->showavailability = $fromform->showavailability; + condition_info::update_cm_from_form($cm,$fromform,true); + } if (!$DB->update_record('course_modules', $cm)) { print_error('cannotupdatecoursemodule'); @@ -335,6 +347,11 @@ $newcm->completionview = $fromform->completionview; $newcm->completionexpected = $fromform->completionexpected; } + if(!empty($CFG->enableavailability)) { + $newcm->availablefrom = $fromform->availablefrom; + $newcm->availableuntil = $fromform->availableuntil; + $newcm->showavailability = $fromform->showavailability; + } if (!$fromform->coursemodule = add_course_module($newcm)) { print_error('cannotaddcoursemodule'); @@ -381,6 +398,12 @@ set_coursemodule_idnumber($fromform->coursemodule, $fromform->cmidnumber); } + // Set up conditions + if($CFG->enableavailability) { + condition_info::update_cm_from_form( + (object)array('id'=>$fromform->coursemodule),$fromform,false); + } + add_to_log($course->id, "course", "add mod", "../mod/$fromform->modulename/view.php?id=$fromform->coursemodule", "$fromform->modulename $fromform->instance"); diff --git a/course/moodleform_mod.php b/course/moodleform_mod.php index 5ededc05cc..4abf1d07cb 100644 --- a/course/moodleform_mod.php +++ b/course/moodleform_mod.php @@ -1,6 +1,11 @@ libdir.'/formslib.php'); -require_once($CFG->libdir.'/completionlib.php'); +if(!empty($CFG->enablecompletion)) { + require_once($CFG->libdir.'/completionlib.php'); +} +if(!empty($CFG->enableavailability)) { + require_once($CFG->libdir.'/conditionlib.php'); +} /** * This class adds extra methods to form wrapper specific to be used for module @@ -174,6 +179,33 @@ class moodleform_mod extends moodleform { $mform->freeze($this->_customcompletionelements); } } + + // Availability conditions + if (!empty($CFG->enableavailability) && $this->_cm) { + $ci = new condition_info($this->_cm); + $fullcm=$ci->get_full_course_module(); + + $num=0; + foreach($fullcm->conditionsgrade as $gradeitemid=>$minmax) { + $groupelements=$mform->getElement('conditiongradegroup['.$num.']')->getElements(); + $groupelements[0]->setValue($gradeitemid); + // These numbers are always in the format 0.00000 - the rtrims remove any final zeros and, + // if it is a whole number, the decimal place. + $groupelements[2]->setValue(is_null($minmax->min)?'':rtrim(rtrim($minmax->min,'0'),'.')); + $groupelements[4]->setValue(is_null($minmax->max)?'':rtrim(rtrim($minmax->max,'0'),'.')); + $num++; + } + + if ($completion->is_enabled()) { + $num=0; + foreach($fullcm->conditionscompletion as $othercmid=>$state) { + $groupelements=$mform->getElement('conditioncompletiongroup['.$num.']')->getElements(); + $groupelements[0]->setValue($othercmid); + $groupelements[1]->setValue($state); + $num++; + } + } + } } // form verification @@ -230,8 +262,9 @@ class moodleform_mod extends moodleform { if (is_object($default_values)) { $default_values = (array)$default_values; } - $this->data_preprocessing($default_values); - parent::set_data($default_values); //never slashed for moodleform_mod + + $this->data_preprocessing($default_values); + parent::set_data($default_values); } /** @@ -352,8 +385,90 @@ class moodleform_mod extends moodleform { $mform->addElement('select', 'gradecat', get_string('gradecategory', 'grades'), $categories); } + if (!empty($CFG->enableavailability)) { + // Conditional availability + $mform->addElement('header', '', get_string('availabilityconditions', 'condition')); + $mform->addElement('date_selector', 'availablefrom', get_string('availablefrom', 'condition'), array('optional'=>true)); + $mform->setHelpButton('availablefrom', array('conditiondates', get_string('help_conditiondates', 'condition'), 'condition')); + $mform->addElement('date_selector', 'availableuntil', get_string('availableuntil', 'condition'), array('optional'=>true)); + $mform->setHelpButton('availableuntil', array('conditiondates', get_string('help_conditiondates', 'condition'), 'condition')); + + // Conditions based on grades + $gradeoptions=array(); + $items=grade_item::fetch_all(array('courseid'=>$COURSE->id)); + foreach($items as $id=>$item) { + $gradeoptions[$id]=$item->get_name(); + } + asort($gradeoptions); + $gradeoptions=array(0=>get_string('none','condition'))+$gradeoptions; + + $grouparray=array(); + $grouparray[] =& $mform->createElement('select','conditiongradeitemid','',$gradeoptions); + $grouparray[] =& $mform->createElement('static', '', '',' '.get_string('grade_atleast','condition')); + $grouparray[] =& $mform->createElement('text', 'conditiongrademin','',array('size'=>3)); + $grouparray[] =& $mform->createElement('static', '', '',' '.get_string('grade_upto','condition')); + $grouparray[] =& $mform->createElement('text', 'conditiongrademax','',array('size'=>3)); + $mform->setType('conditiongrademin',PARAM_FLOAT); + $mform->setType('conditiongrademax',PARAM_FLOAT); + $group = $mform->createElement('group','conditiongradegroup', + get_string('gradecondition', 'condition'),$grouparray); + + // Get version with condition info and store it so we don't ask + // twice + if(!empty($this->_cm)) { + $ci = new condition_info($this->_cm,CONDITION_MISSING_EXTRATABLE); + $this->_cm=$ci->get_full_course_module(); + $count=count($this->_cm->conditionsgrade)+1; + } else { + $count=1; + } + + $this->repeat_elements(array($group),$count,array(),'conditiongraderepeats','conditiongradeadds',2, + get_string('addgrades','condition'),true); + $mform->setHelpButton('conditiongradegroup[0]', array('gradecondition', get_string('help_gradecondition', 'condition'), 'condition')); + + // Conditions based on completion + $completion = new completion_info($COURSE); + if ($completion->is_enabled()) { + $completionoptions=array(); + $modinfo=get_fast_modinfo($COURSE); + foreach($modinfo->cms as $id=>$cm) { + $completionoptions[$id]=$cm->name; + } + asort($completionoptions); + $completionoptions=array(0=>get_string('none','condition'))+$completionoptions; + + $completionvalues=array( + COMPLETION_COMPLETE=>get_string('completion_complete','condition'), + COMPLETION_INCOMPLETE=>get_string('completion_incomplete','condition'), + COMPLETION_COMPLETE_PASS=>get_string('completion_pass','condition'), + COMPLETION_COMPLETE_FAIL=>get_string('completion_fail','condition')); + + $grouparray=array(); + $grouparray[] =& $mform->createElement('select','conditionsourcecmid','',$completionoptions); + $grouparray[] =& $mform->createElement('select','conditionrequiredcompletion','',$completionvalues); + $group = $mform->createElement('group','conditioncompletiongroup', + get_string('completioncondition', 'condition'),$grouparray); + + $count=empty($this->_cm) ? 1 : count($this->_cm->conditionscompletion)+1; + $this->repeat_elements(array($group),$count,array(), + 'conditioncompletionrepeats','conditioncompletionadds',2, + get_string('addcompletions','condition'),true); + $mform->setHelpButton('conditioncompletiongroup[0]', array('completioncondition', get_string('help_completioncondition', 'condition'), 'condition')); + } + + // Do we display availability info to students? + $mform->addElement('select', 'showavailability', get_string('showavailability', 'condition'), + array(CONDITION_STUDENTVIEW_SHOW=>get_string('showavailability_show', 'condition'), + CONDITION_STUDENTVIEW_HIDE=>get_string('showavailability_hide', 'condition'))); + $mform->setDefault('showavailability', CONDITION_STUDENTVIEW_SHOW); + $mform->setHelpButton('showavailability', array('showavailability', get_string('help_showavailability', 'condition'), 'condition')); + } + // Conditional activities: completion tracking section - $completion = new completion_info($COURSE); + if(!isset($completion)) { + $completion = new completion_info($COURSE); + } if ($completion->is_enabled()) { $mform->addElement('header', '', get_string('activitycompletion', 'completion')); @@ -362,7 +477,7 @@ class moodleform_mod extends moodleform { $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'))); diff --git a/lang/en_utf8/condition.php b/lang/en_utf8/condition.php new file mode 100644 index 0000000000..7aa8121b69 --- /dev/null +++ b/lang/en_utf8/condition.php @@ -0,0 +1,35 @@ +$a is incomplete.'; +$string['requires_completion_1']='Not available until the activity $a is marked complete.'; +$string['requires_completion_2']='Not available until the activity $a is complete and passed.'; +$string['requires_completion_3']='Not available unless the activity $a is complete and failed.'; +$string['requires_date']='Not available until $a.'; +$string['requires_date_before']='Not available from $a.'; +$string['requires_grade_any']='Not available until you have a grade in $a.'; +$string['requires_grade_min']='Not available until you achieve a required score in $a.'; +$string['requires_grade_max']='Not available unless you get an appropriate score in $a.'; +$string['requires_grade_range']='Not available unless you get a particular score in $a.'; +$string['showavailability']='Before activity is available'; +$string['showavailability_show']='Show activity greyed-out, with restriction information'; +$string['showavailability_hide']='Hide activity entirely'; +$string['userrestriction_visible']='Activity conditionally restricted: ‘$a’'; +$string['userrestriction_hidden']='Activity conditionally restricted (completely hidden, no message): ‘$a’'; +?> diff --git a/lang/en_utf8/help/condition/completioncondition.html b/lang/en_utf8/help/condition/completioncondition.html new file mode 100644 index 0000000000..1e2b83d695 --- /dev/null +++ b/lang/en_utf8/help/condition/completioncondition.html @@ -0,0 +1,20 @@ +

Activity completion condition

+ +

+You can set a condition based on whether the user has completed another +activity. +

+ +

+This feature uses the completion options that have been configured for the +other activity. You can choose whether the activity must be complete, +incomplete, complete and passed, or complete and failed. The final two +options only work if you use grade-based completion and set a pass mark on +the grade item. (Please look at the documentation for activity completion +if this is unclear.) +

+ +

+You can add more than one completion condition. All conditions must be met in +order for the activity to appear. +

diff --git a/lang/en_utf8/help/condition/conditiondates.html b/lang/en_utf8/help/condition/conditiondates.html new file mode 100644 index 0000000000..d26bf49fce --- /dev/null +++ b/lang/en_utf8/help/condition/conditiondates.html @@ -0,0 +1,23 @@ +

Available dates

+ +

+Using the 'Only available from' and 'Only available until' dates, you can make +an activity appear or disappear. The activity is only shown to students from the +'available from' date, and it disappears on the 'available until' date. Students +cannot access it outside those times, even if they guess the URL. +

+ +

+By default both dates are disabled, meaning that the activity is available at +any time (as long as the student can access the course). +

+ + diff --git a/lang/en_utf8/help/condition/gradecondition.html b/lang/en_utf8/help/condition/gradecondition.html new file mode 100644 index 0000000000..de98b5d2cb --- /dev/null +++ b/lang/en_utf8/help/condition/gradecondition.html @@ -0,0 +1,31 @@ +

Grade condition

+ +

+You can specify a condition on any grade in the course: the full course grade, +the grade for any activity, or a custom grade that you create manually. +

+ +

+You can enter either a minimum value (≥), a maximum value (<), both, or +neither. The activity will only appear if the student has a value for the +specified grade, and if it falls within any specified number range. +

+ +

+You can add more than one grade condition. All conditions must be met in order +for the activity to appear. +

+ + diff --git a/lang/en_utf8/help/condition/showavailability.html b/lang/en_utf8/help/condition/showavailability.html new file mode 100644 index 0000000000..387601e6c5 --- /dev/null +++ b/lang/en_utf8/help/condition/showavailability.html @@ -0,0 +1,27 @@ +

Unavailable activity display

+ +

+When an activity is unavailable due to the restrictions in this box, there are +two possibilities: +

+ +
    +
  1. The activity displays to users, but as greyed-out text instead of a link. + Informational text below the activity indicates when, or under what + conditions, it will become available.
  2. +
  3. The activity does not display to users at all.
  4. +
+ +

+In both cases, once the activity becomes available, it displays as normal. +

+ + diff --git a/lib/completionlib.php b/lib/completionlib.php index dc8a0f2339..110b72eb0d 100644 --- a/lib/completionlib.php +++ b/lib/completionlib.php @@ -394,13 +394,15 @@ class completion_info { * Obtains completion data for a particular activity and user (from the * session cache if available, or by SQL query) * - * @param object $cm Activity + * @param object $cm Activity; only required field is ->id * @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 + * @param array $modinfo Supply the value here - this is used for unit + * testing and so that it can be called recursively from within + * get_fast_modinfo. (Needs only list of all CMs with IDs.) + * Otherwise the method calls get_fast_modinfo itself. * @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 @@ -632,7 +634,7 @@ class completion_info { foreach ($users as $user) { $userids[] = $user->id; $resultobject->users[$user->id]=$user; - $resultobject->users[$user->id]->progress=array(); + $resultobject->users[$user->id]->progress=array(); } for($i=0; $iwwwroot.'/course/view.php?id='.$this->course->id,null,$error); + } - debugging($error, DEBUG_ALL); - print_error('err_system', 'completion', $CFG->wwwroot.'/course/view.php?id='.$this->course->id); + /** For testing only. Wipes information cached in user session. */ + static function wipe_session_cache() { + global $SESSION; + unset($SESSION->completioncache); + unset($SESSION->completioncacheuserid); } } diff --git a/lib/conditionlib.php b/lib/conditionlib.php new file mode 100644 index 0000000000..f82d2c4209 --- /dev/null +++ b/lib/conditionlib.php @@ -0,0 +1,538 @@ +conditionsgrade, ->conditionscompletion which should come from + * get_fast_modinfo. Should have ->availablefrom, ->availableuntil, + * and ->showavailability, ->course; but the only required thing is ->id. + * @param int $expectingmissing Used to control whether or not a developer + * debugging message (performance warning) will be displayed if some of + * the above data is missing and needs to be retrieved; a + * CONDITION_MISSING_xx constant + * @param bool $loaddata If you need a 'write-only' object, set this value + * to false to prevent database access from constructor + * @return condition_info Object which can retrieve information about the + * activity + */ + public function __construct($cm,$expectingmissing=CONDITION_MISSING_NOTHING, + $loaddata=true) { + global $DB; + + // Check ID as otherwise we can't do the other queries + if(empty($cm->id)) { + throw new coding_exception("Invalid parameters; course-module ID not included"); + } + + // If not loading data, don't do anything else + if(!$loaddata) { + $this->cm=(object)array('id'=>$cm->id); + $this->gotdata=false; + return; + } + + // Missing basic data from course_modules + if(!isset($cm->availablefrom) || !isset($cm->availableuntil) || + !isset($cm->showavailability) || !isset($cm->course)) { + if($expectingmissingget_record('course_modules',array('id'=>$cm->id), + 'id,course,availablefrom,availableuntil,showavailability'); + } + + $this->cm=clone($cm); + $this->gotdata=true; + + // Missing extra data + if(!isset($cm->conditionsgrade) || !isset($cm->conditionscompletion)) { + if($expectingmissingcm); + } + } + + /** + * Adds the extra availability conditions (if any) into the given + * course-module object. + * + * @param object &$cm Moodle course-module data object + */ + public static function fill_availability_conditions(&$cm) { + if(empty($cm->id)) { + throw new coding_exception("Invalid parameters; course-module ID not included"); + } + + // Does nothing if the variables are already present + if(!isset($cm->conditionsgrade) || + !isset($cm->conditionscompletion)) { + $cm->conditionsgrade=array(); + $cm->conditionscompletion=array(); + + global $DB,$CFG; + $conditions=$DB->get_records_sql($sql=" +SELECT + cma.id as cmaid, gi.*,cma.sourcecmid,cma.requiredcompletion,cma.gradeitemid, + cma.grademin as conditiongrademin, cma.grademax as conditiongrademax +FROM + {course_modules_availability} cma + LEFT JOIN {grade_items} gi ON gi.id=cma.gradeitemid +WHERE + coursemoduleid=?",array($cm->id)); + foreach($conditions as $condition) { + if(!is_null($condition->sourcecmid)) { + $cm->conditionscompletion[$condition->sourcecmid]= + $condition->requiredcompletion; + } else { + $minmax=new stdClass; + $minmax->min=$condition->conditiongrademin; + $minmax->max=$condition->conditiongrademax; + $minmax->name=self::get_grade_name($condition); + $cm->conditionsgrade[$condition->gradeitemid]=$minmax; + } + } + } + } + + /** + * Obtains the name of a grade item. + * @param object $gradeitemobj Object from get_record on grade_items table, + * (can be empty if you want to just get !missing) + * @return string Name of item of !missing if it didn't exist + */ + private static function get_grade_name($gradeitemobj) { + global $CFG; + if(isset($gradeitemobj->id)) { + require_once($CFG->libdir.'/gradelib.php'); + $item=new grade_item; + grade_object::set_properties($item,$gradeitemobj); + return $item->get_name(); + } else { + return '!missing'; // Ooops, missing grade + } + } + + /** + * @return A course-module object with all the information required to + * determine availability. + * @throws coding_exception If data wasn't loaded + */ + public function get_full_course_module() { + $this->require_data(); + return $this->cm; + } + + /** + * Adds to the database a condition based on completion of another module. + * @param int $cmid ID of other module + * @param int $requiredcompletion COMPLETION_xx constant + */ + public function add_completion_condition($cmid,$requiredcompletion) { + // Add to DB + global $DB; + $DB->insert_record('course_modules_availability', + (object)array('coursemoduleid'=>$this->cm->id, + 'sourcecmid'=>$cmid,'requiredcompletion'=>$requiredcompletion), + false); + + // Store in memory too + $this->cm->conditionscompletion[$cmid]=$requiredcompletion; + } + + /** + * Adds to the database a condition based on the value of a grade item. + * @param int $gradeitemid ID of grade item + * @param float $min Minimum grade (>=), up to 5 decimal points, or null if none + * @param float $max Maximum grade (<), up to 5 decimal points, or null if none + * @param bool $updateinmemory If true, updates data in memory; otherwise, + * memory version may be out of date (this has performance consequences, + * so don't do it unless it really needs updating) + */ + public function add_grade_condition($gradeitemid,$min,$max,$updateinmemory=false) { + // Normalise nulls + if($min==='') { + $min=null; + } + if($max==='') { + $max=null; + } + // Add to DB + global $DB; + $DB->insert_record('course_modules_availability', + (object)array('coursemoduleid'=>$this->cm->id, + 'gradeitemid'=>$gradeitemid,'grademin'=>$min,'grademax'=>$max), + false); + + // Store in memory too + if($updateinmemory) { + $this->cm->conditionsgrade[$gradeitemid]=(object)array( + 'min'=>$min,'max'=>$max); + $this->cm->conditionsgrade[$gradeitemid]->name= + self::get_grade_name($DB->get_record('grade_items', + array('id'=>$gradeitemid))); + } + } + + /** + * Erases from the database all conditions for this activity. + */ + public function wipe_conditions() { + // Wipe from DB + global $DB; + $DB->delete_records('course_modules_availability', + array('coursemoduleid'=>$this->cm->id)); + + // And from memory + $this->cm->conditionsgrade=array(); + $this->cm->conditionscompletion=array(); + } + + /** + * Obtains a string describing all availability restrictions (even if + * they do not apply any more). + * @param object $modinfo Usually leave as null for default. Specify when + * calling recursively from inside get_fast_modinfo. The value supplied + * here must include list of all CMs with 'id' and 'name' + * @return string Information string (for admin) about all restrictions on + * this item + * @throws coding_exception If data wasn't loaded + */ + public function get_full_information($modinfo=null) { + $this->require_data(); + global $COURSE,$DB; + + $information=''; + + // Completion conditions + if(count($this->cm->conditionscompletion)>0) { + if($this->cm->course==$COURSE->id) { + $course=$COURSE; + } else { + $course=$DB->get_record('course',array('id'=>$this->cm->course),'id,enablecompletion,modinfo'); + } + foreach($this->cm->conditionscompletion as $cmid=>$expectedcompletion) { + if(!$modinfo) { + $modinfo=get_fast_modinfo($course); + } + $information.=get_string( + 'requires_completion_'.$expectedcompletion, + 'condition',$modinfo->cms[$cmid]->name).' '; + } + } + + // Grade conditions + if(count($this->cm->conditionsgrade)>0) { + foreach($this->cm->conditionsgrade as $gradeitemid=>$minmax) { + // String depends on type of requirement. We are coy about + // the actual numbers, in case grades aren't released to + // students. + if(is_null($minmax->min) && is_null($minmax->max)) { + $string='any'; + } else if(is_null($minmax->max)) { + $string='min'; + } else if(is_null($minmax->min)) { + $string='max'; + } else { + $string='range'; + } + $information.=get_string('requires_grade_'.$string,'condition',$minmax->name).' '; + } + } + + // Dates + if($this->cm->availablefrom) { + $information.=get_string('requires_date','condition',userdate( + $this->cm->availablefrom,get_string('strftimedate','langconfig'))); + } + + if($this->cm->availableuntil) { + $information.=get_string('requires_date_before','condition',userdate( + $this->cm->availableuntil,get_string('strftimedate','langconfig'))); + } + + $information=trim($information); + return $information; + } + + /** + * Determines whether this particular course-module is currently available + * according to these criteria. + * + * - This does not include the 'visible' setting (i.e. this might return + * true even if visible is false); visible is handled independently. + * - This does not take account of the viewhiddenactivities capability. + * That should apply later. + * + * @param string &$information If the item has availability restrictions, + * a string that describes the conditions will be stored in this variable; + * if this variable is set blank, that means don't display anything + * @param bool $grabthelot Performance hint: if true, caches information + * required for all course-modules, to make the front page and similar + * pages work more quickly (works only for current user) + * @param int $userid If set, specifies a different user ID to check availability for + * @param object $modinfo Usually leave as null for default. Specify when + * calling recursively from inside get_fast_modinfo. The value supplied + * here must include list of all CMs with 'id' and 'name' + * @return bool True if this item is available to the user, false otherwise + * @throws coding_exception If data wasn't loaded + */ + public function is_available(&$information,$grabthelot=false,$userid=0,$modinfo=null) { + $this->require_data(); + global $COURSE,$DB; + + $available=true; + $information=''; + + // Check each completion condition + if(count($this->cm->conditionscompletion)>0) { + if($this->cm->course==$COURSE->id) { + $course=$COURSE; + } else { + $course=$DB->get_record('course',array('id'=>$this->cm->course),'id,enablecompletion,modinfo'); + } + + $completion=new completion_info($course); + foreach($this->cm->conditionscompletion as $cmid=>$expectedcompletion) { + // The completion system caches its own data + $completiondata=$completion->get_data((object)array('id'=>$cmid), + $grabthelot,$userid,$modinfo); + + $thisisok=true; + if($expectedcompletion==COMPLETION_COMPLETE) { + // 'Complete' also allows the pass, fail states + switch($completiondata->completionstate) { + case COMPLETION_COMPLETE: + case COMPLETION_COMPLETE_FAIL: + case COMPLETION_COMPLETE_PASS: + break; + default: + $thisisok=false; + } + } else { + // Other values require exact match + if($completiondata->completionstate!=$expectedcompletion) { + $thisisok=false; + } + } + if(!$thisisok) { + $available=false; + if(!$modinfo) { + $modinfo=get_fast_modinfo($course); + } + $information.=get_string( + 'requires_completion_'.$expectedcompletion, + 'condition',$modinfo->cms[$cmid]->name).' '; + } + } + } + + // Check each grade condition + if(count($this->cm->conditionsgrade)>0) { + foreach($this->cm->conditionsgrade as $gradeitemid=>$minmax) { + $score=$this->get_cached_grade_score($gradeitemid,$grabthelot,$userid); + if($score===false || + (!is_null($minmax->min) && $score<$minmax->min) || + (!is_null($minmax->max) && $score>=$minmax->max)) { + // Grade fail + $available=false; + // String depends on type of requirement. We are coy about + // the actual numbers, in case grades aren't released to + // students. + if(is_null($minmax->min) && is_null($minmax->max)) { + $string='any'; + } else if(is_null($minmax->max)) { + $string='min'; + } else if(is_null($minmax->min)) { + $string='max'; + } else { + $string='range'; + } + $information.=get_string('requires_grade_'.$string,'condition',$minmax->name).' '; + } + } + } + + // Test dates + if($this->cm->availablefrom) { + if(time() < $this->cm->availablefrom) { + $available=false; + $information.=get_string('requires_date','condition',userdate( + $this->cm->availablefrom,get_string('strftimedate','langconfig'))); + } + } + + if($this->cm->availableuntil) { + if(time() >= $this->cm->availableuntil) { + $available=false; + // But we don't display any information about this case. This is + // because the only reason to set a 'disappear' date is usually + // to get rid of outdated information/clutter in which case there + // is no point in showing it... + + // Note it would be nice if we could make it so that the 'until' + // date appears below the item while the item is still accessible, + // unfortunately this is not possible in the current system. Maybe + // later, or if somebody else wants to add it. + } + } + + $information=trim($information); + return $available; + } + + /** + * @return bool True if information about availability should be shown to + * normal users + * @throws coding_exception If data wasn't loaded + */ + public function show_availability() { + $this->require_data(); + return $this->cm->showavailability; + } + + /** + * Internal function cheks that data was loaded. + * @throws coding_exception If data wasn't loaded + */ + private function require_data() { + if(!$this->gotdata) { + throw new coding_exception('Error: cannot call when info was '. + 'constructed without data'); + } + } + + /** + * Obtains a grade score. Note that this score should not be displayed to + * the user, because gradebook rules might prohibit that. It may be a + * non-final score subject to adjustment later. + * + * @param int $gradeitemid Grade item ID we're interested in + * @param bool $grabthelot If true, grabs all scores for current user on + * this course, so that later ones come from cache + * @param int $userid Set if requesting grade for a different user (does + * not use cache) + * @return float Grade score, or false if user does not have a grade yet + */ + private function get_cached_grade_score($gradeitemid,$grabthelot=false,$userid=0) { + global $USER, $DB, $SESSION; + if($userid==0 || $userid=$USER->id) { + // For current user, go via cache in session + if(empty($SESSION->gradescorecache) || $SESSION->gradescorecacheuserid!=$USER->id) { + $SESSION->gradescorecache=array(); + $SESSION->gradescorecacheuserid=$USER->id; + } + if(!array_key_exists($gradeitemid,$SESSION->gradescorecache)) { + if($grabthelot) { + // Get all grades for the current course + $rs=$DB->get_recordset_sql(" +SELECT + gi.id,gg.finalgrade +FROM + {grade_items} gi + LEFT JOIN {grade_grades} gg ON gi.id=gg.itemid AND gg.userid=? +WHERE + gi.courseid=?",array($USER->id,$this->cm->course)); + foreach($rs as $record) { + $SESSION->gradescorecache[$record->id]= + is_null($record->finalgrade) + ? false + : $record->finalgrade; + + } + $rs->close(); + // And if it's still not set, well it doesn't exist (eg + // maybe the user set it as a condition, then deleted the + // grade item) so we call it false + if(!array_key_exists($gradeitemid,$SESSION->gradescorecache)) { + $SESSION->gradescorecache[$gradeitemid]=false; + } + } else { + // Just get current grade + $score=$DB->get_field('grade_grades','finalgrade',array( + 'userid'=>$USER->id,'itemid'=>$gradeitemid)); + // Treat the case where row exists but is null, same as + // case where row doesn't exist + if(is_null($score)) { + $score=false; + } + $SESSION->gradescorecache[$gradeitemid]=$score; + } + } + return $SESSION->gradescorecache[$gradeitemid]; + } else { + // Not the current user, so request the score individually + $score=$DB->get_field('grade_grades','finalgrade',array( + 'userid'=>$userid,'itemid'=>$gradeitemid)); + if($score===null) { + $score=false; + } + return $score; + } + } + + /** For testing only. Wipes information cached in user session. */ + static function wipe_session_cache() { + global $SESSION; + unset($SESSION->gradescorecache); + unset($SESSION->gradescorecacheuserid); + } + + /** + * Utility function called by modedit.php; updates the + * course_modules_availability table based on the module form data. + * + * @param object $cm Course-module with as much data as necessary, min id + * @param unknown_type $fromform + * @param unknown_type $wipefirst + */ + public static function update_cm_from_form($cm,$fromform,$wipefirst=true) { + $ci=new condition_info($cm,CONDITION_MISSING_EVERYTHING,false); + if($wipefirst) { + $ci->wipe_conditions(); + } + foreach($fromform->conditiongradegroup as $record) { + if($record['conditiongradeitemid']) { + $ci->add_grade_condition($record['conditiongradeitemid'], + $record['conditiongrademin'],$record['conditiongrademax']); + } + } + if(isset($fromform->conditioncompletiongroup)) { + foreach($fromform->conditioncompletiongroup as $record) { + if($record['conditionsourcecmid']) { + $ci->add_completion_condition($record['conditionsourcecmid'], + $record['conditionrequiredcompletion']); + } + } + } + } +} +?> diff --git a/lib/datalib.php b/lib/datalib.php index f3cddee115..f5d60abce2 100644 --- a/lib/datalib.php +++ b/lib/datalib.php @@ -1835,7 +1835,7 @@ function instance_is_visible($moduletype, $module) { * @return bool */ function coursemodule_visible_for_user($cm, $userid=0) { - global $USER; + global $USER,$CFG; if (empty($cm->id)) { debugging("Incorrect course module parameter!", DEBUG_DEVELOPER); @@ -1847,6 +1847,15 @@ function coursemodule_visible_for_user($cm, $userid=0) { if (!$cm->visible and !has_capability('moodle/course:viewhiddenactivities', get_context_instance(CONTEXT_MODULE, $cm->id), $userid)) { return false; } + if ($CFG->enableavailability) { + require_once($CFG->libdir.'/conditionlib.php'); + $ci=new condition_info($cm,CONDITION_MISSING_EXTRATABLE); + if(!$ci->is_available($cm->availableinfo,false,$userid) and + !has_capability('moodle/course:viewhiddenactivities', + get_context_instance(CONTEXT_MODULE, $cm->id), $userid)) { + return false; + } + } return groups_course_module_visible($cm, $userid); } diff --git a/lib/db/install.xml b/lib/db/install.xml index c3a4313bfb..e855247707 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -171,8 +171,8 @@ - - + + diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 7364cb1644..8fc44a831e 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -1056,7 +1056,52 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint($result, 2008111801); } - if ($result && $oldversion < 2008112400) { + if ($result && $oldversion < 2008120700) { + + /// Changing precision of field shortname on table course_request to (100) + $table = new xmldb_table('course_request'); + $field = new xmldb_field('shortname', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null, null, null, 'fullname'); + + /// Launch change of precision for field shortname + $dbman->change_field_precision($table, $field); + + /// Main savepoint reached + upgrade_main_savepoint($result, 2008120700); + } + + /// For MDL-17501. Ensure that any role that has moodle/course:update also + /// has moodle/course:visibility. + if ($result && $oldversion < 2008120800) { + /// Get the roles with 'moodle/course:update'. + $systemcontext = get_context_instance(CONTEXT_SYSTEM); + $roles = get_roles_with_capability('moodle/course:update', CAP_ALLOW, $systemcontext); + + /// Give those roles 'moodle/course:visibility'. + foreach ($roles as $role) { + assign_capability('moodle/course:visibility', CAP_ALLOW, $role->id, $systemcontext->id); + } + + /// Force all sessions to refresh access data. + mark_context_dirty($systemcontext->path); + + /// Main savepoint reached + upgrade_main_savepoint($result, 2008120800); + } + + if ($result && $oldversion < 2008120801) { + + /// Changing precision of field shortname on table mnet_enrol_course to (100) + $table = new xmldb_table('mnet_enrol_course'); + $field = new xmldb_field('shortname', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null, null, null, 'fullname'); + + /// Launch change of precision for field shortname + $dbman->change_field_precision($table, $field); + + /// Main savepoint reached + upgrade_main_savepoint($result, 2008120801); + } + + if ($result && $oldversion < 2008121701) { /// Define field availablefrom to be added to course_modules $table = new xmldb_table('course_modules'); @@ -1074,7 +1119,7 @@ function xmldb_main_upgrade($oldversion) { if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } - + /// Define field showavailability to be added to course_modules $field = new xmldb_field('showavailability', XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, '0', 'availableuntil'); @@ -1082,7 +1127,7 @@ function xmldb_main_upgrade($oldversion) { if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } - + /// Define table course_modules_availability to be created $table = new xmldb_table('course_modules_availability'); @@ -1106,53 +1151,18 @@ function xmldb_main_upgrade($oldversion) { $dbman->create_table($table); } - /// Main savepoint reached - upgrade_main_savepoint($result, 2008112400); - } - - if ($result && $oldversion < 2008120700) { - - /// Changing precision of field shortname on table course_request to (100) - $table = new xmldb_table('course_request'); - $field = new xmldb_field('shortname', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null, null, null, 'fullname'); - - /// Launch change of precision for field shortname - $dbman->change_field_precision($table, $field); - - /// Main savepoint reached - upgrade_main_savepoint($result, 2008120700); - } - - /// For MDL-17501. Ensure that any role that has moodle/course:update also - /// has moodle/course:visibility. - if ($result && $oldversion < 2008120800) { - /// Get the roles with 'moodle/course:update'. - $systemcontext = get_context_instance(CONTEXT_SYSTEM); - $roles = get_roles_with_capability('moodle/course:update', CAP_ALLOW, $systemcontext); + /// Changes to modinfo mean we need to rebuild course cache + rebuild_course_cache(0,true); - /// Give those roles 'moodle/course:visibility'. - foreach ($roles as $role) { - assign_capability('moodle/course:visibility', CAP_ALLOW, $role->id, $systemcontext->id); + /// For developer upgrades, turn on the conditional activities and completion + /// features automatically (to gain more testing) + if(debugging('',DEBUG_DEVELOPER)) { + set_config('enableavailability',1); + set_config('enablecompletion',1); } - /// Force all sessions to refresh access data. - mark_context_dirty($systemcontext->path); - - /// Main savepoint reached - upgrade_main_savepoint($result, 2008120800); - } - - if ($result && $oldversion < 2008120801) { - - /// Changing precision of field shortname on table mnet_enrol_course to (100) - $table = new xmldb_table('mnet_enrol_course'); - $field = new xmldb_field('shortname', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null, null, null, 'fullname'); - - /// Launch change of precision for field shortname - $dbman->change_field_precision($table, $field); - /// Main savepoint reached - upgrade_main_savepoint($result, 2008120801); + upgrade_main_savepoint($result, 2008121701); } return $result; diff --git a/lib/grade/grade_grade.php b/lib/grade/grade_grade.php index d1b71cacb9..3326954c79 100644 --- a/lib/grade/grade_grade.php +++ b/lib/grade/grade_grade.php @@ -738,10 +738,16 @@ class grade_grade extends grade_object { /** * Used to notify the completion system (if necessary) that a user's grade - * has changed. + * has changed, and clear up a possible score cache. * @param bool deleted True if grade was actually deleted */ function notify_changed($deleted) { + // Grades may be cached in user session + global $USER,$SESSION; + if($USER->id==$this->userid) { + unset($SESSION->gradescorecache[$this->itemid]); + } + // Ignore during restore // TODO There should be a proper way to determine when we are in restore // so that this hack looking for a $restore global is not needed. diff --git a/lib/moodlelib.php b/lib/moodlelib.php index 3d16c9c4fc..ee999932cc 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -2047,6 +2047,30 @@ function require_login($courseorid=0, $autologinguest=true, $cm=null, $setwantsu print_error('nocontext'); } } + + // Conditional activity access control + if(!empty($CFG->enableavailability) and $cm) { + // We cache conditional access in session + if(!isset($SESSION->conditionaccessok)) { + $SESSION->conditionaccessok=array(); + } + // If you have been allowed into the module once then you are allowed + // in for rest of session, no need to do conditional checks + if(!array_key_exists($cm->id,$SESSION->conditionaccessok)) { + // Get condition info (does a query for the availability table) + require_once($CFG->libdir.'/conditionlib.php'); + $ci=new condition_info($cm,CONDITION_MISSING_EXTRATABLE); + // Check condition for user (this will do a query if the availability + // information depends on grade or completion information) + if($ci->is_available($junk) || + has_capability('moodle/course:viewhiddenactivities', $COURSE->context)) { + $SESSION->conditionaccessok[$cm->id]=true; + } else { + print_error('activityiscurrentlyhidden'); + } + } + } + if ($COURSE->id == SITEID) { /// Eliminate hidden site activities straight away if (!empty($cm) && !$cm->visible diff --git a/lib/simpletest/testconditionlib.php b/lib/simpletest/testconditionlib.php new file mode 100644 index 0000000000..f5b54690e8 --- /dev/null +++ b/lib/simpletest/testconditionlib.php @@ -0,0 +1,344 @@ +dirroot . '/lib/conditionlib.php'); + +class conditionlib_test extends MoodleUnitTestCase { + var $oldcfg; + + public function setUp() { + parent::setUp(); + global $CFG; + $this->oldcfg=clone $CFG; + $CFG->enableavailability=true; + $CFG->enablecompletion=true; + } + + /** + * Method called after each test method. Doesn't do anything extraordinary except restore the global $DB to the real one. + */ + public function tearDown() { + $CFG->enableavailability=$this->oldcfg->enableavailability; + $CFG->enablecompletion=$this->oldcfg->enablecompletion; + parent::tearDown(); + } + + function test_constructor() { + global $DB,$CFG; + $cm=new stdClass; + + // Test records + $id=$DB->insert_record('course_modules',(object)array( + 'showavailability'=>1,'availablefrom'=>17,'availableuntil'=>398,'course'=>64)); + + // no ID + try { + $test=new condition_info($cm); + $this->fail(); + } catch(coding_exception $e) { + } + + // no other data + $cm->id=$id; + $test=new condition_info($cm,CONDITION_MISSING_EVERYTHING); + $this->assertEqual( + (object)array('id'=>$id,'showavailability'=>1, + 'availablefrom'=>17,'availableuntil'=>398,'course'=>64, + 'conditionsgrade'=>array(), 'conditionscompletion'=>array()), + $test->get_full_course_module()); + + // just the course_modules stuff; check it doesn't request that from db + $cm->showavailability=0; + $cm->availablefrom=2; + $cm->availableuntil=74; + $cm->course=38; + $test=new condition_info($cm,CONDITION_MISSING_EXTRATABLE); + $this->assertEqual( + (object)array('id'=>$id,'showavailability'=>0, + 'availablefrom'=>2,'availableuntil'=>74,'course'=>38, + 'conditionsgrade'=>array(), 'conditionscompletion'=>array()), + $test->get_full_course_module()); + + // Now let's add some actual grade/completion conditions + $DB->insert_record('course_modules_availability',(object)array( + 'coursemoduleid'=>$id, + 'sourcecmid'=>42, + 'requiredcompletion'=>2 + )); + $DB->insert_record('course_modules_availability',(object)array( + 'coursemoduleid'=>$id, + 'sourcecmid'=>666, + 'requiredcompletion'=>1 + )); + $DB->insert_record('course_modules_availability',(object)array( + 'coursemoduleid'=>$id, + 'gradeitemid'=>37, + 'grademin'=>5.5 + )); + + $cm=(object)array('id'=>$id); + $test=new condition_info($cm,CONDITION_MISSING_EVERYTHING); + $fullcm=$test->get_full_course_module(); + $this->assertEqual(array(42=>2,666=>1),$fullcm->conditionscompletion); + $this->assertEqual(array(37=>(object)array('min'=>5.5,'max'=>null,'name'=>'!missing')), + $fullcm->conditionsgrade); + } + + private function make_course() { + global $DB; + $categoryid=$DB->insert_record('course_categories',(object)array()); + return $DB->insert_record('course',(object)array( + 'fullname'=>'Condition test','shortname'=>'CT1', + 'category'=>$categoryid,'enablecompletion'=>1)); + } + + private function make_course_module($courseid,$params=array()) { + global $DB; + static $moduleid=0; + if(!$moduleid) { + $moduleid=$DB->get_field('modules','id',array('name'=>'resource')); + } + + $rid=$DB->insert_record('resource',(object)array('course'=>$courseid, + 'name'=>'xxx','alltext'=>'','popup'=>'')); + $settings=(object)array( + 'course'=>$courseid,'module'=>$moduleid,'instance'=>$rid); + foreach($params as $name=>$value) { + $settings->{$name}=$value; + } + return $DB->insert_record('course_modules',$settings); + } + + private function make_section($courseid,$cmids,$sectionnum=0) { + global $DB; + $DB->insert_record('course_sections',(object)array( + 'course'=>$courseid,'sequence'=>implode(',',$cmids),'section'=>$sectionnum)); + } + + function test_modinfo() { + global $DB; + + // Let's make a course + $courseid=$this->make_course(); + + // Now let's make a couple modules on that course + $cmid1=$this->make_course_module($courseid,array( + 'showavailability'=>1,'availablefrom'=>17,'availableuntil'=>398, + 'completion'=>COMPLETION_TRACKING_MANUAL)); + $cmid2=$this->make_course_module($courseid,array( + 'showavailability'=>0,'availablefrom'=>0,'availableuntil'=>0)); + $this->make_section($courseid,array($cmid1,$cmid2)); + + // Add a fake grade item + $gradeitemid=$DB->insert_record('grade_items',(object)array( + 'courseid'=>$courseid,'itemname'=>'frog')); + + // One of the modules has grade and completion conditions, other doesn't + $DB->insert_record('course_modules_availability',(object)array( + 'coursemoduleid'=>$cmid2, + 'sourcecmid'=>$cmid1, + 'requiredcompletion'=>1 + )); + $DB->insert_record('course_modules_availability',(object)array( + 'coursemoduleid'=>$cmid2, + 'gradeitemid'=>$gradeitemid, + 'grademin'=>5.5 + )); + + // Okay sweet, now get modinfo + $modinfo=get_fast_modinfo($DB->get_record('course',array('id'=>$courseid))); + + // Test basic data + $this->assertEqual(1,$modinfo->cms[$cmid1]->showavailability); + $this->assertEqual(17,$modinfo->cms[$cmid1]->availablefrom); + $this->assertEqual(398,$modinfo->cms[$cmid1]->availableuntil); + $this->assertEqual(0,$modinfo->cms[$cmid2]->showavailability); + $this->assertEqual(0,$modinfo->cms[$cmid2]->availablefrom); + $this->assertEqual(0,$modinfo->cms[$cmid2]->availableuntil); + + // Test condition arrays + $this->assertEqual(array(),$modinfo->cms[$cmid1]->conditionscompletion); + $this->assertEqual(array(),$modinfo->cms[$cmid1]->conditionsgrade); + $this->assertEqual(array($cmid1=>1), + $modinfo->cms[$cmid2]->conditionscompletion); + $this->assertEqual(array($gradeitemid=>(object)array('min'=>5.5,'max'=>null,'name'=>'frog')), + $modinfo->cms[$cmid2]->conditionsgrade); + } + + function test_add_and_remove() { + global $DB; + // Make course and module + $courseid=$this->make_course(); + $cmid=$this->make_course_module($courseid,array( + 'showavailability'=>0,'availablefrom'=>0,'availableuntil'=>0)); + $this->make_section($courseid,array($cmid)); + + // Check it has no conditions + $test1=new condition_info((object)array('id'=>$cmid), + CONDITION_MISSING_EVERYTHING); + $cm=$test1->get_full_course_module(); + $this->assertEqual(array(),$cm->conditionscompletion); + $this->assertEqual(array(),$cm->conditionsgrade); + + // Add conditions of each type + $test1->add_completion_condition(13,3); + $this->assertEqual(array(13=>3),$cm->conditionscompletion); + $test1->add_grade_condition(666,0.4,null,true); + $this->assertEqual(array(666=>(object)array('min'=>0.4,'max'=>null,'name'=>'!missing')), + $cm->conditionsgrade); + + // Check they were really added in db + $test2=new condition_info((object)array('id'=>$cmid), + CONDITION_MISSING_EVERYTHING); + $cm=$test2->get_full_course_module(); + $this->assertEqual(array(13=>3),$cm->conditionscompletion); + $this->assertEqual(array(666=>(object)array('min'=>0.4,'max'=>null,'name'=>'!missing')), + $cm->conditionsgrade); + + // Wipe conditions + $test2->wipe_conditions(); + $this->assertEqual(array(),$cm->conditionscompletion); + $this->assertEqual(array(),$cm->conditionsgrade); + + // Check they were really wiped + $test3=new condition_info((object)array('id'=>$cmid), + CONDITION_MISSING_EVERYTHING); + $cm=$test3->get_full_course_module(); + $this->assertEqual(array(),$cm->conditionscompletion); + $this->assertEqual(array(),$cm->conditionsgrade); + } + + function test_is_available() { + global $DB,$USER; + $courseid=$this->make_course(); + + // No conditions + $cmid=$this->make_course_module($courseid); + $ci=new condition_info((object)array('id'=>$cmid), + CONDITION_MISSING_EVERYTHING); + $this->assertTrue($ci->is_available($text,false,0)); + $this->assertEqual('',$text); + + // Time (from) + $time=time()+100; + $cmid=$this->make_course_module($courseid,array('availablefrom'=>$time)); + $ci=new condition_info((object)array('id'=>$cmid), + CONDITION_MISSING_EVERYTHING); + $this->assertFalse($ci->is_available($text)); + $this->assert(new PatternExpectation( + '/'.preg_quote(userdate($time,get_string('strftimedate','langconfig'))).'/'),$text); + + $time=time()-100; + $cmid=$this->make_course_module($courseid,array('availablefrom'=>$time)); + $ci=new condition_info((object)array('id'=>$cmid), + CONDITION_MISSING_EVERYTHING); + $this->assertTrue($ci->is_available($text)); + $this->assertEqual('',$text); + $this->assert(new PatternExpectation( + '/'.preg_quote(userdate($time,get_string('strftimedate','langconfig'))).'/'),$ci->get_full_information()); + + // Time (until) + $cmid=$this->make_course_module($courseid,array('availableuntil'=>time()-100)); + $ci=new condition_info((object)array('id'=>$cmid), + CONDITION_MISSING_EVERYTHING); + $this->assertFalse($ci->is_available($text)); + $this->assertEqual('',$text); + + // Completion + $oldid=$cmid; + $cmid=$this->make_course_module($courseid); + $this->make_section($courseid,array($oldid,$cmid)); + $oldcm=$DB->get_record('course_modules',array('id'=>$oldid)); + $oldcm->completion=COMPLETION_TRACKING_MANUAL; + $DB->update_record('course_modules',$oldcm); + + $ci=new condition_info((object)array('id'=>$cmid),CONDITION_MISSING_EVERYTHING); + $ci->add_completion_condition($oldid,COMPLETION_COMPLETE); + $this->assertFalse($ci->is_available($text,false)); + $this->assertEqual(get_string('requires_completion_1','condition','xxx'),$text); + + $completion=new completion_info($DB->get_record('course',array('id'=>$courseid))); + $completion->update_state($oldcm,COMPLETION_COMPLETE); + completion_info::wipe_session_cache(); + condition_info::wipe_session_cache(); + + $this->assertTrue($ci->is_available($text)); + $this->assertFalse($ci->is_available($text,false,$USER->id+1)); + completion_info::wipe_session_cache(); + condition_info::wipe_session_cache(); + $completion=new completion_info($DB->get_record('course',array('id'=>$courseid))); + $completion->update_state($oldcm,COMPLETION_INCOMPLETE); + $this->assertFalse($ci->is_available($text)); + + $ci->wipe_conditions(); + $ci->add_completion_condition($oldid,COMPLETION_INCOMPLETE); + condition_info::wipe_session_cache(); + $this->assertTrue($ci->is_available($text)); + $this->assertTrue($ci->is_available($text,false,$USER->id+1)); + + condition_info::wipe_session_cache(); + $this->assertTrue($ci->is_available($text,true)); + + // Grade + $ci->wipe_conditions(); + // Add a fake grade item + $gradeitemid=$DB->insert_record('grade_items',(object)array( + 'courseid'=>$courseid,'itemname'=>'frog')); + // Add a condition on a value existing... + $ci->add_grade_condition($gradeitemid,null,null,true); + $this->assertFalse($ci->is_available($text)); + $this->assertEqual(get_string('requires_grade_any','condition','frog'),$text); + + // Fake it existing + $DB->insert_record('grade_grades',(object)array( + 'itemid'=>$gradeitemid,'userid'=>$USER->id,'finalgrade'=>3.78)); + condition_info::wipe_session_cache(); + $this->assertTrue($ci->is_available($text)); + + condition_info::wipe_session_cache(); + $this->assertTrue($ci->is_available($text,true)); + + // Now require that user gets more than 3.78001 + $ci->wipe_conditions(); + $ci->add_grade_condition($gradeitemid,3.78001,null,true); + condition_info::wipe_session_cache(); + $this->assertFalse($ci->is_available($text)); + $this->assertEqual(get_string('requires_grade_min','condition','frog'),$text); + + // ...just on 3.78... + $ci->wipe_conditions(); + $ci->add_grade_condition($gradeitemid,3.78,null,true); + condition_info::wipe_session_cache(); + $this->assertTrue($ci->is_available($text)); + + // ...less than 3.78 + $ci->wipe_conditions(); + $ci->add_grade_condition($gradeitemid,null,3.78,true); + condition_info::wipe_session_cache(); + $this->assertFalse($ci->is_available($text)); + $this->assertEqual(get_string('requires_grade_max','condition','frog'),$text); + + // ...less than 3.78001 + $ci->wipe_conditions(); + $ci->add_grade_condition($gradeitemid,null,3.78001,true); + condition_info::wipe_session_cache(); + $this->assertTrue($ci->is_available($text)); + + // ...in a range that includes it + $ci->wipe_conditions(); + $ci->add_grade_condition($gradeitemid,3,4,true); + condition_info::wipe_session_cache(); + $this->assertTrue($ci->is_available($text)); + + // ...in a range that doesn't include it + $ci->wipe_conditions(); + $ci->add_grade_condition($gradeitemid,4,5,true); + condition_info::wipe_session_cache(); + $this->assertFalse($ci->is_available($text)); + $this->assertEqual(get_string('requires_grade_range','condition','frog'),$text); + } + +} +?> diff --git a/theme/standard/styles_fonts.css b/theme/standard/styles_fonts.css index 22caea6b7a..d614c826de 100644 --- a/theme/standard/styles_fonts.css +++ b/theme/standard/styles_fonts.css @@ -569,6 +569,19 @@ h2.headingblock { color: #666666; } +#course-view .availabilityinfo { + font-size:0.85em; + color:#aaa; +} +#course-view .availabilityinfo strong { + font-weight:normal; + color:black; +} +#course-view .dimmed_text img { + opacity:0.3; + filter: alpha(opacity='30'); +} + /*** *** Doc ***/ @@ -1057,6 +1070,10 @@ body#mod-forum-index .generalbox .cell { #mod-quiz-edit .questionbankwindow div.header{ font-weight:bold; } +#mod-quiz-edit a.configurerandomquestion{ + font-size:small; + text-decoration:underline; +} /*** diff --git a/version.php b/version.php index 9059e854a0..cc2508eece 100644 --- a/version.php +++ b/version.php @@ -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 = 2008121000; // YYYYMMDD = date of the last version bump + $version = 2008121701; // YYYYMMDD = date of the last version bump // XX = daily increments $release = '2.0 dev (Build: 20081217)'; // Human-friendly version name