MDL-24419 (4): Add conditional availability support to sections; section cache
authorsam marshall <s.marshall@open.ac.uk>
Mon, 2 Apr 2012 11:16:13 +0000 (12:16 +0100)
committersam marshall <s.marshall@open.ac.uk>
Mon, 14 May 2012 10:19:25 +0000 (11:19 +0100)
Credit: original version done by Kirill Astashov of NetSpot (netspot.com.au),
finished and tweaked by sam.

This change adds conditional availability support for sections analagous to
that already available for activities. (Backend, UI, backup/restore.)

In order that this feature does not reduce performance, section cacheing has
also been added using a new course 'sectioncache' field analagous to modinfo.

The new feature integrates with activity availability so that activities
inside sections which are not available are automatically not available
themselves (meaning it works to restrict access).

20 files changed:
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.php
course/edit.php
course/editsection.php
course/editsection_form.php
course/format/renderer.php
course/lib.php
course/moodleform_mod.php
course/view.php
index.php
lang/en/condition.php
lang/en/group.php
lib/conditionlib.php
lib/db/install.xml
lib/db/upgrade.php
lib/modinfolib.php
lib/moodlelib.php
lib/navigationlib.php
lib/tests/conditionlib_test.php
version.php

index fa12036..3a8fc5a 100644 (file)
@@ -364,14 +364,20 @@ class backup_section_structure_step extends backup_structure_step {
         // Define each element separated
 
         $section = new backup_nested_element('section', array('id'), array(
-            'number', 'name', 'summary', 'summaryformat', 'sequence', 'visible'));
+                'number', 'name', 'summary', 'summaryformat', 'sequence', 'visible',
+                'availablefrom', 'availableuntil', 'showavailability', 'groupingid'));
 
         // attach format plugin structure to $section element, only one allowed
         $this->add_plugin_structure('format', $section, false);
 
-        // Define sources
+        // Add nested elements for _availability table
+        $avail = new backup_nested_element('availability', array('id'), array(
+                'sourcecmid', 'requiredcompletion', 'gradeitemid', 'grademin', 'grademax'));
+        $section->add_child($avail);
 
+        // Define sources
         $section->set_source_table('course_sections', array('id' => backup::VAR_SECTIONID));
+        $avail->set_source_table('course_sections_availability', array('coursesectionid' => backup::VAR_SECTIONID));
 
         // Aliases
         $section->set_source_alias('section', 'number');
index 1472012..99722d6 100644 (file)
@@ -993,11 +993,54 @@ class restore_section_structure_step extends restore_structure_step {
 
     protected function define_structure() {
         $section = new restore_path_element('section', '/section');
+        $avail = new restore_path_element('availability', '/section/availability');
 
         // Apply for 'format' plugins optional paths at section level
         $this->add_plugin_structure('format', $section);
 
-        return array($section);
+        return array($section, $avail);
+    }
+
+    public function process_availability($data) {
+        global $DB;
+        $data = (object)$data;
+        $data->coursesectionid = $this->task->get_sectionid();
+        // NOTE: Other values in $data need updating, but these (cm,
+        // grade items) have not yet been restored.
+        $DB->insert_record('course_sections_availability', $data);
+    }
+
+    public function after_restore() {
+        global $DB;
+        // Get main data object
+        $sectionid = $this->get_task()->get_sectionid();
+        $data = $DB->get_record('course_sections',
+                array('id' => $sectionid), 'id, groupingid', MUST_EXIST);
+        if ($data->groupingid) {
+            // Correct grouping id
+            $DB->set_field('course_sections', 'groupingid',
+                    $this->get_mappingid('grouping', $data->groupingid),
+                    array('id' => $sectionid));
+        }
+
+        // Get data object for current section availability (if any)
+        $data = $DB->get_record('course_sections_availability',
+                array('coursesectionid' => $sectionid), 'id, sourcecmid, gradeitemid', IGNORE_MISSING);
+
+        // Update mappings
+        if ($data) {
+            $data->sourcecmid = $this->get_mappingid('course_module', $data->sourcecmid);
+            if (!$data->sourcecmid) {
+                $data->sourcecmid = null;
+            }
+            $data->gradeitemid = $this->get_mappingid('grade_item', $data->gradeitemid);
+            if (!$data->gradeitemid) {
+                $data->gradeitemid = null;
+            }
+
+            $DB->update_record('course_sections_availability', $data);
+            rebuild_course_cache($this->get_task()->get_courseid(), true);
+        }
     }
 
     public function process_section($data) {
@@ -1018,6 +1061,10 @@ class restore_section_structure_step extends restore_structure_step {
             $section->summaryformat = $data->summaryformat;
             $section->sequence = '';
             $section->visible = $data->visible;
+            $section->availablefrom = isset($data->availablefrom) ? $data->availablefrom : 0;
+            $section->availableuntil = isset($data->availableuntil) ? $data->availableuntil : 0;
+            $section->showavailability = isset($data->showavailability) ? $data->showavailability : 0;
+            $section->groupingid = isset($data->groupingid) ? $data->groupingid : 0;
             $newitemid = $DB->insert_record('course_sections', $section);
             $restorefiles = true;
 
@@ -1032,6 +1079,14 @@ class restore_section_structure_step extends restore_structure_step {
                 $section->summaryformat = $data->summaryformat;
                 $restorefiles = true;
             }
+
+            // Don't update available from, available until, or show availability
+            // (I didn't see a useful way to define whether existing or new one should
+            // take precedence).
+
+            // Always update groupingid (otherwise it will break later when it updates id)
+            $section->groupingid = isset($data->groupingid) ? $data->groupingid : 0;
+
             $DB->update_record('course_sections', $section);
             $newitemid = $secrec->id;
         }
index 174872c..fc42d0a 100644 (file)
@@ -126,6 +126,7 @@ if ($editform->is_cancelled()) {
         // Save any changes to the files used in the editor
         update_course($data, $editoroptions);
     }
+    rebuild_course_cache($course->id);
 
     switch ($returnto) {
         case 'category':
index 3bce208..ecd905b 100644 (file)
 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
@@ -43,7 +47,17 @@ 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($PAGE->url, array('course'=>$course, 'editoroptions'=>$editoroptions));
+
+if (!empty($CFG->enableavailability)) {
+    // Get section availability conditions from sectioncache.
+    $modinfo = get_fast_modinfo($course);
+    $sectioninfo = $modinfo->get_section_info($section->section);
+    $section->conditionsgrade = $sectioninfo->conditionsgrade;
+    $section->conditionscompletion = $sectioninfo->conditionscompletion;
+}
+
+$mform = new editsection_form($PAGE->url, array('course' => $course, 'editoroptions' => $editoroptions,
+        'cs' => $section, 'showavailability' => $section->showavailability));
 $mform->set_data($section); // set current value
 
 if ($sectionreturn) {
@@ -65,7 +79,21 @@ 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;
+        if (!empty($data->groupingid)) {
+            $section->groupingid = $data->groupingid;
+        }
+        $section->showavailability = $data->showavailability;
+    }
     $DB->update_record('course_sections', $section);
+    if (!empty($CFG->enableavailability)) {
+        // Update grade and completion conditions
+        condition_info_section::update_section_from_form($section, $data);
+    }
+    rebuild_course_cache($course->id);
+
     add_to_log($course->id, "course", "editsection", "editsection.php?id=$section->id", "$section->section");
     $PAGE->navigation->clear_cache();
     redirect($returnurl);
index ab26049..730b372 100644 (file)
@@ -9,11 +9,9 @@ require_once($CFG->libdir.'/formslib.php');
 class editsection_form extends moodleform {
 
     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 +28,156 @@ class editsection_form extends moodleform {
         $mform->addElement('hidden', 'id');
         $mform->setType('id', PARAM_INT);
 
-//--------------------------------------------------------------------------------
+        $mform->_registerCancelButton('cancel');
+    }
+
+    public function definition_after_data() {
+        global $CFG, $DB;
+
+        $mform  = $this->_form;
+        $course = $this->_customdata['course'];
+
+        if (!empty($CFG->enableavailability)) {
+            // Grouping conditions - only if grouping is enabled at site level
+            if (!empty($CFG->enablegroupmembersonly)) {
+                $options = array();
+                $options[0] = get_string('none');
+                if ($groupings = $DB->get_records('groupings', array('courseid' => $course->id))) {
+                    foreach ($groupings as $grouping) {
+                        $context = context_course::instance($course->id);
+                        $options[$grouping->id] = format_string(
+                                $grouping->name, true, array('context' => $context));
+                    }
+                }
+                $mform->addElement('header', '', get_string('availabilityconditions', 'condition'));
+                $mform->addElement('select', 'groupingid', get_string('groupingsection', 'group'), $options);
+                $mform->addHelpButton('groupingid', 'groupingsection', 'group');
+            }
+
+            // Date and time conditions
+            $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) {
+                $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', '', '', '%');
+            $group = $mform->createElement('group', 'conditiongradegroup',
+                    get_string('gradecondition', 'condition'), $grouparray);
+
+            // Get full version (including condition info) of section object
+            $ci = new condition_info_section($this->_customdata['cs']);
+            $fullcs = $ci->get_full_section();
+            $count = count($fullcs->conditionsgrade) + 1;
+
+            // Grade conditions
+            $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 && ($fullcs->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 = 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)) {
+                $num = 0;
+                foreach ($fullcs->conditionsgrade as $gradeitemid => $minmax) {
+                    $groupelements = $mform->getElement(
+                            'conditiongradegroup[' . $num . ']')->getElements();
+                    $groupelements[0]->setValue($gradeitemid);
+                    $groupelements[2]->setValue(is_null($minmax->min) ? '' :
+                            format_float($minmax->min, 5, true, true));
+                    $groupelements[4]->setValue(is_null($minmax->max) ? '' :
+                            format_float($minmax->max, 5, true, true));
+                    $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?
+            $showhide = array(
+                CONDITION_STUDENTVIEW_SHOW => get_string('showavailabilitysection_show', 'condition'),
+                CONDITION_STUDENTVIEW_HIDE => get_string('showavailabilitysection_hide', 'condition'));
+            $mform->addElement('select', 'showavailability',
+                    get_string('showavailabilitysection', 'condition'), $showhide);
+
+            $mform->setDefault('showavailability', $this->_customdata['showavailability']);
+        }
+
         $this->add_action_buttons();
+    }
+
+    public 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;
     }
 }
index 61670a5..33ab76e 100644 (file)
@@ -162,6 +162,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         }
         $o.= html_writer::end_tag('div');
 
+        $o .= $this->section_availability_message($section);
+
         return $o;
     }
 
@@ -272,11 +274,35 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         $o.= html_writer::start_tag('div', array('class' => 'summarytext'));
         $o.= $this->format_summary_text($section);
         $o.= html_writer::end_tag('div');
+
+        $o .= $this->section_availability_message($section);
+
         $o.= html_writer::end_tag('li');
 
         return $o;
     }
 
+    /**
+     * If section is not visible to current user, display the message about that
+     * ('Not available until...', that sort of thing). Otherwise, returns blank.
+     *
+     * @param stdClass $section The course_section entry from DB
+     * @return string HTML to output
+     */
+    protected function section_availability_message($section) {
+        $o = '';
+        if (!$section->uservisible) {
+            $o .= html_writer::start_tag('div', array('class' => 'availabilityinfo'));
+            if (!empty($section->availableinfo)) {
+                $o .= $section->availableinfo;
+            } else {
+                $o .= get_string('notavailable');
+            }
+            $o .= html_writer::end_tag('div');
+        }
+        return $o;
+    }
+
     /**
      * Show if something is on on the course clipboard (moving around)
      *
@@ -514,9 +540,14 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
                 // This will create a course section if it doesn't exist..
                 $thissection = get_course_section($section, $course->id);
             }
-            $showsection = ($canviewhidden or $thissection->visible or !$course->hiddensections);
-            if (!$thissection->visible && !$canviewhidden) {
-                if (!$course->hiddensections) {
+            // Show the section if the user is permitted to access it, OR if it's not available
+            // but showavailability is turned on
+            $showsection = $thissection->uservisible ||
+                    ($thissection->visible && !$thissection->available && $thissection->showavailability);
+            if (!$showsection) {
+                // Hidden section message is overridden by 'unavailable' control
+                // (showavailability option).
+                if (!$course->hiddensections && $thissection->available) {
                     echo $this->section_hidden($section);
                 }
 
@@ -529,9 +560,11 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
                 echo $this->section_summary($thissection, $course);
             } else {
                 echo $this->section_header($thissection, $course, false);
-                print_section($course, $thissection, $mods, $modnamesused);
-                if ($PAGE->user_is_editing()) {
-                    print_section_add_menus($course, $section, $modnames);
+                if ($thissection->uservisible) {
+                    print_section($course, $thissection, $mods, $modnamesused);
+                    if ($PAGE->user_is_editing()) {
+                        print_section_add_menus($course, $section, $modnames);
+                    }
                 }
                 echo $this->section_footer();
             }
index 6f94bcd..2613ee5 100644 (file)
@@ -1262,6 +1262,9 @@ function get_all_mods($courseid, &$mods, &$modnames, &$modnamesplural, &$modname
  * of subsequent requests. This is used all over + in some standard libs and course
  * format callbacks so subsequent requests are a reality.
  *
+ * Note: Since Moodle 2.3, it is more efficient to get this data by calling
+ * get_fast_modinfo, then using $modinfo->get_section_info or get_section_info_all.
+ *
  * @staticvar array $coursesections
  * @param int $courseid
  * @return array Array of sections
@@ -1271,7 +1274,8 @@ 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];
 }
index dd32419..a9a3b53 100644 (file)
@@ -252,10 +252,10 @@ abstract class moodleform_mod extends moodleform {
             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'),'.'));
+                $groupelements[2]->setValue(is_null($minmax->min) ? '' :
+                        format_float($minmax->min, 5, true, true));
+                $groupelements[4]->setValue(is_null($minmax->max) ? '' :
+                        format_float($minmax->max, 5, true, true));
                 $num++;
             }
 
@@ -321,16 +321,18 @@ abstract class moodleform_mod extends moodleform {
         // Conditions: Verify that the grade conditions are numbers, and make sense.
         if (array_key_exists('conditiongradegroup', $data)) {
             foreach ($data['conditiongradegroup'] as $i => $gradedata) {
-                if ($gradedata['conditiongrademin'] !== '' && !is_numeric($gradedata['conditiongrademin'])) {
+                if ($gradedata['conditiongrademin'] !== '' &&
+                        !is_numeric(unformat_float($gradedata['conditiongrademin']))) {
                     $errors["conditiongradegroup[{$i}]"] = get_string('gradesmustbenumeric', 'condition');
                     continue;
                 }
-                if ($gradedata['conditiongrademax'] !== '' && !is_numeric($gradedata['conditiongrademax'])) {
+                if ($gradedata['conditiongrademax'] !== '' &&
+                        !is_numeric(unformat_float($gradedata['conditiongrademax']))) {
                     $errors["conditiongradegroup[{$i}]"] = get_string('gradesmustbenumeric', 'condition');
                     continue;
                 }
                 if ($gradedata['conditiongrademin'] !== '' && $gradedata['conditiongrademax'] !== '' &&
-                        $gradedata['conditiongrademax'] < $gradedata['conditiongrademin']) {
+                        unformat_float($gradedata['conditiongrademax']) < unformat_float($gradedata['conditiongrademin'])) {
                     $errors["conditiongradegroup[{$i}]"] = get_string('badgradelimits', 'condition');
                     continue;
                 }
index 7a4d7aa..4165a95 100644 (file)
@@ -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);
         }
     }
 
-    if (! $sections = get_all_sections($course->id)) {   // No sections found
-        // Double-check to be extra sure
-        if (! $section = $DB->get_record('course_sections', array('course'=>$course->id, 'section'=>0))) {
-            $section->course = $course->id;   // Create a default section.
-            $section->section = 0;
-            $section->visible = 1;
-            $section->summaryformat = FORMAT_HTML;
-            $section->id = $DB->insert_record('course_sections', $section);
-        }
-        if (! $sections = get_all_sections($course->id) ) {      // Try again
+    if (! $sections = $modinfo->get_section_info_all()) {   // No sections found
+        $section = new stdClass;
+        $section->course = $course->id;   // Create a default section.
+        $section->section = 0;
+        $section->visible = 1;
+        $section->summaryformat = FORMAT_HTML;
+        $section->id = $DB->insert_record('course_sections', $section);
+        rebuild_course_cache($course->id);
+        $modinfo = get_fast_modinfo($COURSE);
+        if (! $sections = $modinfo->get_section_info_all()) {      // Try again
             print_error('cannotcreateorfindstructs', 'error');
         }
     }
index e07f11a..d3be6fe 100644 (file)
--- a/index.php
+++ b/index.php
             $section->sequence = '';
             $section->visible = 1;
             $section->id = $DB->insert_record('course_sections', $section);
+            rebuild_course_cache($SITE->id, true);
         }
 
         if (!empty($section->sequence) or !empty($section->summary) or $editing) {
index 1c0fd6b..fddd673 100644 (file)
@@ -38,6 +38,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';
@@ -48,6 +52,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['gradeitembutnolimits'] = 'You must enter an upper or lower limit, or both.';
 $string['gradelimitsbutnoitem'] = 'You must choose a grade item.';
@@ -67,7 +75,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 <strong>{$a}</strong>.';
 $string['requires_grade_range'] = 'Not available unless you get a particular score in <strong>{$a}</strong>.';
 $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): &lsquo;{$a}&rsquo;';
 $string['userrestriction_visible'] = 'Restricted: &lsquo;{$a}&rsquo;';
+$string['groupingnoaccess'] = 'You do not currently belong to a group which has access to this section. ';
index 7a98c5b..1505986 100644 (file)
@@ -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.';
index 0ece429..c95dd9a 100644 (file)
@@ -29,59 +29,43 @@ defined('MOODLE_INTERNAL') || die();
 /**
  * CONDITION_STUDENTVIEW_HIDE - The activity is not displayed to students at all when conditions aren't met.
  */
-define('CONDITION_STUDENTVIEW_HIDE',0);
+define('CONDITION_STUDENTVIEW_HIDE', 0);
 /**
  * CONDITION_STUDENTVIEW_SHOW - The activity is displayed to students as a greyed-out name, with
  * informational text that explains the conditions under which it will be available.
  */
-define('CONDITION_STUDENTVIEW_SHOW',1);
+define('CONDITION_STUDENTVIEW_SHOW', 1);
 
 /**
- * CONDITION_MISSING_NOTHING - The $cm variable is expected to contain all completion-related data
+ * CONDITION_MISSING_NOTHING - The $item variable is expected to contain all completion-related data
  */
-define('CONDITION_MISSING_NOTHING',0);
+define('CONDITION_MISSING_NOTHING', 0);
 /**
- * CONDITION_MISSING_EXTRATABLE - The $cm variable is expected to contain the fields from course_modules
- * but not the course_modules_availability data
+ * CONDITION_MISSING_EXTRATABLE - The $item variable is expected to contain the fields from
+ * the relevant table (course_modules or course_sections) but not the _availability data
  */
-define('CONDITION_MISSING_EXTRATABLE',1);
+define('CONDITION_MISSING_EXTRATABLE', 1);
 /**
- * CONDITION_MISSING_EVERYTHING - The $cm variable is expected to contain nothing except the ID
+ * CONDITION_MISSING_EVERYTHING - The $item variable is expected to contain nothing except the ID
  */
-define('CONDITION_MISSING_EVERYTHING',2);
+define('CONDITION_MISSING_EVERYTHING', 2);
 
 require_once($CFG->libdir.'/completionlib.php');
 
-/**
- * @global stdClass $CONDITIONLIB_PRIVATE
- * @name $CONDITIONLIB_PRIVATE
- */
-global $CONDITIONLIB_PRIVATE;
-$CONDITIONLIB_PRIVATE = new stdClass;
-// Caches whether completion values are used in availability conditions.
-// Array of course => array of cmid => true.
-$CONDITIONLIB_PRIVATE->usedincondition = array();
-
 /**
  * Core class to handle conditional activites
  *
  * @package   core_condition
  * @category  condition
- * @copyright 2008 Sam Marshall
+ * @copyright 2012 The Open University
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class condition_info {
-    /** @var object bool */
-    private $cm, $gotdata;
-
+class condition_info extends condition_info_base {
     /**
      * Constructs with course-module details.
      *
      * @global moodle_database $DB
      * @uses CONDITION_MISSING_NOTHING
-     * @uses CONDITION_MISSING_EVERYTHING
-     * @uses DEBUG_DEVELOPER
-     * @uses CONDITION_MISSING_EXTRATABLE
      * @param object $cm Moodle course-module object. May have extra fields
      *   ->conditionsgrade, ->conditionscompletion which should come from
      *   get_fast_modinfo. Should have ->availablefrom, ->availableuntil,
@@ -92,94 +76,396 @@ class condition_info {
      *   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
+     */
+    public function __construct($cm, $expectingmissing = CONDITION_MISSING_NOTHING,
+        $loaddata=true) {
+        parent::__construct($cm, 'course_modules', 'coursemoduleid',
+                $expectingmissing, $loaddata);
+    }
+
+    /**
+     * Adds the extra availability conditions (if any) into the given
+     * course-module (or section) object.
+     *
+     * This function may be called statically (for the editing form) or
+     * dynamically.
+     *
+     * @param object $cm Moodle course-module data object
+     */
+    public static function fill_availability_conditions($cm) {
+        parent::fill_availability_conditions_inner($cm, 'course_modules', 'coursemoduleid');
+    }
+
+    /**
+     * Gets the course-module object with full necessary data to determine availability.
+     * @return object Course-module object with full data
+     * @throws coding_exception If data was not supplied when constructing object
+    */
+    public function get_full_course_module() {
+        return $this->get_full_item();
+    }
+
+    /**
+     * 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 Data from form
+     * @param bool $wipefirst If true, wipes existing conditions
+     */
+    public static function update_cm_from_form($cm, $fromform, $wipefirst=true) {
+        $ci = new condition_info($cm, CONDITION_MISSING_EVERYTHING, false);
+        parent::update_from_form($ci, $fromform, $wipefirst);
+    }
+
+   /**
+     * Used in course/lib.php because we need to disable the completion JS if
+     * a completion value affects a conditional activity.
+     *
+     * @global stdClass $CONDITIONLIB_PRIVATE
+     * @param object $course Moodle course object
+     * @param object $item 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, $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();
+
+            // Activities
+            foreach ($modinfo->cms as $othercm) {
+                foreach ($othercm->conditionscompletion as $cmid=>$expectedcompletion) {
+                    $CONDITIONLIB_PRIVATE->usedincondition[$course->id][$cmid] = true;
+                }
+            }
+
+            // Sections
+            foreach ($modinfo->get_section_info_all() as $section) {
+                foreach ($section->conditionscompletion as $cmid => $expectedcompletion) {
+                    $CONDITIONLIB_PRIVATE->usedincondition[$course->id][$cmid] = true;
+                }
+            }
+        }
+        return array_key_exists($cm->id, $CONDITIONLIB_PRIVATE->usedincondition[$course->id]);
+    }
+}
+
+
+/**
+ * Handles conditional access to sections.
+ *
+ * @package   core_condition
+ * @category  condition
+ * @copyright 2012 The Open University
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class condition_info_section extends condition_info_base {
+    /**
+     * Constructs with course-module details.
+     *
+     * @global moodle_database $DB
+     * @uses CONDITION_MISSING_NOTHING
+     * @param object $section Moodle section object. May have extra fields
+     *   ->conditionsgrade, ->conditionscompletion. 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
+     */
+    public function __construct($section, $expectingmissing = CONDITION_MISSING_NOTHING,
+        $loaddata=true) {
+        parent::__construct($section, 'course_sections', 'coursesectionid',
+                $expectingmissing, $loaddata);
+    }
+
+    /**
+     * Adds the extra availability conditions (if any) into the given
+     * course-module (or section) object.
+     *
+     * This function may be called statically (for the editing form) or
+     * dynamically.
+     *
+     * @param object $section Moodle section data object
+     */
+    public static function fill_availability_conditions($section) {
+        parent::fill_availability_conditions_inner($section, 'course_sections', 'coursesectionid');
+    }
+
+    /**
+     * Gets the section object with full necessary data to determine availability.
+     * @return object Section object with full data
+     * @throws coding_exception If data was not supplied when constructing object
+    */
+    public function get_full_section() {
+        return $this->get_full_item();
+    }
+
+    /**
+     * Gets list of required fields from main table.
+     * @return array Array of field names
+     */
+    protected function get_main_table_fields() {
+        return array_merge(parent::get_main_table_fields(), array('groupingid'));
+    }
+
+    /**
+     * Determines whether this particular 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 viewhiddenactivities capability.
+     *   That should apply later.
+     *
+     * @global moodle_database $DB
+     * @global stdclass $USER
+     * @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
+     */
+    public function is_available(&$information, $grabthelot=false, $userid=0, $modinfo=null) {
+        global $DB, $USER, $CONDITIONLIB_PRIVATE;
+
+        $available = parent::is_available($information, $grabthelot, $userid, $modinfo);
+
+        // test if user is enrolled to a grouping which has access to the section
+        if (!empty($this->item->groupingid)) {
+            // Get real user id
+            if (!$userid) {
+                $userid = $USER->id;
+            }
+            $context = context_course::instance($this->item->course);
+
+            if ($userid != $USER->id) {
+                // We are requesting for a non-current user so check it individually
+                // (no cache). Do grouping check first, it's probably faster
+                // than the capability check
+                $gotit = $DB->record_exists_sql('
+                        SELECT
+                            1
+                        FROM
+                            {groupings} g
+                            JOIN {groupings_groups} gg ON g.id = gg.groupingid
+                            JOIN {groups_members} gm ON gg.groupid = gm.groupid
+                        WHERE
+                            g.id = ? AND gm.userid = ?',
+                        array($this->item->groupingid, $userid));
+                if (!$gotit && !has_capability('moodle/site:accessallgroups', $context, $userid)) {
+                    $available = false;
+                    $information .= get_string('groupingnoaccess', 'condition');
+                }
+            } else {
+                // Request is for current user - use cache
+                if(!array_key_exists($this->item->course, $CONDITIONLIB_PRIVATE->groupingscache)) {
+                    if (has_capability('moodle/site:accessallgroups', $context)) {
+                        $CONDITIONLIB_PRIVATE->groupingscache[$this->item->course] = true;
+                    } else {
+                        $groupings = $DB->get_records_sql('
+                                SELECT
+                                    g.id as gid
+                                FROM
+                                    {groupings} g
+                                    JOIN {groupings_groups} gg ON g.id = gg.groupingid
+                                    JOIN {groups_members} gm ON gg.groupid = gm.groupid
+                                WHERE
+                                    g.courseid = ? AND gm.userid = ?',
+                                array($this->item->course, $userid));
+                        $list = array();
+                        foreach ($groupings as $grouping) {
+                            $list[$grouping->gid] = true;
+                        }
+                        $CONDITIONLIB_PRIVATE->groupingscache[$this->item->course] = $list;
+                    }
+                }
+
+                $usergroupings = $CONDITIONLIB_PRIVATE->groupingscache[$this->item->course];
+                if ($usergroupings !== true && !array_key_exists($this->item->groupingid, $usergroupings)) {
+                    $available = false;
+                    $information .= get_string('groupingnoaccess', 'condition');
+                }
+            }
+        }
+
+        $information = trim($information);
+        return $available;
+    }
+
+    /**
+    * Utility function called by modedit.php; updates the
+    * course_modules_availability table based on the module form data.
+    *
+    * @param object $section Section object, must at minimum contain id
+    * @param object $fromform Data from form
+    * @param bool $wipefirst If true, wipes existing conditions
+    */
+    public static function update_section_from_form($section, $fromform, $wipefirst=true) {
+        $ci = new condition_info_section($section, CONDITION_MISSING_EVERYTHING);
+        parent::update_from_form($ci, $fromform, $wipefirst);
+    }
+}
+
+
+/**
+ * Base class to handle conditional items of some kind (currently either
+ * course_modules or sections; they must have a corresponding _availability
+ * table).
+ *
+ * @package   core_condition
+ * @category  condition
+ * @copyright 2012 The Open University
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class condition_info_base {
+    /** @var object, bool, string, string, array */
+     protected $item, $gotdata, $availtable, $idfieldname, $usergroupings;
+
+    /**
+     * Constructs with item details.
+     *
+     * @global moodle_database $DB
+     * @uses CONDITION_MISSING_NOTHING
+     * @uses CONDITION_MISSING_EVERYTHING
+     * @uses CONDITION_MISSING_EXTRATABLE
+     * @uses DEBUG_DEVELOPER
+     * @param object $item Object representing some kind of item (cm or section).
+     *   May have extra fields ->conditionsgrade, ->conditionscompletion.
+     *   Should have ->availablefrom, ->availableuntil, and ->showavailability,
+     *   ->course; but the only required thing is ->id.
+     * @param string $tableprefix Prefix for table used to store availability
+     *   data, e.g. 'course_modules' if we are going to look at
+     *   course_modules_availability.
+     * @param string $idfield Within this table, name of field used as item id
+     *   (e.g. 'coursemoduleid')
+     * @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) {
+    public function __construct($item, $tableprefix, $idfield, $expectingmissing, $loaddata) {
         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 (empty($item->id)) {
+            throw new coding_exception('Invalid parameters; item ID not included');
         }
 
+        //  DB table to store availability conditions
+        $this->availtable = $tableprefix . '_availability';
+
+        // name of module/section ID field in DB
+        $this->idfieldname = $idfield;
+
         // If not loading data, don't do anything else
         if (!$loaddata) {
-            $this->cm = (object)array('id'=>$cm->id);
+            $this->item = (object)array('id' => $item->id);
             $this->gotdata = false;
             return;
         }
 
         // Missing basic data from course_modules
-        if (!isset($cm->availablefrom) || !isset($cm->availableuntil) ||
-            !isset($cm->showavailability) || !isset($cm->course)) {
+        $basicfields = $this->get_main_table_fields();
+        $missingbasicfields = false;
+        foreach ($basicfields as $field) {
+            if (!isset($item->{$field})) {
+                $missingbasicfields = true;
+                break;
+            }
+        }
+        if ($missingbasicfields) {
             if ($expectingmissing<CONDITION_MISSING_EVERYTHING) {
-                debugging('Performance warning: condition_info constructor is
-                    faster if you pass in $cm with at least basic fields
-                    (availablefrom,availableuntil,showavailability,course).
-                    [This warning can be disabled, see phpdoc.]',
-                    DEBUG_DEVELOPER);
+                debugging('Performance warning: condition_info constructor is ' .
+                        'faster if you pass in $item with at least basic fields ' .
+                        'from its table. '.
+                        '[This warning can be disabled, see phpdoc.]',
+                        DEBUG_DEVELOPER);
             }
-            $cm = $DB->get_record('course_modules',array('id'=>$cm->id),
-                'id,course,availablefrom,availableuntil,showavailability');
+            $item = $DB->get_record($tableprefix, array('id' => $item->id),
+                    implode(',', $basicfields), MUST_EXIST);
         }
 
-        $this->cm = clone($cm);
+        $this->item = clone($item);
         $this->gotdata = true;
 
         // Missing extra data
-        if (!isset($cm->conditionsgrade) || !isset($cm->conditionscompletion)) {
+        if (!isset($item->conditionsgrade) || !isset($item->conditionscompletion)) {
             if ($expectingmissing<CONDITION_MISSING_EXTRATABLE) {
-                debugging('Performance warning: condition_info constructor is
-                    faster if you pass in a $cm from get_fast_modinfo.
-                    [This warning can be disabled, see phpdoc.]',
-                    DEBUG_DEVELOPER);
+                debugging('Performance warning: condition_info constructor is ' .
+                        'faster if you pass in a $item from get_fast_modinfo or ' .
+                        'the equivalent for sections. ' .
+                        '[This warning can be disabled, see phpdoc.]',
+                        DEBUG_DEVELOPER);
             }
 
-            self::fill_availability_conditions($this->cm);
+            $this->fill_availability_conditions($this->item);
         }
     }
 
     /**
-     * Adds the extra availability conditions (if any) into the given
-     * course-module object.
+     * Gets list of required fields from main table.
      *
-     * @global moodle_database $DB
-     * @global object $CFG
-     * @param object $cm Moodle course-module data object
+     * @return array Array of field names
+     */
+    protected function get_main_table_fields() {
+        return array('id', 'course', 'availablefrom', 'availableuntil', 'showavailability');
+    }
+
+    /**
+     * Fills availability conditions into the item object, if they are missing,
+     * otherwise does nothing. Called by subclass fill_availability_conditions.
+     * @param object $item Item object
+     * @param string $tableprefix Prefix of name for _availability table e.g. 'course_modules'
+     * @param string $idfield Name of field that contains id e.g. 'coursemoduleid'
+     * @throws coding_exception If item object doesn't have id field
      */
-    public static function fill_availability_conditions(&$cm) {
-        if (empty($cm->id)) {
-            throw new coding_exception("Invalid parameters; course-module ID not included");
+    protected static function fill_availability_conditions_inner($item, $tableprefix, $idfield) {
+        global $DB, $CFG;
+        if (empty($item->id)) {
+            throw new coding_exception('Invalid parameters; item 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));
+        if (!isset($item->conditionsgrade) || !isset($item->conditionscompletion)) {
+            $item->conditionsgrade = array();
+            $item->conditionscompletion = array();
+
+            $conditions = $DB->get_records_sql('
+                    SELECT
+                        a.id AS aid, gi.*, a.sourcecmid, a.requiredcompletion, a.gradeitemid,
+                        a.grademin as conditiongrademin, a.grademax as conditiongrademax
+                    FROM
+                        {' . $tableprefix . '_availability} a
+                        LEFT JOIN {grade_items} gi ON gi.id = a.gradeitemid
+                    WHERE ' . $idfield . ' = ?', array($item->id));
             foreach ($conditions as $condition) {
                 if (!is_null($condition->sourcecmid)) {
-                    $cm->conditionscompletion[$condition->sourcecmid] =
+                    $item->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;
+                    $item->conditionsgrade[$condition->gradeitemid] = $minmax;
                 }
             }
         }
@@ -188,7 +474,7 @@ WHERE
     /**
      * Obtains the name of a grade item.
      *
-     * @global moodle_database $DB
+     * @global object $CFG
      * @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
@@ -196,7 +482,7 @@ WHERE
     private static function get_grade_name($gradeitemobj) {
         global $CFG;
         if (isset($gradeitemobj->id)) {
-            require_once($CFG->libdir.'/gradelib.php');
+            require_once($CFG->libdir . '/gradelib.php');
             $item = new grade_item;
             grade_object::set_properties($item, $gradeitemobj);
             return $item->get_name();
@@ -206,15 +492,13 @@ WHERE
     }
 
     /**
-     * Just a wrapper to call require_data()
-     *
-     * @see require_data()
-     * @return object A course-module object with all the information required to
-     *   determine availability.
+     * Gets the item object with full necessary data to determine availability.
+     * @return object Item object with full data
+     * @throws coding_exception If data was not supplied when constructing object
      */
-    public function get_full_course_module() {
+    protected function get_full_item() {
         $this->require_data();
-        return $this->cm;
+        return $this->item;
     }
 
     /**
@@ -225,15 +509,15 @@ WHERE
      * @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);
+        // Add to DB
+        $DB->insert_record($this->availtable, (object)array(
+                $this->idfieldname => $this->item->id,
+                'sourcecmid' => $cmid, 'requiredcompletion' => $requiredcompletion),
+                false);
 
         // Store in memory too
-        $this->cm->conditionscompletion[$cmid] = $requiredcompletion;
+        $this->item->conditionscompletion[$cmid] = $requiredcompletion;
     }
 
     /**
@@ -248,6 +532,7 @@ WHERE
      *   so don't do it unless it really needs updating)
      */
     public function add_grade_condition($gradeitemid, $min, $max, $updateinmemory=false) {
+        global $DB;
         // Normalise nulls
         if ($min==='') {
             $min = null;
@@ -256,23 +541,21 @@ WHERE
             $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);
+        $DB->insert_record($this->availtable, (object)array(
+                $this->idfieldname => $this->item->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)));
+            $this->item->conditionsgrade[$gradeitemid] = (object) array(
+                    'min' => $min, 'max' => $max);
+            $this->item->conditionsgrade[$gradeitemid]->name = self::get_grade_name(
+                    $DB->get_record('grade_items', array('id'=>$gradeitemid)));
         }
     }
 
-    /**
+     /**
      * Erases from the database all conditions for this activity.
      *
      * @global moodle_database $DB
@@ -280,12 +563,11 @@ WHERE
     public function wipe_conditions() {
         // Wipe from DB
         global $DB;
-        $DB->delete_records('course_modules_availability',
-            array('coursemoduleid'=>$this->cm->id));
+        $DB->delete_records($this->availtable, array($this->idfieldname => $this->item->id));
 
         // And from memory
-        $this->cm->conditionsgrade = array();
-        $this->cm->conditionscompletion = array();
+        $this->item->conditionsgrade = array();
+        $this->item->conditionscompletion = array();
     }
 
     /**
@@ -301,19 +583,20 @@ WHERE
      *   this item
      */
     public function get_full_information($modinfo=null) {
-        $this->require_data();
         global $COURSE, $DB;
+        $this->require_data();
 
         $information = '';
 
         // Completion conditions
-        if(count($this->cm->conditionscompletion)>0) {
-            if ($this->cm->course==$COURSE->id) {
+        if (count($this->item->conditionscompletion) > 0) {
+            if ($this->item->course == $COURSE->id) {
                 $course = $COURSE;
             } else {
-                $course = $DB->get_record('course',array('id'=>$this->cm->course),'id,enablecompletion,modinfo');
+                $course = $DB->get_record('course', array('id' => $this->item->course),
+                        'id, enablecompletion, modinfo', MUST_EXIST);
             }
-            foreach ($this->cm->conditionscompletion as $cmid=>$expectedcompletion) {
+            foreach ($this->item->conditionscompletion as $cmid => $expectedcompletion) {
                 if (!$modinfo) {
                     $modinfo = get_fast_modinfo($course);
                 }
@@ -321,14 +604,14 @@ WHERE
                     continue;
                 }
                 $information .= get_string(
-                    'requires_completion_'.$expectedcompletion,
-                    'condition', $modinfo->cms[$cmid]->name).' ';
+                        'requires_completion_' . $expectedcompletion,
+                        'condition', $modinfo->cms[$cmid]->name) . ' ';
             }
         }
 
         // Grade conditions
-        if (count($this->cm->conditionsgrade)>0) {
-            foreach ($this->cm->conditionsgrade as $gradeitemid=>$minmax) {
+        if (count($this->item->conditionsgrade) > 0) {
+            foreach ($this->item->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.
@@ -370,39 +653,39 @@ WHERE
         // k) Until 14 Oct (midnight 15 Oct)
 
         // Check if start and end dates are 'midnights', if so we show in short form
-        $shortfrom = self::is_midnight($this->cm->availablefrom);
-        $shortuntil = self::is_midnight($this->cm->availableuntil);
+        $shortfrom = self::is_midnight($this->item->availablefrom);
+        $shortuntil = self::is_midnight($this->item->availableuntil);
 
         // For some checks and for display, we need the previous day for the 'until'
         // value, if we are going to display it in short form
-        if ($this->cm->availableuntil) {
-            $daybeforeuntil = strtotime("-1 day", usergetmidnight($this->cm->availableuntil));
+        if ($this->item->availableuntil) {
+            $daybeforeuntil = strtotime('-1 day', usergetmidnight($this->item->availableuntil));
         }
 
         // Special case for if one but not both are exact and they are within a day
-        if ($this->cm->availablefrom && $this->cm->availableuntil &&
-                $shortfrom != $shortuntil && $daybeforeuntil < $this->cm->availablefrom) {
+        if ($this->item->availablefrom && $this->item->availableuntil &&
+                $shortfrom != $shortuntil && $daybeforeuntil < $this->item->availablefrom) {
             // Don't use abbreviated version (see examples f, g above)
             $shortfrom = false;
             $shortuntil = false;
         }
 
         // When showing short end date, the display time is the 'day before' one
-        $displayuntil = $shortuntil ? $daybeforeuntil : $this->cm->availableuntil;
+        $displayuntil = $shortuntil ? $daybeforeuntil : $this->item->availableuntil;
 
-        if ($this->cm->availablefrom && $this->cm->availableuntil) {
-            if ($shortfrom && $shortuntil && $daybeforeuntil == $this->cm->availablefrom) {
+        if ($this->item->availablefrom && $this->item->availableuntil) {
+            if ($shortfrom && $shortuntil && $daybeforeuntil == $this->item->availablefrom) {
                 $information .= get_string('requires_date_both_single_day', 'condition',
-                        self::show_time($this->cm->availablefrom, true));
+                        self::show_time($this->item->availablefrom, true));
             } else {
                 $information .= get_string('requires_date_both', 'condition', (object)array(
-                         'from' => self::show_time($this->cm->availablefrom, $shortfrom),
+                         'from' => self::show_time($this->item->availablefrom, $shortfrom),
                          'until' => self::show_time($displayuntil, $shortuntil)));
             }
-        } else if ($this->cm->availablefrom) {
+        } else if ($this->item->availablefrom) {
             $information .= get_string('requires_date', 'condition',
-                self::show_time($this->cm->availablefrom, $shortfrom));
-        } else if ($this->cm->availableuntil) {
+                self::show_time($this->item->availablefrom, $shortfrom));
+        } else if ($this->item->availableuntil) {
             $information .= get_string('requires_date_before', 'condition',
                 self::show_time($displayuntil, $shortuntil));
         }
@@ -424,7 +707,7 @@ WHERE
     }
 
     /**
-     * Determines whether this particular course-module is currently available
+     * Determines whether this particular item is currently available
      * according to these criteria.
      *
      * - This does not include the 'visible' setting (i.e. this might return
@@ -450,22 +733,23 @@ WHERE
      * @return bool True if this item is available to the user, false otherwise
      */
     public function is_available(&$information, $grabthelot=false, $userid=0, $modinfo=null) {
+        global $COURSE, $DB;
         $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) {
+        if (count($this->item->conditionscompletion) > 0) {
+            if ($this->item->course == $COURSE->id) {
                 $course = $COURSE;
             } else {
-                $course = $DB->get_record('course',array('id'=>$this->cm->course),'id,enablecompletion,modinfo');
+                $course = $DB->get_record('course', array('id' => $this->item->course),
+                        'id, enablecompletion, modinfo', MUST_EXIST);
             }
 
             $completion = new completion_info($course);
-            foreach ($this->cm->conditionscompletion as $cmid=>$expectedcompletion) {
+            foreach ($this->item->conditionscompletion as $cmid => $expectedcompletion) {
                 // If this depends on a deleted module, handle that situation
                 // gracefully.
                 if (!$modinfo) {
@@ -474,14 +758,15 @@ WHERE
                 if (empty($modinfo->cms[$cmid])) {
                     global $PAGE, $UNITTEST;
                     if (!empty($UNITTEST) || (isset($PAGE) && strpos($PAGE->pagetype, 'course-view-')===0)) {
-                        debugging("Warning: activity {$this->cm->id} '{$this->cm->name}' has condition on deleted activity $cmid (to get rid of this message, edit the named activity)");
+                        debugging("Warning: activity {$this->cm->id} '{$this->cm->name}' has condition " .
+                                "on deleted activity $cmid (to get rid of this message, edit the named activity)");
                     }
                     continue;
                 }
 
                 // The completion system caches its own data
-                $completiondata = $completion->get_data((object)array('id'=>$cmid),
-                    $grabthelot, $userid, $modinfo);
+                $completiondata = $completion->get_data((object)array('id' => $cmid),
+                        $grabthelot, $userid, $modinfo);
 
                 $thisisok = true;
                 if ($expectedcompletion==COMPLETION_COMPLETE) {
@@ -503,19 +788,19 @@ WHERE
                 if (!$thisisok) {
                     $available = false;
                     $information .= get_string(
-                        'requires_completion_'.$expectedcompletion,
-                        'condition',$modinfo->cms[$cmid]->name).' ';
+                        '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) {
+        if (count($this->item->conditionsgrade)>0) {
+            foreach ($this->item->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)) {
+                        (!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
@@ -530,24 +815,24 @@ WHERE
                     } else {
                         $string = 'range';
                     }
-                    $information .= get_string('requires_grade_'.$string, 'condition', $minmax->name).' ';
+                    $information .= get_string('requires_grade_' . $string, 'condition', $minmax->name) . ' ';
                 }
             }
         }
 
         // Test dates
-        if ($this->cm->availablefrom) {
-            if (time() < $this->cm->availablefrom) {
+        if ($this->item->availablefrom) {
+            if (time() < $this->item->availablefrom) {
                 $available = false;
 
                 $information .= get_string('requires_date', 'condition',
-                        self::show_time($this->cm->availablefrom,
-                            self::is_midnight($this->cm->availablefrom)));
+                        self::show_time($this->item->availablefrom,
+                            self::is_midnight($this->item->availablefrom)));
             }
         }
 
-        if ($this->cm->availableuntil) {
-            if (time() >= $this->cm->availableuntil) {
+        if ($this->item->availableuntil) {
+            if (time() >= $this->item->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
@@ -561,7 +846,7 @@ WHERE
             }
         }
 
-        $information=trim($information);
+        $information = trim($information);
         return $available;
     }
 
@@ -579,7 +864,7 @@ WHERE
     }
 
     /**
-     * This function is used to check if information about availability should be shown to user or not
+     * Checks whether availability information should be shown to normal users.
      *
      * @return bool True if information about availability should be shown to
      *   normal users
@@ -587,22 +872,22 @@ WHERE
      */
     public function show_availability() {
         $this->require_data();
-        return $this->cm->showavailability;
+        return $this->item->showavailability;
     }
 
     /**
      * Internal function cheks that data was loaded.
      *
-     * @return void throws coding_exception If data wasn't 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 '.
+            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.
@@ -629,14 +914,14 @@ WHERE
             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->cm->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->item->course));
                     foreach ($rs as $record) {
                         $SESSION->gradescorecache[$record->id] =
                             is_null($record->finalgrade)
@@ -698,22 +983,32 @@ WHERE
     }
 
     /**
-     * Utility function called by modedit.php; updates the
-     * course_modules_availability table based on the module form data.
+     * Initialises the global cache
+     * @global stdClass $CONDITIONLIB_PRIVATE
+     */
+    static function init_global_cache() {
+        global $CONDITIONLIB_PRIVATE;
+        $CONDITIONLIB_PRIVATE = new stdClass;
+        $CONDITIONLIB_PRIVATE->usedincondition = array();
+        $CONDITIONLIB_PRIVATE->groupingscache = array();
+    }
+
+    /**
+     * Utility function that resets grade/completion conditions in table based
+     * in data from editing form.
      *
-     * @param object $cm Course-module with as much data as necessary, min id
-     * @param object $fromform
-     * @param bool $wipefirst Defaults to true
+     * @param condition_info_base $ci Condition info
+     * @param object $fromform Data from form
+     * @param bool $wipefirst If true, wipes existing conditions
      */
-    public static function update_cm_from_form($cm, $fromform, $wipefirst=true) {
-        $ci=new condition_info($cm, CONDITION_MISSING_EVERYTHING, false);
+    protected static function update_from_form(condition_info_base $ci, $fromform, $wipefirst) {
         if ($wipefirst) {
             $ci->wipe_conditions();
         }
         foreach ($fromform->conditiongradegroup as $record) {
             if($record['conditiongradeitemid']) {
                 $ci->add_grade_condition($record['conditiongradeitemid'],
-                    $record['conditiongrademin'],$record['conditiongrademax']);
+                    unformat_float($record['conditiongrademin']), unformat_float($record['conditiongrademax']));
             }
         }
         if(isset ($fromform->conditioncompletiongroup)) {
@@ -725,30 +1020,6 @@ WHERE
             }
         }
     }
-
-    /**
-     * Used in course/lib.php because we need to disable the completion JS if
-     * a completion value affects a conditional activity.
-     *
-     * @global stdClass $CONDITIONLIB_PRIVATE
-     * @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
-            $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;
-                }
-            }
-        }
-        return array_key_exists($cm->id, $CONDITIONLIB_PRIVATE->usedincondition[$course->id]);
-    }
 }
+
+condition_info::init_global_cache();
index 69e608b..54d288a 100644 (file)
@@ -78,8 +78,9 @@
         <FIELD NAME="summary" TYPE="text" NOTNULL="false" SEQUENCE="false" PREVIOUS="idnumber" NEXT="summaryformat"/>
         <FIELD NAME="summaryformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="summary" NEXT="format"/>
         <FIELD NAME="format" TYPE="char" LENGTH="10" NOTNULL="true" DEFAULT="topics" SEQUENCE="false" PREVIOUS="summaryformat" NEXT="showgrades"/>
-        <FIELD NAME="showgrades" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="1" SEQUENCE="false" PREVIOUS="format" NEXT="modinfo"/>
-        <FIELD NAME="modinfo" TYPE="text" NOTNULL="false" SEQUENCE="false" PREVIOUS="showgrades" NEXT="newsitems"/>
+        <FIELD NAME="showgrades" TYPE="int" LENGTH="2" NOTNULL="true" UNSIGNED="true" DEFAULT="1" SEQUENCE="false" PREVIOUS="format" NEXT="sectioncache"/>
+        <FIELD NAME="sectioncache" TYPE="text" NOTNULL="false" SEQUENCE="false" PREVIOUS="showgrades" NEXT="modinfo"/>
+        <FIELD NAME="modinfo" TYPE="text" NOTNULL="false" SEQUENCE="false" PREVIOUS="sectioncache" NEXT="newsitems"/>
         <FIELD NAME="newsitems" TYPE="int" LENGTH="5" NOTNULL="true" DEFAULT="1" SEQUENCE="false" PREVIOUS="modinfo" NEXT="startdate"/>
         <FIELD NAME="startdate" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="newsitems" NEXT="numsections"/>
         <FIELD NAME="numsections" TYPE="int" LENGTH="5" NOTNULL="true" DEFAULT="1" SEQUENCE="false" PREVIOUS="startdate" NEXT="marker"/>
         <INDEX NAME="userid-coursemoduleid" UNIQUE="true" FIELDS="userid, coursemoduleid" PREVIOUS="coursemoduleid"/>
       </INDEXES>
     </TABLE>
-    <TABLE NAME="course_sections" COMMENT="to define the sections for each course" PREVIOUS="course_modules_completion" NEXT="course_request">
+    <TABLE NAME="course_sections" COMMENT="to define the sections for each course" PREVIOUS="course_modules_completion" NEXT="course_sections_availability">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" NEXT="course"/>
         <FIELD NAME="course" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="id" NEXT="section"/>
         <FIELD NAME="summary" TYPE="text" NOTNULL="false" SEQUENCE="false" PREVIOUS="name" NEXT="summaryformat"/>
         <FIELD NAME="summaryformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="summary" NEXT="sequence"/>
         <FIELD NAME="sequence" TYPE="text" NOTNULL="false" SEQUENCE="false" PREVIOUS="summaryformat" NEXT="visible"/>
-        <FIELD NAME="visible" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" SEQUENCE="false" PREVIOUS="sequence"/>
+        <FIELD NAME="visible" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" SEQUENCE="false" PREVIOUS="sequence" NEXT="availablefrom"/>
+        <FIELD NAME="availablefrom" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="If set non-zero, the section only becomes available from the time given here." PREVIOUS="visible" NEXT="availableuntil"/>
+        <FIELD NAME="availableuntil" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="If set non-zero, the section is only available until the time given here." PREVIOUS="availablefrom" NEXT="showavailability"/>
+        <FIELD NAME="showavailability" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="If this field is set to 1 and the section is not available because the 'availablefrom' date has not been reached, or one of the conditions in course_sections_availability_cg is not matched, then the item will be displayed greyed-out (unclickable) with an information message such as 'Available from (date)'. Otherwise, the item will not be displayed to students at all." PREVIOUS="availableuntil" NEXT="groupingid"/>
+        <FIELD NAME="groupingid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Grouping that has access to this section." PREVIOUS="showavailability"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
         <INDEX NAME="course_section" UNIQUE="true" FIELDS="course, section"/>
       </INDEXES>
     </TABLE>
-    <TABLE NAME="course_request" COMMENT="course requests" PREVIOUS="course_sections" NEXT="filter_active">
+    <TABLE NAME="course_sections_availability" COMMENT="Completion or grade conditions that affect if a section is currently available to students."  PREVIOUS="course_sections" NEXT="course_request">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" NEXT="coursesectionid"/>
+        <FIELD NAME="coursesectionid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="ID of the section whose availability is being restricted by this condition." PREVIOUS="id" NEXT="sourcecmid"/>
+        <FIELD NAME="sourcecmid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="If this condition is based on completion of some activity, then this is the course-module ID of that activity. Otherwise null." PREVIOUS="coursesectionid" NEXT="requiredcompletion"/>
+        <FIELD NAME="requiredcompletion" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="If this condition is on a module's completion, then this should be set to the required completion state. Otherwise null. Suitable values are 1 = completed, 2 = completed-passed, 3 = completed-failed." PREVIOUS="sourcecmid" NEXT="gradeitemid"/>
+        <FIELD NAME="gradeitemid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="If this condition is based on a gradebook score, the item ID is given here (and the item will now not be available until a value is achieved for that grade). Otherwise null." PREVIOUS="requiredcompletion" NEXT="grademin"/>
+        <FIELD NAME="grademin" TYPE="number" LENGTH="10" NOTNULL="false" SEQUENCE="false" DECIMALS="5" COMMENT="If set, this is the minimum grade percentage that must be reached (greater than or equal) in order for this section to appear. Otherwise null." PREVIOUS="gradeitemid" NEXT="grademax"/>
+        <FIELD NAME="grademax" TYPE="number" LENGTH="10" NOTNULL="false" SEQUENCE="false" DECIMALS="5" COMMENT="If set, this is the maximum grade percentage that users must be below (less than) in order to display this item. Otherwise null." PREVIOUS="grademin"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="coursesectionid"/>
+        <KEY NAME="coursesectionid" TYPE="foreign" FIELDS="coursesectionid" REFTABLE="course_sections" REFFIELDS="id" PREVIOUS="primary" NEXT="sourcecmid"/>
+        <KEY NAME="sourcecmid" TYPE="foreign" FIELDS="sourcecmid" REFTABLE="course_modules" REFFIELDS="id" PREVIOUS="coursesectionid" NEXT="gradeitemid"/>
+        <KEY NAME="gradeitemid" TYPE="foreign" FIELDS="gradeitemid" REFTABLE="grade_items" REFFIELDS="id" PREVIOUS="sourcecmid"/>
+      </KEYS>
+       </TABLE>
+    <TABLE NAME="course_request" COMMENT="course requests" PREVIOUS="course_sections_availability" NEXT="filter_active">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" NEXT="fullname"/>
         <FIELD NAME="fullname" TYPE="char" LENGTH="254" NOTNULL="true" SEQUENCE="false" PREVIOUS="id" NEXT="shortname"/>
       </KEYS>
     </TABLE>
   </TABLES>
-</XMLDB>
\ No newline at end of file
+</XMLDB>
index bc48dde..d0f753b 100644 (file)
@@ -501,5 +501,73 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2012050400.02);
     }
 
+    if ($oldversion < 2012051100.01) {
+        // Amend course table to add sectioncache cache
+        $table = new xmldb_table('course');
+        $field = new xmldb_field('sectioncache', XMLDB_TYPE_TEXT, null, null, null, null, null, 'showgrades');
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Amend course_sections to add date, time and groupingid availability
+        // conditions and a setting about whether to show them
+        $table = new xmldb_table('course_sections');
+        $field = new xmldb_field('availablefrom', XMLDB_TYPE_INTEGER, '10', null,
+                XMLDB_NOTNULL, null, '0', 'visible');
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+        $field = new xmldb_field('availableuntil', XMLDB_TYPE_INTEGER, '10', null,
+                XMLDB_NOTNULL, null, '0', 'availablefrom');
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+        $field = new xmldb_field('showavailability', XMLDB_TYPE_INTEGER, '1', null,
+                XMLDB_NOTNULL, null, '0', 'availableuntil');
+        // Conditionally launch add field showavailability
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+        $field = new xmldb_field('groupingid', XMLDB_TYPE_INTEGER, '10', null,
+                XMLDB_NOTNULL, null, '0', 'showavailability');
+        // Conditionally launch add field groupingid
+        if (!$dbman->field_exists($table, $field)) {
+            $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', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('coursesectionid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('sourcecmid', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
+        $table->add_field('requiredcompletion', XMLDB_TYPE_INTEGER, '1', null, null, null, null);
+        $table->add_field('gradeitemid', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
+        $table->add_field('grademin', XMLDB_TYPE_NUMBER, '10, 5', null, null, null, null);
+        $table->add_field('grademax', XMLDB_TYPE_NUMBER, '10, 5', null, null, null, null);
+
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table->add_key('coursesectionid', XMLDB_KEY_FOREIGN,
+                array('coursesectionid'), 'course_sections', array('id'));
+        $table->add_key('sourcecmid', XMLDB_KEY_FOREIGN,
+                array('sourcecmid'), 'course_modules', array('id'));
+        $table->add_key('gradeitemid', XMLDB_KEY_FOREIGN,
+                array('gradeitemid'), 'grade_items', array('id'));
+
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Amend course table to add sectioncache
+        $table = new xmldb_table('course');
+        $field = new xmldb_field('sectioncache', XMLDB_TYPE_TEXT, null, null, null, null, null, 'showgrades');
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        /// Main savepoint reached
+        upgrade_main_savepoint(true, 2012051100.01);
+    }
+
     return true;
 }
index 20ac75c..26f62ad 100644 (file)
@@ -43,6 +43,8 @@ if (!defined('MAX_MODINFO_CACHE_SIZE')) {
 class course_modinfo extends stdClass {
     // For convenience we store the course object here as it is needed in other parts of code
     private $course;
+    // Array of section data from cache
+    private $sectioninfo;
 
     // Existing data fields
     ///////////////////////
@@ -189,6 +191,23 @@ class course_modinfo extends stdClass {
         return $this->groups[$groupingid];
     }
 
+    /**
+     * Gets all sections as array from section number => data about section.
+     * @return array Array of section_info objects organised by section number
+     */
+    public function get_section_info_all() {
+        return $this->sectioninfo;
+    }
+
+    /**
+    * Gets data about specific numbered section.
+    * @param int $sectionnumber Number (not id) of section
+    * @return section_info Information for numbered section
+    */
+    public function get_section_info($sectionnumber) {
+        return $this->sectioninfo[$sectionnumber];
+    }
+
     /**
      * Constructs based on course.
      * Note: This constructor should not usually be called directly.
@@ -199,6 +218,12 @@ class course_modinfo extends stdClass {
     public function __construct($course, $userid) {
         global $CFG, $DB;
 
+        // Check modinfo field is set. If not, build and load it.
+        if (empty($course->modinfo) || empty($course->sectioncache)) {
+            rebuild_course_cache($course->id);
+            $course = $DB->get_record('course', array('id'=>$course->id), '*', MUST_EXIST);
+        }
+
         // Set initial values
         $this->courseid = $course->id;
         $this->userid = $userid;
@@ -208,12 +233,6 @@ class course_modinfo extends stdClass {
         $this->groups = null;
         $this->course = $course;
 
-        // Check modinfo field is set. If not, build and load it.
-        if (empty($course->modinfo)) {
-            rebuild_course_cache($course->id);
-            $course->modinfo = $DB->get_field('course', 'modinfo', array('id'=>$course->id));
-        }
-
         // Load modinfo field into memory as PHP object and check it's valid
         $info = unserialize($course->modinfo);
         if (!is_array($info)) {
@@ -228,6 +247,20 @@ class course_modinfo extends stdClass {
             }
         }
 
+        // Load sectioncache field into memory as PHP object and check it's valid
+        $sectioncache = unserialize($course->sectioncache);
+        if (!is_array($sectioncache) || empty($sectioncache)) {
+            // hmm, something is wrong - let's fix it
+            rebuild_course_cache($course->id);
+            $course->sectioncache = $DB->get_field('course', 'sectioncache', array('id'=>$course->id));
+            $sectioncache = unserialize($course->sectioncache);
+            if (!is_array($sectioncache)) {
+                // If it still fails, abort
+                debugging('Problem with "sectioncache" data for this course');
+                return;
+            }
+        }
+
         // If we haven't already preloaded contexts for the course, do it now
         preload_course_contexts($course->id);
 
@@ -264,6 +297,20 @@ class course_modinfo extends stdClass {
             $this->sections[$cm->sectionnum][] = $cm->id;
         }
 
+        // Expand section objects
+        $this->sectioninfo = array();
+        foreach ($sectioncache as $number => $data) {
+            // Calculate sequence
+            if (isset($this->sections[$number])) {
+                $sequence = implode(',', $this->sections[$number]);
+            } else {
+                $sequence = '';
+            }
+            // Expand
+            $this->sectioninfo[$number] = new section_info($data, $number, $course->id, $sequence,
+                    $this, $userid);
+        }
+
         // We need at least 'dynamic' data from each course-module (this is basically the remaining
         // data which was always present in previous version of get_fast_modinfo, so it's required
         // for BC). Creating it in a second pass is necessary because obtain_dynamic_data sometimes
@@ -272,6 +319,34 @@ class course_modinfo extends stdClass {
             $cm->obtain_dynamic_data();
         }
     }
+
+    /**
+     * Builds a list of information about sections on a course to be stored in
+     * the course cache. (Does not include information that is already cached
+     * in some other way.)
+     *
+     * Used internally by rebuild_course_cache function; do not use otherwise.
+     * @param int $courseid Course ID
+     * @return array Information about sections, indexed by section number (not id)
+     */
+    public static function build_section_cache($courseid) {
+        global $DB;
+
+        // Get section data
+        $sections = $DB->get_records('course_sections', array('course' => $courseid), 'section',
+                'section, id, course, name, summary, summaryformat, sequence, visible, ' .
+                'availablefrom, availableuntil, showavailability, groupingid');
+        $compressedsections = array();
+
+        // Remove unnecessary data and add availability
+        foreach ($sections as $number => $section) {
+            // Clone just in case it is reused elsewhere (get_all_sections cache)
+            $compressedsections[$number] = clone($section);
+            section_info::convert_for_section_cache($compressedsections[$number]);
+        }
+
+        return $compressedsections;
+    }
 }
 
 
@@ -283,7 +358,7 @@ class course_modinfo extends stdClass {
  * data only using the supplied set functions. Setting the fields directly is not supported
  * and may cause problems later.
  */
-class cm_info extends stdClass  {
+class cm_info extends stdClass {
     /**
      * State: Only basic data from modinfo cache is available.
      */
@@ -972,6 +1047,15 @@ class cm_info extends stdClass  {
             // uses basic data.
             $this->available = $ci->is_available($this->availableinfo, true,
                     $userid, $this->modinfo);
+
+            // Check parent section
+            $parentsection = $this->modinfo->get_section_info($this->sectionnum);
+            if (!$parentsection->available) {
+                // Do not store info from section here, as that is already
+                // presented from the section (if appropriate) - just change
+                // the flag
+                $this->available = false;
+            }
         } else {
             $this->available = true;
         }
@@ -1127,14 +1211,17 @@ function rebuild_course_cache($courseid=0, $clearonly=false) {
 
     if ($clearonly) {
         if (empty($courseid)) {
-            $courseselect = array();
+            $DB->set_field('course', 'modinfo', null);
+            $DB->set_field('course', 'sectioncache', null);
         } else {
-            $courseselect = array('id'=>$courseid);
+            // Clear both fields in one update
+            $resetobj = (object)array('id' => $courseid, 'modinfo' => null, 'sectioncache' => null);
+            $DB->update_record('course', $resetobj);
         }
-        $DB->set_field('course', 'modinfo', null, $courseselect);
         // update cached global COURSE too ;-)
         if ($courseid == $COURSE->id or empty($courseid)) {
             $COURSE->modinfo = null;
+            $COURSE->sectioncache = null;
         }
         // reset the fast modinfo cache
         $reset = 'reset';
@@ -1154,10 +1241,14 @@ function rebuild_course_cache($courseid=0, $clearonly=false) {
     $rs = $DB->get_recordset("course", $select,'','id,fullname');
     foreach ($rs as $course) {
         $modinfo = serialize(get_array_of_activities($course->id));
-        $DB->set_field("course", "modinfo", $modinfo, array("id"=>$course->id));
+        $sectioncache = serialize(course_modinfo::build_section_cache($course->id));
+        $updateobj = (object)array('id' => $course->id,
+                'modinfo' => $modinfo, 'sectioncache' => $sectioncache);
+        $DB->update_record("course", $updateobj);
         // update cached global COURSE too ;-)
         if ($course->id == $COURSE->id) {
             $COURSE->modinfo = $modinfo;
+            $COURSE->sectioncache = $sectioncache;
         }
     }
     $rs->close();
@@ -1235,3 +1326,253 @@ class cached_cm_info {
      */
     public $onclick;
 }
+
+
+/**
+ * Data about a single section on a course. This contains the fields from the
+ * course_sections table, plus additional data when required.
+ */
+class section_info extends stdClass {
+    /**
+     * Section ID - from course_sections table
+     * @var int
+     */
+    public $id;
+
+    /**
+     * Course ID - from course_sections table
+     * @var int
+     */
+    public $course;
+
+    /**
+     * Section number - from course_sections table
+     * @var int
+     */
+    public $section;
+
+    /**
+     * Section name if specified - from course_sections table
+     * @var string
+     */
+    public $name;
+
+    /**
+     * Section visibility (1 = visible) - from course_sections table
+     * @var int
+     */
+    public $visible;
+
+    /**
+     * Section summary text if specified - from course_sections table
+     * @var string
+     */
+    public $summary;
+
+    /**
+     * Section summary text format (FORMAT_xx constant) - from course_sections table
+     * @var int
+     */
+    public $summaryformat;
+
+    /**
+     * When section is unavailable, this field controls whether it is shown to students (0 =
+     * hide completely, 1 = show greyed out with information about when it will be available) -
+     * from course_sections table
+     * @var int
+     */
+    public $showavailability;
+
+    /**
+     * Available date for this section (0 if not set, or set to seconds since epoch; before this
+     * date, section does not display to students) - from course_sections table
+     * @var int
+     */
+    public $availablefrom;
+
+    /**
+     * Available until date for this section  (0 if not set, or set to seconds since epoch; from
+     * this date, section does not display to students) - from course_sections table
+     * @var int
+     */
+    public $availableuntil;
+
+    /**
+     * If section is restricted to users of a particular grouping, this is its id
+     * (0 if not set) - from course_sections table
+     * @var int
+     */
+    public $groupingid;
+
+    /**
+     * Availability conditions for this section based on the completion of
+     * course-modules (array from course-module id to required completion state
+     * for that module) - from cached data in sectioncache field
+     * @var array
+     */
+    public $conditionscompletion;
+
+    /**
+     * Availability conditions for this section based on course grades (array from
+     * grade item id to object with ->min, ->max fields) - from cached data in
+     * sectioncache field
+     * @var array
+     */
+    public $conditionsgrade;
+
+    /**
+     * True if this section is available to students i.e. if all availability conditions
+     * are met - obtained dynamically
+     * @var bool
+     */
+    public $available;
+
+    /**
+     * If section is not available to students, this string gives information about
+     * availability which can be displayed to students and/or staff (e.g. 'Available from 3
+     * January 2010') for display on main page - obtained dynamically
+     * @var string
+     */
+    public $availableinfo;
+
+    /**
+     * True if this section is available to the CURRENT user (for example, if current user
+     * has viewhiddensections capability, they can access the section even if it is not
+     * visible or not available, so this would be true in that case)
+     * @var bool
+     */
+    public $uservisible;
+
+    /**
+     * Default values for sectioncache fields; if a field has this value, it won't
+     * be stored in the sectioncache cache, to save space. Checks are done by ===
+     * which means values must all be strings.
+     * @var array
+     */
+    private static $sectioncachedefaults = array(
+        'name' => null,
+        'summary' => '',
+        'summaryformat' => '1', // FORMAT_HTML, but must be a string
+        'visible' => '1',
+        'showavailability' => '0',
+        'availablefrom' => '0',
+        'availableuntil' => '0',
+        'groupingid' => '0',
+    );
+
+    /**
+     * Constructs object from database information plus extra required data.
+     * @param object $data Array entry from cached sectioncache
+     * @param int $number Section number (array key)
+     * @param int $courseid Course ID
+     * @param int $sequence Sequence of course-module ids contained within
+     * @param course_modinfo $modinfo Owner (needed for checking availability)
+     * @param int $userid User ID
+     */
+    public function __construct($data, $number, $courseid, $sequence, $modinfo, $userid) {
+        global $CFG;
+
+        // Data that is always present
+        $this->id = $data->id;
+
+        // Data that may use default values to save cache size
+        foreach (self::$sectioncachedefaults as $field => $value) {
+            if (isset($data->{$field})) {
+                $this->{$field} = $data->{$field};
+            } else {
+                $this->{$field} = $value;
+            }
+        }
+
+        // Data with array defaults
+        $this->conditionscompletion = isset($data->conditionscompletion)
+                ? $data->conditionscompletion : array();
+        $this->conditionsgrade = isset($data->conditionsgrade)
+                ? $data->conditionsgrade : array();
+
+        // Other data from other places
+        $this->course = $courseid;
+        $this->section = $number;
+        $this->sequence = $sequence;
+
+        // Availability data
+        if (!empty($CFG->enableavailability)) {
+            // Get availability information
+            $ci = new condition_info_section($this);
+            $this->available = $ci->is_available($this->availableinfo, true,
+                    $userid, $modinfo);
+            // Display grouping info if available & not already displaying
+            // (it would already display if current user doesn't have access)
+            // for people with managegroups - same logic/class as grouping label
+            // on individual activities.
+            $context = context_course::instance($courseid);
+            if ($this->availableinfo === '' && $this->groupingid &&
+                    has_capability('moodle/course:managegroups', $context)) {
+                $groupings = groups_get_all_groupings($courseid);
+                $this->availableinfo = html_writer::tag('span', '(' . format_string(
+                        $groupings[$this->groupingid]->name, true, array('context' => $context)) .
+                        ')', array('class' => 'groupinglabel'));
+            }
+        } else {
+            $this->available = true;
+        }
+
+        // Update visibility for current user
+        $this->update_user_visible($userid);
+    }
+
+    /**
+     * Works out whether activity is visible *for current user* - if this is false, they
+     * aren't allowed to access it.
+     * @param int $userid User ID
+     * @return void
+     */
+    private function update_user_visible($userid) {
+        global $CFG;
+        $coursecontext = context_course::instance($this->course);
+        $this->uservisible = true;
+        if ((!$this->visible || !$this->available) &&
+                !has_capability('moodle/course:viewhiddensections', $coursecontext, $userid)) {
+            $this->uservisible = false;
+        }
+    }
+
+    /**
+     * Prepares section data for inclusion in sectioncache cache, removing items
+     * that are set to defaults, and adding availability data if required.
+     *
+     * Called by build_section_cache in course_modinfo only; do not use otherwise.
+     * @param object $section Raw section data object
+     */
+    public static function convert_for_section_cache($section) {
+        global $CFG;
+
+        // Course id stored in course table
+        unset($section->course);
+        // Section number stored in array key
+        unset($section->section);
+        // Sequence stored implicity in modinfo $sections array
+        unset($section->sequence);
+
+        // Add availability data if turned on
+        if ($CFG->enableavailability) {
+            require_once($CFG->dirroot . '/lib/conditionlib.php');
+            condition_info_section::fill_availability_conditions($section);
+            if (count($section->conditionscompletion) == 0) {
+                unset($section->conditionscompletion);
+            }
+            if (count($section->conditionsgrade) == 0) {
+                unset($section->conditionsgrade);
+            }
+        }
+
+        // Remove default data
+        foreach(self::$sectioncachedefaults as $field => $value) {
+            // Exact compare as strings to avoid problems if some strings are set
+            // to "0" etc.
+            if (isset($section->{$field}) && $section->{$field} === $value) {
+                unset($section->{$field});
+            }
+        }
+    }
+}
index b621551..a0ede11 100644 (file)
@@ -4641,7 +4641,10 @@ function remove_course_contents($courseid, $showfeedback = true, array $options
     }
     $DB->update_record('course', $oldcourse);
 
-    // Delete course sections.
+    // Delete course sections and availability options.
+    $DB->delete_records_select('course_sections_availability',
+           'coursesectionid IN (SELECT id from {course_sections} WHERE course=?)',
+           array($course->id));
     $DB->delete_records('course_sections', array('course'=>$course->id));
 
     // delete legacy, section and any other course files
index 707ea92..0afce0b 100644 (file)
@@ -1898,8 +1898,7 @@ class global_navigation extends navigation_node {
         require_once($CFG->dirroot.'/course/lib.php');
 
         $modinfo = get_fast_modinfo($course);
-
-        $sections = array_slice(get_all_sections($course->id), 0, $course->numsections+1, true);
+        $sections = array_slice($modinfo->get_section_info_all(), 0, $course->numsections+1, true);
         $activities = array();
 
         foreach ($sections as $key => $section) {
@@ -1961,8 +1960,6 @@ class global_navigation extends navigation_node {
         $namingfunction = 'callback_'.$courseformat.'_get_section_name';
         $namingfunctionexists = (function_exists($namingfunction));
 
-        $viewhiddensections = has_capability('moodle/course:viewhiddensections', $this->page->context);
-
         $urlfunction = 'callback_'.$courseformat.'_get_section_url';
         if (function_exists($urlfunction)) {
             // This code path is deprecated but we decided not to warn developers as
@@ -1982,10 +1979,11 @@ class global_navigation extends navigation_node {
             if ($course->id == $SITE->id) {
                 $this->load_section_activities($coursenode, $section->section, $activities);
             } else {
-                if ((!$viewhiddensections && !$section->visible) || (!$this->showemptysections &&
+                if (!$section->uservisible || (!$this->showemptysections &&
                         !$section->hasactivites && $this->includesectionnum !== $section->section)) {
                     continue;
                 }
+
                 if ($namingfunctionexists) {
                     $sectionname = $namingfunction($course, $section, $sections);
                 } else {
@@ -2003,7 +2001,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->available);
                 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);
@@ -3709,7 +3707,8 @@ class settings_navigation extends navigation_node {
             $formatidentifier = optional_param($requestkey, 0, PARAM_INT);
         }
 
-        $sections = get_all_sections($course->id);
+        $modinfo = get_fast_modinfo($course);
+        $sections = $modinfo->get_section_info_all();
 
         $addresource = $this->add(get_string('addresource'));
         $addactivity = $this->add(get_string('addactivity'));
index 84c15f9..fb2d612 100644 (file)
@@ -31,14 +31,22 @@ require_once($CFG->dirroot . '/lib/conditionlib.php');
 
 
 class conditionlib_testcase extends advanced_testcase {
+    private $oldcfg, $olduser;
+
     protected function setUp() {
-        global $CFG;
+        global $CFG, $USER, $DB;
         parent::setUp();
 
         $this->resetAfterTest(true);
 
         $CFG->enableavailability = 1;
         $CFG->enablecompletion = 1;
+        $user = $this->getDataGenerator()->create_user();;
+        $this->setUser($user);
+
+        // Reset modinfo cache before each request
+        $reset = 'reset';
+        get_fast_modinfo($reset);
     }
 
     function test_constructor() {
@@ -102,12 +110,81 @@ class conditionlib_testcase extends advanced_testcase {
             $fullcm->conditionsgrade);
     }
 
+    /**
+     * Same as above test but for course_sections instead of course_modules.
+     */
+    function test_section_constructor() {
+        global $DB, $CFG;
+
+        // Test records
+        $id = $DB->insert_record('course_sections',(object)array(
+                'showavailability' => 1, 'availablefrom' => 17,
+                'availableuntil' => 398, 'course' => 64, 'groupingid' => 13));
+
+        // no ID
+        $section = new stdClass;
+        try {
+            $test = new condition_info_section($section);
+            $this->fail();
+        } catch(coding_exception $e) {
+        }
+
+        // no other data
+        $section->id = $id;
+        $test = new condition_info_section($section, CONDITION_MISSING_EVERYTHING);
+        $this->assertEquals(
+                (object)array('id' => $id, 'showavailability' => 1, 'groupingid' => 13,
+                    'availablefrom' => 17, 'availableuntil' => 398, 'course' => 64,
+                    'conditionsgrade' => array(), 'conditionscompletion' => array()),
+                $test->get_full_section());
+
+        // just the course_sections stuff; check it doesn't request that from db
+        // (by using fake values and ensuring it still has those)
+        $section->showavailability = 0;
+        $section->availablefrom = 2;
+        $section->availableuntil = 74;
+        $section->course = 38;
+        $section->groupingid = 99;
+        $test = new condition_info_section($section, CONDITION_MISSING_EXTRATABLE);
+        $this->assertEquals(
+                (object)array('id' => $id, 'showavailability' => 0, 'groupingid' => 99,
+                    'availablefrom' => 2, 'availableuntil' => 74, 'course' => 38,
+                    'conditionsgrade' => array(), 'conditionscompletion' => array()),
+                $test->get_full_section());
+
+        // Now let's add some actual grade/completion conditions
+        $DB->insert_record('course_sections_availability',(object)array(
+                'coursesectionid' => $id,
+                'sourcecmid' => 42,
+                'requiredcompletion' => 2
+        ));
+        $DB->insert_record('course_sections_availability',(object)array(
+                'coursesectionid' => $id,
+                'sourcecmid' => 666,
+                'requiredcompletion' => 1
+        ));
+        $DB->insert_record('course_sections_availability',(object)array(
+                'coursesectionid' => $id,
+                'gradeitemid' => 37,
+                'grademin' => 5.5
+        ));
+
+        $section = (object)array('id' => $id);
+        $test = new condition_info_section($section, CONDITION_MISSING_EVERYTHING);
+        $fullsection = $test->get_full_section();
+        $this->assertEquals(array(42 => 2, 666 => 1), $fullsection->conditionscompletion);
+        $this->assertEquals(array(37 => (object)array('min' => 5.5, 'max' => null, 'name' => '!missing')),
+                $fullsection->conditionsgrade);
+    }
+
     private function make_course() {
         global $DB;
-        $categoryid=$DB->insert_record('course_categories',(object)array('name'=>'conditionlibtest'));
-        return $DB->insert_record('course',(object)array(
+        $categoryid = $DB->insert_record('course_categories',(object)array('name'=>'conditionlibtest'));
+        $courseid = $DB->insert_record('course',(object)array(
             'fullname'=>'Condition test','shortname'=>'CT1',
             'category'=>$categoryid,'enablecompletion'=>1));
+        context_course::instance($courseid);
+        return $courseid;
     }
 
     private function make_course_module($courseid,$params=array()) {
@@ -127,10 +204,34 @@ class conditionlib_testcase extends advanced_testcase {
         return $DB->insert_record('course_modules',$settings);
     }
 
-    private function make_section($courseid,$cmids,$sectionnum=0) {
+    private function make_section($courseid, $cmids, $sectionnum=0, $params=array()) {
         global $DB;
-        $DB->insert_record('course_sections',(object)array(
-            'course'=>$courseid,'sequence'=>implode(',',$cmids),'section'=>$sectionnum));
+        $record = (object)array(
+            'course' => $courseid,
+            'sequence' => implode(',',$cmids),
+            'section' => $sectionnum);
+        foreach ($params as $name => $value) {
+            $record->{$name} = $value;
+        }
+        return $DB->insert_record('course_sections', $record);
+    }
+
+    private function make_grouping($courseid, $name) {
+        global $CFG;
+        require_once($CFG->dirroot . '/group/lib.php');
+        return groups_create_grouping((object)array('courseid' => $courseid,
+                'name' => $name));
+    }
+
+    private function make_group($courseid, $name, $groupingid=0) {
+        global $CFG;
+        require_once($CFG->dirroot . '/group/lib.php');
+        $groupid = groups_create_group((object)array('courseid' => $courseid,
+                'name' => $name));
+        if ($groupingid) {
+            groups_assign_grouping($groupingid, $groupid);
+        }
+        return $groupid;
     }
 
     function test_modinfo() {
@@ -184,6 +285,60 @@ class conditionlib_testcase extends advanced_testcase {
             $modinfo->cms[$cmid2]->conditionsgrade);
     }
 
+    function test_section_modinfo() {
+        global $DB;
+
+        // Let's make a course
+        $courseid = $this->make_course();
+
+        // Now let's make a couple sections on that course, one of which has a cm
+        $cmid = $this->make_course_module($courseid);
+        $sectionid1 = $this->make_section($courseid, array($cmid), 1, array(
+                'showavailability' => 1, 'availablefrom' => 17,
+                'availableuntil' => 398, 'groupingid' => 13));
+        $sectionid2 = $this->make_section($courseid, array(), 2);
+
+        // Add a fake grade item
+        $gradeitemid = $DB->insert_record('grade_items', (object)array(
+                'courseid' => $courseid, 'itemname' => 'frog'));
+
+        // One of the sections has grade and completion conditions, other doesn't
+        $DB->insert_record('course_sections_availability',(object)array(
+            'coursesectionid' => $sectionid2,
+            'sourcecmid' => $cmid,
+            'requiredcompletion'=>1
+        ));
+        $DB->insert_record('course_sections_availability',(object)array(
+            'coursesectionid' => $sectionid2,
+            'gradeitemid' => $gradeitemid,
+            'grademin' => 5.5
+        ));
+
+        // Okay sweet, now get modinfo
+        $course = $DB->get_record('course', array('id' => $courseid));
+        $modinfo = get_fast_modinfo($course);
+
+        // Test basic data
+        $section1 = $modinfo->get_section_info(1);
+        $this->assertEquals(1, $section1->showavailability);
+        $this->assertEquals(17, $section1->availablefrom);
+        $this->assertEquals(398, $section1->availableuntil);
+        $this->assertEquals(13, $section1->groupingid);
+        $section2 = $modinfo->get_section_info(2);
+        $this->assertEquals(0, $section2->showavailability);
+        $this->assertEquals(0, $section2->availablefrom);
+        $this->assertEquals(0, $section2->availableuntil);
+        $this->assertEquals(0, $section2->groupingid);
+
+        // Test condition arrays
+        $this->assertEquals(array(), $section1->conditionscompletion);
+        $this->assertEquals(array(), $section1->conditionsgrade);
+        $this->assertEquals(array($cmid => 1),
+                $section2->conditionscompletion);
+        $this->assertEquals(array($gradeitemid => (object)array('min' => 5.5, 'max' => null, 'name' => 'frog')),
+                $section2->conditionsgrade);
+    }
+
     function test_add_and_remove() {
         global $DB;
         // Make course and module
@@ -227,6 +382,49 @@ class conditionlib_testcase extends advanced_testcase {
         $this->assertEquals(array(),$cm->conditionsgrade);
     }
 
+    function test_section_add_and_remove() {
+        global $DB;
+
+        // Make course and module
+        $courseid = $this->make_course();
+        $cmid = $this->make_course_module($courseid);
+        $sectionid = $this->make_section($courseid, array($cmid));
+
+        // Check it has no conditions
+        $test1 = new condition_info_section((object)array('id'=>$sectionid),
+                CONDITION_MISSING_EVERYTHING);
+        $section = $test1->get_full_section();
+        $this->assertEquals(array(), $section->conditionscompletion);
+        $this->assertEquals(array(), $section->conditionsgrade);
+
+        // Add conditions of each type
+        $test1->add_completion_condition(13, 3);
+        $this->assertEquals(array(13 => 3), $section->conditionscompletion);
+        $test1->add_grade_condition(666, 0.4, null, true);
+        $this->assertEquals(array(666 => (object)array('min' => 0.4, 'max' => null, 'name' => '!missing')),
+                $section->conditionsgrade);
+
+        // Check they were really added in db
+        $test2 = new condition_info_section((object)array('id' => $sectionid),
+                CONDITION_MISSING_EVERYTHING);
+        $section = $test2->get_full_section();
+        $this->assertEquals(array(13 => 3), $section->conditionscompletion);
+        $this->assertEquals(array(666 => (object)array('min' => 0.4, 'max' => null, 'name' => '!missing')),
+                $section->conditionsgrade);
+
+        // Wipe conditions
+        $test2->wipe_conditions();
+        $this->assertEquals(array(), $section->conditionscompletion);
+        $this->assertEquals(array(), $section->conditionsgrade);
+
+        // Check they were really wiped
+        $test3 = new condition_info_section((object)array('id' => $cmid),
+                CONDITION_MISSING_EVERYTHING);
+        $section = $test3->get_full_section();
+        $this->assertEquals(array(), $section->conditionscompletion);
+        $this->assertEquals(array(), $section->conditionsgrade);
+    }
+
     function test_is_available() {
         global $DB,$USER;
         $courseid=$this->make_course();
@@ -362,5 +560,178 @@ class conditionlib_testcase extends advanced_testcase {
         $this->assertEquals(get_string('requires_grade_range','condition','frog'),$text);
     }
 
+    function test_section_is_available() {
+        global $DB, $USER;
+        $courseid = $this->make_course();
+
+        // Enrol user (needed for groups)
+        $enrolplugin = enrol_get_plugin('manual');
+        $course = $DB->get_record('course', array('id' => $courseid));
+        $enrolplugin->add_instance($course);
+        $enrolinstances = enrol_get_instances($courseid, false);
+        foreach ($enrolinstances as $enrolinstance) {
+            if ($enrolinstance->enrol === 'manual') {
+                break;
+            }
+        }
+        $enrolplugin->enrol_user($enrolinstance, $USER->id);
+
+        // Module for conditions later
+        $cmid = $this->make_course_module($courseid);
+
+        // No conditions
+        $sectionid = $this->make_section($courseid, array($cmid), 1);
+        $ci = new condition_info_section((object)array('id' => $sectionid),
+                CONDITION_MISSING_EVERYTHING);
+        $this->assertTrue($ci->is_available($text, false, 0));
+        $this->assertEquals('', $text);
+
+        // Time (from)
+        $time = time() + 100;
+        $sectionid = $this->make_section($courseid, array(), 2, array('availablefrom' => $time));
+        $ci = new condition_info_section((object)array('id' => $sectionid),
+                CONDITION_MISSING_EVERYTHING);
+        $this->assertFalse($ci->is_available($text));
+        $timetext = userdate($time, get_string('strftimedate', 'langconfig'));
+        $this->assertRegExp('~' . preg_quote($timetext) . '~', $text);
+
+        $time=time()-100;
+        $sectionid = $this->make_section($courseid, array(), 3, array('availablefrom' => $time));
+        $ci = new condition_info_section((object)array('id' => $sectionid),
+                CONDITION_MISSING_EVERYTHING);
+        $this->assertTrue($ci->is_available($text));
+        $this->assertEquals('',$text);
+        $timetext = userdate($time, get_string('strftimedate', 'langconfig'));
+        $this->assertRegExp('~' . preg_quote($timetext) . '~', $ci->get_full_information());
+
+        // Time (until)
+        $sectionid = $this->make_section($courseid, array(), 4, array('availableuntil' => time() - 100));
+        $ci = new condition_info_section((object)array('id' => $sectionid),
+            CONDITION_MISSING_EVERYTHING);
+        $this->assertFalse($ci->is_available($text));
+        $this->assertEquals('',$text);
+
+        // Completion: first set up cm
+        $sectionid = $this->make_section($courseid, array(), 5);
+        $cm = $DB->get_record('course_modules', array('id' => $cmid));
+        $cm->completion = COMPLETION_TRACKING_MANUAL;
+        $DB->update_record('course_modules', $cm);
+
+        // Completion: Reset modinfo after changing the options
+        rebuild_course_cache($courseid);
+        $reset = 'reset';
+        get_fast_modinfo($reset);
+
+        // Completion: Add condition
+        $ci = new condition_info_section((object)array('id' => $sectionid),
+                CONDITION_MISSING_EVERYTHING);
+        $ci->add_completion_condition($cmid, COMPLETION_COMPLETE);
+        condition_info_section::wipe_session_cache();
+
+        // Completion: Check
+        $this->assertFalse($ci->is_available($text, false));
+        $this->assertEquals(get_string('requires_completion_1', 'condition', 'xxx'), $text);
+        completion_info::wipe_session_cache();
+        $completion = new completion_info($DB->get_record('course', array('id' => $courseid)));
+        $completion->update_state($cm, COMPLETION_COMPLETE);
+        completion_info::wipe_session_cache();
+        condition_info_section::wipe_session_cache();
+        $this->assertTrue($ci->is_available($text));
+        $this->assertFalse($ci->is_available($text, false, $USER->id + 1));
+
+        // Completion: Uncheck
+        completion_info::wipe_session_cache();
+        condition_info_section::wipe_session_cache();
+        $completion = new completion_info($DB->get_record('course', array('id' => $courseid)));
+        $completion->update_state($cm, COMPLETION_INCOMPLETE);
+        $this->assertFalse($ci->is_available($text));
+
+        // Completion: Incomplete condition
+        $ci->wipe_conditions();
+        $ci->add_completion_condition($cmid, COMPLETION_INCOMPLETE);
+        condition_info_section::wipe_session_cache();
+        $this->assertTrue($ci->is_available($text));
+        $this->assertTrue($ci->is_available($text, false, $USER->id + 1));
+        condition_info_section::wipe_session_cache();
+        $this->assertTrue($ci->is_available($text, true));
+
+        // Grade: Add a fake grade item
+        $gradeitemid = $DB->insert_record('grade_items', (object)array(
+            'courseid' => $courseid, 'itemname' => 'frog'));
+
+        // Grade: Add a condition on a value existing
+        $ci->wipe_conditions();
+        $ci->add_grade_condition($gradeitemid, null, null, true);
+        $this->assertFalse($ci->is_available($text));
+        $this->assertEquals(get_string('requires_grade_any', 'condition', 'frog'), $text);
+
+        // Grade: Fake it existing
+        $DB->insert_record('grade_grades', (object)array(
+            'itemid' => $gradeitemid, 'userid' => $USER->id, 'finalgrade' => 3.78));
+        condition_info_section::wipe_session_cache();
+        $this->assertTrue($ci->is_available($text));
+        condition_info_section::wipe_session_cache();
+        $this->assertTrue($ci->is_available($text, true));
+
+        // Grade: Now require that user gets more than 3.78001
+        $ci->wipe_conditions();
+        $ci->add_grade_condition($gradeitemid, 3.78001, null, true);
+        condition_info_section::wipe_session_cache();
+        $this->assertFalse($ci->is_available($text));
+        $this->assertEquals(get_string('requires_grade_min', 'condition', 'frog'), $text);
+
+        // Grade: ...just on 3.78...
+        $ci->wipe_conditions();
+        $ci->add_grade_condition($gradeitemid, 3.78, null, true);
+        condition_info_section::wipe_session_cache();
+        $this->assertTrue($ci->is_available($text));
+
+        // Grade: ...less than 3.78
+        $ci->wipe_conditions();
+        $ci->add_grade_condition($gradeitemid, null, 3.78, true);
+        condition_info_section::wipe_session_cache();
+        $this->assertFalse($ci->is_available($text));
+        $this->assertEquals(get_string('requires_grade_max', 'condition', 'frog'), $text);
+
+        // Grade: ...less than 3.78001
+        $ci->wipe_conditions();
+        $ci->add_grade_condition($gradeitemid, null, 3.78001, true);
+        condition_info_section::wipe_session_cache();
+        $this->assertTrue($ci->is_available($text));
+
+        // Grade: ...in a range that includes it
+        $ci->wipe_conditions();
+        $ci->add_grade_condition($gradeitemid, 3, 4, true);
+        condition_info_section::wipe_session_cache();
+        $this->assertTrue($ci->is_available($text));
+
+        // Grade: ...in a range that doesn't include it
+        $ci->wipe_conditions();
+        $ci->add_grade_condition($gradeitemid,4,5,true);
+        condition_info_section::wipe_session_cache();
+        $this->assertFalse($ci->is_available($text));
+        $this->assertEquals(get_string('requires_grade_range','condition','frog'),$text);
+
+        // Grouping: Not member
+        $groupingid = $this->make_grouping($courseid, 'Grouping');
+        $groupid = $this->make_group($courseid, 'Group', $groupingid);
+        $sectionid = $this->make_section($courseid, array(), 6, array('groupingid' => $groupingid));
+        $ci = new condition_info_section((object)array('id' => $sectionid),
+                CONDITION_MISSING_EVERYTHING);
+        $this->assertFalse($ci->is_available($text));
+        $this->assertEquals(trim(get_string('groupingnoaccess', 'condition')), $text);
+
+        // Grouping: Member
+        $this->assertTrue(groups_add_member($groupid, $USER->id));
+
+        condition_info_section::init_global_cache();
+        $this->assertTrue($ci->is_available($text));
+        $this->assertEquals('', $text);
+        $this->assertTrue($ci->is_available($text, true));
+
+        // Grouping: Somebody else
+        $this->assertFalse($ci->is_available($text, false, $USER->id + 1));
+        $this->assertFalse($ci->is_available($text, true, $USER->id + 1));
+    }
 }
 
index 4168b33..1389596 100644 (file)
@@ -30,7 +30,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 
-$version  = 2012051100.00;              // YYYYMMDD      = weekly release date of this DEV branch
+$version  = 2012051100.01;              // YYYYMMDD      = weekly release date of this DEV branch
                                         //         RR    = release increments - 00 in DEV branches
                                         //           .XX = incremental changes