diff --git a/lib/modinfolib.php b/lib/modinfolib.php index 961ada3e9a3..e22610e6dde 100644 --- a/lib/modinfolib.php +++ b/lib/modinfolib.php @@ -350,15 +350,8 @@ class course_modinfo extends stdClass { // 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); + $this->sectioninfo[$number] = new section_info($data, $number, null, null, + $this, null); } // We need at least 'dynamic' data from each course-module (this is basically the remaining @@ -1175,6 +1168,7 @@ class cm_info extends stdClass { $userid = $this->modinfo->get_user_id(); if (!empty($CFG->enableavailability)) { + require_once($CFG->libdir. '/conditionlib.php'); // Get availability information $ci = new condition_info($this); // Note that the modinfo currently available only includes minimal details (basic data) @@ -1290,6 +1284,8 @@ class cm_info extends stdClass { return false; } + require_once($CFG->libdir. '/conditionlib.php'); + // If module will always be visible anyway (but greyed out), don't bother checking anything else if ($this->showavailability == CONDITION_STUDENTVIEW_SHOW) { return false; @@ -1590,6 +1586,40 @@ class cached_cm_info { /** * Data about a single section on a course. This contains the fields from the * course_sections table, plus additional data when required. + * + * @property-read int $id Section ID - from course_sections table + * @property-read int $course Course ID - from course_sections table + * @property-read int $section Section number - from course_sections table + * @property-read string $name Section name if specified - from course_sections table + * @property-read int $visible Section visibility (1 = visible) - from course_sections table + * @property-read string $summary Section summary text if specified - from course_sections table + * @property-read int $summaryformat Section summary text format (FORMAT_xx constant) - from course_sections table + * @property-read int $showavailability 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 + * @property-read int $availablefrom 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 + * @property-read int $availableuntil 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 + * @property-read int $groupingid If section is restricted to users of a particular grouping, this is its id (0 if not set) - + * from course_sections table + * @property-read array $conditionscompletion 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 + * @property-read array $conditionsgrade 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 + * @property-read array $conditionsfield Availability conditions for this section based on user fields + * @property-read bool $available True if this section is available to the given user i.e. if all availability conditions + * are met - obtained dynamically + * @property-read string $availableinfo If section is not available to some users, 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 + * @property-read bool $uservisible True if this section is available to the given 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) - obtained dynamically + * @property-read string $sequence Comma-separated list of all modules in the section. Note, this field may not exactly + * match course_sections.sequence if later has references to non-existing modules or not modules of not available module types. */ class section_info implements IteratorAggregate { /** @@ -1598,12 +1628,6 @@ class section_info implements IteratorAggregate { */ private $_id; - /** - * Course ID - from course_sections table - * @var int - */ - private $_course; - /** * Section number - from course_sections table * @var int @@ -1687,15 +1711,16 @@ class section_info implements IteratorAggregate { /** * True if this section is available to students i.e. if all availability conditions - * are met - obtained dynamically - * @var bool + * are met - obtained dynamically on request, see function {@link section_info::get_available()} + * @var bool|null */ private $_available; /** - * If section is not available to students, this string gives information about + * If section is not available to some users, 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 + * January 2010') for display on main page - obtained dynamically on request, see + * function {@link section_info::get_availableinfo()} * @var string */ private $_availableinfo; @@ -1703,8 +1728,9 @@ class section_info implements IteratorAggregate { /** * 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 + * visible or not available, so this would be true in that case) - obtained dynamically + * on request, see function {@link section_info::get_uservisible()} + * @var bool|null */ private $_uservisible; @@ -1732,16 +1758,30 @@ class section_info implements IteratorAggregate { */ private $cachedformatoptions = array(); + /** + * Stores the list of all possible section options defined in each used course format. + * @var array + */ + static private $sectionformatoptions = array(); + + /** + * Stores the modinfo object passed in constructor, may be used when requesting + * dynamically obtained attributes such as available, availableinfo, uservisible. + * Also used to retrun information about current course or user. + * @var course_modinfo + */ + private $modinfo; + /** * 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 int $notused1 argument not used (informaion is available in $modinfo) + * @param int $notused2 argument not used (informaion is available in $modinfo) * @param course_modinfo $modinfo Owner (needed for checking availability) - * @param int $userid User ID + * @param int $notused3 argument not used (informaion is available in $modinfo) */ - public function __construct($data, $number, $courseid, $sequence, $modinfo, $userid) { + public function __construct($data, $number, $notused1, $notused2, $modinfo, $notused3) { global $CFG; require_once($CFG->dirroot.'/course/lib.php'); @@ -1762,9 +1802,20 @@ class section_info implements IteratorAggregate { } } - // cached course format data - $formatoptionsdef = course_get_format($courseid)->section_format_options(); - foreach ($formatoptionsdef as $field => $option) { + // Other data from constructor arguments. + $this->_section = $number; + $this->modinfo = $modinfo; + + // Cached course format data. + $course = $modinfo->get_course(); + if (!isset($course->format) || !isset(self::$sectionformatoptions[$course->format])) { + $courseformat = course_get_format(isset($course->format) ? $course : $course->id); + if (!isset($course->format)) { + $course->format = $courseformat->get_format(); + } + self::$sectionformatoptions[$course->format] = $courseformat->section_format_options(); + } + foreach (self::$sectionformatoptions[$course->format] as $field => $option) { if (!empty($option['cache'])) { if (isset($data->{$field})) { $this->cachedformatoptions[$field] = $data->{$field}; @@ -1773,37 +1824,6 @@ class section_info implements IteratorAggregate { } } } - - // Other data from other places - $this->_course = $courseid; - $this->_section = $number; - $this->_sequence = $sequence; - - // Availability data - if (!empty($CFG->enableavailability)) { - require_once($CFG->libdir. '/conditionlib.php'); - // 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); } /** @@ -1813,11 +1833,9 @@ class section_info implements IteratorAggregate { * @return bool */ public function __isset($name) { - if (property_exists($this, '_'.$name)) { - return isset($this->{'_'.$name}); - } - $defaultformatoptions = course_get_format($this->_course)->section_format_options(); - if (array_key_exists($name, $defaultformatoptions)) { + if (method_exists($this, 'get_'.$name) || + property_exists($this, '_'.$name) || + array_key_exists($name, self::$sectionformatoptions[$this->modinfo->get_course()->format])) { $value = $this->__get($name); return isset($value); } @@ -1831,11 +1849,9 @@ class section_info implements IteratorAggregate { * @return bool */ public function __empty($name) { - if (property_exists($this, '_'.$name)) { - return empty($this->{'_'.$name}); - } - $defaultformatoptions = course_get_format($this->_course)->section_format_options(); - if (array_key_exists($name, $defaultformatoptions)) { + if (method_exists($this, 'get_'.$name) || + property_exists($this, '_'.$name) || + in_array($name, self::$sectionformatoptions[$this->modinfo->get_course()->format])) { $value = $this->__get($name); return empty($value); } @@ -1850,22 +1866,83 @@ class section_info implements IteratorAggregate { * @return bool */ public function __get($name) { + if (method_exists($this, 'get_'.$name)) { + return $this->{'get_'.$name}(); + } if (property_exists($this, '_'.$name)) { return $this->{'_'.$name}; } if (array_key_exists($name, $this->cachedformatoptions)) { return $this->cachedformatoptions[$name]; } - $defaultformatoptions = course_get_format($this->_course)->section_format_options(); // precheck if the option is defined in format to avoid unnecessary DB queries in get_format_options() - if (array_key_exists($name, $defaultformatoptions)) { - $formatoptions = course_get_format($this->_course)->get_format_options($this); + if (in_array($name, self::$sectionformatoptions[$this->modinfo->get_course()->format])) { + $formatoptions = course_get_format($this->modinfo->get_course())->get_format_options($this); return $formatoptions[$name]; } debugging('Invalid section_info property accessed! '.$name); return null; } + /** + * Finds whether this section is available at the moment for the current user. + * + * The value can be accessed publicly as $sectioninfo->available + * + * @return bool + */ + private function get_available() { + global $CFG; + if ($this->_available !== null) { + // Has already been calculated. + return $this->_available; + } + if (!empty($CFG->enableavailability)) { + require_once($CFG->libdir. '/conditionlib.php'); + // Get availability information + $ci = new condition_info_section($this); + $this->_available = $ci->is_available($this->_availableinfo, true, + $this->modinfo->get_user_id(), $this->modinfo); + if ($this->_availableinfo === '' && $this->_groupingid) { + // Still may have some extra text in availableinfo because of groupping. + // Set as undefined so the next call to get_availabeinfo() calculates it. + $this->_availableinfo = null; + } + } else { + $this->_available = true; + $this->_availableinfo = ''; + } + return $this->_available; + } + + /** + * Returns the availability text shown next to the section on course page. + * + * @return string + */ + private function get_availableinfo() { + // Make sure $this->_available has been calculated, it may also fill the _availableinfo property. + $this->get_available(); + if ($this->_availableinfo !== null) { + // It has been already calculated. + return $this->_availableinfo; + } + $this->_availableinfo = ''; + // 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($this->get_course()); + $userid = $this->modinfo->get_user_id(); + if ($this->_groupingid && has_capability('moodle/course:managegroups', $context, $userid)) { + $groupings = groups_get_all_groupings($this->get_course()); + $this->_availableinfo = html_writer::tag('span', '(' . format_string( + $groupings[$this->_groupingid]->name, true, array('context' => $context)) . + ')', array('class' => 'groupinglabel')); + } + return $this->_availableinfo; + } + /** * Implementation of IteratorAggregate::getIterator(), allows to cycle through properties * and use {@link convert_to_array()} @@ -1876,27 +1953,61 @@ class section_info implements IteratorAggregate { $ret = array(); foreach (get_object_vars($this) as $key => $value) { if (substr($key, 0, 1) == '_') { - $ret[substr($key, 1)] = $this->$key; + if (method_exists($this, 'get'.$key)) { + $ret[substr($key, 1)] = $this->{'get'.$key}(); + } else { + $ret[substr($key, 1)] = $this->$key; + } } } - $ret = array_merge($ret, course_get_format($this->_course)->get_format_options($this)); + $ret['sequence'] = $this->get_sequence(); + $ret['course'] = $this->get_course(); + $ret = array_merge($ret, course_get_format($this->modinfo->get_course())->get_format_options($this->_section)); return new ArrayIterator($ret); } /** * 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 + * + * @return bool */ - 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; + private function get_uservisible() { + if ($this->_uservisible !== null) { + // Has already been calculated. + return $this->_uservisible; } + $this->_uservisible = true; + if (!$this->_visible || !$this->get_available()) { + $coursecontext = context_course::instance($this->get_course()); + $userid = $this->modinfo->get_user_id(); + if (!has_capability('moodle/course:viewhiddensections', $coursecontext, $userid)) { + $this->_uservisible = false; + } + } + return $this->_uservisible; + } + + /** + * Restores the course_sections.sequence value + * + * @return string + */ + private function get_sequence() { + if (!empty($this->modinfo->sections[$this->_section])) { + return implode(',', $this->modinfo->sections[$this->_section]); + } else { + return ''; + } + } + + /** + * Returns course ID - from course_sections table + * + * @return int + */ + private function get_course() { + return $this->modinfo->get_course_id(); } /** diff --git a/lib/tests/modinfolib_test.php b/lib/tests/modinfolib_test.php index bda5899dbac..8bcc5ee0f24 100644 --- a/lib/tests/modinfolib_test.php +++ b/lib/tests/modinfolib_test.php @@ -35,6 +35,88 @@ require_once($CFG->libdir . '/conditionlib.php'); * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_modinfolib_testcase extends advanced_testcase { + public function test_section_info_properties() { + global $DB, $CFG; + + $this->resetAfterTest(); + $oldcfgenableavailability = $CFG->enableavailability; + $oldcfgenablecompletion = $CFG->enablecompletion; + set_config('enableavailability', 1); + set_config('enablecompletion', 1); + $this->setAdminUser(); + + // Generate the course and pre-requisite module. + $course = $this->getDataGenerator()->create_course( + array('format' => 'topics', + 'numsections' => 3, + 'enablecompletion' => 1, + 'groupmode' => SEPARATEGROUPS, + 'forcegroupmode' => 0), + array('createsections' => true)); + $coursecontext = context_course::instance($course->id); + $prereqforum = $this->getDataGenerator()->create_module('forum', + array('course' => $course->id), + array('completion' => 1)); + + // Generate the module and add availability conditions. + $conditionscompletion = array($prereqforum->cmid => COMPLETION_COMPLETE); + $conditionsgrade = array(666 => (object)array('min' => 0.4, 'max' => null, 'name' => '!missing')); + $conditionsfield = array('email' => (object)array( + 'fieldname' => 'email', + 'operator' => 'contains', + 'value' => 'test' + )); + $sectiondb = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 2)); + $ci = new condition_info_section((object)array('id' => $sectiondb->id), CONDITION_MISSING_EVERYTHING); + foreach ($conditionscompletion as $cmid => $requiredcompletion) { + $ci->add_completion_condition($cmid, $requiredcompletion); + } + foreach ($conditionsgrade as $gradeid => $conditiongrade) { + $ci->add_grade_condition($gradeid, $conditiongrade->min, $conditiongrade->max, true); + } + foreach ($conditionsfield as $conditionfield) { + $ci->add_user_field_condition($conditionfield->fieldname, $conditionfield->operator, $conditionfield->value); + } + + // Create and enrol a student. + $studentrole = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST); + $student = $this->getDataGenerator()->create_user(); + role_assign($studentrole->id, $student->id, $coursecontext); + $enrolplugin = enrol_get_plugin('manual'); + $enrolinstance = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'manual')); + $enrolplugin->enrol_user($enrolinstance, $student->id); + $this->setUser($student); + + // Get modinfo. + $modinfo = get_fast_modinfo($course->id); + $si = $modinfo->get_section_info(2); + + $this->assertEquals($sectiondb->id, $si->id); + $this->assertEquals($sectiondb->course, $si->course); + $this->assertEquals($sectiondb->section, $si->section); + $this->assertEquals($sectiondb->name, $si->name); + $this->assertEquals($sectiondb->visible, $si->visible); + $this->assertEquals($sectiondb->summary, $si->summary); + $this->assertEquals($sectiondb->summaryformat, $si->summaryformat); + $this->assertEquals($sectiondb->showavailability, $si->showavailability); + $this->assertEquals($sectiondb->availablefrom, $si->availablefrom); + $this->assertEquals($sectiondb->availableuntil, $si->availableuntil); + $this->assertEquals($sectiondb->groupingid, $si->groupingid); + $this->assertEquals($sectiondb->sequence, $si->sequence); // Since this section does not contain invalid modules. + $this->assertEquals($conditionscompletion, $si->conditionscompletion); + $this->assertEquals($conditionsgrade, $si->conditionsgrade); + $this->assertEquals($conditionsfield, $si->conditionsfield); + + // Dynamic fields, just test that they can be retrieved (must be carefully tested in each activity type). + $this->assertEquals(0, $si->available); + $this->assertNotEmpty($si->availableinfo); // Lists all unmet availability conditions. + $this->assertEquals(0, $si->uservisible); + + // Restore settings. + set_config('enableavailability', $oldcfgenableavailability); + set_config('enablecompletion', $oldcfgenablecompletion); + } + /** * Test is_user_access_restricted_by_group()