Files
moodle/public/course/classes/section_info.php
T

639 lines
22 KiB
PHP

<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_course;
use ArrayIterator;
use IteratorAggregate;
use Traversable;
use core\context\course as context_course;
use core_courseformat\sectiondelegate;
use core_courseformat\sectiondelegatemodule;
// phpcs:disable PSR2.Classes.PropertyDeclaration.Underscore
/**
* Data about a single section on a course.
*
* This contains the fields from the.course_sections table, plus additional data when required.
*
* @package core_course
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @copyright Sam Marshall
* @property-read int $id Section ID - from course_sections table
* @property-read int $course Course ID - from course_sections table
* @property-read int $sectionnum 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 string $availability Availability information as JSON string - from course_sections table
* @property-read string|null $component Optional section delegate component - from course_sections table
* @property-read int|null $itemid Optional section delegate item id - 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.
* @property-read course_modinfo $modinfo
*/
class section_info implements IteratorAggregate {
/**
* Section ID - from course_sections table
* @var int
*/
private $_id;
/**
* Section number - from course_sections table
* @var int
*/
private $_sectionnum;
/**
* Section name if specified - from course_sections table
* @var string
*/
private $_name;
/**
* Section visibility (1 = visible) - from course_sections table
* @var int
*/
private $_visible;
/**
* Section summary text if specified - from course_sections table
* @var string
*/
private $_summary;
/**
* Section summary text format (FORMAT_xx constant) - from course_sections table
* @var int
*/
private $_summaryformat;
/**
* Availability information as JSON string - from course_sections table
* @var string
*/
private $_availability;
/**
* @var string|null the delegated component if any.
*/
private ?string $_component = null;
/**
* @var int|null the delegated instance item id if any.
*/
private ?int $_itemid = null;
/**
* @var sectiondelegate|null Section delegate instance if any.
*/
private ?sectiondelegate $_delegateinstance = null;
/** @var cm_info[]|null Section cm_info activities, null when it is not loaded yet. */
private array|null $_sequencecminfos = null;
/**
* @var bool|null $_isorphan True if the section is orphan for some reason.
*/
private $_isorphan = null;
/**
* 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
*/
private $_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
*/
private $_conditionsgrade;
/**
* Availability conditions for this section based on user fields
* @var array
*/
private $_conditionsfield;
/**
* True if this section is available to students i.e. if all availability conditions
* are met - obtained dynamically on request, see function {@see section_info::get_available()}
* @var bool|null
*/
private $_available;
/**
* 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 on request, see
* function {@see section_info::get_availableinfo()}
* @var string
*/
private $_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) - obtained dynamically
* on request, see function {@see section_info::get_uservisible()}
* @var bool|null
*/
private $_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 = [
'name' => null,
'summary' => '',
'summaryformat' => '1', // FORMAT_HTML, but must be a string.
'visible' => '1',
'availability' => null,
'component' => null,
'itemid' => null,
];
/**
* Stores format options that have been cached when building 'coursecache'
* When the format option is requested we look first if it has been cached
* @var array
*/
private $cachedformatoptions = [];
/**
* Stores the list of all possible section options defined in each used course format.
* @var array
*/
private static $sectionformatoptions = [];
/**
* 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;
/**
* True if has activities, otherwise false.
* @var bool
*/
public $hasactivites;
/**
* List of class read-only properties' getter methods.
* Used by magic functions __get(), __isset().
* @var array
*/
private static $standardproperties = [
'section' => 'get_section_number',
];
/**
* 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 mixed $notused1 argument not used (informaion is available in $modinfo)
* @param mixed $notused2 argument not used (informaion is available in $modinfo)
* @param modinfo $modinfo Owner (needed for checking availability)
* @param mixed $notused3 argument not used (informaion is available in $modinfo)
*/
public function __construct($data, $number, $notused1, $notused2, $modinfo, $notused3) {
global $CFG;
require_once($CFG->dirroot . '/course/lib.php');
// Data that is always present.
$this->_id = $data->id;
$defaults = self::$sectioncachedefaults + [
'conditionscompletion' => [],
'conditionsgrade' => [],
'conditionsfield' => [],
];
// Data that may use default values to save cache size.
foreach ($defaults as $field => $value) {
if (isset($data->{$field})) {
$this->{'_' . $field} = $data->{$field};
} else {
$this->{'_' . $field} = $value;
}
}
// Other data from constructor arguments.
$this->_sectionnum = $number;
$this->modinfo = $modinfo;
// Cached course format data.
$course = $modinfo->get_course();
if (!isset(self::$sectionformatoptions[$course->format])) {
// Store list of section format options defined in each used course format.
// They do not depend on particular course but only on its format.
self::$sectionformatoptions[$course->format] =
course_get_format($course)->section_format_options();
}
foreach (self::$sectionformatoptions[$course->format] as $field => $option) {
if (!empty($option['cache'])) {
if (isset($data->{$field})) {
$this->cachedformatoptions[$field] = $data->{$field};
} else if (array_key_exists('cachedefault', $option)) {
$this->cachedformatoptions[$field] = $option['cachedefault'];
}
}
}
}
/**
* Magic method to check if the property is set
*
* @param string $name name of the property
* @return bool
*/
public function __isset($name) {
if (isset(self::$standardproperties[$name])) {
$value = $this->__get($name);
return isset($value);
}
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);
}
return false;
}
/**
* Magic method to retrieve the property, this is either basic section property
* or availability information or additional properties added by course format
*
* @param string $name name of the property
* @return mixed
*/
public function __get($name) {
if (isset(self::$standardproperties[$name])) {
if ($method = self::$standardproperties[$name]) {
return $this->$method();
}
}
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];
}
// Precheck if the option is defined in format to avoid unnecessary DB queries in get_format_options().
if (array_key_exists($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, but can be called directly if there
* is a case when it might be called recursively (you can't call property values recursively).
*
* @return bool
*/
public function get_available() {
global $CFG;
$userid = $this->modinfo->get_user_id();
if ($this->_available !== null || $userid == -1) {
// Has already been calculated or does not need calculation.
return $this->_available;
}
$this->_available = true;
$this->_availableinfo = '';
if (!empty($CFG->enableavailability)) {
// Get availability information.
$ci = new \core_availability\info_section($this);
$this->_available = $ci->is_available(
$this->_availableinfo,
true,
$userid,
$this->modinfo,
);
}
if ($this->_available) {
$this->_available = $this->check_delegated_available();
}
// 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;
}
/**
* Check if the delegated component is available.
*
* @return bool
*/
private function check_delegated_available(): bool {
/** @var sectiondelegatemodule $sectiondelegate */
$sectiondelegate = $this->get_component_instance();
if (!$sectiondelegate) {
return true;
}
if ($sectiondelegate instanceof sectiondelegatemodule) {
$parentcm = $sectiondelegate->get_cm();
if (!$parentcm->available) {
return false;
}
return $parentcm->get_section_info()->available;
}
return true;
}
/**
* Returns the availability text shown next to the section on course page.
*
* @return string
*/
private function get_availableinfo() {
// Calling get_available() will also fill the availableinfo property
// (or leave it null if there is no userid).
$this->get_available();
return $this->_availableinfo;
}
#[\Override]
public function getIterator(): Traversable {
$ret = [];
foreach (get_object_vars($this) as $key => $value) {
if (substr($key, 0, 1) == '_') {
if (method_exists($this, 'get' . $key)) {
$ret[substr($key, 1)] = $this->{'get' . $key}();
} else {
$ret[substr($key, 1)] = $this->$key;
}
}
}
$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));
return new ArrayIterator($ret);
}
/**
* Works out whether activity is visible *for current user* - if this is false, they
* aren't allowed to access it.
*
* @return bool
*/
private function get_uservisible() {
$userid = $this->modinfo->get_user_id();
if ($this->_uservisible !== null || $userid == -1) {
// Has already been calculated or does not need calculation.
return $this->_uservisible;
}
if (!$this->check_delegated_uservisible()) {
$this->_uservisible = false;
return $this->_uservisible;
}
$this->_uservisible = true;
if ($this->is_orphan() || !$this->_visible || !$this->get_available()) {
$coursecontext = context_course::instance($this->get_course());
if (
($this->_isorphan || !$this->_visible)
&& !has_capability('moodle/course:viewhiddensections', $coursecontext, $userid)
) {
$this->_uservisible = false;
}
if (
$this->_uservisible
&& !$this->get_available()
&& !has_capability('moodle/course:ignoreavailabilityrestrictions', $coursecontext, $userid)
) {
$this->_uservisible = false;
}
}
return $this->_uservisible;
}
/**
* Check if the delegated component is user visible.
*
* @return bool
*/
private function check_delegated_uservisible(): bool {
/** @var sectiondelegatemodule $sectiondelegate */
$sectiondelegate = $this->get_component_instance();
if (!$sectiondelegate) {
return true;
}
if ($sectiondelegate instanceof sectiondelegatemodule) {
$parentcm = $sectiondelegate->get_cm();
if (!$parentcm->uservisible) {
return false;
}
$result = $parentcm->get_section_info()->uservisible;
return $result;
}
return true;
}
/**
* Restores the course_sections.sequence value
*
* @return string
*/
private function get_sequence() {
if (!empty($this->modinfo->sections[$this->_sectionnum])) {
return implode(',', $this->modinfo->sections[$this->_sectionnum]);
} else {
return '';
}
}
/**
* Returns the course modules in this section.
*
* @return cm_info[]
*/
public function get_sequence_cm_infos(): array {
if ($this->_sequencecminfos !== null) {
return $this->_sequencecminfos;
}
$sequence = $this->modinfo->sections[$this->_sectionnum] ?? [];
$cms = $this->modinfo->get_cms();
$result = [];
foreach ($sequence as $cmid) {
if (isset($cms[$cmid])) {
$result[] = $cms[$cmid];
}
}
$this->_sequencecminfos = $result;
return $result;
}
/**
* Returns course ID - from course_sections table
*
* @return int
*/
private function get_course() {
return $this->modinfo->get_course_id();
}
/**
* Modinfo object
*
* @return course_modinfo
*/
private function get_modinfo() {
return $this->modinfo;
}
/**
* Returns section number.
*
* This method is called by the property ->section.
*
* @return int
*/
private function get_section_number(): int {
return $this->sectionnum;
}
/**
* Get the delegate component instance.
*
* @return sectiondelegate|null
*/
public function get_component_instance(): ?sectiondelegate {
if (!$this->is_delegated()) {
return null;
}
if ($this->_delegateinstance !== null) {
return $this->_delegateinstance;
}
$this->_delegateinstance = sectiondelegate::instance($this);
return $this->_delegateinstance;
}
/**
* Returns true if this section is a delegate to a component.
* @return bool
*/
public function is_delegated(): bool {
return !empty($this->_component);
}
/**
* Returns true if this section is orphan.
*
* @return bool
*/
public function is_orphan(): bool {
if ($this->_isorphan !== null) {
return $this->_isorphan;
}
$courseformat = course_get_format($this->modinfo->get_course());
// There are some cases where a restored course using third-party formats can
// have orphaned sections due to a fixed section number.
if ($this->_sectionnum > $courseformat->get_last_section_number()) {
$this->_isorphan = true;
return $this->_isorphan;
}
// Some delegated sections can belong to a plugin that is disabled or not present.
if ($this->is_delegated() && !$this->get_component_instance()) {
$this->_isorphan = true;
return $this->_isorphan;
}
$this->_isorphan = false;
return $this->_isorphan;
}
/**
* 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);
// Sequence stored implicity in modinfo $sections array.
unset($section->sequence);
// 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});
}
}
}
}
// Alias this class to the old name.
// This file will be autoloaded by the legacyclasses autoload system.
// In future all uses of this class will be corrected and the legacy references will be removed.
class_alias(section_info::class, \section_info::class);