first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,77 @@
<?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/>.
/**
* Used while evaluating conditions in bulk.
*
* This object caches get_users_by_capability results in case they are needed
* by multiple conditions.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_availability;
defined('MOODLE_INTERNAL') || die();
/**
* Used while evaluating conditions in bulk.
*
* This object caches get_users_by_capability results in case they are needed
* by multiple conditions.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class capability_checker {
/** @var \context Course or module context */
protected $context;
/** @var array Associative array of capability => result */
protected $cache = array();
/**
* Constructs for given context.
*
* @param \context $context Context
*/
public function __construct(\context $context) {
$this->context = $context;
}
/**
* Gets users on course who have the specified capability. Returns an array
* of user objects which only contain the 'id' field. If the same capability
* has already been checked (e.g. by another condition) then a cached
* result will be used.
*
* More fields are not necessary because this code is only used to filter
* users from an existing list.
*
* @param string $capability Required capability
* @return array Associative array of user id => objects containing only id
*/
public function get_users_by_capability($capability) {
if (!array_key_exists($capability, $this->cache)) {
$this->cache[$capability] = get_users_by_capability(
$this->context, $capability, 'u.id');
}
return $this->cache[$capability];
}
}
+260
View File
@@ -0,0 +1,260 @@
<?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/>.
/**
* Base class for a single availability condition.
*
* All condition types must extend this class.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_availability;
defined('MOODLE_INTERNAL') || die();
/**
* Base class for a single availability condition.
*
* All condition types must extend this class.
*
* The structure of a condition in JSON input data is:
*
* { type:'date', ... }
*
* where 'date' is the name of the plugin (availability_date in this case) and
* ... is arbitrary extra data to be used by the plugin.
*
* Conditions require a constructor with one parameter: $structure. This will
* contain all the JSON data for the condition. If the structure of the data
* is incorrect (e.g. missing fields) then the constructor may throw a
* coding_exception. However, the constructor should cope with all data that
* was previously valid (e.g. if the format changes, old data may still be
* present in a restore, so there should be a default value for any new fields
* and old ones should be handled correctly).
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class condition extends tree_node {
/**
* Determines whether a particular item is currently available
* according to this availability condition.
*
* If implementations require a course or modinfo, they should use
* the get methods in $info.
*
* The $not option is potentially confusing. This option always indicates
* the 'real' value of NOT. For example, a condition inside a 'NOT AND'
* group will get this called with $not = true, but if you put another
* 'NOT OR' group inside the first group, then a condition inside that will
* be called with $not = false. We need to use the real values, rather than
* the more natural use of the current value at this point inside the tree,
* so that the information displayed to users makes sense.
*
* @param bool $not Set true if we are inverting the condition
* @param info $info Item we're checking
* @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 User ID to check availability for
* @return bool True if available
*/
abstract public function is_available($not, info $info, $grabthelot, $userid);
public function check_available($not, info $info, $grabthelot, $userid) {
// Use is_available, and we always display (at this stage).
$allow = $this->is_available($not, $info, $grabthelot, $userid);
return new result($allow, $this);
}
public function is_available_for_all($not = false) {
// Default is that all conditions may make something unavailable.
return false;
}
/**
* Display a representation of this condition (used for debugging).
*
* @return string Text representation of condition
*/
public function __toString() {
return '{' . $this->get_type() . ':' . $this->get_debug_string() . '}';
}
/**
* Gets the type name (e.g. 'date' for availability_date) of plugin.
*
* @return string The type name for this plugin
*/
protected function get_type() {
return preg_replace('~^availability_(.*?)\\\\condition$~', '$1', get_class($this));
}
/**
* Returns a marker indicating that an activity name should be placed in a description.
*
* Gets placeholder text which will be decoded by info::format_info later when we can safely
* display names.
*
* @param int $cmid Course-module id
* @return string Placeholder text
* @since Moodle 4.0
*/
public static function description_cm_name(int $cmid): string {
return '<AVAILABILITY_CMNAME_' . $cmid . '/>';
}
/**
* Returns a marker indicating that formatted text should be placed in a description.
*
* Gets placeholder text which will be decoded by info::format_info later when we can safely
* call format_string.
*
* @param string $str Text to be processed with format_string
* @return string Placeholder text
* @since Moodle 4.0
*/
public static function description_format_string(string $str): string {
return '<AVAILABILITY_FORMAT_STRING>' . htmlspecialchars($str, ENT_NOQUOTES) .
'</AVAILABILITY_FORMAT_STRING>';
}
/**
* Returns a marker indicating that some of the description text should be computed at display
* time.
*
* This will result in a call to the get_description_callback_value static function within
* the condition class.
*
* Gets placeholder text which will be decoded by info::format_info later when we can safely
* call most Moodle functions.
*
* @param string[] $params Array of arbitrary parameters
* @return string Placeholder text
* @since Moodle 4.0
*/
public function description_callback(array $params): string {
$out = '<AVAILABILITY_CALLBACK type="' . $this->get_type() . '">';
$first = true;
foreach ($params as $param) {
if ($first) {
$first = false;
} else {
$out .= '<P/>';
}
$out .= htmlspecialchars($param, ENT_NOQUOTES);
}
$out .= '</AVAILABILITY_CALLBACK>';
return $out;
}
/**
* Obtains a string describing this restriction (whether or not
* it actually applies). Used to obtain information that is displayed to
* students if the activity is not available to them, and for staff to see
* what conditions are.
*
* The $full parameter can be used to distinguish between 'staff' cases
* (when displaying all information about the activity) and 'student' cases
* (when displaying only conditions they don't meet).
*
* If implementations require a course or modinfo, they should use
* the get methods in $info. They should not use any other functions that
* might rely on modinfo, such as format_string.
*
* To work around this limitation, use the functions:
*
* description_cm_name()
* description_format_string()
* description_callback()
*
* These return special markers which will be added to the string and processed
* later after modinfo is complete.
*
* @param bool $full Set true if this is the 'full information' view
* @param bool $not Set true if we are inverting the condition
* @param info $info Item we're checking
* @return string Information string (for admin) about all restrictions on
* this item
*/
abstract public function get_description($full, $not, info $info);
/**
* Obtains a string describing this restriction, used when there is only
* a single restriction to display. (I.e. this provides a 'short form'
* rather than showing in a list.)
*
* Default behaviour sticks the prefix text, normally displayed above
* the list, in front of the standard get_description call.
*
* If implementations require a course or modinfo, they should use
* the get methods in $info. They should not use any other functions that
* might rely on modinfo, such as format_string.
*
* To work around this limitation, use the functions:
*
* description_cm_name()
* description_format_string()
* description_callback()
*
* These return special markers which will be added to the string and processed
* later after modinfo is complete.
*
* @param bool $full Set true if this is the 'full information' view
* @param bool $not Set true if we are inverting the condition
* @param info $info Item we're checking
* @return string Information string (for admin) about all restrictions on
* this item
*/
public function get_standalone_description($full, $not, info $info) {
return get_string('list_root_and', 'availability') . ' ' .
$this->get_description($full, $not, $info);
}
/**
* Obtains a representation of the options of this condition as a string,
* for debugging.
*
* @return string Text representation of parameters
*/
abstract protected function get_debug_string();
public function update_dependency_id($table, $oldid, $newid) {
// By default, assumes there are no dependent ids.
return false;
}
/**
* If the plugin has been configured to rely on a particular activity's
* completion value, it should return true here. (This is necessary so that
* we know the course page needs to update when that activity becomes
* complete.)
*
* Default implementation returns false.
*
* @param \stdClass $course Moodle course object
* @param int $cmid ID of activity whose completion value is considered
* @return boolean True if the availability of something else may rely on it
*/
public static function completion_value_used($course, $cmid) {
return false;
}
}
+209
View File
@@ -0,0 +1,209 @@
<?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/>.
/**
* Class with front-end (editing form) functionality.
*
* This is a base class of a class implemented by each component, and also has
* static methods.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_availability;
defined('MOODLE_INTERNAL') || die();
/**
* Class with front-end (editing form) functionality.
*
* This is a base class of a class implemented by each component, and also has
* static methods.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class frontend {
/**
* Decides whether this plugin should be available in a given course. The
* plugin can do this depending on course or system settings.
*
* Default returns true.
*
* @param \stdClass $course Course object
* @param \cm_info $cm Course-module currently being edited (null if none)
* @param \section_info $section Section currently being edited (null if none)
*/
protected function allow_add($course, \cm_info $cm = null,
\section_info $section = null) {
return true;
}
/**
* Gets a list of string identifiers (in the plugin's language file) that
* are required in JavaScript for this plugin. The default returns nothing.
*
* You do not need to include the 'title' string (which is used by core) as
* this is automatically added.
*
* @return array Array of required string identifiers
*/
protected function get_javascript_strings() {
return array();
}
/**
* Gets additional parameters for the plugin's initInner function.
*
* Default returns no parameters.
*
* @param \stdClass $course Course object
* @param \cm_info $cm Course-module currently being edited (null if none)
* @param \section_info $section Section currently being edited (null if none)
* @return array Array of parameters for the JavaScript function
*/
protected function get_javascript_init_params($course, \cm_info $cm = null,
\section_info $section = null) {
return array();
}
/**
* Gets the Frankenstyle component name for this plugin.
*
* @return string The component name for this plugin
*/
protected function get_component() {
return preg_replace('~^(availability_.*?)\\\\frontend$~', '$1', get_class($this));
}
/**
* Includes JavaScript for the main system and all plugins.
*
* @param \stdClass $course Course object
* @param \cm_info $cm Course-module currently being edited (null if none)
* @param \section_info $section Section currently being edited (null if none)
*/
public static function include_all_javascript($course, \cm_info $cm = null,
\section_info $section = null) {
global $PAGE;
// Prepare array of required YUI modules. It is bad for performance to
// make multiple yui_module calls, so we group all the plugin modules
// into a single call (the main init function will call init for each
// plugin).
$modules = array('moodle-core_availability-form', 'base', 'node',
'panel', 'moodle-core-notification-dialogue', 'json');
// Work out JS to include for all components.
$pluginmanager = \core_plugin_manager::instance();
$enabled = $pluginmanager->get_enabled_plugins('availability');
$componentparams = new \stdClass();
foreach ($enabled as $plugin => $info) {
// Create plugin front-end object.
$class = '\availability_' . $plugin . '\frontend';
$frontend = new $class();
// Add to array of required YUI modules.
$component = $frontend->get_component();
$modules[] = 'moodle-' . $component . '-form';
// Get parameters for this plugin.
$componentparams->{$plugin} = array($component,
$frontend->allow_add($course, $cm, $section),
$frontend->get_javascript_init_params($course, $cm, $section));
// Include strings for this plugin.
$identifiers = $frontend->get_javascript_strings();
$identifiers[] = 'title';
$identifiers[] = 'description';
$PAGE->requires->strings_for_js($identifiers, $component);
}
// Include all JS (in one call). The init function runs on DOM ready.
$PAGE->requires->yui_module($modules,
'M.core_availability.form.init', array($componentparams), null, true);
// Include main strings.
$PAGE->requires->strings_for_js(array('none', 'cancel', 'delete', 'choosedots'),
'moodle');
$PAGE->requires->strings_for_js(array('addrestriction', 'invalid',
'listheader_sign_before', 'listheader_sign_pos',
'listheader_sign_neg', 'listheader_single',
'listheader_multi_after', 'listheader_multi_before',
'listheader_multi_or', 'listheader_multi_and',
'unknowncondition', 'hide_verb', 'hidden_individual',
'show_verb', 'shown_individual', 'hidden_all', 'shown_all',
'condition_group', 'condition_group_info', 'and', 'or',
'label_multi', 'label_sign', 'setheading', 'itemheading',
'missingplugin', 'disabled_verb'),
'availability');
}
/**
* For use within forms, reports any validation errors from the availability
* field.
*
* @param array $data Form data fields
* @param array $errors Error array
*/
public static function report_validation_errors(array $data, array &$errors) {
// Empty value is allowed!
if ($data['availabilityconditionsjson'] === '') {
return;
}
// Decode value.
$decoded = json_decode($data['availabilityconditionsjson']);
if (!$decoded) {
// This shouldn't be possible.
throw new \coding_exception('Invalid JSON from availabilityconditionsjson field');
}
if (!empty($decoded->errors)) {
$error = '';
foreach ($decoded->errors as $stringinfo) {
list ($component, $stringname) = explode(':', $stringinfo);
if ($error !== '') {
$error .= ' ';
}
$error .= get_string($stringname, $component);
}
$errors['availabilityconditionsjson'] = $error;
}
}
/**
* Converts an associative array into an array of objects with two fields.
*
* This is necessary because JavaScript associative arrays/objects are not
* ordered (at least officially according to the language specification).
*
* @param array $inarray Associative array key => value
* @param string $keyname Name to use for key in resulting array objects
* @param string $valuename Name to use for value in resulting array objects
* @return array Non-associative (numeric) array
*/
protected static function convert_associative_array_for_js(array $inarray,
$keyname, $valuename) {
$result = array();
foreach ($inarray as $key => $value) {
$result[] = (object)array($keyname => $key, $valuename => $value);
}
return $result;
}
}
+833
View File
@@ -0,0 +1,833 @@
<?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/>.
/**
* Base class for conditional availability information (for module or section).
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_availability;
defined('MOODLE_INTERNAL') || die();
/**
* Base class for conditional availability information (for module or section).
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class info {
/** @var \stdClass Course */
protected $course;
/** @var \course_modinfo Modinfo (available only during some functions) */
protected $modinfo = null;
/** @var bool Visibility flag (eye icon) */
protected $visible;
/** @var string Availability data as JSON string */
protected $availability;
/** @var tree Availability configuration, decoded from JSON; null if unset */
protected $availabilitytree;
/** @var array The groups each user belongs to. */
protected $groups = [];
/** @var array|null Array of information about current restore if any */
protected static $restoreinfo = null;
/**
* Constructs with item details.
*
* @param \stdClass $course Course object
* @param int $visible Value of visible flag (eye icon)
* @param string $availability Availability definition (JSON format) or null
*/
public function __construct($course, $visible, $availability) {
// Set basic values.
$this->course = $course;
$this->visible = (bool)$visible;
$this->availability = $availability;
}
/**
* Obtains the course associated with this availability information.
*
* @return \stdClass Moodle course object
*/
public function get_course() {
return $this->course;
}
/**
* Gets context used for checking capabilities for this item.
*
* @return \context Context for this item
*/
abstract public function get_context();
/**
* Obtains the modinfo associated with this availability information.
*
* Note: This field is available ONLY for use by conditions when calculating
* availability or information.
*
* @return \course_modinfo Modinfo
* @throws \coding_exception If called at incorrect times
*/
public function get_modinfo() {
if (!$this->modinfo) {
throw new \coding_exception(
'info::get_modinfo available only during condition checking');
}
return $this->modinfo;
}
/**
* Gets the availability tree, decoding it if not already done.
*
* @return tree Availability tree
*/
public function get_availability_tree() {
if (is_null($this->availabilitytree)) {
if (is_null($this->availability)) {
throw new \coding_exception(
'Cannot call get_availability_tree with null availability');
}
$this->availabilitytree = $this->decode_availability($this->availability, true);
}
return $this->availabilitytree;
}
/**
* Decodes availability data from JSON format.
*
* This function also validates the retrieved data as follows:
* 1. Data that does not meet the API-defined structure causes a
* coding_exception (this should be impossible unless there is
* a system bug or somebody manually hacks the database).
* 2. Data that meets the structure but cannot be implemented (e.g.
* reference to missing plugin or to module that doesn't exist) is
* either silently discarded (if $lax is true) or causes a
* coding_exception (if $lax is false).
*
* @param string $availability Availability string in JSON format
* @param boolean $lax If true, throw exceptions only for invalid structure
* @return tree Availability tree
* @throws \coding_exception If data is not valid JSON format
*/
protected function decode_availability($availability, $lax) {
// Decode JSON data.
$structure = json_decode($availability);
if (is_null($structure)) {
throw new \coding_exception('Invalid availability text', $availability);
}
// Recursively decode tree.
return new tree($structure, $lax);
}
/**
* Determines whether this particular item is currently available
* according to the availability 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.
*
* Depending on options selected, a description of the restrictions which
* mean the student can't view it (in HTML format) may be stored in
* $information. If there is nothing in $information and this function
* returns false, then the activity should not be displayed at all.
*
* This function displays debugging() messages if the availability
* information is invalid.
*
* @param string $information String describing restrictions in HTML format
* @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 \course_modinfo $modinfo Usually leave as null for default. Specify when
* calling recursively from inside get_fast_modinfo()
* @return bool True if this item is available to the user, false otherwise
*/
public function is_available(&$information, $grabthelot = false, $userid = 0,
\course_modinfo $modinfo = null) {
global $USER;
// Default to no information.
$information = '';
// Do nothing if there are no availability restrictions.
if (is_null($this->availability)) {
return true;
}
// Resolve optional parameters.
if (!$userid) {
$userid = $USER->id;
}
if (!$modinfo) {
$modinfo = get_fast_modinfo($this->course, $userid);
}
$this->modinfo = $modinfo;
// Get availability from tree.
try {
$tree = $this->get_availability_tree();
$result = $tree->check_available(false, $this, $grabthelot, $userid);
} catch (\coding_exception $e) {
$this->warn_about_invalid_availability($e);
$this->modinfo = null;
return false;
}
// See if there are any messages.
if ($result->is_available()) {
$this->modinfo = null;
return true;
} else {
// If the item is marked as 'not visible' then we don't change the available
// flag (visible/available are treated distinctly), but we remove any
// availability info. If the item is hidden with the eye icon, it doesn't
// make sense to show 'Available from <date>' or similar, because even
// when that date arrives it will still not be available unless somebody
// toggles the eye icon.
if ($this->visible) {
$information = $tree->get_result_information($this, $result);
}
$this->modinfo = null;
return false;
}
}
/**
* Checks whether this activity is going to be available for all users.
*
* Normally, if there are any conditions, then it may be hidden depending
* on the user. However in the case of date conditions there are some
* conditions which will definitely not result in it being hidden for
* anyone.
*
* @return bool True if activity is available for all
*/
public function is_available_for_all() {
global $CFG;
if (is_null($this->availability) || empty($CFG->enableavailability)) {
return true;
} else {
try {
return $this->get_availability_tree()->is_available_for_all();
} catch (\coding_exception $e) {
$this->warn_about_invalid_availability($e);
return false;
}
}
}
/**
* Obtains a string describing all availability restrictions (even if
* they do not apply any more). Used to display information for staff
* editing the website.
*
* The modinfo parameter must be specified when it is called from inside
* get_fast_modinfo, to avoid infinite recursion.
*
* This function displays debugging() messages if the availability
* information is invalid.
*
* @param \course_modinfo $modinfo Usually leave as null for default
* @return string Information string (for admin) about all restrictions on
* this item
*/
public function get_full_information(\course_modinfo $modinfo = null) {
// Do nothing if there are no availability restrictions.
if (is_null($this->availability)) {
return '';
}
// Resolve optional parameter.
if (!$modinfo) {
$modinfo = get_fast_modinfo($this->course);
}
$this->modinfo = $modinfo;
try {
$result = $this->get_availability_tree()->get_full_information($this);
$this->modinfo = null;
return $result;
} catch (\coding_exception $e) {
$this->warn_about_invalid_availability($e);
return false;
}
}
/**
* In some places we catch coding_exception because if a bug happens, it
* would be fatal for the course page GUI; instead we just show a developer
* debug message.
*
* @param \coding_exception $e Exception that occurred
*/
protected function warn_about_invalid_availability(\coding_exception $e) {
$name = $this->get_thing_name();
$htmlname = $this->format_info($name, $this->course);
// Because we call format_info here, likely in the middle of building dynamic data for the
// activity, there could be a chance that the name might not be available.
if ($htmlname === '') {
// So instead use the numbers (cmid) from the tag.
$htmlname = preg_replace('~[^0-9]~', '', $name);
}
$htmlname = html_to_text($htmlname, 75, false);
$info = 'Error processing availability data for &lsquo;' . $htmlname
. '&rsquo;: ' . s($e->a);
debugging($info, DEBUG_DEVELOPER);
}
/**
* Called during restore (near end of restore). Updates any necessary ids
* and writes the updated tree to the database. May output warnings if
* necessary (e.g. if a course-module cannot be found after restore).
*
* @param string $restoreid Restore identifier
* @param int $courseid Target course id
* @param \base_logger $logger Logger for any warnings
* @param int $dateoffset Date offset to be added to any dates (0 = none)
* @param \base_task $task Restore task
*/
public function update_after_restore($restoreid, $courseid, \base_logger $logger,
$dateoffset, \base_task $task) {
$tree = $this->get_availability_tree();
// Set static data for use by get_restore_date_offset function.
self::$restoreinfo = array('restoreid' => $restoreid, 'dateoffset' => $dateoffset,
'task' => $task);
$changed = $tree->update_after_restore($restoreid, $courseid, $logger,
$this->get_thing_name());
if ($changed) {
// Save modified data.
if ($tree->is_empty()) {
// If the tree is empty, but the tree has changed, remove this condition.
$this->set_in_database(null);
} else {
$structure = $tree->save();
$this->set_in_database(json_encode($structure));
}
}
}
/**
* Gets the date offset (amount by which any date values should be
* adjusted) for the current restore.
*
* @param string $restoreid Restore identifier
* @return int Date offset (0 if none)
* @throws coding_exception If not in a restore (or not in that restore)
*/
public static function get_restore_date_offset($restoreid) {
if (!self::$restoreinfo) {
throw new coding_exception('Only valid during restore');
}
if (self::$restoreinfo['restoreid'] !== $restoreid) {
throw new coding_exception('Data not available for that restore id');
}
return self::$restoreinfo['dateoffset'];
}
/**
* Gets the restore task (specifically, the task that calls the
* update_after_restore method) for the current restore.
*
* @param string $restoreid Restore identifier
* @return \base_task Restore task
* @throws coding_exception If not in a restore (or not in that restore)
*/
public static function get_restore_task($restoreid) {
if (!self::$restoreinfo) {
throw new coding_exception('Only valid during restore');
}
if (self::$restoreinfo['restoreid'] !== $restoreid) {
throw new coding_exception('Data not available for that restore id');
}
return self::$restoreinfo['task'];
}
/**
* Obtains the name of the item (cm_info or section_info, at present) that
* this is controlling availability of. Name should be formatted ready
* for on-screen display.
*
* @return string Name of item
*/
abstract protected function get_thing_name();
/**
* Stores an updated availability tree JSON structure into the relevant
* database table.
*
* @param string $availabilty New JSON value
*/
abstract protected function set_in_database($availabilty);
/**
* In rare cases the system may want to change all references to one ID
* (e.g. one course-module ID) to another one, within a course. This
* function does that for the conditional availability data for all
* modules and sections on the course.
*
* @param int|\stdClass $courseorid Course id or object
* @param string $table Table name e.g. 'course_modules'
* @param int $oldid Previous ID
* @param int $newid New ID
* @return bool True if anything changed, otherwise false
*/
public static function update_dependency_id_across_course(
$courseorid, $table, $oldid, $newid) {
global $DB;
$transaction = $DB->start_delegated_transaction();
$modinfo = get_fast_modinfo($courseorid);
$anychanged = false;
foreach ($modinfo->get_cms() as $cm) {
$info = new info_module($cm);
$changed = $info->update_dependency_id($table, $oldid, $newid);
$anychanged = $anychanged || $changed;
}
foreach ($modinfo->get_section_info_all() as $section) {
$info = new info_section($section);
$changed = $info->update_dependency_id($table, $oldid, $newid);
$anychanged = $anychanged || $changed;
}
$transaction->allow_commit();
if ($anychanged) {
get_fast_modinfo($courseorid, 0, true);
}
return $anychanged;
}
/**
* Called on a single item. If necessary, updates availability data where
* it has a dependency on an item with a particular id.
*
* @param string $table Table name e.g. 'course_modules'
* @param int $oldid Previous ID
* @param int $newid New ID
* @return bool True if it changed, otherwise false
*/
protected function update_dependency_id($table, $oldid, $newid) {
// Do nothing if there are no availability restrictions.
if (is_null($this->availability)) {
return false;
}
// Pass requirement on to tree object.
$tree = $this->get_availability_tree();
$changed = $tree->update_dependency_id($table, $oldid, $newid);
if ($changed) {
// Save modified data.
$structure = $tree->save();
$this->set_in_database(json_encode($structure));
}
return $changed;
}
/**
* Converts legacy data from fields (if provided) into the new availability
* syntax.
*
* Supported fields: availablefrom, availableuntil, showavailability
* (and groupingid for sections).
*
* It also supports the groupmembersonly field for modules. This part was
* optional in 2.7 but now always runs (because groupmembersonly has been
* removed).
*
* @param \stdClass $rec Object possibly containing legacy fields
* @param bool $section True if this is a section
* @param bool $modgroupmembersonlyignored Ignored option, previously used
* @return string|null New availability value or null if none
*/
public static function convert_legacy_fields($rec, $section, $modgroupmembersonlyignored = false) {
// Do nothing if the fields are not set.
if (empty($rec->availablefrom) && empty($rec->availableuntil) &&
(empty($rec->groupmembersonly)) &&
(!$section || empty($rec->groupingid))) {
return null;
}
// Handle legacy availability data.
$conditions = array();
$shows = array();
// Groupmembersonly condition (if enabled) for modules, groupingid for
// sections.
if (!empty($rec->groupmembersonly) ||
(!empty($rec->groupingid) && $section)) {
if (!empty($rec->groupingid)) {
$conditions[] = '{"type":"grouping"' .
($rec->groupingid ? ',"id":' . $rec->groupingid : '') . '}';
} else {
// No grouping specified, so allow any group.
$conditions[] = '{"type":"group"}';
}
// Group members only condition was not displayed to students.
$shows[] = 'false';
}
// Date conditions.
if (!empty($rec->availablefrom)) {
$conditions[] = '{"type":"date","d":">=","t":' . $rec->availablefrom . '}';
$shows[] = !empty($rec->showavailability) ? 'true' : 'false';
}
if (!empty($rec->availableuntil)) {
$conditions[] = '{"type":"date","d":"<","t":' . $rec->availableuntil . '}';
// Until dates never showed to students.
$shows[] = 'false';
}
// If there are some conditions, return them.
if ($conditions) {
return '{"op":"&","showc":[' . implode(',', $shows) . '],' .
'"c":[' . implode(',', $conditions) . ']}';
} else {
return null;
}
}
/**
* Adds a condition from the legacy availability condition.
*
* (For use during restore only.)
*
* This function assumes that the activity either has no conditions, or
* that it has an AND tree with one or more conditions.
*
* @param string|null $availability Current availability conditions
* @param \stdClass $rec Object containing information from old table
* @param bool $show True if 'show' option should be enabled
* @return string New availability conditions
*/
public static function add_legacy_availability_condition($availability, $rec, $show) {
if (!empty($rec->sourcecmid)) {
// Completion condition.
$condition = '{"type":"completion","cm":' . $rec->sourcecmid .
',"e":' . $rec->requiredcompletion . '}';
} else {
// Grade condition.
$minmax = '';
if (!empty($rec->grademin)) {
$minmax .= ',"min":' . sprintf('%.5f', $rec->grademin);
}
if (!empty($rec->grademax)) {
$minmax .= ',"max":' . sprintf('%.5f', $rec->grademax);
}
$condition = '{"type":"grade","id":' . $rec->gradeitemid . $minmax . '}';
}
return self::add_legacy_condition($availability, $condition, $show);
}
/**
* Adds a condition from the legacy availability field condition.
*
* (For use during restore only.)
*
* This function assumes that the activity either has no conditions, or
* that it has an AND tree with one or more conditions.
*
* @param string|null $availability Current availability conditions
* @param \stdClass $rec Object containing information from old table
* @param bool $show True if 'show' option should be enabled
* @return string New availability conditions
*/
public static function add_legacy_availability_field_condition($availability, $rec, $show) {
if (isset($rec->userfield)) {
// Standard field.
$fieldbit = ',"sf":' . json_encode($rec->userfield);
} else {
// Custom field.
$fieldbit = ',"cf":' . json_encode($rec->shortname);
}
// Value is not included for certain operators.
switch($rec->operator) {
case 'isempty':
case 'isnotempty':
$valuebit = '';
break;
default:
$valuebit = ',"v":' . json_encode($rec->value);
break;
}
$condition = '{"type":"profile","op":"' . $rec->operator . '"' .
$fieldbit . $valuebit . '}';
return self::add_legacy_condition($availability, $condition, $show);
}
/**
* Adds a condition to an AND group.
*
* (For use during restore only.)
*
* This function assumes that the activity either has no conditions, or
* that it has only conditions added by this function.
*
* @param string|null $availability Current availability conditions
* @param string $condition Condition text '{...}'
* @param bool $show True if 'show' option should be enabled
* @return string New availability conditions
*/
protected static function add_legacy_condition($availability, $condition, $show) {
$showtext = ($show ? 'true' : 'false');
if (is_null($availability)) {
$availability = '{"op":"&","showc":[' . $showtext .
'],"c":[' . $condition . ']}';
} else {
$matches = array();
if (!preg_match('~^({"op":"&","showc":\[(?:true|false)(?:,(?:true|false))*)' .
'(\],"c":\[.*)(\]})$~', $availability, $matches)) {
throw new \coding_exception('Unexpected availability value');
}
$availability = $matches[1] . ',' . $showtext . $matches[2] .
',' . $condition . $matches[3];
}
return $availability;
}
/**
* Tests against a user list. Users who cannot access the activity due to
* availability restrictions will be removed from the list.
*
* Note this only includes availability restrictions (those handled within
* this API) and not other ways of restricting access.
*
* This test ONLY includes conditions which are marked as being applied to
* user lists. For example, group conditions are included but date
* conditions are not included.
*
* The function operates reasonably efficiently i.e. should not do per-user
* database queries. It is however likely to be fairly slow.
*
* @param array $users Array of userid => object
* @return array Filtered version of input array
*/
public function filter_user_list(array $users) {
global $CFG;
if (is_null($this->availability) || !$CFG->enableavailability) {
return $users;
}
$tree = $this->get_availability_tree();
$checker = new capability_checker($this->get_context());
// Filter using availability tree.
$this->modinfo = get_fast_modinfo($this->get_course());
$filtered = $tree->filter_user_list($users, false, $this, $checker);
$this->modinfo = null;
// Include users in the result if they're either in the filtered list,
// or they have viewhidden. This logic preserves ordering of the
// passed users array.
$result = array();
$canviewhidden = $checker->get_users_by_capability($this->get_view_hidden_capability());
foreach ($users as $userid => $data) {
if (array_key_exists($userid, $filtered) || array_key_exists($userid, $canviewhidden)) {
$result[$userid] = $users[$userid];
}
}
return $result;
}
/**
* Gets the capability used to view hidden activities/sections (as
* appropriate).
*
* @return string Name of capability used to view hidden items of this type
*/
abstract protected function get_view_hidden_capability();
/**
* Obtains SQL that returns a list of enrolled users that has been filtered
* by the conditions applied in the availability API, similar to calling
* get_enrolled_users and then filter_user_list. As for filter_user_list,
* this ONLY filters out users with conditions that are marked as applying
* to user lists. For example, group conditions are included but date
* conditions are not included.
*
* The returned SQL is a query that returns a list of user IDs. It does not
* include brackets, so you neeed to add these to make it into a subquery.
* You would normally use it in an SQL phrase like "WHERE u.id IN ($sql)".
*
* The function returns an array with '' and an empty array, if there are
* no restrictions on users from these conditions.
*
* The SQL will be complex and may be slow. It uses named parameters (sorry,
* I know they are annoying, but it was unavoidable here).
*
* @param bool $onlyactive True if including only active enrolments
* @return array Array of SQL code (may be empty) and params
*/
public function get_user_list_sql($onlyactive) {
global $CFG;
if (is_null($this->availability) || !$CFG->enableavailability) {
return array('', array());
}
// Get SQL for the availability filter.
$tree = $this->get_availability_tree();
list ($filtersql, $filterparams) = $tree->get_user_list_sql(false, $this, $onlyactive);
if ($filtersql === '') {
// No restrictions, so return empty query.
return array('', array());
}
// Get SQL for the view hidden list.
list ($viewhiddensql, $viewhiddenparams) = get_enrolled_sql(
$this->get_context(), $this->get_view_hidden_capability(), 0, $onlyactive);
// Result is a union of the two.
return array('(' . $filtersql . ') UNION (' . $viewhiddensql . ')',
array_merge($filterparams, $viewhiddenparams));
}
/**
* Formats the $cm->availableinfo string for display. This includes
* filling in the names of any course-modules that might be mentioned.
* Should be called immediately prior to display, or at least somewhere
* that we can guarantee does not happen from within building the modinfo
* object.
*
* @param \renderable|string $inforenderable Info string or renderable
* @param int|\stdClass $courseorid
* @return string Correctly formatted info string
*/
public static function format_info($inforenderable, $courseorid) {
global $PAGE, $OUTPUT;
// Use renderer if required.
if (is_string($inforenderable)) {
$info = $inforenderable;
} else {
$renderable = new \core_availability\output\availability_info($inforenderable);
$info = $OUTPUT->render($renderable);
}
// Don't waste time if there are no special tags.
if (strpos($info, '<AVAILABILITY_') === false) {
return $info;
}
// Handle CMNAME tags.
$modinfo = get_fast_modinfo($courseorid);
$context = \context_course::instance($modinfo->courseid);
$info = preg_replace_callback('~<AVAILABILITY_CMNAME_([0-9]+)/>~',
function($matches) use($modinfo, $context) {
$cm = $modinfo->get_cm($matches[1]);
$modulename = format_string($cm->get_name(), true, ['context' => $context]);
// We make sure that we add a data attribute to the name so we can change it later if the
// original module name changes.
if ($cm->has_view() && $cm->get_user_visible()) {
// Help student by providing a link to the module which is preventing availability.
return \html_writer::link($cm->get_url(), $modulename, ['data-cm-name-for' => $cm->id]);
} else {
return \html_writer::span($modulename, '', ['data-cm-name-for' => $cm->id]);
}
}, $info);
$info = preg_replace_callback('~<AVAILABILITY_FORMAT_STRING>(.*?)</AVAILABILITY_FORMAT_STRING>~s',
function($matches) use ($context) {
$decoded = htmlspecialchars_decode($matches[1], ENT_NOQUOTES);
return format_string($decoded, true, ['context' => $context]);
}, $info);
$info = preg_replace_callback('~<AVAILABILITY_CALLBACK type="([a-z0-9_]+)">(.*?)</AVAILABILITY_CALLBACK>~s',
function($matches) use ($modinfo, $context) {
// Find the class, it must have already been loaded by now.
$fullclassname = 'availability_' . $matches[1] . '\condition';
if (!class_exists($fullclassname, false)) {
return '<!-- Error finding class ' . $fullclassname .' -->';
}
// Load the parameters.
$params = [];
$encodedparams = preg_split('~<P/>~', $matches[2], 0);
foreach ($encodedparams as $encodedparam) {
$params[] = htmlspecialchars_decode($encodedparam, ENT_NOQUOTES);
}
return $fullclassname::get_description_callback_value($modinfo, $context, $params);
}, $info);
return $info;
}
/**
* Used in course/lib.php because we need to disable the completion tickbox
* JS (using the non-JS version instead, which causes a page reload) if a
* completion tickbox value may affect a conditional activity.
*
* @param \stdClass $course Moodle course object
* @param int $cmid Course-module id
* @return bool True if this is used in a condition, false otherwise
*/
public static function completion_value_used($course, $cmid) {
// Access all plugins. Normally only the completion plugin is going
// to affect this value, but it's potentially possible that some other
// plugin could also rely on the completion plugin.
$pluginmanager = \core_plugin_manager::instance();
$enabled = $pluginmanager->get_enabled_plugins('availability');
$componentparams = new \stdClass();
foreach ($enabled as $plugin => $info) {
// Use the static method.
$class = '\availability_' . $plugin . '\condition';
if ($class::completion_value_used($course, $cmid)) {
return true;
}
}
return false;
}
/**
* Returns groups that the given user belongs to on the course. Note: If not already
* available, this may make a database query.
*
* This will include groups the user is not allowed to see themselves, so check visibility
* before displaying groups to the user.
*
* @param int $groupingid Grouping ID or 0 (default) for all groups
* @param int $userid User ID or 0 (default) for current user
* @return int[] Array of int (group id) => int (same group id again); empty array if none
*/
public function get_groups(int $groupingid = 0, int $userid = 0): array {
global $USER;
if (empty($userid)) {
$userid = $USER->id;
}
if (!array_key_exists($userid, $this->groups)) {
$allgroups = groups_get_user_groups($this->course->id, $userid, true);
$this->groups[$userid] = $allgroups;
} else {
$allgroups = $this->groups[$userid];
}
if (!isset($allgroups[$groupingid])) {
return [];
}
return $allgroups[$groupingid];
}
}
+226
View File
@@ -0,0 +1,226 @@
<?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/>.
/**
* Class handles conditional availability information for an activity.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_availability;
defined('MOODLE_INTERNAL') || die();
/**
* Class handles conditional availability information for an activity.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class info_module extends info {
/** @var \cm_info Activity. */
protected $cm;
/**
* Constructs with item details.
*
* @param \cm_info $cm Course-module object
*/
public function __construct(\cm_info $cm) {
parent::__construct($cm->get_course(), $cm->visible, $cm->availability);
$this->cm = $cm;
}
protected function get_thing_name() {
// We cannot access $cm->name as a property at this point, because this
// code may itself run in response to the $cm->name property access, and
// PHP magic function properties do not allow recursion (because PHP).
return condition::description_cm_name($this->cm->id);
}
protected function set_in_database($availability) {
global $DB;
$DB->set_field('course_modules', 'availability', $availability,
array('id' => $this->cm->id));
}
/**
* Gets the course-module object. Intended for use by conditions.
*
* @return \cm_info Course module
*/
public function get_course_module() {
return $this->cm;
}
public function get_context() {
return \context_module::instance($this->cm->id);
}
/**
* Tests against a user list. Users who cannot access the activity due to
* availability restrictions will be removed from the list.
*
* Note this only includes availability restrictions (those handled within
* this API) and not other ways of restricting access.
*
* This test ONLY includes conditions which are marked as being applied to
* user lists. For example, group conditions are included but date
* conditions are not included.
*
* When called on a module, this test DOES also include restrictions on the
* section (if any).
*
* The function operates reasonably efficiently i.e. should not do per-user
* database queries. It is however likely to be fairly slow.
*
* @param array $users Array of userid => object
* @return array Filtered version of input array
*/
public function filter_user_list(array $users) {
global $CFG;
if (!$CFG->enableavailability) {
return $users;
}
// Apply section filtering first.
$section = $this->cm->get_modinfo()->get_section_info(
$this->cm->sectionnum, MUST_EXIST);
$sectioninfo = new info_section($section);
$filtered = $sectioninfo->filter_user_list($users);
// Now do base class (module) filtering on top.
return parent::filter_user_list($filtered);
}
protected function get_view_hidden_capability() {
return 'moodle/course:ignoreavailabilityrestrictions';
}
public function get_user_list_sql($onlyactive = true) {
global $CFG, $DB;
if (!$CFG->enableavailability) {
return array('', array());
}
// Get query for section (if any) and module.
$section = $this->cm->get_modinfo()->get_section_info(
$this->cm->sectionnum, MUST_EXIST);
$sectioninfo = new info_section($section);
$sectionresult = $sectioninfo->get_user_list_sql($onlyactive);
$moduleresult = parent::get_user_list_sql($onlyactive);
if (!$sectionresult[0]) {
return $moduleresult;
}
if (!$moduleresult[0]) {
return $sectionresult;
}
return array($DB->sql_intersect(array($sectionresult[0], $moduleresult[0]), 'id'),
array_merge($sectionresult[1], $moduleresult[1]));
}
/**
* Checks if an activity is visible to the given user.
*
* Unlike other checks in the availability system, this check includes the
* $cm->visible flag. It is equivalent to $cm->uservisible.
*
* If you have already checked (or do not care whether) the user has access
* to the course, you can set $checkcourse to false to save it checking
* course access.
*
* When checking for the current user, you should generally not call
* this function. Instead, use get_fast_modinfo to get a cm_info object,
* then simply check the $cm->uservisible flag. This function is intended
* to obtain that information for a separate course-module object that
* wasn't loaded with get_fast_modinfo, or for a different user.
*
* This function has a performance cost unless the availability system is
* disabled, and you supply a $cm object with necessary fields, and you
* don't check course access.
*
* @param int|\stdClass|\cm_info $cmorid Object or id representing activity
* @param int $userid User id (0 = current user)
* @param bool $checkcourse If true, checks whether the user has course access
* @return bool True if the activity is visible to the specified user
* @throws \moodle_exception If the cmid doesn't exist
*/
public static function is_user_visible($cmorid, $userid = 0, $checkcourse = true) {
global $USER, $DB, $CFG;
// Evaluate user id.
if (!$userid) {
$userid = $USER->id;
}
// If this happens to be already called with a cm_info for the right user
// then just return uservisible.
if (($cmorid instanceof \cm_info) && $cmorid->get_modinfo()->userid == $userid) {
return $cmorid->uservisible;
}
// If the $cmorid isn't an object or doesn't have required fields, load it.
if (is_object($cmorid) && isset($cmorid->course) && isset($cmorid->visible)) {
$cm = $cmorid;
} else {
if (is_object($cmorid)) {
$cmorid = $cmorid->id;
}
$cm = $DB->get_record('course_modules', array('id' => $cmorid));
if (!$cm) {
// In some error cases, the course module may not exist.
debugging('info_module::is_user_visible called with invalid cmid ' . $cmorid, DEBUG_DEVELOPER);
return false;
}
}
// If requested, check user can access the course.
if ($checkcourse) {
$coursecontext = \context_course::instance($cm->course);
if (!is_enrolled($coursecontext, $userid, '', true) &&
!has_capability('moodle/course:view', $coursecontext, $userid)) {
return false;
}
}
// If availability is disabled, then all we need to do is check the visible flag.
if (!$CFG->enableavailability && $cm->visible) {
return true;
}
// When availability is enabled, access can depend on 3 things:
// 1. $cm->visible
// 2. $cm->availability
// 3. $section->availability (for activity section and possibly for
// parent sections)
// As a result we cannot take short cuts any longer and must get
// standard modinfo.
$modinfo = get_fast_modinfo($cm->course, $userid);
$cms = $modinfo->get_cms();
if (!isset($cms[$cm->id])) {
// In some cases this might get called with a cmid that is no longer
// available, for example when a module is hidden at system level.
debugging('info_module::is_user_visible called with invalid cmid ' . $cm->id, DEBUG_DEVELOPER);
return false;
}
return $cms[$cm->id]->uservisible;
}
}
+82
View File
@@ -0,0 +1,82 @@
<?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/>.
/**
* Class handles conditional availability information for a section.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_availability;
defined('MOODLE_INTERNAL') || die();
/**
* Class handles conditional availability information for a section.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class info_section extends info {
/** @var \section_info Section. */
protected $section;
/**
* Constructs with item details.
*
* @param \section_info $section Section object
*/
public function __construct(\section_info $section) {
parent::__construct($section->modinfo->get_course(), $section->visible,
$section->availability);
$this->section = $section;
}
protected function get_thing_name() {
return get_section_name($this->section->course, $this->section->section);
}
public function get_context() {
return \context_course::instance($this->get_course()->id);
}
protected function get_view_hidden_capability() {
return 'moodle/course:ignoreavailabilityrestrictions';
}
protected function set_in_database($availability) {
global $DB;
$section = new \stdClass();
$section->id = $this->section->id;
$section->availability = $availability;
$section->timemodified = time();
$DB->update_record('course_sections', $section);
}
/**
* Gets the section object. Intended for use by conditions.
*
* @return \section_info Section
*/
public function get_section() {
return $this->section;
}
}
@@ -0,0 +1,70 @@
<?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/>.
/**
* Represents multiple availability messages.
*
* These are messages like 'Not available until <date>'. This class includes
* multiple messages so that they can be rendered into a combined display, e.g.
* using bulleted lists.
*
* The tree structure of this object matches that of the availability
* restrictions.
*
* @package core_availability
* @copyright 2015 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Represents multiple availability messages.
*
* These are messages like 'Not available until <date>'. This class includes
* multiple messages so that they can be rendered into a combined display, e.g.
* using bulleted lists.
*
* The tree structure of this object matches that of the availability
* restrictions.
*
* @package core_availability
* @copyright 2015 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_availability_multiple_messages implements renderable {
/** @var bool True if this object represents the root of the tree */
public $root;
/** @var bool True if items use the AND operator (false = OR) */
public $andoperator;
/** @var bool True if this part of the tree is marked 'hide entirely' for non-matching users */
public $treehidden;
/** @var array Array of child items (may be string or this type) */
public $items;
/**
* Constructor.
*
* @param bool $root True if this object represents the root of the tree
* @param bool $andoperator True if items use the AND operator (false = OR)
* @param bool $treehidden True if this part of the tree is marked 'hide entirely' for non-matching users
* @param array $items Array of items (may be string or this type)
*/
public function __construct($root, $andoperator, $treehidden, array $items) {
$this->root = $root;
$this->andoperator = $andoperator;
$this->treehidden = $treehidden;
$this->items = $items;
}
}
@@ -0,0 +1,122 @@
<?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/>.
/**
* Renderable for the availability info.
*
* @package core_availability
* @copyright 2021 Bas Brands <bas@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_availability\output;
use core_availability_multiple_messages;
use renderable;
use templatable;
use stdClass;
/**
* Base class to render availability info.
*
* @package core_availability
* @copyright 2021 Bas Brands <bas@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class availability_info implements renderable, templatable {
/** @var core_availability_multiple_messages availabilitymessages the course format class */
protected $availabilitymessages;
/**
* Constructor.
*
* @param core_availability_multiple_messages $renderable the availability messages
*/
public function __construct(core_availability_multiple_messages $renderable) {
$this->availabilitymessages = $renderable;
}
/**
* Export this data so it can be used as the context for a mustache template.
*
* @param \renderer_base $output typically, the renderer that's calling this function
* @return stdClass data context for a mustache template
*/
public function export_for_template(\renderer_base $output): stdClass {
$template = $this->get_item_template($this->availabilitymessages);
$template->id = uniqid();
return $template;
}
/**
* Get the item base template.
*
* @return stdClass the template base
*/
protected function get_item_base_template(): stdClass {
return (object)[
'id' => false,
'items' => [],
'hasitems' => false,
];
}
/**
* Get the item template.
*
* @param core_availability_multiple_messages $availability the availability messages
* @return stdClass the template
*/
protected function get_item_template(core_availability_multiple_messages $availability): stdClass {
$template = $this->get_item_base_template();
$template->header = $this->get_item_header($availability);
foreach ($availability->items as $item) {
if (is_string($item)) {
$simple_item = $this->get_item_base_template();
$simple_item->header = $item;
$template->items[] = $simple_item;
} else {
$template->items[] = $this->get_item_template($item);
}
}
$template->hasitems = !empty($template->items);
return $template;
}
/**
* Get the item header.
* Depending on availability configuration this will return a string from a combined string identifier.
* For example: list_root_and_hidden, list_and, list_root_or_hidden, list_root_or, etc.
*
* @param core_availability_multiple_messages $availability the availability messages
* @return string the item header
*/
protected function get_item_header(core_availability_multiple_messages $availability): string {
$stridentifier = 'list_' . ($availability->root ? 'root_' : '') .
($availability->andoperator ? 'and' : 'or') .
($availability->treehidden ? '_hidden' : '');
return get_string($stridentifier, 'availability');
}
}
+46
View File
@@ -0,0 +1,46 @@
<?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/>.
/**
* Privacy Subsystem implementation for core_availability.
*
* @package core_availability
* @copyright 2018 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_availability\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for core_availability implementing null_provider.
*
* @copyright 2018 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
+95
View File
@@ -0,0 +1,95 @@
<?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/>.
/**
* Class represents the result of an availability check for the user.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_availability;
defined('MOODLE_INTERNAL') || die();
/**
* Class represents the result of an availability check for the user.
*
* You can pass an object of this class to tree::get_result_information to
* display suitable student information about the result.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class result {
/** @var bool True if the item is available */
protected $available;
/** @var tree_node[] Array of nodes to display in failure information (node=>node). */
protected $shownodes = array();
/**
* Constructs result.
*
* @param bool $available True if available
* @param tree_node $node Node if failed & should be displayed
* @param result[] $failedchildren Array of children who failed too
*/
public function __construct($available, tree_node $node = null,
array $failedchildren = array()) {
$this->available = $available;
if (!$available) {
if ($node) {
$this->shownodes[spl_object_hash($node)] = $node;
}
foreach ($failedchildren as $child) {
foreach ($child->shownodes as $key => $node) {
$this->shownodes[$key] = $node;
}
}
}
}
/**
* Checks if the result was a yes.
*
* @return bool True if the activity is available
*/
public function is_available() {
return $this->available;
}
/**
* Filters the provided array so that it only includes nodes which are
* supposed to be displayed in the result output. (I.e. those for which
* the user failed the test, and which are not set to totally hide
* output.)
*
* @param tree_node[] $array Input array of nodes
* @return array Output array containing only those nodes set for display
*/
public function filter_nodes(array $array) {
$out = array();
foreach ($array as $key => $node) {
if (array_key_exists(spl_object_hash($node), $this->shownodes)) {
$out[$key] = $node;
}
}
return $out;
}
}
+797
View File
@@ -0,0 +1,797 @@
<?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/>.
/**
* Class that holds a tree of availability conditions.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_availability;
defined('MOODLE_INTERNAL') || die();
/**
* Class that holds a tree of availability conditions.
*
* The structure of this tree in JSON input data is:
*
* { op:'&', c:[] }
*
* where 'op' is one of the OP_xx constants and 'c' is an array of children.
*
* At the root level one of the following additional values must be included:
*
* op '|' or '!&'
* show:true
* Boolean value controlling whether a failed match causes the item to
* display to students with information, or be completely hidden.
* op '&' or '!|'
* showc:[]
* Array of same length as c with booleans corresponding to each child; you
* can make it be hidden or shown depending on which one they fail. (Anything
* with false takes precedence.)
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tree extends tree_node {
/** @var int Operator: AND */
const OP_AND = '&';
/** @var int Operator: OR */
const OP_OR = '|';
/** @var int Operator: NOT(AND) */
const OP_NOT_AND = '!&';
/** @var int Operator: NOT(OR) */
const OP_NOT_OR = '!|';
/** @var bool True if this tree is at root level */
protected $root;
/** @var string Operator type (OP_xx constant) */
protected $op;
/** @var tree_node[] Children in this branch (may be empty array if needed) */
protected $children;
/**
* Array of 'show information or hide completely' options for each child.
* This array is only set for the root tree if it is in AND or NOT OR mode,
* otherwise it is null.
*
* @var bool[]
*/
protected $showchildren;
/**
* Single 'show information or hide completely' option for tree. This option
* is only set for the root tree if it is in OR or NOT AND mode, otherwise
* it is true.
*
* @var bool
*/
protected $show;
/**
* Display a representation of this tree (used for debugging).
*
* @return string Text representation of tree
*/
public function __toString() {
$result = '';
if ($this->root && is_null($this->showchildren)) {
$result .= $this->show ? '+' : '-';
}
$result .= $this->op . '(';
$first = true;
foreach ($this->children as $index => $child) {
if ($first) {
$first = false;
} else {
$result .= ',';
}
if (!is_null($this->showchildren)) {
$result .= $this->showchildren[$index] ? '+' : '-';
}
$result .= (string)$child;
}
$result .= ')';
return $result;
}
/**
* Decodes availability structure.
*
* This function also validates the retrieved data as follows:
* 1. Data that does not meet the API-defined structure causes a
* coding_exception (this should be impossible unless there is
* a system bug or somebody manually hacks the database).
* 2. Data that meets the structure but cannot be implemented (e.g.
* reference to missing plugin or to module that doesn't exist) is
* either silently discarded (if $lax is true) or causes a
* coding_exception (if $lax is false).
*
* @see decode_availability
* @param \stdClass $structure Structure (decoded from JSON)
* @param boolean $lax If true, throw exceptions only for invalid structure
* @param boolean $root If true, this is the root tree
* @return tree Availability tree
* @throws \coding_exception If data is not valid structure
*/
public function __construct($structure, $lax = false, $root = true) {
$this->root = $root;
// Check object.
if (!is_object($structure)) {
throw new \coding_exception('Invalid availability structure (not object)');
}
// Extract operator.
if (!isset($structure->op)) {
throw new \coding_exception('Invalid availability structure (missing ->op)');
}
$this->op = $structure->op;
if (!in_array($this->op, array(self::OP_AND, self::OP_OR,
self::OP_NOT_AND, self::OP_NOT_OR), true)) {
throw new \coding_exception('Invalid availability structure (unknown ->op)');
}
// For root tree, get show options.
$this->show = true;
$this->showchildren = null;
if ($root) {
if ($this->op === self::OP_AND || $this->op === self::OP_NOT_OR) {
// Per-child show options.
if (!isset($structure->showc)) {
throw new \coding_exception(
'Invalid availability structure (missing ->showc)');
}
if (!is_array($structure->showc)) {
throw new \coding_exception(
'Invalid availability structure (->showc not array)');
}
foreach ($structure->showc as $value) {
if (!is_bool($value)) {
throw new \coding_exception(
'Invalid availability structure (->showc value not bool)');
}
}
// Set it empty now - add corresponding ones later.
$this->showchildren = array();
} else {
// Entire tree show option. (Note: This is because when you use
// OR mode, say you have A OR B, the user does not meet conditions
// for either A or B. A is set to 'show' and B is set to 'hide'.
// But they don't have either, so how do we know which one to do?
// There might as well be only one value.)
if (!isset($structure->show)) {
throw new \coding_exception(
'Invalid availability structure (missing ->show)');
}
if (!is_bool($structure->show)) {
throw new \coding_exception(
'Invalid availability structure (->show not bool)');
}
$this->show = $structure->show;
}
}
// Get list of enabled plugins.
$pluginmanager = \core_plugin_manager::instance();
$enabled = $pluginmanager->get_enabled_plugins('availability');
// For unit tests, also allow the mock plugin type (even though it
// isn't configured in the code as a proper plugin).
if (PHPUNIT_TEST) {
$enabled['mock'] = true;
}
// Get children.
if (!isset($structure->c)) {
throw new \coding_exception('Invalid availability structure (missing ->c)');
}
if (!is_array($structure->c)) {
throw new \coding_exception('Invalid availability structure (->c not array)');
}
if (is_array($this->showchildren) && count($structure->showc) != count($structure->c)) {
throw new \coding_exception('Invalid availability structure (->c, ->showc mismatch)');
}
$this->children = array();
foreach ($structure->c as $index => $child) {
if (!is_object($child)) {
throw new \coding_exception('Invalid availability structure (child not object)');
}
// First see if it's a condition. These have a defined type.
if (isset($child->type)) {
// Look for a plugin of this type.
$classname = '\availability_' . $child->type . '\condition';
if (!array_key_exists($child->type, $enabled)) {
if ($lax) {
// On load of existing settings, ignore if class
// doesn't exist.
continue;
} else {
throw new \coding_exception('Unknown condition type: ' . $child->type);
}
}
$this->children[] = new $classname($child);
} else {
// Not a condition. Must be a subtree.
$this->children[] = new tree($child, $lax, false);
}
if (!is_null($this->showchildren)) {
$this->showchildren[] = $structure->showc[$index];
}
}
}
public function check_available($not, info $info, $grabthelot, $userid) {
// If there are no children in this group, we just treat it as available.
$information = '';
if (!$this->children) {
return new result(true);
}
// Get logic flags from operator.
list($innernot, $andoperator) = $this->get_logic_flags($not);
if ($andoperator) {
$allow = true;
} else {
$allow = false;
}
$failedchildren = array();
$totallyhide = !$this->show;
foreach ($this->children as $index => $child) {
// Check available and get info.
$childresult = $child->check_available(
$innernot, $info, $grabthelot, $userid);
$childyes = $childresult->is_available();
if (!$childyes) {
$failedchildren[] = $childresult;
if (!is_null($this->showchildren) && !$this->showchildren[$index]) {
$totallyhide = true;
}
}
if ($andoperator && !$childyes) {
$allow = false;
// Do not exit loop at this point, as we will still include other info.
} else if (!$andoperator && $childyes) {
// Exit loop since we are going to allow access (from this tree at least).
$allow = true;
break;
}
}
if ($allow) {
return new result(true);
} else if ($totallyhide) {
return new result(false);
} else {
return new result(false, $this, $failedchildren);
}
}
public function is_applied_to_user_lists() {
return true;
}
/**
* Tests against a user list. Users who cannot access the activity due to
* availability restrictions will be removed from the list.
*
* This test ONLY includes conditions which are marked as being applied to
* user lists. For example, group conditions are included but date
* conditions are not included.
*
* The function operates reasonably efficiently i.e. should not do per-user
* database queries. It is however likely to be fairly slow.
*
* @param array $users Array of userid => object
* @param bool $not If tree's parent indicates it's being checked negatively
* @param info $info Info about current context
* @param capability_checker $checker Capability checker
* @return array Filtered version of input array
*/
public function filter_user_list(array $users, $not, info $info,
capability_checker $checker) {
// Get logic flags from operator.
list($innernot, $andoperator) = $this->get_logic_flags($not);
if ($andoperator) {
// For AND, start with the whole result and whittle it down.
$result = $users;
} else {
// For OR, start with nothing.
$result = array();
$anyconditions = false;
}
// Loop through all valid children.
foreach ($this->children as $index => $child) {
if (!$child->is_applied_to_user_lists()) {
if ($andoperator) {
continue;
} else {
// OR condition with one option that doesn't restrict user
// lists = everyone is allowed.
$anyconditions = false;
break;
}
}
$childresult = $child->filter_user_list($users, $innernot, $info, $checker);
if ($andoperator) {
$result = array_intersect_key($result, $childresult);
} else {
// Combine results into array.
foreach ($childresult as $id => $user) {
$result[$id] = $user;
}
$anyconditions = true;
}
}
// For OR operator, if there were no conditions just return input.
if (!$andoperator && !$anyconditions) {
return $users;
} else {
return $result;
}
}
public function get_user_list_sql($not, info $info, $onlyactive) {
global $DB;
// Get logic flags from operator.
list($innernot, $andoperator) = $this->get_logic_flags($not);
// Loop through all valid children, getting SQL for each.
$childresults = array();
foreach ($this->children as $index => $child) {
if (!$child->is_applied_to_user_lists()) {
if ($andoperator) {
continue;
} else {
// OR condition with one option that doesn't restrict user
// lists = everyone is allowed.
$childresults = array();
break;
}
}
$childresult = $child->get_user_list_sql($innernot, $info, $onlyactive);
if ($childresult[0]) {
$childresults[] = $childresult;
} else if (!$andoperator) {
// When using OR operator, if any part doesn't have restrictions,
// then nor does the whole thing.
return array('', array());
}
}
// If there are no conditions, return null.
if (!$childresults) {
return array('', array());
}
// If there is a single condition, return it.
if (count($childresults) === 1) {
return $childresults[0];
}
// Combine results using INTERSECT or UNION.
$outsql = null;
$subsql = array();
$outparams = array();
foreach ($childresults as $childresult) {
$subsql[] = $childresult[0];
$outparams = array_merge($outparams, $childresult[1]);
}
if ($andoperator) {
$outsql = $DB->sql_intersect($subsql, 'id');
} else {
$outsql = '(' . join(') UNION (', $subsql) . ')';
}
return array($outsql, $outparams);
}
public function is_available_for_all($not = false) {
// Get logic flags.
list($innernot, $andoperator) = $this->get_logic_flags($not);
// No children = always available.
if (!$this->children) {
return true;
}
// Check children.
foreach ($this->children as $child) {
$innerall = $child->is_available_for_all($innernot);
if ($andoperator) {
// When there is an AND operator, then any child that results
// in unavailable status would cause the whole thing to be
// unavailable.
if (!$innerall) {
return false;
}
} else {
// When there is an OR operator, then any child which must only
// be available means the whole thing must be available.
if ($innerall) {
return true;
}
}
}
// If we get to here then for an AND operator that means everything must
// be available. From OR it means that everything must be possibly
// not available.
return $andoperator;
}
/**
* Gets full information about this tree (including all children) as HTML
* for display to staff.
*
* @param info $info Information about location of condition tree
* @throws \coding_exception If you call on a non-root tree
* @return string HTML data (empty string if none)
*/
public function get_full_information(info $info) {
if (!$this->root) {
throw new \coding_exception('Only supported on root item');
}
return $this->get_full_information_recursive(false, $info, null, true);
}
/**
* Gets information about this tree corresponding to the given result
* object. (In other words, only conditions which the student actually
* fails will be shown - and nothing if display is turned off.)
*
* @param info $info Information about location of condition tree
* @param result $result Result object
* @throws \coding_exception If you call on a non-root tree
* @return string HTML data (empty string if none)
*/
public function get_result_information(info $info, result $result) {
if (!$this->root) {
throw new \coding_exception('Only supported on root item');
}
return $this->get_full_information_recursive(false, $info, $result, true);
}
/**
* Gets information about this tree (including all or selected children) as
* HTML for display to staff or student.
*
* @param bool $not True if there is a NOT in effect
* @param info $info Information about location of condition tree
* @param result|null $result Result object if this is a student display, else null
* @param bool $root True if this is the root item
* @param bool $hidden Staff display; true if this tree has show=false (from parent)
* @return string|renderable Information to render
*/
protected function get_full_information_recursive(
$not, info $info, ?result $result, $root, $hidden = false) {
// Get list of children - either full list, or those which are shown.
$children = $this->children;
$staff = true;
if ($result) {
$children = $result->filter_nodes($children);
$staff = false;
}
// If no children, return empty string.
if (!$children) {
return '';
}
list($innernot, $andoperator) = $this->get_logic_flags($not);
// If there is only one child, don't bother displaying this tree
// (AND and OR makes no difference). Recurse to the child if a tree,
// otherwise display directly.
if (count ($children) === 1) {
$child = reset($children);
if ($this->root && is_null($result)) {
if (is_null($this->showchildren)) {
$childhidden = !$this->show;
} else {
$childhidden = !$this->showchildren[0];
}
} else {
$childhidden = $hidden;
}
if ($child instanceof tree) {
return $child->get_full_information_recursive(
$innernot, $info, $result, $root, $childhidden);
} else {
if ($root) {
$result = $child->get_standalone_description($staff, $innernot, $info);
} else {
$result = $child->get_description($staff, $innernot, $info);
}
if ($childhidden) {
$result .= ' ' . get_string('hidden_marker', 'availability');
}
return $result;
}
}
// Multiple children, so prepare child messages (recursive).
$items = array();
$index = 0;
foreach ($children as $child) {
// Work out if this node is hidden (staff view only).
$childhidden = $this->root && is_null($result) &&
!is_null($this->showchildren) && !$this->showchildren[$index];
if ($child instanceof tree) {
$items[] = $child->get_full_information_recursive(
$innernot, $info, $result, false, $childhidden);
} else {
$childdescription = $child->get_description($staff, $innernot, $info);
if ($childhidden) {
$childdescription .= ' ' . get_string('hidden_marker', 'availability');
}
$items[] = $childdescription;
}
$index++;
}
// If showing output to staff, and root is set to hide completely,
// then include this information in the message.
if ($this->root) {
$treehidden = !$this->show && is_null($result);
} else {
$treehidden = $hidden;
}
// Format output for display.
return new \core_availability_multiple_messages($root, $andoperator, $treehidden, $items);
}
/**
* Converts the operator for the tree into two flags used for computing
* the result.
*
* The 2 flags are $innernot (whether to set $not when calling for children)
* and $andoperator (whether to use AND or OR operator to combine children).
*
* @param bool $not Not flag passed to this tree
* @return array Array of the 2 flags ($innernot, $andoperator)
*/
public function get_logic_flags($not) {
// Work out which type of logic to use for the group.
switch($this->op) {
case self::OP_AND:
case self::OP_OR:
$negative = false;
break;
case self::OP_NOT_AND:
case self::OP_NOT_OR:
$negative = true;
break;
default:
throw new \coding_exception('Unknown operator');
}
switch($this->op) {
case self::OP_AND:
case self::OP_NOT_AND:
$andoperator = true;
break;
case self::OP_OR:
case self::OP_NOT_OR:
$andoperator = false;
break;
default:
throw new \coding_exception('Unknown operator');
}
// Select NOT (or not) for children. It flips if this is a 'not' group.
$innernot = $negative ? !$not : $not;
// Select operator to use for this group. If flips for negative, because:
// NOT (a AND b) = (NOT a) OR (NOT b)
// NOT (a OR b) = (NOT a) AND (NOT b).
if ($innernot) {
$andoperator = !$andoperator;
}
return array($innernot, $andoperator);
}
public function save() {
$result = new \stdClass();
$result->op = $this->op;
// Only root tree has the 'show' options.
if ($this->root) {
if ($this->op === self::OP_AND || $this->op === self::OP_NOT_OR) {
$result->showc = $this->showchildren;
} else {
$result->show = $this->show;
}
}
$result->c = array();
foreach ($this->children as $child) {
$result->c[] = $child->save();
}
return $result;
}
/**
* Checks whether this tree is empty (contains no children).
*
* @return boolean True if empty
*/
public function is_empty() {
return count($this->children) === 0;
}
/**
* Recursively gets all children of a particular class (you can use a base
* class to get all conditions, or a specific class).
*
* @param string $classname Full class name e.g. core_availability\condition
* @return array Array of nodes of that type (flattened, not a tree any more)
*/
public function get_all_children($classname) {
$result = array();
$this->recursive_get_all_children($classname, $result);
return $result;
}
/**
* Internal function that implements get_all_children efficiently.
*
* @param string $classname Full class name e.g. core_availability\condition
* @param array $result Output array of nodes
*/
protected function recursive_get_all_children($classname, array &$result) {
foreach ($this->children as $child) {
if (is_a($child, $classname)) {
$result[] = $child;
}
if ($child instanceof tree) {
$child->recursive_get_all_children($classname, $result);
}
}
}
public function update_after_restore($restoreid, $courseid,
\base_logger $logger, $name) {
$changed = false;
foreach ($this->children as $index => $child) {
if ($child->include_after_restore($restoreid, $courseid, $logger, $name,
info::get_restore_task($restoreid))) {
$thischanged = $child->update_after_restore($restoreid, $courseid,
$logger, $name);
$changed = $changed || $thischanged;
} else {
unset($this->children[$index]);
unset($this->showchildren[$index]);
$this->showchildren = !is_null($this->showchildren) ? array_values($this->showchildren) : null;
$changed = true;
}
}
return $changed;
}
public function update_dependency_id($table, $oldid, $newid) {
$changed = false;
foreach ($this->children as $child) {
$thischanged = $child->update_dependency_id($table, $oldid, $newid);
$changed = $changed || $thischanged;
}
return $changed;
}
/**
* Returns a JSON object which corresponds to a tree.
*
* Intended for unit testing, as normally the JSON values are constructed
* by JavaScript code.
*
* This function generates 'nested' (i.e. not root-level) trees.
*
* @param array $children Array of JSON objects from component children
* @param string $op Operator (tree::OP_xx)
* @return stdClass JSON object
* @throws coding_exception If you get parameters wrong
*/
public static function get_nested_json(array $children, $op = self::OP_AND) {
// Check $op and work out its type.
switch($op) {
case self::OP_AND:
case self::OP_NOT_OR:
case self::OP_OR:
case self::OP_NOT_AND:
break;
default:
throw new \coding_exception('Invalid $op');
}
// Do simple tree.
$result = new \stdClass();
$result->op = $op;
$result->c = $children;
return $result;
}
/**
* Returns a JSON object which corresponds to a tree at root level.
*
* Intended for unit testing, as normally the JSON values are constructed
* by JavaScript code.
*
* The $show parameter can be a boolean for all OP_xx options. For OP_AND
* and OP_NOT_OR where you have individual show options, you can specify
* a boolean (same for all) or an array.
*
* @param array $children Array of JSON objects from component children
* @param string $op Operator (tree::OP_xx)
* @param bool|array $show Whether 'show' option is turned on (see above)
* @return stdClass JSON object ready for encoding
* @throws coding_exception If you get parameters wrong
*/
public static function get_root_json(array $children, $op = self::OP_AND, $show = true) {
// Get the basic object.
$result = self::get_nested_json($children, $op);
// Check $op type.
switch($op) {
case self::OP_AND:
case self::OP_NOT_OR:
$multishow = true;
break;
case self::OP_OR:
case self::OP_NOT_AND:
$multishow = false;
break;
}
// Add show options depending on operator.
if ($multishow) {
if (is_bool($show)) {
$result->showc = array_pad(array(), count($result->c), $show);
} else if (is_array($show)) {
// The JSON will break if anything isn't an actual bool, so check.
foreach ($show as $item) {
if (!is_bool($item)) {
throw new \coding_exception('$show array members must be bool');
}
}
// Check the size matches.
if (count($show) != count($result->c)) {
throw new \coding_exception('$show array size does not match $children');
}
$result->showc = $show;
} else {
throw new \coding_exception('$show must be bool or array');
}
} else {
if (!is_bool($show)) {
throw new \coding_exception('For this operator, $show must be bool');
}
$result->show = $show;
}
return $result;
}
}
+256
View File
@@ -0,0 +1,256 @@
<?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/>.
/**
* Node (base class) used to construct a tree of availability conditions.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_availability;
defined('MOODLE_INTERNAL') || die();
/**
* Node (base class) used to construct a tree of availability conditions.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class tree_node {
/** @var int Counter to be used in {@link tree_node::unique_sql_parameter()}. */
protected static $uniquesqlparametercounter = 1;
/**
* Determines whether this particular item is currently available
* according to the availability 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.
*
* The $not option is potentially confusing. This option always indicates
* the 'real' value of NOT. For example, a condition inside a 'NOT AND'
* group will get this called with $not = true, but if you put another
* 'NOT OR' group inside the first group, then a condition inside that will
* be called with $not = false. We need to use the real values, rather than
* the more natural use of the current value at this point inside the tree,
* so that the information displayed to users makes sense.
*
* @param bool $not Set true if we are inverting the condition
* @param \core_availability\info $info Item we're checking
* @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 User ID to check availability for
* @return result Availability check result
*/
abstract public function check_available($not,
\core_availability\info $info, $grabthelot, $userid);
/**
* Checks whether this condition is actually going to be available for
* all users under normal circumstances.
*
* Normally, if there are any conditions, then it may be hidden. However
* in the case of date conditions there are some conditions which will
* definitely not result in it being hidden for anyone.
*
* @param bool $not Set true if we are inverting the condition
* @return bool True if condition will return available for everyone
*/
abstract public function is_available_for_all($not = false);
/**
* Saves tree data back to a structure object.
*
* @return \stdClass Structure object (ready to be made into JSON format)
*/
abstract public function save();
/**
* Checks whether this node should be included after restore or not. The
* node may be removed depending on restore settings, which you can get from
* the $task object.
*
* By default nodes are still included after restore.
*
* @param string $restoreid Restore ID
* @param int $courseid ID of target course
* @param \base_logger $logger Logger for any warnings
* @param string $name Name of this item (for use in warning messages)
* @param \base_task $task Current restore task
* @return bool True if there was any change
*/
public function include_after_restore($restoreid, $courseid, \base_logger $logger, $name,
\base_task $task) {
return true;
}
/**
* Updates this node after restore, returning true if anything changed.
* The default behaviour is simply to return false. If there is a problem
* with the update, $logger can be used to output a warning.
*
* Note: If you need information about the date offset, call
* \core_availability\info::get_restore_date_offset($restoreid). For
* information on the restoring task and its settings, call
* \core_availability\info::get_restore_task($restoreid).
*
* @param string $restoreid Restore ID
* @param int $courseid ID of target course
* @param \base_logger $logger Logger for any warnings
* @param string $name Name of this item (for use in warning messages)
* @return bool True if there was any change
*/
public function update_after_restore($restoreid, $courseid, \base_logger $logger, $name) {
return false;
}
/**
* Updates this node if it contains any references (dependencies) to the
* given table and id.
*
* @param string $table Table name e.g. 'course_modules'
* @param int $oldid Previous ID
* @param int $newid New ID
* @return bool True if it changed, otherwise false
*/
abstract public function update_dependency_id($table, $oldid, $newid);
/**
* Checks whether this condition applies to user lists. The default is
* false (the condition is used to control access, but does not prevent
* the student from appearing in lists).
*
* For example, group conditions apply to user lists: we do not want to
* include a student in a list of users if they are prohibited from
* accessing the activity because they don't belong to a relevant group.
* However, date conditions do not apply - we still want to show users
* in a list of people who might have submitted an assignment, even if they
* are no longer able to access the assignment in question because there is
* a date restriction.
*
* The general idea is that conditions which are likely to be permanent
* (group membership, user profile) apply to user lists. Conditions which
* are likely to be temporary (date, grade requirement) do not.
*
* Conditions which do apply to user lists must implement the
* filter_user_list function.
*
* @return bool True if this condition applies to user lists
*/
public function is_applied_to_user_lists() {
return false;
}
/**
* Tests this condition against a user list. Users who do not meet the
* condition will be removed from the list, unless they have the ability
* to view hidden activities/sections.
*
* This function must be implemented if is_applied_to_user_lists returns
* true. Otherwise it will not be called.
*
* The function must operate efficiently, e.g. by using a fixed number of
* database queries regardless of how many users are in the list.
*
* Within this function, if you need to check capabilities, please use
* the provided checker which caches results where possible.
*
* Conditions do not need to check the viewhiddenactivities or
* viewhiddensections capabilities. These are handled by
* core_availability\info::filter_user_list.
*
* @param array $users Array of userid => object
* @param bool $not True if this condition is applying in negative mode
* @param \core_availability\info $info Item we're checking
* @param capability_checker $checker
* @return array Filtered version of input array
* @throws \coding_exception If called on a condition that doesn't apply to user lists
*/
public function filter_user_list(array $users, $not,
\core_availability\info $info, capability_checker $checker) {
throw new \coding_exception('Not implemented (do not call unless '.
'is_applied_to_user_lists is true)');
}
/**
* Obtains SQL that returns a list of enrolled users that has been filtered
* by the conditions applied in the availability API, similar to calling
* get_enrolled_users and then filter_user_list. As for filter_user_list,
* this ONLY filters out users with conditions that are marked as applying
* to user lists. For example, group conditions are included but date
* conditions are not included.
*
* The returned SQL is a query that returns a list of user IDs. It does not
* include brackets, so you neeed to add these to make it into a subquery.
* You would normally use it in an SQL phrase like "WHERE u.id IN ($sql)".
*
* The SQL will be complex and may be slow. It uses named parameters (sorry,
* I know they are annoying, but it was unavoidable here).
*
* If there are no conditions, the returned result is array('', array()).
*
* Conditions do not need to check the viewhiddenactivities or
* viewhiddensections capabilities. These are handled by
* core_availability\info::get_user_list_sql.
*
* @param bool $not True if this condition is applying in negative mode
* @param \core_availability\info $info Item we're checking
* @param bool $onlyactive If true, only returns active enrolments
* @return array Array with two elements: SQL subquery and parameters array
* @throws \coding_exception If called on a condition that doesn't apply to user lists
*/
public function get_user_list_sql($not, \core_availability\info $info, $onlyactive) {
if (!$this->is_applied_to_user_lists()) {
throw new \coding_exception('Not implemented (do not call unless '.
'is_applied_to_user_lists is true)');
}
// Handle situation where plugin does not implement this, by returning a
// default (all enrolled users). This ensures compatibility with 2.7
// plugins and behaviour. Plugins should be updated to support this
// new function (if they return true to is_applied_to_user_lists).
debugging('Availability plugins that return true to is_applied_to_user_lists ' .
'should also now implement get_user_list_sql: ' . get_class($this),
DEBUG_DEVELOPER);
return get_enrolled_sql($info->get_context(), '', 0, $onlyactive);
}
/**
* Utility function for generating SQL parameters (because we can't use ?
* parameters because get_enrolled_sql has infected us with horrible named
* parameters).
*
* @param array $params Params array (value will be added to this array)
* @param string|int $value Value
* @return SQL code for the parameter, e.g. ':pr1234'
*/
protected static function unique_sql_parameter(array &$params, $value) {
// Note we intentionally do not use self:: here.
$count = tree_node::$uniquesqlparametercounter++;
$unique = 'usp' . $count;
$params[$unique] = $value;
return ':' . $unique;
}
}