commit 46b5d7cbd5bf97b5a87d1e91a794c0e69b9a4d4a Author: Kirill Astashov Date: Tue Dec 20 17:32:14 2011 +1030 Add custom release criteria for topics/weeks Date-time, grade and activity completion options. Section (topic or week) may be greyed-out or completely hidden until requirements are met. diff --git a/course/editsection.php b/course/editsection.php index e9a065b..c59fd1f 100644 --- a/course/editsection.php +++ b/course/editsection.php @@ -26,6 +26,10 @@ require_once("../config.php"); require_once("lib.php"); require_once($CFG->libdir.'/filelib.php'); +require_once($CFG->libdir.'/gradelib.php'); +require_once($CFG->libdir.'/completionlib.php'); +require_once($CFG->libdir.'/conditionlib.php'); + require_once('editsection_form.php'); $id = required_param('id',PARAM_INT); // Week/topic ID @@ -42,8 +46,11 @@ require_capability('moodle/course:update', $context); $editoroptions = array('context'=>$context ,'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes'=>$CFG->maxbytes, 'trusttext'=>false, 'noclean'=>true); $section = file_prepare_standard_editor($section, 'summary', $editoroptions, $context, 'course', 'section', $section->id); $section->usedefaultname = (is_null($section->name)); + $mform = new editsection_form(null, array('course'=>$course, 'editoroptions'=>$editoroptions)); $mform->set_data($section); // set current value +$mform->cs = $section; +$mform->showavailability = isset($section->showavailability) ? $section->showavailability : null; /// If data submitted, then process and store. if ($mform->is_cancelled()){ @@ -58,7 +65,43 @@ if ($mform->is_cancelled()){ $data = file_postupdate_standard_editor($data, 'summary', $editoroptions, $context, 'course', 'section', $section->id); $section->summary = $data->summary; $section->summaryformat = $data->summaryformat; + if (!empty($CFG->enableavailability)) { + $section->availablefrom = $data->availablefrom; + $section->availableuntil = $data->availableuntil; + $section->groupingid = $data->groupingid; + $section->showavailability = $data->showavailability; + } $DB->update_record('course_sections', $section); + if (!empty($CFG->enableavailability)) { + //updating grade & completion conditions table + //first let's delete existing conditions for this section from db + $DB->delete_records('course_sections_availability', array('coursesectionid' => $section->id)); + //now insert new conditions received from user + if (!empty($data->conditiongradegroup)) { + foreach ($data->conditiongradegroup as $groupvalue) { + if ($groupvalue['conditiongradeitemid'] > 0) { + $datacg = new stdClass; + $datacg->coursesectionid = $section->id; + $datacg->gradeitemid = $groupvalue['conditiongradeitemid']; + $datacg->grademin = $groupvalue['conditiongrademin']; + $datacg->grademax = $groupvalue['conditiongrademax']; + $DB->insert_record('course_sections_availability', $datacg); + } + } + } + if (!empty($data->conditioncompletiongroup)) { + foreach ($data->conditioncompletiongroup as $groupvalue){ + if ($groupvalue['conditionsourcecmid'] > 0) { + $datacg = new stdClass; + $datacg->coursesectionid = $section->id; + $datacg->sourcecmid = $groupvalue['conditionsourcecmid']; + $datacg->requiredcompletion = $groupvalue['conditionrequiredcompletion']; + $DB->insert_record('course_sections_availability', $datacg); + } + } + } + } + add_to_log($course->id, "course", "editsection", "editsection.php?id=$section->id", "$section->section"); $PAGE->navigation->clear_cache(); redirect("view.php?id=$course->id"); diff --git a/course/editsection_form.php b/course/editsection_form.php index ab26049..28b7803 100644 --- a/course/editsection_form.php +++ b/course/editsection_form.php @@ -8,12 +8,13 @@ require_once($CFG->libdir.'/formslib.php'); class editsection_form extends moodleform { + public $cs; + public $showavailability; + function definition() { - global $CFG, $DB; $mform = $this->_form; $course = $this->_customdata['course']; - $mform->addElement('checkbox', 'usedefaultname', get_string('sectionusedefaultname')); $mform->setDefault('usedefaultname', true); @@ -30,8 +31,159 @@ class editsection_form extends moodleform { $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); + $mform->_registerCancelButton('cancel'); + + } + + function definition_after_data() { + global $CFG, $DB; + + $mform = $this->_form; + $course = $this->_customdata['course']; + + if (!empty($CFG->enableavailability)) { + // Conditional availability + + $options = array(); + $options[0] = get_string('none'); + if ($groupings = $DB->get_records('groupings', array('courseid'=>$course->id))) { + foreach ($groupings as $grouping) { + $options[$grouping->id] = format_string($grouping->name); + } + } + $mform->addElement('header', '', get_string('availabilityconditions', 'condition')); + $mform->addElement('select', 'groupingid', get_string('groupingsection', 'group'), $options); + $mform->addHelpButton('groupingid', 'groupingsection', 'group'); + $mform->addElement('date_time_selector', 'availablefrom', get_string('availablefrom', 'condition'), array('optional'=>true)); + $mform->addElement('date_time_selector', 'availableuntil', get_string('availableuntil', 'condition'), array('optional'=>true)); + + // Conditions based on grades + $gradeoptions = array(); + $items = grade_item::fetch_all(array('courseid'=>$course->id)); + $items = $items ? $items : array(); + foreach($items as $id=>$item) { + // Do not include grades for current item - TO DO + $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)); + $grouparray[] =& $mform->createElement('static', '', '','%'); + $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->cs)) { + $ci = new condition_info_section($this->cs); + $fullcs=$ci->get_full_course_section(); + $count = count($fullcs->conditionsgrade)+1; + } else { + $count = 1; + } + + $this->repeat_elements(array($group), $count, array(), 'conditiongraderepeats', 'conditiongradeadds', 2, + get_string('addgrades', 'condition'), true); + $mform->addHelpButton('conditiongradegroup[0]', 'gradecondition', '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) { + // Add each course-module if it: + // (a) has completion turned on + // (b) does not belong to current course-section + if ($cm->completion && (empty($course) || $this->cs->id != $cm->section)) { + $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($fullcs) ? 1 : count($fullcs->conditionscompletion)+1; + $this->repeat_elements(array($group),$count,array(), + 'conditioncompletionrepeats','conditioncompletionadds',2, + get_string('addcompletions','condition'),true); + $mform->addHelpButton('conditioncompletiongroup[0]', 'completionconditionsection', 'condition'); + } + + // Availability conditions - set up form values + if (!empty($CFG->enableavailability) && $this->cs) { + $num=0; + foreach($fullcs->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($fullcs->conditionscompletion as $othercmid=>$state) { + $groupelements=$mform->getElement('conditioncompletiongroup['.$num.']')->getElements(); + $groupelements[0]->setValue($othercmid); + $groupelements[1]->setValue($state); + $num++; + } + } + } + + + // Do we display availability info to students? + $mform->addElement('select', 'showavailability', get_string('showavailabilitysection', 'condition'), + array(CONDITION_STUDENTVIEW_SHOW=>get_string('showavailabilitysection_show', 'condition'), + CONDITION_STUDENTVIEW_HIDE=>get_string('showavailabilitysection_hide', 'condition'))); + + if (isset($this->showavailability)) { + $mform->setDefault('showavailability', $this->showavailability); + } else { + $mform->setDefault('showavailability', CONDITION_STUDENTVIEW_SHOW); + } + } + + //-------------------------------------------------------------------------------- $this->add_action_buttons(); } + + // form verification + function validation($data, $files) { + $errors = parent::validation($data, $files); + // Conditions: Don't let them set dates which make no sense + if (array_key_exists('availablefrom', $data) && + $data['availablefrom'] && $data['availableuntil'] && + $data['availablefrom'] > $data['availableuntil']) { + $errors['availablefrom'] = get_string('badavailabledates', 'condition'); + } + + return $errors; + } + } diff --git a/course/format/topics/format.php b/course/format/topics/format.php index fe8fb3e..cde2d80 100644 --- a/course/format/topics/format.php +++ b/course/format/topics/format.php @@ -135,7 +135,9 @@ while ($section <= $course->numsections) { if (!empty($sections[$section])) { $thissection = $sections[$section]; - + //Checking availability conditions + $si = new condition_info_section($thissection); + $thissection->is_available = $si->is_available($thissection->information, true, $USER->id); //if not available 'information' will tell why } else { $thissection = new stdClass; $thissection->course = $course->id; // Create a new section structure @@ -147,7 +149,7 @@ while ($section <= $course->numsections) { $thissection->id = $DB->insert_record('course_sections', $thissection); } - $showsection = (has_capability('moodle/course:viewhiddensections', $context) or $thissection->visible or !$course->hiddensections); + $showsection = !$course->hiddensections && (has_capability('moodle/course:viewhiddensections', $context) || ($thissection->visible && ($thissection->is_available || $thissection->showavailability))); if (!empty($displaysection) and $displaysection != $section) { // Check this topic is visible if ($showsection) { @@ -214,12 +216,24 @@ while ($section <= $course->numsections) { echo ''; echo '
'; - if (!has_capability('moodle/course:viewhiddensections', $context) and !$thissection->visible) { // Hidden for students - echo get_string('notavailable'); + if (!has_capability('moodle/course:viewhiddensections', $context) && (!$thissection->visible || (!$thissection->is_available && $thissection->showavailability==1)) ) { // Hidden for students + echo '
'; + if (!empty($thissection->information)) { + echo $thissection->information; + } else { + echo get_string('notavailable'); + } + echo '
'; } else { if (!is_null($thissection->name)) { echo $OUTPUT->heading(format_string($thissection->name, true, array('context' => $context)), 3, 'sectionname'); } + if (!empty($thissection->information)) + { + echo '
'; + echo $thissection->information; + echo '
'; + } echo '
'; if ($thissection->summary) { $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id); diff --git a/course/format/weeks/format.php b/course/format/weeks/format.php index cf48505..060e1a1 100644 --- a/course/format/weeks/format.php +++ b/course/format/weeks/format.php @@ -139,6 +139,9 @@ defined('MOODLE_INTERNAL') || die(); if (!empty($sections[$section])) { $thissection = $sections[$section]; + //Checking availability conditions + $si = new condition_info_section($thissection); + $thissection->is_available = $si->is_available($thissection->information, true, $USER->id); //if not available 'information' will tell why } else { unset($thissection); @@ -151,7 +154,7 @@ defined('MOODLE_INTERNAL') || die(); $thissection->id = $DB->insert_record('course_sections', $thissection); } - $showsection = (has_capability('moodle/course:viewhiddensections', $context) or $thissection->visible or !$course->hiddensections); + $showsection = !$course->hiddensections && (has_capability('moodle/course:viewhiddensections', $context) || ($thissection->visible && ($thissection->is_available || $thissection->showavailability))); if (!empty($displaysection) and $displaysection != $section) { // Check this week is visible if ($showsection) { @@ -215,15 +218,27 @@ defined('MOODLE_INTERNAL') || die(); $weekperiod = $weekday.' - '.$endweekday; echo '
'; - if (!has_capability('moodle/course:viewhiddensections', $context) and !$thissection->visible) { // Hidden for students + if (!has_capability('moodle/course:viewhiddensections', $context) && (!$thissection->visible || (!$thissection->is_available && $thissection->showavailability==1)) ) { // Hidden for students echo $OUTPUT->heading($currenttext.$weekperiod.' ('.get_string('notavailable').')', 3, 'weekdates'); - + echo '
'; + if (!empty($thissection->information)) { + echo $thissection->information; + } else { + echo get_string('notavailable'); + } + echo '
'; } else { if (isset($thissection->name) && ($thissection->name !== NULL)) { // empty string is ok echo $OUTPUT->heading(format_string($thissection->name, true, array('context' => $context)), 3, 'weekdates'); } else { echo $OUTPUT->heading($currenttext.$weekperiod, 3, 'weekdates'); } + if (!empty($thissection->information)) + { + echo '
'; + echo $thissection->information; + echo '
'; + } echo '
'; $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id); $summarytext = file_rewrite_pluginfile_urls($thissection->summary, 'pluginfile.php', $coursecontext->id, 'course', 'section', $thissection->id); diff --git a/course/lib.php b/course/lib.php index 8a9b0d2..d6fb1de 100644 --- a/course/lib.php +++ b/course/lib.php @@ -1262,7 +1262,7 @@ function get_all_sections($courseid) { static $coursesections = array(); if (!array_key_exists($courseid, $coursesections)) { $coursesections[$courseid] = $DB->get_records("course_sections", array("course"=>"$courseid"), "section", - "section, id, course, name, summary, summaryformat, sequence, visible"); + "section, id, course, name, summary, summaryformat, sequence, visible, availablefrom, availableuntil, showavailability, groupingid"); } return $coursesections[$courseid]; } diff --git a/course/view.php b/course/view.php index 28ed124..26444ba 100644 --- a/course/view.php +++ b/course/view.php @@ -5,6 +5,7 @@ require_once('../config.php'); require_once('lib.php'); require_once($CFG->dirroot.'/mod/forum/lib.php'); + require_once($CFG->libdir.'/conditionlib.php'); require_once($CFG->libdir.'/completionlib.php'); $id = optional_param('id', 0, PARAM_INT); diff --git a/lang/en/condition.php b/lang/en/condition.php index ed05e32..034c746 100644 --- a/lang/en/condition.php +++ b/lang/en/condition.php @@ -37,6 +37,10 @@ $string['completioncondition'] = 'Activity completion condition'; $string['completioncondition_help'] = 'This setting determines any activity completion conditions which must be met in order to access the activity. Note that completion tracking must first be set before an activity completion condition can be set. Multiple activity completion conditions may be set if desired. If so, access to the activity will only be permitted when ALL activity completion conditions are met.'; +$string['completionconditionsection'] = 'Activity completion condition'; +$string['completionconditionsection_help'] = 'This setting determines any activity completion conditions which must be met in order to access the section. Note that completion tracking must first be set before an activity completion condition can be set. + +Multiple activity completion conditions may be set if desired. If so, access to the section will only be permitted when ALL activity completion conditions are met.'; $string['completion_fail'] = 'must be complete with fail grade'; $string['completion_incomplete'] = 'must not be marked complete'; $string['completion_pass'] = 'must be complete with pass grade'; @@ -47,6 +51,10 @@ $string['gradecondition'] = 'Grade condition'; $string['gradecondition_help'] = 'This setting determines any grade conditions which must be met in order to access the activity. Multiple grade conditions may be set if desired. If so, the activity will only allow access when ALL grade conditions are met.'; +$string['gradeconditionsection'] = 'Grade condition'; +$string['gradeconditionsection_help'] = 'This setting determines any grade conditions which must be met in order to access the section. + +Multiple grade conditions may be set if desired. If so, the section will only allow access when ALL grade conditions are met.'; $string['grade_upto'] = 'and less than'; $string['none'] = '(none)'; $string['notavailableyet'] = 'Not available yet'; @@ -63,7 +71,11 @@ $string['requires_grade_max'] = 'Not available unless you get an appropriate sco $string['requires_grade_min'] = 'Not available until you achieve a required score in {$a}.'; $string['requires_grade_range'] = 'Not available unless you get a particular score in {$a}.'; $string['showavailability'] = 'Before activity can be accessed'; +$string['showavailabilitysection'] = 'Before section can be accessed'; $string['showavailability_hide'] = 'Hide activity entirely'; $string['showavailability_show'] = 'Show activity greyed-out, with restriction information'; +$string['showavailabilitysection_hide'] = 'Hide section entirely'; +$string['showavailabilitysection_show'] = 'Show section greyed-out, with restriction information'; $string['userrestriction_hidden'] = 'Restricted (completely hidden, no message): ‘{$a}’'; $string['userrestriction_visible'] = 'Restricted: ‘{$a}’'; +$string['groupingnoaccess'] = 'You have to become a member of the group which has access to this section.'; diff --git a/lang/en/group.php b/lang/en/group.php index 7a98c5b..1505986 100644 --- a/lang/en/group.php +++ b/lang/en/group.php @@ -76,6 +76,8 @@ $string['groupinfomembers'] = 'Info about selected members'; $string['groupinfopeople'] = 'Info about selected people'; $string['grouping'] = 'Grouping'; $string['grouping_help'] = 'A grouping is a collection of groups within a course. If a grouping is selected, students assigned to groups within the grouping will be able to work together.'; +$string['groupingsection'] = 'Grouping access'; +$string['groupingsection_help'] = 'A grouping is a collection of groups within a course. If a grouping is selected here, only students assigned to groups within this grouping will have access to the section.'; $string['groupingdescription'] = 'Grouping description'; $string['groupingname'] = 'Grouping name'; $string['groupingnameexists'] = 'The grouping name \'{$a}\' already exists in this course, please choose another one.'; diff --git a/lib/conditionlib.php b/lib/conditionlib.php index f3279a1..db82ae8 100644 --- a/lib/conditionlib.php +++ b/lib/conditionlib.php @@ -720,6 +720,660 @@ WHERE public static function completion_value_used_as_condition($course, $cm) { // Have we already worked out a list of required completion values // for this course? If so just use that + global $CONDITIONLIB_PRIVATE, $DB; + if (!array_key_exists($course->id, $CONDITIONLIB_PRIVATE->usedincondition)) { + // We don't have data for this course, build it + $modinfo = get_fast_modinfo($course); + $CONDITIONLIB_PRIVATE->usedincondition[$course->id] = array(); + foreach ($modinfo->cms as $othercm) { + foreach ($othercm->conditionscompletion as $cmid=>$expectedcompletion) { + $CONDITIONLIB_PRIVATE->usedincondition[$course->id][$cmid] = true; + } + } + } + $founddependant = array_key_exists($cm->id, $CONDITIONLIB_PRIVATE->usedincondition[$course->id]); + if (!$founddependant) { + if ($DB->record_exists('course_sections_availability', array('sourcecmid'=>$cm->id)) > 0 ) { + $founddependant = true; + } + } + return $founddependant; + } +} + + +class condition_info_section { + /** + * @var object, bool + */ + private $cs, $gotdata; + + /** + * Constructs with course-section details. + * + * @global object + * @uses CONDITION_MISSING_NOTHING + * @uses CONDITION_MISSING_EVERYTHING + * @uses DEBUG_DEVELOPER + * @uses CONDITION_MISSING_EXTRATABLE + * @param object $cs Moodle course-section object. 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($cs, $expectingmissing=CONDITION_MISSING_NOTHING, + $loaddata=true) { + global $DB; + + // Check ID as otherwise we can't do the other queries + if (empty($cs->id)) { + throw new coding_exception("Invalid parameters; course-section ID not included"); + } + + // If not loading data, don't do anything else + if (!$loaddata) { + $this->cs = (object)array('id'=>$cs->id); + $this->gotdata = false; + return; + } + + // Missing basic data from course_sections + if (!isset($cs->course) or !isset($cs->availablefrom) or !isset($cs->availableuntil) or !isset($cs->availableuntil) or !isset($cs->showavailability)) { + if ($expectingmissingget_record('course_sections',array('id'=>$cs->id), 'id,course,availablefrom,availableuntil,showavailability,groupingid'); + } + + $this->cs = clone($cs); + $this->gotdata = true; + + // Missing extra data + if (!isset($cs->conditionsgrade) || !isset($cs->conditionscompletion)) { + /* + if ($expectingmissingcs); + } + } + + /** + * Adds the extra availability conditions (if any) into the given + * course-section object. + * + * @global object + * @global object + * @param object $cs Moodle course-section data object + */ + public static function fill_availability_conditions(&$cs) { + if (empty($cs->id)) { + throw new coding_exception("Invalid parameters; course-section ID not included"); + } + + // Does nothing if the variables are already present + if (!isset($cs->conditionsgrade) || + !isset($cs->conditionscompletion)) { + $cs->conditionsgrade=array(); + $cs->conditionscompletion=array(); + + global $DB, $CFG; + $conditions = $DB->get_records_sql($sql=" +SELECT + csacg.id as csacgid, gi.*,csacg.sourcecmid,csacg.requiredcompletion,csacg.gradeitemid, + csacg.grademin as conditiongrademin, csacg.grademax as conditiongrademax +FROM + {course_sections_availability} csacg + LEFT JOIN {grade_items} gi ON gi.id=csacg.gradeitemid +WHERE + coursesectionid=?",array($cs->id)); + foreach ($conditions as $condition) { + if (!is_null($condition->sourcecmid)) { + $cs->conditionscompletion[$condition->sourcecmid] = + $condition->requiredcompletion; + } else { + $minmax = new stdClass; + $minmax->min = $condition->conditiongrademin; + $minmax->max = $condition->conditiongrademax; + $minmax->name = self::get_grade_name($condition); + $cs->conditionsgrade[$condition->gradeitemid] = $minmax; + } + } + } + } + + /** + * Obtains the name of a grade item. + * + * @global object + * @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 + } + } + + /** + * @see require_data() + * @return object A course-section object with all the information required to + * determine availability. + */ + public function get_full_course_section() { + $this->require_data(); + return $this->cs; + } + + /** + * Adds to the database a condition based on completion of a module. + * + * @global object + * @param int $cmid ID of a module + * @param int $requiredcompletion COMPLETION_xx constant + */ + public function add_completion_condition($cmid, $requiredcompletion) { + // Add to DB + global $DB; + $DB->insert_record('course_sections_availability', + (object)array('coursesectionid'=>$this->cs->id, + 'sourcecmid'=>$cmid, 'requiredcompletion'=>$requiredcompletion), + false); + + // Store in memory too + $this->cs->conditionscompletion[$cmid] = $requiredcompletion; + } + + /** + * Adds to the database a condition based on the value of a grade item. + * + * @global object + * @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_sections_availability', + (object)array('coursesectionid'=>$this->cs->id, + 'gradeitemid'=>$gradeitemid, 'grademin'=>$min, 'grademax'=>$max), + false); + + // Store in memory too + if ($updateinmemory) { + $this->cs->conditionsgrade[$gradeitemid]=(object)array( + 'min'=>$min, 'max'=>$max); + $this->cs->conditionsgrade[$gradeitemid]->name = + self::get_grade_name($DB->get_record('grade_items', + array('id'=>$gradeitemid))); + } + } + + /** + * Erases from the database all conditions for this section. + * + * @global object + */ + public function wipe_conditions() { + // Wipe from DB + global $DB; + $DB->delete_records('course_sections_availability', + array('coursesectionid'=>$this->cs->id)); + + // And from memory + $this->cs->conditionsgrade = array(); + $this->cs->conditionscompletion = array(); + } + + /** + * Obtains a string describing all availability restrictions (even if + * they do not apply any more). + * + * @global object + * @global object + * @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 CSs with 'id' and 'name' + * @return string Information string (for admin) about all restrictions on + * this item + */ + public function get_full_information($modinfo=null) { + $this->require_data(); + global $COURSE, $DB; + + $information = ''; + + // Completion conditions + if(count($this->cs->conditionscompletion)>0) { + if ($this->cs->course==$COURSE->id) { + $course = $COURSE; + } else { + $course = $DB->get_record('course',array('id'=>$this->cs->course),'id,enablecompletion,modinfo'); + } + foreach ($this->cs->conditionscompletion as $cmid=>$expectedcompletion) { + if (!$modinfo) { + $modinfo = get_fast_modinfo($course); + } + if (empty($modinfo->cms[$cmid])) { + continue; + } + $information .= get_string( + 'requires_completion_'.$expectedcompletion, + 'condition', $modinfo->cms[$cmid]->name).' '; + } + } + + // Grade conditions + if (count($this->cs->conditionsgrade)>0) { + foreach ($this->cs->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->cs->availablefrom && $this->cs->availableuntil) { + $information .= get_string('requires_date_both', 'condition', + (object)array( + 'from' => self::show_time($this->cs->availablefrom, false), + 'until' => self::show_time($this->cs->availableuntil, true))); + } else if ($this->cs->availablefrom) { + $information .= get_string('requires_date', 'condition', + self::show_time($this->cs->availablefrom, false)); + } else if ($this->cs->availableuntil) { + $information .= get_string('requires_date_before', 'condition', + self::show_time($this->cs->availableuntil, true)); + } + + $information = trim($information); + return $information; + } + + /** + * Determines whether this particular course-section 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 viewhiddensections capability. + * That should apply later. + * + * @global object + * @global object + * @uses COMPLETION_COMPLETE + * @uses COMPLETION_COMPLETE_FAIL + * @uses COMPLETION_COMPLETE_PASS + * @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-sections, 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 + */ + 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->cs->conditionscompletion)>0) { + if ($this->cs->course==$COURSE->id) { + $course = $COURSE; + } else { + $course = $DB->get_record('course',array('id'=>$this->cs->course),'id,enablecompletion,modinfo'); + } + + $completion = new completion_info($course); + foreach ($this->cs->conditionscompletion as $cmid=>$expectedcompletion) { + // If this depends on a deleted module, handle that situation + // gracefully. + if (!$modinfo) { + $modinfo = get_fast_modinfo($course); + } + if (empty($modinfo->cms[$cmid])) { + global $PAGE, $UNITTEST; + if (!empty($UNITTEST) || (isset($PAGE) && strpos($PAGE->pagetype, 'course-view-')===0)) { + debugging("Warning: section {$this->cs->id} '{$this->cs->name}' has condition on deleted activity $cmid (to get rid of this message, edit the named section)"); + } + continue; + } + + // 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; + $information .= get_string( + 'requires_completion_'.$expectedcompletion, + 'condition',$modinfo->cms[$cmid]->name).' '; + } + } + } + + // Check each grade condition + if (count($this->cs->conditionsgrade)>0) { + foreach ($this->cs->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->cs->availablefrom) { + if (time() < $this->cs->availablefrom) { + $available = false; + + $information .= get_string('requires_date', 'condition', + self::show_time($this->cs->availablefrom, false)); + } + } + + if ($this->cs->availableuntil) { + if (time() >= $this->cs->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. + } + } + + // test if user is enrolled to a grouping which has access to the section + if ($this->cs->groupingid) { + $usergrouping = $DB->get_record_sql($sql=" +SELECT + g.id +FROM + {groupings} g + LEFT JOIN {groupings_groups} gg ON g.id = gg.groupingid + LEFT JOIN {groups_members} gm ON gg.groupid = gm.groupid +WHERE + g.courseid=? AND gm.userid=? AND g.id=?",array($COURSE->id, $userid, $this->cs->groupingid)); + + if (!$usergrouping) { + $available = false; + $information .= get_string('groupingnoaccess', 'condition'); + } + } + + $information=trim($information); + return $available; + } + + /** + * Shows a time either as a date (if it falls exactly on the day) or + * a full date and time, according to user's timezone. + * + * @param int $time Time + * @param bool $until True if this date should be treated as the second of + * an inclusive pair - if so the time will be shown unless date is 23:59:59. + * Without this the date shows for 0:00:00. + * @return string Date + */ + private function show_time($time, $until) { + // Break down the time into fields + $userdate = usergetdate($time); + + // Handle the 'inclusive' second date + if($until) { + $dateonly = $userdate['hours']==23 && $userdate['minutes']==59 && + $userdate['seconds']==59; + } else { + $dateonly = $userdate['hours']==0 && $userdate['minutes']==0 && + $userdate['seconds']==0; + } + + return userdate($time, get_string( + $dateonly ? 'strftimedate' : 'strftimedatetime', 'langconfig')); + } + + /** + * @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->cs->showavailability; + } + + /** + * Internal function cheks that data was loaded. + * + * @return void 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. + * + * @global object + * @global object + * @global object + * @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 as a percentage in range 0-100 (e.g. 100.0 + * or 37.21), 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,gg.rawgrademin,gg.rawgrademax +FROM + {grade_items} gi + LEFT JOIN {grade_grades} gg ON gi.id=gg.itemid AND gg.userid=? +WHERE + gi.courseid=?", array($USER->id, $this->cs->course)); + foreach ($rs as $record) { + $SESSION->gradescorecache[$record->id] = + is_null($record->finalgrade) + // No grade = false + ? false + // Otherwise convert grade to percentage + : (($record->finalgrade - $record->rawgrademin) * 100) / + ($record->rawgrademax - $record->rawgrademin); + + } + $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 + $record = $DB->get_record('grade_grades', array( + 'userid'=>$USER->id, 'itemid'=>$gradeitemid)); + if ($record && !is_null($record->finalgrade)) { + $score = (($record->finalgrade - $record->rawgrademin) * 100) / + ($record->rawgrademax - $record->rawgrademin); + } else { + // Treat the case where row exists but is null, same as + // case where row doesn't exist + $score = false; + } + $SESSION->gradescorecache[$gradeitemid]=$score; + } + } + return $SESSION->gradescorecache[$gradeitemid]; + } else { + // Not the current user, so request the score individually + $record = $DB->get_record('grade_grades', array( + 'userid'=>$userid, 'itemid'=>$gradeitemid)); + if ($record && !is_null($record->finalgrade)) { + $score = (($record->finalgrade - $record->rawgrademin) * 100) / + ($record->rawgrademax - $record->rawgrademin); + } else { + // Treat the case where row exists but is null, same as + // case where row doesn't exist + $score = false; + } + return $score; + } + } + + /** + * For testing only. Wipes information cached in user session. + * + * @global object + */ + 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 object $fromform + * @param bool $wipefirst Defaults to true + */ + public static function update_cs_from_form($cs, $fromform, $wipefirst=true) { + $ci=new condition_info_section($cs, 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']); + } + } + } + } + + /** + * Used in course/lib.php because we need to disable the completion JS if + * a completion value affects a conditional section. + * + * @global object + * @param object $course Moodle course object + * @param object $cm Moodle course-module + * @return bool True if this is used in a condition, false otherwise + */ + public static function completion_value_used_as_condition($course, $cm) { + // Have we already worked out a list of required completion values + // for this course? If so just use that global $CONDITIONLIB_PRIVATE; if (!array_key_exists($course->id, $CONDITIONLIB_PRIVATE->usedincondition)) { // We don't have data for this course, build it diff --git a/lib/db/install.xml b/lib/db/install.xml index 8a9fdac..63d1643 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -373,7 +373,7 @@ - +
@@ -382,7 +382,11 @@ - + + + + + @@ -391,7 +395,21 @@
- +
+ + + + + + + + + + + + +
+ diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index e540801..a3a4f82 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -6951,6 +6951,39 @@ FROM // Moodle v2.2.0 release upgrade line // Put any upgrade step following this + if ($oldversion < 2011120500.02) { + // Amend course_sections to add date, time & groupingid availability conditions and a setting + $table = new xmldb_table('course_sections'); + $field = new xmldb_field('availablefrom'); + $field->set_attributes(XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, true, false, '0', null); + $dbman->add_field($table, $field); + $field = new xmldb_field('availableuntil'); + $field->set_attributes(XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, true, false, '0', null); + $dbman->add_field($table, $field); + $field = new xmldb_field('showavailability'); + $field->set_attributes(XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, true, false, '0', null); + $dbman->add_field($table, $field); + $field = new xmldb_field('groupingid'); + $field->set_attributes(XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, true, false, '0', null); + $dbman->add_field($table, $field); + + // Add course_sections_availability to add completion & grade availability conditions + $table = new xmldb_table('course_sections_availability'); + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('coursesectionid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); + $table->add_field('sourcecmid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); + $table->add_field('requiredcompletion', XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); + $table->add_field('gradeitemid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); + $table->add_field('grademin', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); + $table->add_field('grademax', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_index('coursesectionid', XMLDB_INDEX_UNIQUE, array('coursesectionid')); + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + /// Main savepoint reached + upgrade_main_savepoint(true, 2011120500.02); + } return true; } diff --git a/lib/navigationlib.php b/lib/navigationlib.php index 8645e27..a0f09cd 100644 --- a/lib/navigationlib.php +++ b/lib/navigationlib.php @@ -28,6 +28,9 @@ defined('MOODLE_INTERNAL') || die(); +require_once($CFG->libdir.'/conditionlib.php'); +require_once($CFG->libdir.'/completionlib.php'); + /** * The name that will be used to separate the navigation cache within SESSION */ @@ -1677,9 +1680,19 @@ class global_navigation extends navigation_node { if ($course->id == SITEID) { $this->load_section_activities($coursenode, $section->section, $activities); } else { - if ((!$viewhiddensections && !$section->visible) || (!$this->showemptysections && !$section->hasactivites)) { + //Checking availability conditions + $si = new condition_info_section($section); + $section->is_available = $si->is_available($information, true, $USER->id); //if not available 'information' will tell why + if (!$section->is_available && $section->showavailability) { + $section->greyout = true; + } else { + $section->greyout = false; + } + + if (!$section->is_available || (!$viewhiddensections && !$section->visible) || (!$this->showemptysections && !$section->hasactivites)) { continue; } + if ($namingfunctionexists) { $sectionname = $namingfunction($course, $section, $sections); } else { @@ -1692,7 +1705,7 @@ class global_navigation extends navigation_node { } $sectionnode = $coursenode->add($sectionname, $url, navigation_node::TYPE_SECTION, null, $section->id); $sectionnode->nodetype = navigation_node::NODETYPE_BRANCH; - $sectionnode->hidden = (!$section->visible); + $sectionnode->hidden = (!$section->visible || $section->greyout); if ($key != '0' && $section->section != '0' && $section->section == $key && $this->page->context->contextlevel != CONTEXT_MODULE && $section->hasactivites) { $sectionnode->make_active(); $this->load_section_activities($sectionnode, $section->section, $activities); diff --git a/version.php b/version.php index 868b083..5fef754 100644 --- a/version.php +++ b/version.php @@ -30,7 +30,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2011120500.01; // YYYYMMDD = weekly release date of this DEV branch +$version = 2011120500.02; // 20111205 = branching date YYYYMMDD - do not modify! // RR = release increments - 00 in DEV branches // .XX = incremental changes