diff --git a/course/format/lib.php b/course/format/lib.php index b51d77dbb3c..33b9f553f18 100644 --- a/course/format/lib.php +++ b/course/format/lib.php @@ -109,6 +109,11 @@ abstract class format_base { return self::$classesforformat[$format]; } + if (PHPUNIT_TEST && class_exists('format_' . $format)) { + // Allow unittests to use non-existing course formats. + return $format; + } + // Else return default format $defaultformat = get_config('moodlecourse', 'format'); if (!in_array($defaultformat, $plugins)) { @@ -136,7 +141,7 @@ abstract class format_base { if (!isset($classnames[$format])) { $plugins = core_component::get_plugin_list('format'); $usedformat = self::get_format_or_default($format); - if (file_exists($plugins[$usedformat].'/lib.php')) { + if (isset($plugins[$usedformat]) && file_exists($plugins[$usedformat].'/lib.php')) { require_once($plugins[$usedformat].'/lib.php'); } $classnames[$format] = 'format_'. $usedformat; @@ -906,6 +911,29 @@ abstract class format_base { } return ($sectionnum && ($course = $this->get_course()) && $course->marker == $sectionnum); } + + /** + * Allows to specify for modinfo that section is not available even when it is visible and conditionally available. + * + * Note: affected user can be retrieved as: $section->modinfo->userid + * + * Course format plugins can override the method to change the properties $available and $availableinfo that were + * calculated by conditional availability. + * To make section unavailable set: + * $available = false; + * To make unavailable section completely hidden set: + * $availableinfo = ''; + * To make unavailable section visible with availability message set: + * $availableinfo = get_string('sectionhidden', 'format_xxx'); + * + * @param section_info $section + * @param bool $available the 'available' propery of the section_info as it was evaluated by conditional availability. + * Can be changed by the method but 'false' can not be overridden by 'true'. + * @param string $availableinfo the 'availableinfo' propery of the section_info as it was evaluated by conditional availability. + * Can be changed by the method + */ + public function section_get_available_hook(section_info $section, &$available, &$availableinfo) { + } } /** diff --git a/course/format/upgrade.txt b/course/format/upgrade.txt index 1eed444c7f2..79168540e32 100644 --- a/course/format/upgrade.txt +++ b/course/format/upgrade.txt @@ -7,6 +7,7 @@ Overview of this plugin type at http://docs.moodle.org/dev/Course_formats to determine the section selector, rather than a hard-coded `li.section`. * Activity duplication in /course/modduplicate.php is deprecated and is now done in /course/mod.php. Deprecated calls will be honored by redirecting to /course/mod.php for 3rd party plugin support. +* New method format_base::section_get_available_hook() allows plugins to override section availability. === 2.7 === * The ->testedbrowsers array no longer needs to be defined in supports_ajax(). diff --git a/course/tests/courseformat_test.php b/course/tests/courseformat_test.php new file mode 100644 index 00000000000..c7529cb9c24 --- /dev/null +++ b/course/tests/courseformat_test.php @@ -0,0 +1,104 @@ +. + +/** + * Course related unit tests + * + * @package core_course + * @copyright 2014 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/course/lib.php'); +require_once($CFG->dirroot . '/course/tests/fixtures/format_theunittest.php'); + +class core_course_courseformat_testcase extends advanced_testcase { + public function test_available_hook() { + global $DB; + $this->resetAfterTest(); + + // Generate a course with two sections (0 and 1) and two modules. Course format is set to 'theunittest'. + $generator = $this->getDataGenerator(); + $course1 = $generator->create_course(array('format' => 'theunittest')); + $this->assertEquals('theunittest', $course1->format); + course_create_sections_if_missing($course1, array(0, 1)); + $assign0 = $generator->create_module('assign', array('course' => $course1, 'section' => 0)); + $assign1 = $generator->create_module('assign', array('course' => $course1, 'section' => 1)); + + // Enrol student and teacher. + $roleids = $DB->get_records_menu('role', null, '', 'shortname, id'); + $student = $generator->create_user(); + $generator->enrol_user($student->id, $course1->id, $roleids['student']); + $teacher = $generator->create_user(); + $generator->enrol_user($teacher->id, $course1->id, $roleids['editingteacher']); + + // Make sure that initially both sections and both modules are available and visible for a student. + $modinfostudent = get_fast_modinfo($course1, $student->id); + $this->assertTrue($modinfostudent->get_section_info(1)->available); + $this->assertTrue($modinfostudent->get_cm($assign0->cmid)->available); + $this->assertTrue($modinfostudent->get_cm($assign0->cmid)->uservisible); + $this->assertTrue($modinfostudent->get_cm($assign1->cmid)->available); + $this->assertTrue($modinfostudent->get_cm($assign1->cmid)->uservisible); + + // Set 'hideoddsections' for the course to 1. + // Section1 and assign1 will be unavailable, uservisible will be false for student and true for teacher. + $data = (object)array('id' => $course1->id, 'hideoddsections' => 1); + course_get_format($course1)->update_course_format_options($data); + $modinfostudent = get_fast_modinfo($course1, $student->id); + $this->assertFalse($modinfostudent->get_section_info(1)->available); + $this->assertEmpty($modinfostudent->get_section_info(1)->availableinfo); + $this->assertFalse($modinfostudent->get_section_info(1)->uservisible); + $this->assertTrue($modinfostudent->get_cm($assign0->cmid)->available); + $this->assertTrue($modinfostudent->get_cm($assign0->cmid)->uservisible); + $this->assertFalse($modinfostudent->get_cm($assign1->cmid)->available); + $this->assertFalse($modinfostudent->get_cm($assign1->cmid)->uservisible); + + $modinfoteacher = get_fast_modinfo($course1, $teacher->id); + $this->assertFalse($modinfoteacher->get_section_info(1)->available); + $this->assertEmpty($modinfoteacher->get_section_info(1)->availableinfo); + $this->assertTrue($modinfoteacher->get_section_info(1)->uservisible); + $this->assertTrue($modinfoteacher->get_cm($assign0->cmid)->available); + $this->assertTrue($modinfoteacher->get_cm($assign0->cmid)->uservisible); + $this->assertFalse($modinfoteacher->get_cm($assign1->cmid)->available); + $this->assertTrue($modinfoteacher->get_cm($assign1->cmid)->uservisible); + + // Set 'hideoddsections' for the course to 2. + // Section1 and assign1 will be unavailable, uservisible will be false for student and true for teacher. + // Property availableinfo will be not empty. + $data = (object)array('id' => $course1->id, 'hideoddsections' => 2); + course_get_format($course1)->update_course_format_options($data); + $modinfostudent = get_fast_modinfo($course1, $student->id); + $this->assertFalse($modinfostudent->get_section_info(1)->available); + $this->assertNotEmpty($modinfostudent->get_section_info(1)->availableinfo); + $this->assertFalse($modinfostudent->get_section_info(1)->uservisible); + $this->assertTrue($modinfostudent->get_cm($assign0->cmid)->available); + $this->assertTrue($modinfostudent->get_cm($assign0->cmid)->uservisible); + $this->assertFalse($modinfostudent->get_cm($assign1->cmid)->available); + $this->assertFalse($modinfostudent->get_cm($assign1->cmid)->uservisible); + + $modinfoteacher = get_fast_modinfo($course1, $teacher->id); + $this->assertFalse($modinfoteacher->get_section_info(1)->available); + $this->assertNotEmpty($modinfoteacher->get_section_info(1)->availableinfo); + $this->assertTrue($modinfoteacher->get_section_info(1)->uservisible); + $this->assertTrue($modinfoteacher->get_cm($assign0->cmid)->available); + $this->assertTrue($modinfoteacher->get_cm($assign0->cmid)->uservisible); + $this->assertFalse($modinfoteacher->get_cm($assign1->cmid)->available); + $this->assertTrue($modinfoteacher->get_cm($assign1->cmid)->uservisible); + } +} diff --git a/course/tests/fixtures/format_theunittest.php b/course/tests/fixtures/format_theunittest.php new file mode 100644 index 00000000000..b53dfd872d0 --- /dev/null +++ b/course/tests/fixtures/format_theunittest.php @@ -0,0 +1,81 @@ +. + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/course/format/lib.php'); + +/** + * Fixture for fake course format testing course format API. + * + * @package core_course + * @copyright 2014 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class format_theunittest extends format_base { + + /** + * Definitions of the additional options that format uses + * + * @param bool $foreditform + * @return array of options + */ + public function course_format_options($foreditform = false) { + static $courseformatoptions = false; + if ($courseformatoptions === false) { + $courseformatoptions = array( + 'hideoddsections' => array( + 'default' => 0, + 'type' => PARAM_INT, + ), + ); + } + if ($foreditform && !isset($courseformatoptions['hideoddsections']['label'])) { + $sectionmenu = array( + 0 => 'Never', + 1 => 'Hide without notice', + 2 => 'Hide with notice' + ); + $courseformatoptionsedit = array( + 'hideoddsections' => array( + 'label' => 'Hide odd sections', + 'element_type' => 'select', + 'element_attributes' => array($sectionmenu), + ), + ); + $courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsedit); + } + return $courseformatoptions; + } + + /** + * Allows to specify for modinfo that section is not available even when it is visible and conditionally available. + * + * @param section_info $section + * @param bool $available + * @param string $availableinfo + */ + public function section_get_available_hook(section_info $section, &$available, &$availableinfo) { + if (($section->section % 2) && ($hideoddsections = $this->get_course()->hideoddsections)) { + $available = false; + if ($hideoddsections == 2) { + $availableinfo = 'Odd sections are oddly hidden'; + } else { + $availableinfo = ''; + } + } + } +} \ No newline at end of file diff --git a/lib/modinfolib.php b/lib/modinfolib.php index 13380a5ce5e..6576123b4b1 100644 --- a/lib/modinfolib.php +++ b/lib/modinfolib.php @@ -1791,8 +1791,12 @@ class cm_info implements IteratorAggregate { // but we know that this function does not need anything more than basic data. $this->available = $ci->is_available($this->availableinfo, true, $userid, $this->modinfo); + } else { + $this->available = true; + } - // Check parent section + // Check parent section. + if ($this->available) { $parentsection = $this->modinfo->get_section_info($this->sectionnum); if (!$parentsection->available) { // Do not store info from section here, as that is already @@ -1800,11 +1804,9 @@ class cm_info implements IteratorAggregate { // the flag $this->available = false; } - } else { - $this->available = true; } - // Update visible state for current user + // Update visible state for current user. $this->update_user_visible(); // Let module make dynamic changes at this point @@ -2678,15 +2680,22 @@ class section_info implements IteratorAggregate { // Has already been calculated or does not need calculation. return $this->_available; } + $this->_available = true; + $this->_availableinfo = ''; if (!empty($CFG->enableavailability)) { require_once($CFG->libdir. '/conditionlib.php'); // Get availability information. $ci = new \core_availability\info_section($this); $this->_available = $ci->is_available($this->_availableinfo, true, $userid, $this->modinfo); - } else { - $this->_available = true; - $this->_availableinfo = ''; + } + // Execute the hook from the course format that may override the available/availableinfo properties. + $currentavailable = $this->_available; + course_get_format($this->modinfo->get_course())-> + section_get_available_hook($this, $this->_available, $this->_availableinfo); + if (!$currentavailable && $this->_available) { + debugging('section_get_available_hook() can not make unavailable section available', DEBUG_DEVELOPER); + $this->_available = $currentavailable; } return $this->_available; }