File manager - Edit - /home/u466501803/domains/qurdis.my.id/public_html/availability.tar
Back
classes/result.php 0000644 00000006131 15215712063 0010233 0 ustar 00 <?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; } } classes/tree_node.php 0000644 00000026247 15215712063 0010673 0 ustar 00 <?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; } } classes/output/availability_info.php 0000644 00000007727 15215712063 0013756 0 ustar 00 <?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'); } } classes/privacy/provider.php 0000644 00000002775 15215712063 0012236 0 ustar 00 <?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'; } } classes/multiple_messages.php 0000644 00000005271 15215712063 0012443 0 ustar 00 <?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; } } classes/info.php 0000644 00000101644 15215712063 0007655 0 ustar 00 <?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 ‘' . $htmlname . '’: ' . 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'); foreach ($enabled as $plugin => $info) { /** @var \core_availability\condition $class */ $class = '\availability_' . $plugin . '\condition'; if (class_exists($class) && $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]; } } classes/condition.php 0000644 00000023603 15215712063 0010706 0 ustar 00 <?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; } } classes/info_module.php 0000644 00000020711 15215712063 0011215 0 ustar 00 <?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; } } classes/capability_checker.php 0000644 00000005047 15215712063 0012527 0 ustar 00 <?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]; } } classes/frontend.php 0000644 00000020452 15215712063 0010536 0 ustar 00 <?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'; if (!class_exists($class)) { continue; } /** @var \core_availability\frontend $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} = [ $component, $frontend->allow_add($course, $cm, $section), $frontend->get_javascript_init_params($course, $cm, $section), get_config('availability_' . $plugin, 'defaultdisplaymode'), ]; // 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; } } classes/info_section.php 0000644 00000004704 15215712063 0011400 0 ustar 00 <?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; } } classes/tree.php 0000644 00000072166 15215712063 0007667 0 ustar 00 <?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)) { /** @var \core_availability\condition $classname */ $classname = '\availability_' . $child->type . '\condition'; if (!array_key_exists($child->type, $enabled) || !class_exists($classname)) { 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; } } condition/date/classes/privacy/provider.php 0000644 00000003011 15215712063 0015121 0 ustar 00 <?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 availability_date. * * @package availability_date * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace availability_date\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for availability_date implementing null_provider. * * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @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'; } } condition/date/classes/condition.php 0000644 00000024571 15215712063 0013616 0 ustar 00 <?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/>. /** * Date condition. * * @package availability_date * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace availability_date; defined('MOODLE_INTERNAL') || die(); /** * Date condition. * * @package availability_date * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class condition extends \core_availability\condition { /** @var string Availabile only from specified date. */ const DIRECTION_FROM = '>='; /** @var string Availabile only until specified date. */ const DIRECTION_UNTIL = '<'; /** @var string One of the DIRECTION_xx constants. */ private $direction; /** @var int Time (Unix epoch seconds) for condition. */ private $time; /** @var int Forced current time (for unit tests) or 0 for normal. */ private static $forcecurrenttime = 0; /** * Constructor. * * @param \stdClass $structure Data structure from JSON decode * @throws \coding_exception If invalid data structure. */ public function __construct($structure) { // Get direction. if (isset($structure->d) && in_array($structure->d, array(self::DIRECTION_FROM, self::DIRECTION_UNTIL))) { $this->direction = $structure->d; } else { throw new \coding_exception('Missing or invalid ->d for date condition'); } // Get time. if (isset($structure->t) && is_int($structure->t)) { $this->time = $structure->t; } else { throw new \coding_exception('Missing or invalid ->t for date condition'); } } public function save() { return (object)array('type' => 'date', 'd' => $this->direction, 't' => $this->time); } /** * Returns a JSON object which corresponds to a condition of this type. * * Intended for unit testing, as normally the JSON values are constructed * by JavaScript code. * * @param string $direction DIRECTION_xx constant * @param int $time Time in epoch seconds * @return stdClass Object representing condition */ public static function get_json($direction, $time) { return (object)array('type' => 'date', 'd' => $direction, 't' => (int)$time); } public function is_available($not, \core_availability\info $info, $grabthelot, $userid) { return $this->is_available_for_all($not); } public function is_available_for_all($not = false) { // Check condition. $now = self::get_time(); switch ($this->direction) { case self::DIRECTION_FROM: $allow = $now >= $this->time; break; case self::DIRECTION_UNTIL: $allow = $now < $this->time; break; default: throw new \coding_exception('Unexpected direction'); } if ($not) { $allow = !$allow; } return $allow; } /** * Obtains the actual direction of checking based on the $not value. * * @param bool $not True if condition is negated * @return string Direction constant * @throws \coding_exception */ protected function get_logical_direction($not) { switch ($this->direction) { case self::DIRECTION_FROM: return $not ? self::DIRECTION_UNTIL : self::DIRECTION_FROM; case self::DIRECTION_UNTIL: return $not ? self::DIRECTION_FROM : self::DIRECTION_UNTIL; default: throw new \coding_exception('Unexpected direction'); } } public function get_description($full, $not, \core_availability\info $info) { return $this->get_either_description($not, false); } public function get_standalone_description( $full, $not, \core_availability\info $info) { return $this->get_either_description($not, true); } /** * Shows the description using the different lang strings for the standalone * version or the full one. * * @param bool $not True if NOT is in force * @param bool $standalone True to use standalone lang strings */ protected function get_either_description($not, $standalone) { $direction = $this->get_logical_direction($not); $midnight = self::is_midnight($this->time); $midnighttag = $midnight ? '_date' : ''; $satag = $standalone ? 'short_' : 'full_'; switch ($direction) { case self::DIRECTION_FROM: return get_string($satag . 'from' . $midnighttag, 'availability_date', self::show_time($this->time, $midnight, false)); case self::DIRECTION_UNTIL: return get_string($satag . 'until' . $midnighttag, 'availability_date', self::show_time($this->time, $midnight, true)); } } protected function get_debug_string() { return $this->direction . ' ' . gmdate('Y-m-d H:i:s', $this->time); } /** * Gets time. This function is implemented here rather than calling time() * so that it can be overridden in unit tests. (Would really be nice if * Moodle had a generic way of doing that, but it doesn't.) * * @return int Current time (seconds since epoch) */ protected static function get_time() { if (self::$forcecurrenttime) { return self::$forcecurrenttime; } else { return time(); } } /** * Forces the current time for unit tests. * * @param int $forcetime Time to return from the get_time function */ public static function set_current_time_for_test($forcetime = 0) { self::$forcecurrenttime = $forcetime; } /** * Shows a time either as a date or a full date and time, according to * user's timezone. * * @param int $time Time * @param bool $dateonly If true, uses date only * @param bool $until If true, and if using date only, shows previous date * @return string Date */ protected function show_time($time, $dateonly, $until = false) { // For 'until' dates that are at midnight, e.g. midnight 5 March, it // is better to word the text as 'until end 4 March'. $daybefore = false; if ($until && $dateonly) { $daybefore = true; $time = strtotime('-1 day', $time); } return userdate($time, get_string($dateonly ? 'strftimedate' : 'strftimedatetime', 'langconfig')); } /** * Checks whether a given time refers exactly to midnight (in current user * timezone). * * @param int $time Time * @return bool True if time refers to midnight, false otherwise */ protected static function is_midnight($time) { return usergetmidnight($time) == $time; } public function update_after_restore( $restoreid, $courseid, \base_logger $logger, $name) { // Update the date, if restoring with changed date. $dateoffset = \core_availability\info::get_restore_date_offset($restoreid); if ($dateoffset) { $this->time += $dateoffset; return true; } return false; } /** * Changes all date restrictions on a course by the specified shift amount. * Used by the course reset feature. * * @param int $courseid Course id * @param int $timeshift Offset in seconds */ public static function update_all_dates($courseid, $timeshift) { global $DB; $modinfo = get_fast_modinfo($courseid); $anychanged = false; // Adjust dates from all course modules. foreach ($modinfo->cms as $cm) { if (!$cm->availability) { continue; } $info = new \core_availability\info_module($cm); $tree = $info->get_availability_tree(); $dates = $tree->get_all_children('availability_date\condition'); $changed = false; foreach ($dates as $date) { $date->time += $timeshift; $changed = true; } // Save the updated course module. if ($changed) { $DB->set_field('course_modules', 'availability', json_encode($tree->save()), array('id' => $cm->id)); $anychanged = true; } } // Adjust dates from all course sections. foreach ($modinfo->get_section_info_all() as $section) { if (!$section->availability) { continue; } $info = new \core_availability\info_section($section); $tree = $info->get_availability_tree(); $dates = $tree->get_all_children('availability_date\condition'); $changed = false; foreach ($dates as $date) { $date->time += $timeshift; $changed = true; } // Save the updated course module. if ($changed) { $updatesection = new \stdClass(); $updatesection->id = $section->id; $updatesection->availability = json_encode($tree->save()); $updatesection->timemodified = time(); $DB->update_record('course_sections', $updatesection); // Invalidate the section cache by given section id. \course_modinfo::purge_course_section_cache_by_id($courseid, $section->id); $anychanged = true; } } if ($anychanged) { // Partial rebuild the sections which have been invalidated. rebuild_course_cache($courseid, true, true); } } } condition/date/classes/frontend.php 0000644 00000015604 15215712063 0013444 0 ustar 00 <?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/>. /** * Front-end class. * * @package availability_date * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace availability_date; defined('MOODLE_INTERNAL') || die(); /** * Front-end class. * * @package availability_date * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class frontend extends \core_availability\frontend { /** * The date selector popup is not currently supported because the date * selector is in a messy state (about to be replaced with a new YUI3 * implementation) and MDL-44814 was rejected. I have left the code in * place, but disabled. When the date selector situation is finalised, * then this constant should be removed (either applying MDL-44814 if old * selector is still in use, or modifying the JavaScript code to support the * new date selector if it has landed). * * @var bool */ const DATE_SELECTOR_SUPPORTED = false; protected function get_javascript_strings() { return array('ajaxerror', 'direction_before', 'direction_from', 'direction_until', 'direction_label', 'error_dateconflict'); } /** * Given field values, obtains the corresponding timestamp. * * @param int $year Year * @param int $month Month * @param int $day Day * @param int $hour Hour * @param int $minute Minute * @return int Timestamp */ public static function get_time_from_fields($year, $month, $day, $hour, $minute) { $calendartype = \core_calendar\type_factory::get_calendar_instance(); $gregoriandate = $calendartype->convert_to_gregorian( $year, $month, $day, $hour, $minute); return make_timestamp($gregoriandate['year'], $gregoriandate['month'], $gregoriandate['day'], $gregoriandate['hour'], $gregoriandate['minute'], 0); } /** * Given a timestamp, obtains corresponding field values. * * @param int $time Timestamp * @return \stdClass Object with fields for year, month, day, hour, minute */ public static function get_fields_from_time($time) { $calendartype = \core_calendar\type_factory::get_calendar_instance(); $wrongfields = $calendartype->timestamp_to_date_array($time); return array('day' => $wrongfields['mday'], 'month' => $wrongfields['mon'], 'year' => $wrongfields['year'], 'hour' => $wrongfields['hours'], 'minute' => $wrongfields['minutes']); } protected function get_javascript_init_params($course, ?\cm_info $cm = null, ?\section_info $section = null) { global $CFG, $OUTPUT; require_once($CFG->libdir . '/formslib.php'); // Support internationalised calendars. $calendartype = \core_calendar\type_factory::get_calendar_instance(); // Get current date, but set time to 00:00 (to make it easier to // specify whole days) and change name of mday field to match below. $wrongfields = $calendartype->timestamp_to_date_array(time()); $current = array('day' => $wrongfields['mday'], 'month' => $wrongfields['mon'], 'year' => $wrongfields['year'], 'hour' => 0, 'minute' => 0); // Time part is handled the same everywhere. $hours = array(); for ($i = 0; $i <= 23; $i++) { $hours[$i] = sprintf("%02d", $i); } $minutes = array(); for ($i = 0; $i < 60; $i += 5) { $minutes[$i] = sprintf("%02d", $i); } // List date fields. $fields = $calendartype->get_date_order( $calendartype->get_min_year(), $calendartype->get_max_year()); // Add time fields - in RTL mode these are switched. $fields['split'] = '/'; if (right_to_left()) { $fields['minute'] = $minutes; $fields['colon'] = ':'; $fields['hour'] = $hours; } else { $fields['hour'] = $hours; $fields['colon'] = ':'; $fields['minute'] = $minutes; } // Output all date fields. $html = '<span class="availability-group">'; foreach ($fields as $field => $options) { if ($options === '/') { $html = rtrim($html); // In Gregorian calendar mode only, we support a date selector popup, reusing // code from form to ensure consistency. if ($calendartype->get_name() === 'gregorian' && self::DATE_SELECTOR_SUPPORTED) { $image = $OUTPUT->pix_icon('i/calendar', get_string('calendar', 'calendar'), 'moodle'); $html .= ' ' . \html_writer::link('#', $image, array('name' => 'x[calendar]')); form_init_date_js(); } $html .= '</span> <span class="availability-group">'; continue; } if ($options === ':') { $html .= ': '; continue; } $html .= \html_writer::start_tag('label'); $html .= \html_writer::span(get_string($field) . ' ', 'accesshide'); // NOTE: The fields need to have these weird names in order that they // match the standard Moodle form control, otherwise the date selector // won't find them. $html .= \html_writer::start_tag('select', array('name' => 'x[' . $field . ']', 'class' => 'custom-select')); foreach ($options as $key => $value) { $params = array('value' => $key); if ($current[$field] == $key) { $params['selected'] = 'selected'; } $html .= \html_writer::tag('option', s($value), $params); } $html .= \html_writer::end_tag('select'); $html .= \html_writer::end_tag('label'); $html .= ' '; } $html = rtrim($html) . '</span>'; // Also get the time that corresponds to this default date. $time = self::get_time_from_fields($current['year'], $current['month'], $current['day'], $current['hour'], $current['minute']); return array($html, $time); } } condition/date/lang/en/availability_date.php 0000644 00000003646 15215712063 0015165 0 ustar 00 <?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/>. /** * Language strings. * * @package availability_date * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['ajaxerror'] = 'Error contacting server to convert times'; $string['direction_before'] = 'Date'; $string['direction_from'] = 'from'; $string['direction_label'] = 'Direction'; $string['direction_until'] = 'until'; $string['description'] = 'Prevent access until (or from) a specified date and time.'; $string['error_dateconflict'] = 'Conflicts with other date restrictions'; $string['full_from'] = 'It is after <strong>{$a}</strong>'; $string['full_from_date'] = 'It is on or after <strong>{$a}</strong>'; $string['full_until'] = 'It is before <strong>{$a}</strong>'; $string['full_until_date'] = 'It is before end of <strong>{$a}</strong>'; $string['pluginname'] = 'Restriction by date'; $string['short_from'] = 'Available from <strong>{$a}</strong>'; $string['short_from_date'] = 'Available from <strong>{$a}</strong>'; $string['short_until'] = 'Available until <strong>{$a}</strong>'; $string['short_until_date'] = 'Available until end of <strong>{$a}</strong>'; $string['title'] = 'Date'; $string['privacy:metadata'] = 'The Restriction by date plugin does not store any personal data.'; condition/date/version.php 0000644 00000001750 15215712063 0011652 0 ustar 00 <?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/>. /** * Version info. * * @package availability_date * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2024100700; $plugin->requires = 2024100100; $plugin->component = 'availability_date'; condition/date/tests/behat/availability_date.feature 0000644 00000004036 15215712063 0016725 0 ustar 00 @availability @availability_date Feature: availability_date In order to control student access to activities As a teacher I need to set date conditions which prevent student access Background: Given the following "courses" exist: | fullname | shortname | format | enablecompletion | | Course 1 | C1 | topics | 1 | And the following "users" exist: | username | | teacher1 | | student1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | course | name | | page | C1 | Page 1 | | page | C1 | Page 2 | @javascript Scenario: Test condition # Add a Page with a date condition that does match (from the past). Given I am on the "Page 1" "page activity editing" page logged in as "teacher1" And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Date" "button" in the "Add restriction..." "dialogue" And I click on ".availability-item .availability-eye img" "css_element" And I set the field "year" to "2013" And I press "Save and return to course" # Add a Page with a date condition that doesn't match (until the past). And I am on the "Page 2" "page activity editing" page And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Date" "button" in the "Add restriction..." "dialogue" And I click on ".availability-item .availability-eye img" "css_element" And I set the field "Direction" to "until" And I set the field "year" to "2013" And I press "Save and return to course" # Log back in as student. When I am on the "Course 1" "course" page logged in as "student1" # Page 1 should appear, but page 2 does not. Then I should see "Page 1" in the "region-main" "region" And I should not see "Page 2" in the "region-main" "region" condition/date/tests/behat/availability_date_conflict.feature 0000644 00000020342 15215712063 0020604 0 ustar 00 @availability @availability_date @javascript Feature: As a teacher I can set availability dates restriction to an activity and see a warning when conflicting dates are set Background: Given the following "courses" exist: | fullname | shortname | format | enablecompletion | | Course 1 | C1 | topics | 1 | And the following "users" exist: | username | | teacher1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | name | intro | introformat | course | content | contentformat | idnumber | | page | PageName1 | PageDesc1 | 1 | C1 | Page 1 | 1 | 1 | Scenario: When I set dates to potential conflicting dates in the same subset, I should see a warning. Given I am on the PageName1 "page activity editing" page logged in as teacher1 And I expand all fieldsets And I click on "Add restriction..." "button" in the "root" "core_availability > Availability Button Area" And I click on "Date" "button" in the "Add restriction..." "dialogue" And I set the field "year" in the "1" "availability_date > Date Restriction" to "2023" And I set the field "Month" in the "1" "availability_date > Date Restriction" to "April" And I set the field "day" in the "1" "availability_date > Date Restriction" to "4" And I set the field "Direction" in the "1" "availability_date > Date Restriction" to "from" And I click on "Add restriction..." "button" in the "root" "core_availability > Availability Button Area" And I click on "Date" "button" in the "Add restriction..." "dialogue" And I set the field "year" in the "2" "availability_date > Date Restriction" to "2023" And I set the field "Month" in the "2" "availability_date > Date Restriction" to "April" And I set the field "day" in the "2" "availability_date > Date Restriction" to "6" And I set the field "Direction" in the "2" "availability_date > Date Restriction" to "until" And I click on "Add restriction..." "button" in the "root" "core_availability > Availability Button Area" And I click on "Date" "button" in the "Add restriction..." "dialogue" And I set the field "year" in the "3" "availability_date > Date Restriction" to "2023" And I set the field "Month" in the "3" "availability_date > Date Restriction" to "April" And I set the field "day" in the "3" "availability_date > Date Restriction" to "6" When I set the field "Direction" in the "3" "availability_date > Date Restriction" to "from" Then I should see "Conflicts with other date restrictions" Scenario: If there are conflicting dates in the same subset, I should not see a warning if condition are separated by "any". Given I am on the PageName1 "page activity editing" page logged in as teacher1 And I expand all fieldsets And I click on "Add restriction..." "button" in the "root" "core_availability > Availability Button Area" And I click on "Restriction set" "button" in the "Add restriction..." "dialogue" And I click on "Add restriction..." "button" in the "1" "core_availability > Availability Button Area" And I click on "Date" "button" in the "Add restriction..." "dialogue" And I set the field "year" in the "1.1" "availability_date > Date Restriction" to "2023" And I set the field "Month" in the "1.1" "availability_date > Date Restriction" to "April" And I set the field "day" in the "1.1" "availability_date > Date Restriction" to "4" And I set the field "Direction" in the "1.1" "availability_date > Date Restriction" to "from" And I click on "Add restriction..." "button" in the "1" "core_availability > Availability Button Area" And I click on "Date" "button" in the "Add restriction..." "dialogue" And I set the field "year" in the "1.2" "availability_date > Date Restriction" to "2023" And I set the field "Month" in the "1.2" "availability_date > Date Restriction" to "April" And I set the field "day" in the "1.2" "availability_date > Date Restriction" to "6" And I set the field "Direction" in the "1.2" "availability_date > Date Restriction" to "until" And I click on "Add restriction..." "button" in the "1" "core_availability > Availability Button Area" And I click on "Date" "button" in the "Add restriction..." "dialogue" And I set the field "year" in the "1.3" "availability_date > Date Restriction" to "2023" And I set the field "Month" in the "1.3" "availability_date > Date Restriction" to "April" And I set the field "day" in the "1.3" "availability_date > Date Restriction" to "6" And I set the field "Direction" in the "1.3" "availability_date > Date Restriction" to "from" When I set the field "Required restrictions" in the "1" "core_availability > Set Of Restrictions" to "any" Then I should not see "Conflicts with other date restrictions" Scenario: There should a conflicting availability dates are in the same subset separated by "all". Given I am on the PageName1 "page activity editing" page logged in as teacher1 And I expand all fieldsets # Root level: Student "must" match the following. And I click on "Add restriction..." "button" in the "root" "core_availability > Availability Button Area" And I click on "Restriction set" "button" in the "Add restriction..." "dialogue" # This is the second level: Student "must" match any of the following. And I click on "Add restriction..." "button" in the "1" "core_availability > Availability Button Area" And I click on "Restriction set" "button" in the "Add restriction..." "dialogue" # And now the third and final level. And I click on "Add restriction..." "button" in the "1.1" "core_availability > Availability Button Area" And I click on "Date" "button" in the "Add restriction..." "dialogue" And I set the field "year" in the "1.1.1" "availability_date > Date Restriction" to "2023" And I set the field "Month" in the "1.1.1" "availability_date > Date Restriction" to "April" And I set the field "day" in the "1.1.1" "availability_date > Date Restriction" to "2" And I set the field "Direction" in the "1.1.1" "availability_date > Date Restriction" to "from" And I click on "Add restriction..." "button" in the "1.1" "core_availability > Availability Button Area" And I click on "Date" "button" in the "Add restriction..." "dialogue" And I set the field "year" in the "1.1.2" "availability_date > Date Restriction" to "2023" And I set the field "Month" in the "1.1.2" "availability_date > Date Restriction" to "April" And I set the field "day" in the "1.1.2" "availability_date > Date Restriction" to "3" And I set the field "Direction" in the "1.1.2" "availability_date > Date Restriction" to "until" # Then add a restriction to the second level. And I click on "Add restriction..." "button" in the "1" "core_availability > Availability Button Area" And I click on "Restriction set" "button" in the "Add restriction..." "dialogue" And I click on "Add restriction..." "button" in the "1.2" "core_availability > Availability Button Area" And I click on "Date" "button" in the "Add restriction..." "dialogue" And I set the field "year" in the "1.2.1" "availability_date > Date Restriction" to "2023" And I set the field "Month" in the "1.2.1" "availability_date > Date Restriction" to "April" And I set the field "day" in the "1.2.1" "availability_date > Date Restriction" to "4" And I set the field "Direction" in the "1.2.1" "availability_date > Date Restriction" to "from" And I click on "Add restriction..." "button" in the "1.2" "core_availability > Availability Button Area" And I click on "Date" "button" in the "Add restriction..." "dialogue" And I set the field "year" in the "1.2.2" "availability_date > Date Restriction" to "2023" And I set the field "Month" in the "1.2.2" "availability_date > Date Restriction" to "April" And I set the field "day" in the "1.2.2" "availability_date > Date Restriction" to "3" When I set the field "Direction" in the "1.2.2" "availability_date > Date Restriction" to "until" # Same subset, we can detect conflicts. Then I should see "Conflicts with other date restrictions" condition/date/tests/behat/behat_availability_date.php 0000644 00000002600 15215712063 0017217 0 ustar 00 <?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/>. use Behat\Mink\Element\NodeElement; /** * Behat availabilty-related steps definitions. * * @package availability_date * @category test * @copyright 2023 Laurent David <laurent.david@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_availability_date extends behat_base { /** * Return the list of partial named selectors. * * @return array */ public static function get_partial_named_selectors(): array { return [ new behat_component_named_selector( 'Date Restriction', ["//div[h3[@data-restriction-order=%locator% and contains(text(), 'Date restriction')]]"] ), ]; } } condition/date/tests/condition_test.php 0000644 00000031132 15215712063 0014351 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace availability_date; use core_availability\tree; /** * Unit tests for the date condition. * * @package availability_date * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class condition_test extends \advanced_testcase { /** * Load required classes. */ public function setUp(): void { // Load the mock info class so that it can be used. global $CFG; require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info.php'); parent::setUp(); } /** * Tests constructing and using date condition as part of tree. */ public function test_in_tree(): void { global $SITE, $USER, $CFG; $this->resetAfterTest(); $this->setAdminUser(); // Set server timezone for test. (Important as otherwise the timezone // could be anything - this is modified by other unit tests, too.) $this->setTimezone('UTC'); // SEt user to GMT+5. $USER->timezone = 5; // Construct tree with date condition. $time = strtotime('2014-02-18 14:20:00 GMT'); $structure = (object)array('op' => '|', 'show' => true, 'c' => array( (object)array('type' => 'date', 'd' => '>=', 't' => $time))); $tree = new \core_availability\tree($structure); $info = new \core_availability\mock_info(); // Check if available (when not available). condition::set_current_time_for_test($time - 1); $information = ''; $result = $tree->check_available(false, $info, true, $USER->id); $this->assertFalse($result->is_available()); $information = $tree->get_result_information($info, $result); // Note: PM is normally upper-case, but an issue with PHP on Mac means // that on that platform, it is reported lower-case. $this->assertMatchesRegularExpression('~from.*18 February 2014, 7:20 (PM|pm)~', $information); // Check if available (when available). condition::set_current_time_for_test($time); $result = $tree->check_available(false, $info, true, $USER->id); $this->assertTrue($result->is_available()); $information = $tree->get_result_information($info, $result); $this->assertEquals('', $information); } /** * Tests the constructor including error conditions. Also tests the * string conversion feature (intended for debugging only). */ public function test_constructor(): void { // No parameters. $structure = (object)array(); try { $date = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Missing or invalid ->d', $e->getMessage()); } // Invalid ->d. $structure->d = 'woo hah!!'; try { $date = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Missing or invalid ->d', $e->getMessage()); } // Missing ->t. $structure->d = '>='; try { $date = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Missing or invalid ->t', $e->getMessage()); } // Invalid ->t. $structure->t = 'got you all in check'; try { $date = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Missing or invalid ->t', $e->getMessage()); } // Valid conditions of both types. $structure = (object)array('d' => '>=', 't' => strtotime('2014-02-18 14:43:17 GMT')); $date = new condition($structure); $this->assertEquals('{date:>= 2014-02-18 14:43:17}', (string)$date); $structure->d = '<'; $date = new condition($structure); $this->assertEquals('{date:< 2014-02-18 14:43:17}', (string)$date); } /** * Tests the save() function. */ public function test_save(): void { $structure = (object)array('d' => '>=', 't' => 12345); $cond = new condition($structure); $structure->type = 'date'; $this->assertEquals($structure, $cond->save()); } /** * Tests the is_available() and is_available_to_all() functions. */ public function test_is_available(): void { global $SITE, $USER; $time = strtotime('2014-02-18 14:50:10 GMT'); $info = new \core_availability\mock_info(); // Test with >=. $date = new condition((object)array('d' => '>=', 't' => $time)); condition::set_current_time_for_test($time - 1); $this->assertFalse($date->is_available(false, $info, true, $USER->id)); condition::set_current_time_for_test($time); $this->assertTrue($date->is_available(false, $info, true, $USER->id)); // Test with <. $date = new condition((object)array('d' => '<', 't' => $time)); condition::set_current_time_for_test($time); $this->assertFalse($date->is_available(false, $info, true, $USER->id)); condition::set_current_time_for_test($time - 1); $this->assertTrue($date->is_available(false, $info, true, $USER->id)); // Repeat this test with is_available_to_all() - it should be the same. $date = new condition((object)array('d' => '<', 't' => $time)); condition::set_current_time_for_test($time); $this->assertFalse($date->is_available_for_all(false)); condition::set_current_time_for_test($time - 1); $this->assertTrue($date->is_available_for_all(false)); } /** * Tests the get_description and get_standalone_description functions. */ public function test_get_description(): void { global $SITE, $CFG; $this->resetAfterTest(); $this->setTimezone('UTC'); $modinfo = get_fast_modinfo($SITE); $info = new \core_availability\mock_info(); $time = strtotime('2014-02-18 14:55:01 GMT'); // Test with >=. $date = new condition((object)array('d' => '>=', 't' => $time)); $information = $date->get_description(true, false, $info); $this->assertMatchesRegularExpression('~after.*18 February 2014, 2:55 (PM|pm)~', $information); $information = $date->get_description(true, true, $info); $this->assertMatchesRegularExpression('~before.*18 February 2014, 2:55 (PM|pm)~', $information); $information = $date->get_standalone_description(true, false, $info); $this->assertMatchesRegularExpression('~from.*18 February 2014, 2:55 (PM|pm)~', $information); $information = $date->get_standalone_description(true, true, $info); $this->assertMatchesRegularExpression('~until.*18 February 2014, 2:55 (PM|pm)~', $information); // Test with <. $date = new condition((object)array('d' => '<', 't' => $time)); $information = $date->get_description(true, false, $info); $this->assertMatchesRegularExpression('~before.*18 February 2014, 2:55 (PM|pm)~', $information); $information = $date->get_description(true, true, $info); $this->assertMatchesRegularExpression('~after.*18 February 2014, 2:55 (PM|pm)~', $information); $information = $date->get_standalone_description(true, false, $info); $this->assertMatchesRegularExpression('~until.*18 February 2014, 2:55 (PM|pm)~', $information); $information = $date->get_standalone_description(true, true, $info); $this->assertMatchesRegularExpression('~from.*18 February 2014, 2:55 (PM|pm)~', $information); // Test special case for dates that are midnight. $date = new condition((object)array('d' => '>=', 't' => strtotime('2014-03-05 00:00 GMT'))); $information = $date->get_description(true, false, $info); $this->assertMatchesRegularExpression('~on or after.*5 March 2014([^0-9]*)$~', $information); $information = $date->get_description(true, true, $info); $this->assertMatchesRegularExpression('~before.*end of.*4 March 2014([^0-9]*)$~', $information); $information = $date->get_standalone_description(true, false, $info); $this->assertMatchesRegularExpression('~from.*5 March 2014([^0-9]*)$~', $information); $information = $date->get_standalone_description(true, true, $info); $this->assertMatchesRegularExpression('~until end of.*4 March 2014([^0-9]*)$~', $information); // In the 'until' case for midnight, it shows the previous day. (I.e. // if the date is 5 March 00:00, then we show it as available until 4 // March, implying 'the end of'.) $date = new condition((object)array('d' => '<', 't' => strtotime('2014-03-05 00:00 GMT'))); $information = $date->get_description(true, false, $info); $this->assertMatchesRegularExpression('~before end of.*4 March 2014([^0-9]*)$~', $information); $information = $date->get_description(true, true, $info); $this->assertMatchesRegularExpression('~on or after.*5 March 2014([^0-9]*)$~', $information); $information = $date->get_standalone_description(true, false, $info); $this->assertMatchesRegularExpression('~until end of.*4 March 2014([^0-9]*)$~', $information); $information = $date->get_standalone_description(true, true, $info); $this->assertMatchesRegularExpression('~from.*5 March 2014([^0-9]*)$~', $information); } /** * Tests the update_all_dates function. */ public function test_update_all_dates(): void { global $DB; $this->resetAfterTest(); // Create a course with 3 pages. $generator = $this->getDataGenerator(); $course = $generator->create_course(); $rec = array('course' => $course); $page1 = $generator->get_plugin_generator('mod_page')->create_instance($rec); $page2 = $generator->get_plugin_generator('mod_page')->create_instance($rec); $page3 = $generator->get_plugin_generator('mod_page')->create_instance($rec); // Set the availability page 2 to a simple date condition. You can access // it from 1337 onwards. $simplecondition = tree::get_root_json(array( condition::get_json(condition::DIRECTION_FROM, 1337))); $DB->set_field('course_modules', 'availability', json_encode($simplecondition), array('id' => $page2->cmid)); // Set page 3 to a complex set of conditions including a nested date condition. // You can access it until 1459, *or* after 2810 if you belong to a group. $complexcondition = tree::get_root_json(array( condition::get_json(condition::DIRECTION_UNTIL, 1459), tree::get_nested_json(array( condition::get_json(condition::DIRECTION_FROM, 2810), \availability_group\condition::get_json()))), tree::OP_OR); $DB->set_field('course_modules', 'availability', json_encode($complexcondition), array('id' => $page3->cmid)); // Now use the update_all_dates function to move date forward 100000. condition::update_all_dates($course->id, 100000); // Get the expected conditions after adjusting time, and compare to database. $simplecondition->c[0]->t = 101337; $complexcondition->c[0]->t = 101459; $complexcondition->c[1]->c[0]->t = 102810; $this->assertEquals($simplecondition, json_decode( $DB->get_field('course_modules', 'availability', array('id' => $page2->cmid)))); $this->assertEquals($complexcondition, json_decode( $DB->get_field('course_modules', 'availability', array('id' => $page3->cmid)))); // The one without availability conditions should still be null. $this->assertNull($DB->get_field('course_modules', 'availability', array('id' => $page1->cmid))); } } condition/date/yui/src/form/js/form.js 0000644 00000021451 15215712063 0013731 0 ustar 00 /** * JavaScript for form editing date conditions. * * @module moodle-availability_date-form */ M.availability_date = M.availability_date || {}; /** * @class M.availability_date.form * @extends M.core_availability.plugin */ M.availability_date.form = Y.Object(M.core_availability.plugin); /** * Initialises this plugin. * * Because the date fields are complex depending on Moodle calendar settings, * we create the HTML for these fields in PHP and pass it to this method. * * @method initInner * @param {String} html HTML to use for date fields * @param {Number} defaultTime Time value that corresponds to initial fields */ M.availability_date.form.initInner = function(html, defaultTime) { this.html = html; this.defaultTime = defaultTime; }; M.availability_date.form.getNode = function(json) { var html = '<span class="col-form-label pe-3">' + M.util.get_string('direction_before', 'availability_date') + '</span> <span class="availability-group">' + '<label><span class="accesshide">' + M.util.get_string('direction_label', 'availability_date') + ' </span>' + '<select name="direction" class="custom-select">' + '<option value=">=">' + M.util.get_string('direction_from', 'availability_date') + '</option>' + '<option value="<">' + M.util.get_string('direction_until', 'availability_date') + '</option>' + '</select></label></span> ' + this.html; var node = Y.Node.create('<span>' + html + '</span>'); // Set initial value if non-default. if (json.t !== undefined) { node.setData('time', json.t); // Disable everything. node.all('select:not([name=direction])').each(function(select) { select.set('disabled', true); }); var url = M.cfg.wwwroot + '/availability/condition/date/ajax.php?action=fromtime' + '&time=' + json.t; Y.io(url, {on: { success: function(id, response) { var fields = Y.JSON.parse(response.responseText); for (var field in fields) { var select = node.one('select[name=x\\[' + field + '\\]]'); select.set('value', '' + fields[field]); select.set('disabled', false); } }, failure: function() { window.alert(M.util.get_string('ajaxerror', 'availability_date')); } }}); } else { // Set default time that corresponds to the HTML selectors. node.setData('time', this.defaultTime); } if (json.nodeUID === undefined) { var miliTime = new Date(); json.nodeUID = miliTime.getTime(); } node.setData('nodeUID', json.nodeUID); if (json.d !== undefined) { node.one('select[name=direction]').set('value', json.d); } // Add event handlers (first time only). if (!M.availability_date.form.addedEvents) { M.availability_date.form.addedEvents = true; var root = Y.one('.availability-field'); root.delegate('change', function() { // For the direction, just update the form fields. M.core_availability.form.update(); }, '.availability_date select[name=direction]'); root.delegate('change', function() { // Update time using AJAX call from root node. M.availability_date.form.updateTime(this.ancestor('span.availability_date')); }, '.availability_date select:not([name=direction])'); } if (node.one('a[href=#]')) { // Add the date selector magic. M.form.dateselector.init_single_date_selector(node); // This special handler detects when the date selector changes the year. var yearSelect = node.one('select[name=x\\[year\\]]'); var oldSet = yearSelect.set; yearSelect.set = function(name, value) { oldSet.call(yearSelect, name, value); if (name === 'selectedIndex') { // Do this after timeout or the other fields haven't been set yet. setTimeout(function() { M.availability_date.form.updateTime(node); }, 0); } }; } return node; }; /** * Updates time from AJAX. Whenever the field values change, we recompute the * actual time via an AJAX request to Moodle. * * This will set the 'time' data on the node and then update the form, once it * gets an AJAX response. * * @method updateTime * @param {Y.Node} node Node for plugin controls */ M.availability_date.form.updateTime = function(node) { // After a change to the date/time we need to recompute the // actual time using AJAX because it depends on the user's // time zone and calendar options. var url = M.cfg.wwwroot + '/availability/condition/date/ajax.php?action=totime' + '&year=' + node.one('select[name=x\\[year\\]]').get('value') + '&month=' + node.one('select[name=x\\[month\\]]').get('value') + '&day=' + node.one('select[name=x\\[day\\]]').get('value') + '&hour=' + node.one('select[name=x\\[hour\\]]').get('value') + '&minute=' + node.one('select[name=x\\[minute\\]]').get('value'); Y.io(url, {on: { success: function(id, response) { node.setData('time', response.responseText); M.core_availability.form.update(); }, failure: function() { window.alert(M.util.get_string('ajaxerror', 'availability_date')); } }}); }; M.availability_date.form.fillValue = function(value, node) { value.d = node.one('select[name=direction]').get('value'); value.t = parseInt(node.getData('time'), 10); value.nodeUID = node.getData('nodeUID'); }; /** * List out Date node value in the same branch. * * This will go through all array node and list nodes that are sibling of the current node. * * @method findAllDateSiblings * @param {Array} tree Tree items to convert * @param {Number} nodeUIDToFind node UID to find. * @return {Array|null} array of surrounding date avaiability values */ M.availability_date.form.findAllDateSiblings = function(tree, nodeUIDToFind) { var itemValue = null; var siblingsFinderRecursive = function(itemsTree) { var dateSiblings = []; var nodeFound = false; var index; var childDates; var currentOp = itemsTree.op !== undefined ? itemsTree.op : null; if (itemsTree.c !== undefined) { var children = itemsTree.c; for (index = 0; index < children.length; index++) { itemValue = children.at(index); if (itemValue.type === undefined) { childDates = siblingsFinderRecursive(itemValue); if (childDates) { return childDates; } } if (itemValue.type === 'date') { // We go through all tree node, if we meet the current node then we add all nodes in the current branch. if (nodeUIDToFind === itemValue.nodeUID) { nodeFound = true; } else if (currentOp === '&') { dateSiblings.push(itemValue); } } } if (nodeFound) { return dateSiblings; } } return null; }; return siblingsFinderRecursive(tree); }; /** * Check current node. * * This will check current date node with all date node in tree node. * * @method checkConditionDate * @param {Y.Node} currentNode The curent node. * * @return {boolean} error Return true if the date is conflict. */ M.availability_date.form.checkConditionDate = function(currentNode) { var error = false; var currentNodeUID = currentNode.getData('nodeUID'); var currentNodeDirection = currentNode.one('select[name=direction]').get('value'); var currentNodeTime = parseInt(currentNode.getData('time'), 10); var dateSiblings = M.availability_date.form.findAllDateSiblings( M.core_availability.form.rootList.getValue(), currentNodeUID); if (dateSiblings) { dateSiblings.forEach(function(dateSibling) { // Validate if the date is conflict. if (dateSibling.d === '<') { if (currentNodeDirection === '>=' && currentNodeTime >= dateSibling.t) { error = true; } } else { if (currentNodeDirection === '<' && currentNodeTime <= dateSibling.t) { error = true; } } return error; }); } return error; }; M.availability_date.form.fillErrors = function(errors, node) { var error = M.availability_date.form.checkConditionDate(node); if (error) { errors.push('availability_date:error_dateconflict'); } }; condition/date/yui/src/form/build.json 0000644 00000000233 15215712063 0014001 0 ustar 00 { "name": "moodle-availability_date-form", "builds": { "moodle-availability_date-form": { "jsfiles": [ "form.js" ] } } } condition/date/yui/src/form/meta/form.json 0000644 00000000254 15215712063 0014576 0 ustar 00 { "moodle-availability_date-form": { "requires": [ "base", "node", "event", "io", "moodle-core_availability-form" ] } } condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form.js 0000644 00000021713 15215712063 0025472 0 ustar 00 YUI.add('moodle-availability_date-form', function (Y, NAME) { /** * JavaScript for form editing date conditions. * * @module moodle-availability_date-form */ M.availability_date = M.availability_date || {}; /** * @class M.availability_date.form * @extends M.core_availability.plugin */ M.availability_date.form = Y.Object(M.core_availability.plugin); /** * Initialises this plugin. * * Because the date fields are complex depending on Moodle calendar settings, * we create the HTML for these fields in PHP and pass it to this method. * * @method initInner * @param {String} html HTML to use for date fields * @param {Number} defaultTime Time value that corresponds to initial fields */ M.availability_date.form.initInner = function(html, defaultTime) { this.html = html; this.defaultTime = defaultTime; }; M.availability_date.form.getNode = function(json) { var html = '<span class="col-form-label pe-3">' + M.util.get_string('direction_before', 'availability_date') + '</span> <span class="availability-group">' + '<label><span class="accesshide">' + M.util.get_string('direction_label', 'availability_date') + ' </span>' + '<select name="direction" class="custom-select">' + '<option value=">=">' + M.util.get_string('direction_from', 'availability_date') + '</option>' + '<option value="<">' + M.util.get_string('direction_until', 'availability_date') + '</option>' + '</select></label></span> ' + this.html; var node = Y.Node.create('<span>' + html + '</span>'); // Set initial value if non-default. if (json.t !== undefined) { node.setData('time', json.t); // Disable everything. node.all('select:not([name=direction])').each(function(select) { select.set('disabled', true); }); var url = M.cfg.wwwroot + '/availability/condition/date/ajax.php?action=fromtime' + '&time=' + json.t; Y.io(url, {on: { success: function(id, response) { var fields = Y.JSON.parse(response.responseText); for (var field in fields) { var select = node.one('select[name=x\\[' + field + '\\]]'); select.set('value', '' + fields[field]); select.set('disabled', false); } }, failure: function() { window.alert(M.util.get_string('ajaxerror', 'availability_date')); } }}); } else { // Set default time that corresponds to the HTML selectors. node.setData('time', this.defaultTime); } if (json.nodeUID === undefined) { var miliTime = new Date(); json.nodeUID = miliTime.getTime(); } node.setData('nodeUID', json.nodeUID); if (json.d !== undefined) { node.one('select[name=direction]').set('value', json.d); } // Add event handlers (first time only). if (!M.availability_date.form.addedEvents) { M.availability_date.form.addedEvents = true; var root = Y.one('.availability-field'); root.delegate('change', function() { // For the direction, just update the form fields. M.core_availability.form.update(); }, '.availability_date select[name=direction]'); root.delegate('change', function() { // Update time using AJAX call from root node. M.availability_date.form.updateTime(this.ancestor('span.availability_date')); }, '.availability_date select:not([name=direction])'); } if (node.one('a[href=#]')) { // Add the date selector magic. M.form.dateselector.init_single_date_selector(node); // This special handler detects when the date selector changes the year. var yearSelect = node.one('select[name=x\\[year\\]]'); var oldSet = yearSelect.set; yearSelect.set = function(name, value) { oldSet.call(yearSelect, name, value); if (name === 'selectedIndex') { // Do this after timeout or the other fields haven't been set yet. setTimeout(function() { M.availability_date.form.updateTime(node); }, 0); } }; } return node; }; /** * Updates time from AJAX. Whenever the field values change, we recompute the * actual time via an AJAX request to Moodle. * * This will set the 'time' data on the node and then update the form, once it * gets an AJAX response. * * @method updateTime * @param {Y.Node} node Node for plugin controls */ M.availability_date.form.updateTime = function(node) { // After a change to the date/time we need to recompute the // actual time using AJAX because it depends on the user's // time zone and calendar options. var url = M.cfg.wwwroot + '/availability/condition/date/ajax.php?action=totime' + '&year=' + node.one('select[name=x\\[year\\]]').get('value') + '&month=' + node.one('select[name=x\\[month\\]]').get('value') + '&day=' + node.one('select[name=x\\[day\\]]').get('value') + '&hour=' + node.one('select[name=x\\[hour\\]]').get('value') + '&minute=' + node.one('select[name=x\\[minute\\]]').get('value'); Y.io(url, {on: { success: function(id, response) { node.setData('time', response.responseText); M.core_availability.form.update(); }, failure: function() { window.alert(M.util.get_string('ajaxerror', 'availability_date')); } }}); }; M.availability_date.form.fillValue = function(value, node) { value.d = node.one('select[name=direction]').get('value'); value.t = parseInt(node.getData('time'), 10); value.nodeUID = node.getData('nodeUID'); }; /** * List out Date node value in the same branch. * * This will go through all array node and list nodes that are sibling of the current node. * * @method findAllDateSiblings * @param {Array} tree Tree items to convert * @param {Number} nodeUIDToFind node UID to find. * @return {Array|null} array of surrounding date avaiability values */ M.availability_date.form.findAllDateSiblings = function(tree, nodeUIDToFind) { var itemValue = null; var siblingsFinderRecursive = function(itemsTree) { var dateSiblings = []; var nodeFound = false; var index; var childDates; var currentOp = itemsTree.op !== undefined ? itemsTree.op : null; if (itemsTree.c !== undefined) { var children = itemsTree.c; for (index = 0; index < children.length; index++) { itemValue = children.at(index); if (itemValue.type === undefined) { childDates = siblingsFinderRecursive(itemValue); if (childDates) { return childDates; } } if (itemValue.type === 'date') { // We go through all tree node, if we meet the current node then we add all nodes in the current branch. if (nodeUIDToFind === itemValue.nodeUID) { nodeFound = true; } else if (currentOp === '&') { dateSiblings.push(itemValue); } } } if (nodeFound) { return dateSiblings; } } return null; }; return siblingsFinderRecursive(tree); }; /** * Check current node. * * This will check current date node with all date node in tree node. * * @method checkConditionDate * @param {Y.Node} currentNode The curent node. * * @return {boolean} error Return true if the date is conflict. */ M.availability_date.form.checkConditionDate = function(currentNode) { var error = false; var currentNodeUID = currentNode.getData('nodeUID'); var currentNodeDirection = currentNode.one('select[name=direction]').get('value'); var currentNodeTime = parseInt(currentNode.getData('time'), 10); var dateSiblings = M.availability_date.form.findAllDateSiblings( M.core_availability.form.rootList.getValue(), currentNodeUID); if (dateSiblings) { dateSiblings.forEach(function(dateSibling) { // Validate if the date is conflict. if (dateSibling.d === '<') { if (currentNodeDirection === '>=' && currentNodeTime >= dateSibling.t) { error = true; } } else { if (currentNodeDirection === '<' && currentNodeTime <= dateSibling.t) { error = true; } } return error; }); } return error; }; M.availability_date.form.fillErrors = function(errors, node) { var error = M.availability_date.form.checkConditionDate(node); if (error) { errors.push('availability_date:error_dateconflict'); } }; }, '@VERSION@', {"requires": ["base", "node", "event", "io", "moodle-core_availability-form"]}); condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-debug.js 0000644 00000021713 15215712063 0026556 0 ustar 00 YUI.add('moodle-availability_date-form', function (Y, NAME) { /** * JavaScript for form editing date conditions. * * @module moodle-availability_date-form */ M.availability_date = M.availability_date || {}; /** * @class M.availability_date.form * @extends M.core_availability.plugin */ M.availability_date.form = Y.Object(M.core_availability.plugin); /** * Initialises this plugin. * * Because the date fields are complex depending on Moodle calendar settings, * we create the HTML for these fields in PHP and pass it to this method. * * @method initInner * @param {String} html HTML to use for date fields * @param {Number} defaultTime Time value that corresponds to initial fields */ M.availability_date.form.initInner = function(html, defaultTime) { this.html = html; this.defaultTime = defaultTime; }; M.availability_date.form.getNode = function(json) { var html = '<span class="col-form-label pe-3">' + M.util.get_string('direction_before', 'availability_date') + '</span> <span class="availability-group">' + '<label><span class="accesshide">' + M.util.get_string('direction_label', 'availability_date') + ' </span>' + '<select name="direction" class="custom-select">' + '<option value=">=">' + M.util.get_string('direction_from', 'availability_date') + '</option>' + '<option value="<">' + M.util.get_string('direction_until', 'availability_date') + '</option>' + '</select></label></span> ' + this.html; var node = Y.Node.create('<span>' + html + '</span>'); // Set initial value if non-default. if (json.t !== undefined) { node.setData('time', json.t); // Disable everything. node.all('select:not([name=direction])').each(function(select) { select.set('disabled', true); }); var url = M.cfg.wwwroot + '/availability/condition/date/ajax.php?action=fromtime' + '&time=' + json.t; Y.io(url, {on: { success: function(id, response) { var fields = Y.JSON.parse(response.responseText); for (var field in fields) { var select = node.one('select[name=x\\[' + field + '\\]]'); select.set('value', '' + fields[field]); select.set('disabled', false); } }, failure: function() { window.alert(M.util.get_string('ajaxerror', 'availability_date')); } }}); } else { // Set default time that corresponds to the HTML selectors. node.setData('time', this.defaultTime); } if (json.nodeUID === undefined) { var miliTime = new Date(); json.nodeUID = miliTime.getTime(); } node.setData('nodeUID', json.nodeUID); if (json.d !== undefined) { node.one('select[name=direction]').set('value', json.d); } // Add event handlers (first time only). if (!M.availability_date.form.addedEvents) { M.availability_date.form.addedEvents = true; var root = Y.one('.availability-field'); root.delegate('change', function() { // For the direction, just update the form fields. M.core_availability.form.update(); }, '.availability_date select[name=direction]'); root.delegate('change', function() { // Update time using AJAX call from root node. M.availability_date.form.updateTime(this.ancestor('span.availability_date')); }, '.availability_date select:not([name=direction])'); } if (node.one('a[href=#]')) { // Add the date selector magic. M.form.dateselector.init_single_date_selector(node); // This special handler detects when the date selector changes the year. var yearSelect = node.one('select[name=x\\[year\\]]'); var oldSet = yearSelect.set; yearSelect.set = function(name, value) { oldSet.call(yearSelect, name, value); if (name === 'selectedIndex') { // Do this after timeout or the other fields haven't been set yet. setTimeout(function() { M.availability_date.form.updateTime(node); }, 0); } }; } return node; }; /** * Updates time from AJAX. Whenever the field values change, we recompute the * actual time via an AJAX request to Moodle. * * This will set the 'time' data on the node and then update the form, once it * gets an AJAX response. * * @method updateTime * @param {Y.Node} node Node for plugin controls */ M.availability_date.form.updateTime = function(node) { // After a change to the date/time we need to recompute the // actual time using AJAX because it depends on the user's // time zone and calendar options. var url = M.cfg.wwwroot + '/availability/condition/date/ajax.php?action=totime' + '&year=' + node.one('select[name=x\\[year\\]]').get('value') + '&month=' + node.one('select[name=x\\[month\\]]').get('value') + '&day=' + node.one('select[name=x\\[day\\]]').get('value') + '&hour=' + node.one('select[name=x\\[hour\\]]').get('value') + '&minute=' + node.one('select[name=x\\[minute\\]]').get('value'); Y.io(url, {on: { success: function(id, response) { node.setData('time', response.responseText); M.core_availability.form.update(); }, failure: function() { window.alert(M.util.get_string('ajaxerror', 'availability_date')); } }}); }; M.availability_date.form.fillValue = function(value, node) { value.d = node.one('select[name=direction]').get('value'); value.t = parseInt(node.getData('time'), 10); value.nodeUID = node.getData('nodeUID'); }; /** * List out Date node value in the same branch. * * This will go through all array node and list nodes that are sibling of the current node. * * @method findAllDateSiblings * @param {Array} tree Tree items to convert * @param {Number} nodeUIDToFind node UID to find. * @return {Array|null} array of surrounding date avaiability values */ M.availability_date.form.findAllDateSiblings = function(tree, nodeUIDToFind) { var itemValue = null; var siblingsFinderRecursive = function(itemsTree) { var dateSiblings = []; var nodeFound = false; var index; var childDates; var currentOp = itemsTree.op !== undefined ? itemsTree.op : null; if (itemsTree.c !== undefined) { var children = itemsTree.c; for (index = 0; index < children.length; index++) { itemValue = children.at(index); if (itemValue.type === undefined) { childDates = siblingsFinderRecursive(itemValue); if (childDates) { return childDates; } } if (itemValue.type === 'date') { // We go through all tree node, if we meet the current node then we add all nodes in the current branch. if (nodeUIDToFind === itemValue.nodeUID) { nodeFound = true; } else if (currentOp === '&') { dateSiblings.push(itemValue); } } } if (nodeFound) { return dateSiblings; } } return null; }; return siblingsFinderRecursive(tree); }; /** * Check current node. * * This will check current date node with all date node in tree node. * * @method checkConditionDate * @param {Y.Node} currentNode The curent node. * * @return {boolean} error Return true if the date is conflict. */ M.availability_date.form.checkConditionDate = function(currentNode) { var error = false; var currentNodeUID = currentNode.getData('nodeUID'); var currentNodeDirection = currentNode.one('select[name=direction]').get('value'); var currentNodeTime = parseInt(currentNode.getData('time'), 10); var dateSiblings = M.availability_date.form.findAllDateSiblings( M.core_availability.form.rootList.getValue(), currentNodeUID); if (dateSiblings) { dateSiblings.forEach(function(dateSibling) { // Validate if the date is conflict. if (dateSibling.d === '<') { if (currentNodeDirection === '>=' && currentNodeTime >= dateSibling.t) { error = true; } } else { if (currentNodeDirection === '<' && currentNodeTime <= dateSibling.t) { error = true; } } return error; }); } return error; }; M.availability_date.form.fillErrors = function(errors, node) { var error = M.availability_date.form.checkConditionDate(node); if (error) { errors.push('availability_date:error_dateconflict'); } }; }, '@VERSION@', {"requires": ["base", "node", "event", "io", "moodle-core_availability-form"]}); condition/date/yui/build/moodle-availability_date-form/moodle-availability_date-form-min.js 0000644 00000007317 15215712063 0026257 0 ustar 00 YUI.add("moodle-availability_date-form",function(o,e){M.availability_date=M.availability_date||{},M.availability_date.form=o.Object(M.core_availability.plugin),M.availability_date.form.initInner=function(e,a){this.html=e,this.defaultTime=a},M.availability_date.form.getNode=function(e){var t,i,a='<span class="col-form-label pe-3">'+M.util.get_string("direction_before","availability_date")+'</span> <span class="availability-group"><label><span class="accesshide">'+M.util.get_string("direction_label","availability_date")+' </span><select name="direction" class="custom-select"><option value=">=">'+M.util.get_string("direction_from","availability_date")+'</option><option value="<">'+M.util.get_string("direction_until","availability_date")+"</option></select></label></span> "+this.html,l=o.Node.create("<span>"+a+"</span>");return e.t!==undefined?(l.setData("time",e.t),l.all("select:not([name=direction])").each(function(e){e.set("disabled",!0)}),a=M.cfg.wwwroot+"/availability/condition/date/ajax.php?action=fromtime&time="+e.t,o.io(a,{on:{success:function(e,a){var t,i,n=o.JSON.parse(a.responseText);for(t in n)(i=l.one("select[name=x\\["+t+"\\]]")).set("value",""+n[t]),i.set("disabled",!1)},failure:function(){window.alert(M.util.get_string("ajaxerror","availability_date"))}}})):l.setData("time",this.defaultTime),e.nodeUID===undefined&&(a=new Date,e.nodeUID=a.getTime()),l.setData("nodeUID",e.nodeUID),e.d!==undefined&&l.one("select[name=direction]").set("value",e.d),M.availability_date.form.addedEvents||(M.availability_date.form.addedEvents=!0,(a=o.one(".availability-field")).delegate("change",function(){M.core_availability.form.update()},".availability_date select[name=direction]"),a.delegate("change",function(){M.availability_date.form.updateTime(this.ancestor("span.availability_date"))},".availability_date select:not([name=direction])")),l.one("a[href=#]")&&(M.form.dateselector.init_single_date_selector(l),t=l.one("select[name=x\\[year\\]]"),i=t.set,t.set=function(e,a){i.call(t,e,a),"selectedIndex"===e&&setTimeout(function(){M.availability_date.form.updateTime(l)},0)}),l},M.availability_date.form.updateTime=function(t){var e=M.cfg.wwwroot+"/availability/condition/date/ajax.php?action=totime&year="+t.one("select[name=x\\[year\\]]").get("value")+"&month="+t.one("select[name=x\\[month\\]]").get("value")+"&day="+t.one("select[name=x\\[day\\]]").get("value")+"&hour="+t.one("select[name=x\\[hour\\]]").get("value")+"&minute="+t.one("select[name=x\\[minute\\]]").get("value");o.io(e,{on:{success:function(e,a){t.setData("time",a.responseText),M.core_availability.form.update()},failure:function(){window.alert(M.util.get_string("ajaxerror","availability_date"))}}})},M.availability_date.form.fillValue=function(e,a){e.d=a.one("select[name=direction]").get("value"),e.t=parseInt(a.getData("time"),10),e.nodeUID=a.getData("nodeUID")},M.availability_date.form.findAllDateSiblings=function(e,d){var r,c=function(e){var a,t,i,n=[],l=!1,o=e.op!==undefined?e.op:null;if(e.c!==undefined){for(i=e.c,a=0;a<i.length;a++){if((r=i.at(a)).type===undefined&&(t=c(r)))return t;"date"===r.type&&(d===r.nodeUID?l=!0:"&"===o&&n.push(r))}if(l)return n}return null};return c(e)},M.availability_date.form.checkConditionDate=function(e){var a=!1,t=e.getData("nodeUID"),i=e.one("select[name=direction]").get("value"),n=parseInt(e.getData("time"),10),e=M.availability_date.form.findAllDateSiblings(M.core_availability.form.rootList.getValue(),t);return e&&e.forEach(function(e){return"<"===e.d?">="===i&&n>=e.t&&(a=!0):"<"===i&&n<=e.t&&(a=!0),a}),a},M.availability_date.form.fillErrors=function(e,a){M.availability_date.form.checkConditionDate(a)&&e.push("availability_date:error_dateconflict")}},"@VERSION@",{requires:["base","node","event","io","moodle-core_availability-form"]}); condition/date/ajax.php 0000644 00000003567 15215712063 0011120 0 ustar 00 <?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/>. /** * Handles AJAX processing (convert date to timestamp using current calendar). * * @package availability_date * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('AJAX_SCRIPT', true); require(__DIR__ . '/../../../config.php'); // Action verb. $action = required_param('action', PARAM_ALPHA); switch ($action) { case 'totime': // Converts from time fields to timestamp using current user's calendar and time zone. echo \availability_date\frontend::get_time_from_fields( required_param('year', PARAM_INT), required_param('month', PARAM_INT), required_param('day', PARAM_INT), required_param('hour', PARAM_INT), required_param('minute', PARAM_INT)); exit; case 'fromtime' : // Converts from timestamp to time fields. echo json_encode(\availability_date\frontend::get_fields_from_time( required_param('time', PARAM_INT))); exit; } // Unexpected actions throw coding_exception (this error should not occur // unless there is a code bug). throw new coding_exception('Unexpected action parameter'); condition/grade/classes/callbacks.php 0000644 00000003411 15215712063 0013702 0 ustar 00 <?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/>. /** * Observer handling events. * * @package availability_grade * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace availability_grade; defined('MOODLE_INTERNAL') || die(); /** * Callbacks handling grade changes (to clear cache). * * This ought to use the hooks system, but it doesn't exist - calls are * hard-coded. (The new event system is not suitable for this type of use.) * * @package availability_grade * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class callbacks { /** * A user grade has been updated in gradebook. * * @param int $userid User ID */ public static function grade_changed($userid) { \cache::make('availability_grade', 'scores')->delete($userid); } /** * A grade item has been updated in gradebook. * * @param int $courseid Course id */ public static function grade_item_changed($courseid) { \cache::make('availability_grade', 'items')->delete($courseid); } } condition/grade/classes/privacy/provider.php 0000644 00000003015 15215712063 0015272 0 ustar 00 <?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 availability_grade. * * @package availability_grade * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace availability_grade\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for availability_grade implementing null_provider. * * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @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'; } } condition/grade/classes/condition.php 0000644 00000031105 15215712063 0013752 0 ustar 00 <?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/>. /** * Condition on grades of current user. * * @package availability_grade * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace availability_grade; defined('MOODLE_INTERNAL') || die(); /** * Condition on grades of current user. * * @package availability_grade * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class condition extends \core_availability\condition { /** @var int Grade item id */ private $gradeitemid; /** @var float|null Min grade (must be >= this) or null if none */ private $min; /** @var float|null Max grade (must be < this) or null if none */ private $max; /** * Constructor. * * @param \stdClass $structure Data structure from JSON decode * @throws \coding_exception If invalid data structure. */ public function __construct($structure) { // Get grade item id. if (isset($structure->id) && is_int($structure->id)) { $this->gradeitemid = $structure->id; } else { throw new \coding_exception('Missing or invalid ->id for grade condition'); } // Get min and max. if (!property_exists($structure, 'min')) { $this->min = null; } else if (is_float($structure->min) || is_int($structure->min)) { $this->min = $structure->min; } else { throw new \coding_exception('Missing or invalid ->min for grade condition'); } if (!property_exists($structure, 'max')) { $this->max = null; } else if (is_float($structure->max) || is_int($structure->max)) { $this->max = $structure->max; } else { throw new \coding_exception('Missing or invalid ->max for grade condition'); } } public function save() { $result = (object)array('type' => 'grade', 'id' => $this->gradeitemid); if (!is_null($this->min)) { $result->min = $this->min; } if (!is_null($this->max)) { $result->max = $this->max; } return $result; } /** * Returns a JSON object which corresponds to a condition of this type. * * Intended for unit testing, as normally the JSON values are constructed * by JavaScript code. * * @param int $gradeitemid Grade item id * @param number|null $min Min grade (or null if no min) * @param number|null $max Max grade (or null if no max) * @return stdClass Object representing condition */ public static function get_json($gradeitemid, $min = null, $max = null) { $result = (object)array('type' => 'grade', 'id' => (int)$gradeitemid); if (!is_null($min)) { $result->min = $min; } if (!is_null($max)) { $result->max = $max; } return $result; } public function is_available($not, \core_availability\info $info, $grabthelot, $userid) { $course = $info->get_course(); $score = $this->get_cached_grade_score($this->gradeitemid, $course->id, $grabthelot, $userid); $allow = $score !== false && (is_null($this->min) || $score >= $this->min) && (is_null($this->max) || $score < $this->max); if ($not) { $allow = !$allow; } return $allow; } public function get_description($full, $not, \core_availability\info $info) { $course = $info->get_course(); // String depends on type of requirement. We are coy about // the actual numbers, in case grades aren't released to // students. if (is_null($this->min) && is_null($this->max)) { $string = 'any'; } else if (is_null($this->max)) { $string = 'min'; } else if (is_null($this->min)) { $string = 'max'; } else { $string = 'range'; } if ($not) { // The specific strings don't make as much sense with 'not'. if ($string === 'any') { $string = 'notany'; } else { $string = 'notgeneral'; } } // We cannot get the name at this point because it requires format_string which is not // allowed here. Instead, get it later with the callback function below. $name = $this->description_callback([$this->gradeitemid]); return get_string('requires_' . $string, 'availability_grade', $name); } /** * Gets the grade name at display time. * * @param \course_modinfo $modinfo Modinfo * @param \context $context Context * @param string[] $params Parameters (just grade item id) * @return string Text value */ public static function get_description_callback_value( \course_modinfo $modinfo, \context $context, array $params): string { if (count($params) !== 1 || !is_number($params[0])) { return '<!-- Invalid grade description callback -->'; } $gradeitemid = (int)$params[0]; return self::get_cached_grade_name($modinfo->get_course_id(), $gradeitemid); } protected function get_debug_string() { $out = '#' . $this->gradeitemid; if (!is_null($this->min)) { $out .= ' >= ' . sprintf('%.5f', $this->min); } if (!is_null($this->max)) { if (!is_null($this->min)) { $out .= ','; } $out .= ' < ' . sprintf('%.5f', $this->max); } return $out; } /** * Obtains the name of a grade item, also checking that it exists. Uses a * cache. The name returned is suitable for display. * * @param int $courseid Course id * @param int $gradeitemid Grade item id * @return string Grade name or empty string if no grade with that id */ private static function get_cached_grade_name($courseid, $gradeitemid) { global $DB, $CFG; require_once($CFG->libdir . '/gradelib.php'); // Get all grade item names from cache, or using db query. $cache = \cache::make('availability_grade', 'items'); if (($cacheditems = $cache->get($courseid)) === false) { // We cache the whole items table not the name; the format_string // call for the name might depend on current user (e.g. multilang) // and this is a shared cache. $cacheditems = $DB->get_records('grade_items', array('courseid' => $courseid)); $cache->set($courseid, $cacheditems); } // Return name from cached item or a lang string. if (!array_key_exists($gradeitemid, $cacheditems)) { return get_string('missing', 'availability_grade'); } $gradeitemobj = $cacheditems[$gradeitemid]; $item = new \grade_item; \grade_object::set_properties($item, $gradeitemobj); return $item->get_name(); } /** * Obtains a grade score. Note that this score should not be displayed to * the user, because gradebook rules might prohibit that. It may be a * non-final score subject to adjustment later. * * @param int $gradeitemid Grade item ID we're interested in * @param int $courseid Course id * @param bool $grabthelot If true, grabs all scores for current user on * this course, so that later ones come from cache * @param int $userid Set if requesting grade for a different user (does * not use cache) * @return float Grade score as a percentage in range 0-100 (e.g. 100.0 * or 37.21), or false if user does not have a grade yet */ protected static function get_cached_grade_score($gradeitemid, $courseid, $grabthelot=false, $userid=0) { global $USER, $DB; if (!$userid) { $userid = $USER->id; } $cache = \cache::make('availability_grade', 'scores'); if (($cachedgrades = $cache->get($userid)) === false) { $cachedgrades = array(); } if (!array_key_exists($gradeitemid, $cachedgrades)) { if ($grabthelot) { // Get all grades for the current course. $rs = $DB->get_recordset_sql(' SELECT gi.id,gg.finalgrade,gg.rawgrademin,gg.rawgrademax FROM {grade_items} gi LEFT JOIN {grade_grades} gg ON gi.id=gg.itemid AND gg.userid=? WHERE gi.courseid = ?', array($userid, $courseid)); foreach ($rs as $record) { // This function produces division by zero error warnings when rawgrademax and rawgrademin // are equal. Below change does not affect function behavior, just avoids the warning. if (is_null($record->finalgrade) || $record->rawgrademax == $record->rawgrademin) { // No grade = false. $cachedgrades[$record->id] = false; } else { // Otherwise convert grade to percentage. $cachedgrades[$record->id] = (($record->finalgrade - $record->rawgrademin) * 100) / ($record->rawgrademax - $record->rawgrademin); } } $rs->close(); // And if it's still not set, well it doesn't exist (eg // maybe the user set it as a condition, then deleted the // grade item) so we call it false. if (!array_key_exists($gradeitemid, $cachedgrades)) { $cachedgrades[$gradeitemid] = false; } } else { // Just get current grade. $record = $DB->get_record('grade_grades', array( 'userid' => $userid, 'itemid' => $gradeitemid)); // This function produces division by zero error warnings when rawgrademax and rawgrademin // are equal. Below change does not affect function behavior, just avoids the warning. if ($record && !is_null($record->finalgrade) && $record->rawgrademax != $record->rawgrademin) { $score = (($record->finalgrade - $record->rawgrademin) * 100) / ($record->rawgrademax - $record->rawgrademin); } else { // Treat the case where row exists but is null, same as // case where row doesn't exist. $score = false; } $cachedgrades[$gradeitemid] = $score; } $cache->set($userid, $cachedgrades); } return $cachedgrades[$gradeitemid]; } public function update_after_restore($restoreid, $courseid, \base_logger $logger, $name) { global $DB; $rec = \restore_dbops::get_backup_ids_record($restoreid, 'grade_item', $this->gradeitemid); if (!$rec || !$rec->newitemid) { // If we are on the same course (e.g. duplicate) then we can just // use the existing one. if ($DB->record_exists('grade_items', array('id' => $this->gradeitemid, 'courseid' => $courseid))) { return false; } // Otherwise it's a warning. $this->gradeitemid = 0; $logger->process('Restored item (' . $name . ') has availability condition on grade that was not restored', \backup::LOG_WARNING); } else { $this->gradeitemid = (int)$rec->newitemid; } return true; } public function update_dependency_id($table, $oldid, $newid) { if ($table === 'grade_items' && (int)$this->gradeitemid === (int)$oldid) { $this->gradeitemid = $newid; return true; } else { return false; } } } condition/grade/classes/frontend.php 0000644 00000005264 15215712063 0013612 0 ustar 00 <?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/>. /** * Front-end class. * * @package availability_grade * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace availability_grade; defined('MOODLE_INTERNAL') || die(); /** * Front-end class. * * @package availability_grade * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class frontend extends \core_availability\frontend { protected function get_javascript_strings() { return array('option_min', 'option_max', 'label_min', 'label_max'); } protected function get_javascript_init_params($course, ?\cm_info $cm = null, ?\section_info $section = null) { global $DB, $CFG; require_once($CFG->libdir . '/gradelib.php'); require_once($CFG->dirroot . '/course/lib.php'); // Get grades as basic associative array. $gradeoptions = array(); $items = \grade_item::fetch_all(array('courseid' => $course->id)); // For some reason the fetch_all things return null if none. $items = $items ? $items : array(); foreach ($items as $id => $item) { // Don't include the grade item if it's linked with a module that is being deleted. if (course_module_instance_pending_deletion($item->courseid, $item->itemmodule, $item->iteminstance)) { continue; } // Do not include grades for current item. if ($cm && $cm->instance == $item->iteminstance && $cm->modname == $item->itemmodule && $item->itemtype == 'mod') { continue; } $gradeoptions[$id] = $item->get_name(true); } \core_collator::asort($gradeoptions); // Change to JS array format and return. $jsarray = array(); foreach ($gradeoptions as $id => $name) { $jsarray[] = (object)array('id' => $id, 'name' => $name); } return array($jsarray); } } condition/grade/lang/en/availability_grade.php 0000644 00000004476 15215712063 0015501 0 ustar 00 <?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/>. /** * Language strings. * * @package availability_grade * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['cachedef_items'] = 'Grade items cached for evaluating conditional availability'; $string['cachedef_scores'] = 'User grades cached for evaluating conditional availability'; $string['description'] = 'Require students to achieve a specified grade.'; $string['error_backwardrange'] = 'When specifying a grade range, the minimum must be lower than the maximum.'; $string['error_invalidnumber'] = 'Grade ranges must be specified with valid percentages.'; $string['error_selectgradeid'] = 'You must select a grade item for the grade condition.'; $string['label_min'] = 'Minimum grade percentage (inclusive)'; $string['label_max'] = 'Maximum grade percentage (exclusive)'; $string['option_min'] = 'must be ≥'; $string['option_max'] = 'must be <'; $string['pluginname'] = 'Restriction by grades'; $string['requires_any'] = 'You have a grade in <strong>{$a}</strong>'; $string['requires_max'] = 'You achieve lower than a certain score in <strong>{$a}</strong>'; $string['requires_min'] = 'You achieve higher than a certain score in <strong>{$a}</strong>'; $string['requires_notany'] = 'You do not have a grade in <strong>{$a}</strong>'; $string['requires_notgeneral'] = 'You do not get certain scores in <strong>{$a}</strong>'; $string['requires_range'] = 'You achieve a score within a certain range in <strong>{$a}</strong>'; $string['missing'] = '(missing activity)'; $string['title'] = 'Grade'; $string['privacy:metadata'] = 'The Restriction by grades plugin does not store any personal data.'; condition/grade/version.php 0000644 00000001752 15215712063 0012021 0 ustar 00 <?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/>. /** * Version info. * * @package availability_grade * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2024100700; $plugin->requires = 2024100100; $plugin->component = 'availability_grade'; condition/grade/db/caches.php 0000644 00000002756 15215712063 0012154 0 ustar 00 <?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/>. /** * Cache definitions. * * @package availability_grade * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $definitions = array( // Used to cache user grades for conditional availability purposes. 'scores' => array( 'mode' => cache_store::MODE_APPLICATION, 'staticacceleration' => true, 'staticaccelerationsize' => 2, // Should not be required for more than one user at a time. 'ttl' => 3600, ), // Used to cache course grade items for conditional availability purposes. 'items' => array( 'mode' => cache_store::MODE_APPLICATION, 'staticacceleration' => true, 'staticaccelerationsize' => 2, // Should not be required for more than one course at a time. 'ttl' => 3600, ), ); condition/grade/tests/behat/availability_grade.feature 0000644 00000015122 15215712063 0017235 0 ustar 00 @availability @availability_grade Feature: availability_grade In order to control student access to activities As a teacher I need to set date conditions which prevent student access Background: Given the following "courses" exist: | fullname | shortname | format | enablecompletion | | Course 1 | C1 | topics | 1 | And the following "users" exist: | username | email | | teacher1 | t@example.com | | student1 | s@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | # Add an assignment. And the following "activities" exist: | activity | course | name | assignsubmission_onlinetext_enabled | | assign | C1 | A1 | 1 | | page | C1 | P1 | | | page | C1 | P2 | | | page | C1 | P3 | | | page | C1 | P4 | | @javascript Scenario: Test condition Given I am on the "P2" "page activity editing" page logged in as "teacher1" And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Grade" "button" in the "Add restriction..." "dialogue" And I click on ".availability-item .availability-eye img" "css_element" And I set the field "Grade" to "A1" And I press "Save and return to course" # Add a Page with a grade condition for 50%. And I am on the "P3" "page activity editing" page And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Grade" "button" in the "Add restriction..." "dialogue" And I click on ".availability-item .availability-eye img" "css_element" And I set the field "Grade" to "A1" And I click on "min" "checkbox" in the ".availability-item" "css_element" And I set the field "Minimum grade percentage (inclusive)" to "50" And I click on "max" "checkbox" in the ".availability-item" "css_element" And I set the field "Maximum grade percentage (exclusive)" to "80" And I press "Save and return to course" # Check if disabling a part of the restriction is get saved. And I am on the "P3" "page activity editing" page And I expand all fieldsets And I click on "max" "checkbox" in the ".availability-item" "css_element" And I press "Save and return to course" And I am on the "P3" "page activity editing" page And the field "Maximum grade percentage (exclusive)" matches value "" And I am on "Course 1" course homepage # Add a Page with a grade condition for 10%. And I am on the "P4" "page activity editing" page And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Grade" "button" in the "Add restriction..." "dialogue" And I click on ".availability-item .availability-eye img" "css_element" And I set the field "Grade" to "A1" And I click on "min" "checkbox" in the ".availability-item" "css_element" And I set the field "Minimum grade percentage (inclusive)" to "10" And I press "Save and return to course" # Log in as student without a grade yet. When I am on the "A1" "assign activity" page logged in as student1 # Do the assignment. And I click on "Add submission" "button" And I set the field "Online text" to "Q" And I click on "Save changes" "button" And I am on "Course 1" course homepage # None of the pages should appear (check assignment though). Then I should not see "P2" in the "region-main" "region" And I should not see "P3" in the "region-main" "region" And I should not see "P4" in the "region-main" "region" And I should see "A1" in the "region-main" "region" # Log back in as teacher. When I am on the "A1" "assign activity" page logged in as teacher1 And I change window size to "large" # Give the assignment 40%. And I go to "s@example.com" "A1" activity advanced grading page And I change window size to "medium" And I set the field "Grade out of 100" to "40" And I click on "Save changes" "button" And I click on "Edit settings" "link" And I log out # Log back in as student. And I am on the "Course 1" course page logged in as student1 # Check pages are visible. Then I should see "P2" in the "region-main" "region" And I should see "P4" in the "region-main" "region" And I should not see "P3" in the "region-main" "region" @javascript Scenario: Condition display with filters # Teacher sets up a restriction on group G1, using multilang filter. Given the following "activity" exists: | activity | assign | | name | <span lang="en" class="multilang">A-One</span><span lang="fr" class="multilang">A-Un</span> | | intro | Test | | course | C1 | | idnumber | 0001 | | section | 1 | And the "multilang" filter is "on" And the "multilang" filter applies to "content and headings" # The activity names filter is enabled because it triggered a bug in older versions. And the "activitynames" filter is "on" And the "activitynames" filter applies to "content and headings" And I am on the "C1" "Course" page logged in as "teacher1" And I turn editing mode on And I am on the "P1" "page activity editing" page And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Grade" "button" in the "Add restriction..." "dialogue" And I set the field "Grade" to "A-One" And I click on "min" "checkbox" in the ".availability-item" "css_element" And I set the field "Minimum grade percentage (inclusive)" to "10" And I press "Save and return to course" And I log out # Student sees information about no access to group, with group name in correct language. When I am on the "C1" "Course" page logged in as "student1" Then I should see "Not available unless: You achieve higher than a certain score in A-One" And I should not see "A-Un" condition/grade/tests/condition_test.php 0000644 00000025007 15215712063 0014522 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace availability_grade; /** * Unit tests for the grade condition. * * @package availability_grade * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class condition_test extends \advanced_testcase { /** * Tests constructing and using grade condition. */ public function test_usage(): void { global $USER, $CFG; require_once($CFG->dirroot . '/mod/assign/locallib.php'); $this->resetAfterTest(); $CFG->enableavailability = true; // Make a test course and user. $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($user->id, $course->id); // Make assign module. $assignrow = $this->getDataGenerator()->create_module('assign', array( 'course' => $course->id, 'name' => 'Test!')); $assign = new \assign(\context_module::instance($assignrow->cmid), false, false); $modinfo = get_fast_modinfo($course); $cm = $modinfo->get_cm($assignrow->cmid); $info = new \core_availability\info_module($cm); // Get the actual grade item. $item = $assign->get_grade_item(); // Construct tree with grade condition (any grade, specified item). $structure = (object)array('type' => 'grade', 'id' => (int)$item->id); $cond = new condition($structure); // Check if available (not available). $this->assertFalse($cond->is_available(false, $info, true, $user->id)); $information = $cond->get_description(false, false, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression('~have a grade.*Test!~', $information); $this->assertTrue($cond->is_available(true, $info, true, $user->id)); // Add grade and check available. self::set_grade($assignrow, $user->id, 37.2); $this->assertTrue($cond->is_available(false, $info, true, $user->id)); $this->assertFalse($cond->is_available(true, $info, true, $user->id)); $information = $cond->get_description(false, true, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression('~do not have a grade.*Test!~', $information); // Construct directly and test remaining conditions; first, min grade (fail). self::set_grade($assignrow, $user->id, 29.99999); $structure->min = 30.0; $cond = new condition($structure); $this->assertFalse($cond->is_available(false, $info, true, $user->id)); $information = $cond->get_description(false, false, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression('~achieve higher than.*Test!~', $information); $this->assertTrue($cond->is_available(true, $info, true, $user->id)); // Min grade (success). self::set_grade($assignrow, $user->id, 30); $this->assertTrue($cond->is_available(false, $info, true, $user->id)); $this->assertFalse($cond->is_available(true, $info, true, $user->id)); $information = $cond->get_description(false, true, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression('~do not get certain scores.*Test!~', $information); // Max grade (fail). unset($structure->min); $structure->max = 30.0; $cond = new condition($structure); $this->assertFalse($cond->is_available(false, $info, true, $user->id)); $information = $cond->get_description(false, false, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression('~achieve lower than a certain score in.*Test!~', $information); $this->assertTrue($cond->is_available(true, $info, true, $user->id)); // Max grade (success). self::set_grade($assignrow, $user->id, 29.99999); $this->assertTrue($cond->is_available(false, $info, true, $user->id)); $this->assertFalse($cond->is_available(true, $info, true, $user->id)); $information = $cond->get_description(false, true, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression('~do not get certain scores.*Test!~', $information); // Max and min (fail). $structure->min = 30.0; $structure->max = 34.12345; $cond = new condition($structure); $this->assertFalse($cond->is_available(false, $info, true, $user->id)); $information = $cond->get_description(false, false, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression('~achieve a score within a certain range.*Test!~', $information); $this->assertTrue($cond->is_available(true, $info, true, $user->id)); // Still fail (other end). self::set_grade($assignrow, $user->id, 34.12345); $this->assertFalse($cond->is_available(false, $info, true, $user->id)); // Success (top end). self::set_grade($assignrow, $user->id, 34.12344); $this->assertTrue($cond->is_available(false, $info, true, $user->id)); $this->assertFalse($cond->is_available(true, $info, true, $user->id)); $information = $cond->get_description(false, true, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression('~do not get certain scores.*Test!~', $information); // Success (bottom end). self::set_grade($assignrow, $user->id, 30.0); $this->assertTrue($cond->is_available(false, $info, true, $user->id)); $this->assertFalse($cond->is_available(true, $info, true, $user->id)); $information = $cond->get_description(false, true, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression('~do not get certain scores.*Test!~', $information); } /** * Tests the constructor including error conditions. Also tests the * string conversion feature (intended for debugging only). */ public function test_constructor(): void { // No parameters. $structure = new \stdClass(); try { $cond = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Missing or invalid ->id', $e->getMessage()); } // Invalid id (not int). $structure->id = 'bourne'; try { $cond = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Missing or invalid ->id', $e->getMessage()); } // Invalid min (not number). $structure->id = 42; $structure->min = 'ute'; try { $cond = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Missing or invalid ->min', $e->getMessage()); } // Invalid max (not number). $structure->min = 3.89; $structure->max = '9000'; try { $cond = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Missing or invalid ->max', $e->getMessage()); } // All valid. $structure->max = 4.0; $cond = new condition($structure); $this->assertEquals('{grade:#42 >= 3.89000, < 4.00000}', (string)$cond); // No max. unset($structure->max); $cond = new condition($structure); $this->assertEquals('{grade:#42 >= 3.89000}', (string)$cond); // No min. unset($structure->min); $structure->max = 32.768; $cond = new condition($structure); $this->assertEquals('{grade:#42 < 32.76800}', (string)$cond); // No nothing (only requires that grade exists). unset($structure->max); $cond = new condition($structure); $this->assertEquals('{grade:#42}', (string)$cond); } /** * Tests the save() function. */ public function test_save(): void { $structure = (object)array('id' => 19); $cond = new condition($structure); $structure->type = 'grade'; $this->assertEquals($structure, $cond->save()); $structure = (object)array('id' => 19, 'min' => 4.12345, 'max' => 90); $cond = new condition($structure); $structure->type = 'grade'; $this->assertEquals($structure, $cond->save()); } /** * Updates the grade of a user in the given assign module instance. * * @param \stdClass $assignrow Assignment row from database * @param int $userid User id * @param float $grade Grade */ protected static function set_grade($assignrow, $userid, $grade) { $grades = array(); $grades[$userid] = (object)array( 'rawgrade' => $grade, 'userid' => $userid); $assignrow->cmidnumber = null; assign_grade_item_update($assignrow, $grades); } /** * Tests the update_dependency_id() function. */ public function test_update_dependency_id(): void { $cond = new condition((object)array('id' => 123)); $this->assertFalse($cond->update_dependency_id('frogs', 123, 456)); $this->assertFalse($cond->update_dependency_id('grade_items', 12, 34)); $this->assertTrue($cond->update_dependency_id('grade_items', 123, 456)); $after = $cond->save(); $this->assertEquals(456, $after->id); } } condition/grade/yui/src/form/js/form.js 0000644 00000013717 15215712063 0014104 0 ustar 00 /** * JavaScript for form editing grade conditions. * * @module moodle-availability_grade-form */ M.availability_grade = M.availability_grade || {}; /** * @class M.availability_grade.form * @extends M.core_availability.plugin */ M.availability_grade.form = Y.Object(M.core_availability.plugin); /** * Grade items available for selection. * * @property grades * @type Array */ M.availability_grade.form.grades = null; /** * Initialises this plugin. * * @method initInner * @param {Array} grades Array of objects containing gradeid => name */ M.availability_grade.form.initInner = function(grades) { this.grades = grades; this.nodesSoFar = 0; }; M.availability_grade.form.getNode = function(json) { // Increment number used for unique ids. this.nodesSoFar++; // Create HTML structure. var html = '<label class="mb-3"><span class="pe-3">' + M.util.get_string('title', 'availability_grade') + '</span> ' + '<span class="availability-group">' + '<select name="id" class="custom-select"><option value="0">' + M.util.get_string('choosedots', 'moodle') + '</option>'; for (var i = 0; i < this.grades.length; i++) { var grade = this.grades[i]; // String has already been escaped using format_string. html += '<option value="' + grade.id + '">' + grade.name + '</option>'; } html += '</select></span></label> <br><span class="availability-group mb-3">' + '<label><input type="checkbox" class="form-check-input position-static mt-0 mx-1" name="min"/>' + M.util.get_string('option_min', 'availability_grade') + '</label> <label><span class="accesshide">' + M.util.get_string('label_min', 'availability_grade') + '</span><input type="text" class="form-control mx-1" name="minval" title="' + M.util.get_string('label_min', 'availability_grade') + '"/></label>%</span><br>' + '<span class="availability-group mb-3">' + '<label><input type="checkbox" class="form-check-input position-static mt-0 mx-1" name="max"/>' + M.util.get_string('option_max', 'availability_grade') + '</label> <label><span class="accesshide">' + M.util.get_string('label_max', 'availability_grade') + '</span><input type="text" class="form-control mx-1" name="maxval" title="' + M.util.get_string('label_max', 'availability_grade') + '"/></label>%</span>'; var node = Y.Node.create('<div class="d-inline-block d-flex flex-wrap align-items-center">' + html + '</div>'); // Set initial values. if (json.id !== undefined && node.one('select[name=id] > option[value=' + json.id + ']')) { node.one('select[name=id]').set('value', '' + json.id); } if (json.min !== undefined) { node.one('input[name=min]').set('checked', true); node.one('input[name=minval]').set('value', json.min); } if (json.max !== undefined) { node.one('input[name=max]').set('checked', true); node.one('input[name=maxval]').set('value', json.max); } // Disables/enables text input fields depending on checkbox. var updateCheckbox = function(check, focus) { var input = check.ancestor('label').next('label').one('input'); var checked = check.get('checked'); input.set('disabled', !checked); if (focus && checked) { input.focus(); } return checked; }; node.all('input[type=checkbox]').each(updateCheckbox); // Add event handlers (first time only). if (!M.availability_grade.form.addedEvents) { M.availability_grade.form.addedEvents = true; var root = Y.one('.availability-field'); root.delegate('change', function() { // For the grade item, just update the form fields. M.core_availability.form.update(); }, '.availability_grade select[name=id]'); root.delegate('click', function() { updateCheckbox(this, true); M.core_availability.form.update(); }, '.availability_grade input[type=checkbox]'); root.delegate('valuechange', function() { // For grade values, just update the form fields. M.core_availability.form.update(); }, '.availability_grade input[type=text]'); } return node; }; M.availability_grade.form.fillValue = function(value, node) { value.id = parseInt(node.one('select[name=id]').get('value'), 10); if (node.one('input[name=min]').get('checked')) { value.min = this.getValue('minval', node); } if (node.one('input[name=max]').get('checked')) { value.max = this.getValue('maxval', node); } }; /** * Gets the numeric value of an input field. Supports decimal points (using * dot or comma). * * @method getValue * @return {Number|String} Value of field as number or string if not valid */ M.availability_grade.form.getValue = function(field, node) { // Get field value. var value = node.one('input[name=' + field + ']').get('value'); // If it is not a valid positive number, return false. if (!(/^[0-9]+([.,][0-9]+)?$/.test(value))) { return value; } // Replace comma with dot and parse as floating-point. var result = parseFloat(value.replace(',', '.')); if (result < 0 || result > 100) { return value; } else { return result; } }; M.availability_grade.form.fillErrors = function(errors, node) { var value = {}; this.fillValue(value, node); // Check grade item id. if (value.id === 0) { errors.push('availability_grade:error_selectgradeid'); } // Check numeric values. if ((value.min !== undefined && typeof (value.min) === 'string') || (value.max !== undefined && typeof (value.max) === 'string')) { errors.push('availability_grade:error_invalidnumber'); } else if (value.min !== undefined && value.max !== undefined && value.min >= value.max) { errors.push('availability_grade:error_backwardrange'); } }; condition/grade/yui/src/form/build.json 0000644 00000000235 15215712063 0014150 0 ustar 00 { "name": "moodle-availability_grade-form", "builds": { "moodle-availability_grade-form": { "jsfiles": [ "form.js" ] } } } condition/grade/yui/src/form/meta/form.json 0000644 00000000237 15215712063 0014744 0 ustar 00 { "moodle-availability_grade-form": { "requires": [ "base", "node", "event", "moodle-core_availability-form" ] } } condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form-debug.js 0000644 00000014154 15215712063 0027236 0 ustar 00 YUI.add('moodle-availability_grade-form', function (Y, NAME) { /** * JavaScript for form editing grade conditions. * * @module moodle-availability_grade-form */ M.availability_grade = M.availability_grade || {}; /** * @class M.availability_grade.form * @extends M.core_availability.plugin */ M.availability_grade.form = Y.Object(M.core_availability.plugin); /** * Grade items available for selection. * * @property grades * @type Array */ M.availability_grade.form.grades = null; /** * Initialises this plugin. * * @method initInner * @param {Array} grades Array of objects containing gradeid => name */ M.availability_grade.form.initInner = function(grades) { this.grades = grades; this.nodesSoFar = 0; }; M.availability_grade.form.getNode = function(json) { // Increment number used for unique ids. this.nodesSoFar++; // Create HTML structure. var html = '<label class="mb-3"><span class="pe-3">' + M.util.get_string('title', 'availability_grade') + '</span> ' + '<span class="availability-group">' + '<select name="id" class="custom-select"><option value="0">' + M.util.get_string('choosedots', 'moodle') + '</option>'; for (var i = 0; i < this.grades.length; i++) { var grade = this.grades[i]; // String has already been escaped using format_string. html += '<option value="' + grade.id + '">' + grade.name + '</option>'; } html += '</select></span></label> <br><span class="availability-group mb-3">' + '<label><input type="checkbox" class="form-check-input position-static mt-0 mx-1" name="min"/>' + M.util.get_string('option_min', 'availability_grade') + '</label> <label><span class="accesshide">' + M.util.get_string('label_min', 'availability_grade') + '</span><input type="text" class="form-control mx-1" name="minval" title="' + M.util.get_string('label_min', 'availability_grade') + '"/></label>%</span><br>' + '<span class="availability-group mb-3">' + '<label><input type="checkbox" class="form-check-input position-static mt-0 mx-1" name="max"/>' + M.util.get_string('option_max', 'availability_grade') + '</label> <label><span class="accesshide">' + M.util.get_string('label_max', 'availability_grade') + '</span><input type="text" class="form-control mx-1" name="maxval" title="' + M.util.get_string('label_max', 'availability_grade') + '"/></label>%</span>'; var node = Y.Node.create('<div class="d-inline-block d-flex flex-wrap align-items-center">' + html + '</div>'); // Set initial values. if (json.id !== undefined && node.one('select[name=id] > option[value=' + json.id + ']')) { node.one('select[name=id]').set('value', '' + json.id); } if (json.min !== undefined) { node.one('input[name=min]').set('checked', true); node.one('input[name=minval]').set('value', json.min); } if (json.max !== undefined) { node.one('input[name=max]').set('checked', true); node.one('input[name=maxval]').set('value', json.max); } // Disables/enables text input fields depending on checkbox. var updateCheckbox = function(check, focus) { var input = check.ancestor('label').next('label').one('input'); var checked = check.get('checked'); input.set('disabled', !checked); if (focus && checked) { input.focus(); } return checked; }; node.all('input[type=checkbox]').each(updateCheckbox); // Add event handlers (first time only). if (!M.availability_grade.form.addedEvents) { M.availability_grade.form.addedEvents = true; var root = Y.one('.availability-field'); root.delegate('change', function() { // For the grade item, just update the form fields. M.core_availability.form.update(); }, '.availability_grade select[name=id]'); root.delegate('click', function() { updateCheckbox(this, true); M.core_availability.form.update(); }, '.availability_grade input[type=checkbox]'); root.delegate('valuechange', function() { // For grade values, just update the form fields. M.core_availability.form.update(); }, '.availability_grade input[type=text]'); } return node; }; M.availability_grade.form.fillValue = function(value, node) { value.id = parseInt(node.one('select[name=id]').get('value'), 10); if (node.one('input[name=min]').get('checked')) { value.min = this.getValue('minval', node); } if (node.one('input[name=max]').get('checked')) { value.max = this.getValue('maxval', node); } }; /** * Gets the numeric value of an input field. Supports decimal points (using * dot or comma). * * @method getValue * @return {Number|String} Value of field as number or string if not valid */ M.availability_grade.form.getValue = function(field, node) { // Get field value. var value = node.one('input[name=' + field + ']').get('value'); // If it is not a valid positive number, return false. if (!(/^[0-9]+([.,][0-9]+)?$/.test(value))) { return value; } // Replace comma with dot and parse as floating-point. var result = parseFloat(value.replace(',', '.')); if (result < 0 || result > 100) { return value; } else { return result; } }; M.availability_grade.form.fillErrors = function(errors, node) { var value = {}; this.fillValue(value, node); // Check grade item id. if (value.id === 0) { errors.push('availability_grade:error_selectgradeid'); } // Check numeric values. if ((value.min !== undefined && typeof (value.min) === 'string') || (value.max !== undefined && typeof (value.max) === 'string')) { errors.push('availability_grade:error_invalidnumber'); } else if (value.min !== undefined && value.max !== undefined && value.min >= value.max) { errors.push('availability_grade:error_backwardrange'); } }; }, '@VERSION@', {"requires": ["base", "node", "event", "moodle-core_availability-form"]}); condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form-min.js 0000644 00000006754 15215712063 0026742 0 ustar 00 YUI.add("moodle-availability_grade-form",function(r,a){M.availability_grade=M.availability_grade||{},M.availability_grade.form=r.Object(M.core_availability.plugin),M.availability_grade.form.grades=null,M.availability_grade.form.initInner=function(a){this.grades=a,this.nodesSoFar=0},M.availability_grade.form.getNode=function(a){var e,i,l,t,n;for(this.nodesSoFar++,e='<label class="mb-3"><span class="pe-3">'+M.util.get_string("title","availability_grade")+'</span> <span class="availability-group"><select name="id" class="custom-select"><option value="0">'+M.util.get_string("choosedots","moodle")+"</option>",i=0;i<this.grades.length;i++)e+='<option value="'+(l=this.grades[i]).id+'">'+l.name+"</option>";return e+='</select></span></label> <br><span class="availability-group mb-3"><label><input type="checkbox" class="form-check-input position-static mt-0 mx-1" name="min"/>'+M.util.get_string("option_min","availability_grade")+'</label> <label><span class="accesshide">'+M.util.get_string("label_min","availability_grade")+'</span><input type="text" class="form-control mx-1" name="minval" title="'+M.util.get_string("label_min","availability_grade")+'"/></label>%</span><br><span class="availability-group mb-3"><label><input type="checkbox" class="form-check-input position-static mt-0 mx-1" name="max"/>'+M.util.get_string("option_max","availability_grade")+'</label> <label><span class="accesshide">'+M.util.get_string("label_max","availability_grade")+'</span><input type="text" class="form-control mx-1" name="maxval" title="'+M.util.get_string("label_max","availability_grade")+'"/></label>%</span>',t=r.Node.create('<div class="d-inline-block d-flex flex-wrap align-items-center">'+e+"</div>"),a.id!==undefined&&t.one("select[name=id] > option[value="+a.id+"]")&&t.one("select[name=id]").set("value",""+a.id),a.min!==undefined&&(t.one("input[name=min]").set("checked",!0),t.one("input[name=minval]").set("value",a.min)),a.max!==undefined&&(t.one("input[name=max]").set("checked",!0),t.one("input[name=maxval]").set("value",a.max)),n=function(a,e){var i=a.ancestor("label").next("label").one("input"),a=a.get("checked");return i.set("disabled",!a),e&&a&&i.focus(),a},t.all("input[type=checkbox]").each(n),M.availability_grade.form.addedEvents||(M.availability_grade.form.addedEvents=!0,(a=r.one(".availability-field")).delegate("change",function(){M.core_availability.form.update()},".availability_grade select[name=id]"),a.delegate("click",function(){n(this,!0),M.core_availability.form.update()},".availability_grade input[type=checkbox]"),a.delegate("valuechange",function(){M.core_availability.form.update()},".availability_grade input[type=text]")),t},M.availability_grade.form.fillValue=function(a,e){a.id=parseInt(e.one("select[name=id]").get("value"),10),e.one("input[name=min]").get("checked")&&(a.min=this.getValue("minval",e)),e.one("input[name=max]").get("checked")&&(a.max=this.getValue("maxval",e))},M.availability_grade.form.getValue=function(a,e){a=e.one("input[name="+a+"]").get("value");return!/^[0-9]+([.,][0-9]+)?$/.test(a)||(e=parseFloat(a.replace(",",".")))<0||100<e?a:e},M.availability_grade.form.fillErrors=function(a,e){var i={};this.fillValue(i,e),0===i.id&&a.push("availability_grade:error_selectgradeid"),i.min!==undefined&&"string"==typeof i.min||i.max!==undefined&&"string"==typeof i.max?a.push("availability_grade:error_invalidnumber"):i.min!==undefined&&i.max!==undefined&&i.max<=i.min&&a.push("availability_grade:error_backwardrange")}},"@VERSION@",{requires:["base","node","event","moodle-core_availability-form"]}); condition/grade/yui/build/moodle-availability_grade-form/moodle-availability_grade-form.js 0000644 00000014154 15215712063 0026152 0 ustar 00 YUI.add('moodle-availability_grade-form', function (Y, NAME) { /** * JavaScript for form editing grade conditions. * * @module moodle-availability_grade-form */ M.availability_grade = M.availability_grade || {}; /** * @class M.availability_grade.form * @extends M.core_availability.plugin */ M.availability_grade.form = Y.Object(M.core_availability.plugin); /** * Grade items available for selection. * * @property grades * @type Array */ M.availability_grade.form.grades = null; /** * Initialises this plugin. * * @method initInner * @param {Array} grades Array of objects containing gradeid => name */ M.availability_grade.form.initInner = function(grades) { this.grades = grades; this.nodesSoFar = 0; }; M.availability_grade.form.getNode = function(json) { // Increment number used for unique ids. this.nodesSoFar++; // Create HTML structure. var html = '<label class="mb-3"><span class="pe-3">' + M.util.get_string('title', 'availability_grade') + '</span> ' + '<span class="availability-group">' + '<select name="id" class="custom-select"><option value="0">' + M.util.get_string('choosedots', 'moodle') + '</option>'; for (var i = 0; i < this.grades.length; i++) { var grade = this.grades[i]; // String has already been escaped using format_string. html += '<option value="' + grade.id + '">' + grade.name + '</option>'; } html += '</select></span></label> <br><span class="availability-group mb-3">' + '<label><input type="checkbox" class="form-check-input position-static mt-0 mx-1" name="min"/>' + M.util.get_string('option_min', 'availability_grade') + '</label> <label><span class="accesshide">' + M.util.get_string('label_min', 'availability_grade') + '</span><input type="text" class="form-control mx-1" name="minval" title="' + M.util.get_string('label_min', 'availability_grade') + '"/></label>%</span><br>' + '<span class="availability-group mb-3">' + '<label><input type="checkbox" class="form-check-input position-static mt-0 mx-1" name="max"/>' + M.util.get_string('option_max', 'availability_grade') + '</label> <label><span class="accesshide">' + M.util.get_string('label_max', 'availability_grade') + '</span><input type="text" class="form-control mx-1" name="maxval" title="' + M.util.get_string('label_max', 'availability_grade') + '"/></label>%</span>'; var node = Y.Node.create('<div class="d-inline-block d-flex flex-wrap align-items-center">' + html + '</div>'); // Set initial values. if (json.id !== undefined && node.one('select[name=id] > option[value=' + json.id + ']')) { node.one('select[name=id]').set('value', '' + json.id); } if (json.min !== undefined) { node.one('input[name=min]').set('checked', true); node.one('input[name=minval]').set('value', json.min); } if (json.max !== undefined) { node.one('input[name=max]').set('checked', true); node.one('input[name=maxval]').set('value', json.max); } // Disables/enables text input fields depending on checkbox. var updateCheckbox = function(check, focus) { var input = check.ancestor('label').next('label').one('input'); var checked = check.get('checked'); input.set('disabled', !checked); if (focus && checked) { input.focus(); } return checked; }; node.all('input[type=checkbox]').each(updateCheckbox); // Add event handlers (first time only). if (!M.availability_grade.form.addedEvents) { M.availability_grade.form.addedEvents = true; var root = Y.one('.availability-field'); root.delegate('change', function() { // For the grade item, just update the form fields. M.core_availability.form.update(); }, '.availability_grade select[name=id]'); root.delegate('click', function() { updateCheckbox(this, true); M.core_availability.form.update(); }, '.availability_grade input[type=checkbox]'); root.delegate('valuechange', function() { // For grade values, just update the form fields. M.core_availability.form.update(); }, '.availability_grade input[type=text]'); } return node; }; M.availability_grade.form.fillValue = function(value, node) { value.id = parseInt(node.one('select[name=id]').get('value'), 10); if (node.one('input[name=min]').get('checked')) { value.min = this.getValue('minval', node); } if (node.one('input[name=max]').get('checked')) { value.max = this.getValue('maxval', node); } }; /** * Gets the numeric value of an input field. Supports decimal points (using * dot or comma). * * @method getValue * @return {Number|String} Value of field as number or string if not valid */ M.availability_grade.form.getValue = function(field, node) { // Get field value. var value = node.one('input[name=' + field + ']').get('value'); // If it is not a valid positive number, return false. if (!(/^[0-9]+([.,][0-9]+)?$/.test(value))) { return value; } // Replace comma with dot and parse as floating-point. var result = parseFloat(value.replace(',', '.')); if (result < 0 || result > 100) { return value; } else { return result; } }; M.availability_grade.form.fillErrors = function(errors, node) { var value = {}; this.fillValue(value, node); // Check grade item id. if (value.id === 0) { errors.push('availability_grade:error_selectgradeid'); } // Check numeric values. if ((value.min !== undefined && typeof (value.min) === 'string') || (value.max !== undefined && typeof (value.max) === 'string')) { errors.push('availability_grade:error_invalidnumber'); } else if (value.min !== undefined && value.max !== undefined && value.min >= value.max) { errors.push('availability_grade:error_backwardrange'); } }; }, '@VERSION@', {"requires": ["base", "node", "event", "moodle-core_availability-form"]}); condition/UPGRADING.md 0000644 00000000515 15215712063 0010377 0 ustar 00 # availability (plugin type) Upgrade notes ## 4.5 ### Changed - The base class `\core_availability\info::get_groups()` method now accepts a `$userid` parameter to specify which user you want to retrieve course groups (defaults to current user). For more information see [MDL-81850](https://tracker.moodle.org/browse/MDL-81850) condition/completion/classes/privacy/provider.php 0000644 00000003041 15215712063 0016360 0 ustar 00 <?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 availability_completion. * * @package availability_completion * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace availability_completion\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for availability_completion implementing null_provider. * * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @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'; } } condition/completion/classes/condition.php 0000644 00000047106 15215712063 0015051 0 ustar 00 <?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/>. /** * Activity completion condition. * * @package availability_completion * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace availability_completion; use cache; use core_availability\info; use core_availability\info_module; use core_availability\info_section; use stdClass; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/completionlib.php'); /** * Activity completion condition. * * @package availability_completion * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class condition extends \core_availability\condition { /** @var int previous module cm value used to calculate relative completions */ public const OPTION_PREVIOUS = -1; /** @var int ID of module that this depends on */ protected $cmid; /** @var array IDs of the current module and section */ protected $selfids; /** @var int Expected completion type (one of the COMPLETE_xx constants) */ protected $expectedcompletion; /** @var array Array of previous cmids used to calculate relative completions */ protected $modfastprevious = []; /** @var array Array of cmids previous to each course section */ protected $sectionfastprevious = []; /** @var array Array of modules used in these conditions for course */ protected static $modsusedincondition = []; /** * Constructor. * * @param \stdClass $structure Data structure from JSON decode * @throws \coding_exception If invalid data structure. */ public function __construct($structure) { // Get cmid. if (isset($structure->cm) && is_number($structure->cm)) { $this->cmid = (int)$structure->cm; } else { throw new \coding_exception('Missing or invalid ->cm for completion condition'); } // Get expected completion. if (isset($structure->e) && in_array($structure->e, [COMPLETION_COMPLETE, COMPLETION_INCOMPLETE, COMPLETION_COMPLETE_PASS, COMPLETION_COMPLETE_FAIL])) { $this->expectedcompletion = $structure->e; } else { throw new \coding_exception('Missing or invalid ->e for completion condition'); } } /** * Saves tree data back to a structure object. * * @return stdClass Structure object (ready to be made into JSON format) */ public function save(): stdClass { return (object) [ 'type' => 'completion', 'cm' => $this->cmid, 'e' => $this->expectedcompletion, ]; } /** * Returns a JSON object which corresponds to a condition of this type. * * Intended for unit testing, as normally the JSON values are constructed * by JavaScript code. * * @param int $cmid Course-module id of other activity * @param int $expectedcompletion Expected completion value (COMPLETION_xx) * @return stdClass Object representing condition */ public static function get_json(int $cmid, int $expectedcompletion): stdClass { return (object) [ 'type' => 'completion', 'cm' => (int)$cmid, 'e' => (int)$expectedcompletion, ]; } /** * Determines whether a particular item is currently available * according to this availability condition. * * @see \core_availability\tree_node\update_after_restore * * @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 */ public function is_available($not, info $info, $grabthelot, $userid): bool { list($selfcmid, $selfsectionid) = $this->get_selfids($info); $cmid = $this->get_cmid($info->get_course(), $selfcmid, $selfsectionid); $modinfo = $info->get_modinfo(); $completion = new \completion_info($modinfo->get_course()); if (!array_key_exists($cmid, $modinfo->cms) || $modinfo->cms[$cmid]->deletioninprogress) { // If the cmid cannot be found, always return false regardless // of the condition or $not state. (Will be displayed in the // information message.) $allow = false; } else { // The completion system caches its own data so no caching needed here. $completiondata = $completion->get_data((object)['id' => $cmid], $grabthelot, $userid); $allow = true; if ($this->expectedcompletion == COMPLETION_COMPLETE) { // Complete also allows the pass state. switch ($completiondata->completionstate) { case COMPLETION_COMPLETE: case COMPLETION_COMPLETE_PASS: break; default: $allow = false; } } else if ($this->expectedcompletion == COMPLETION_INCOMPLETE) { // Incomplete also allows the fail state. switch ($completiondata->completionstate) { case COMPLETION_INCOMPLETE: case COMPLETION_COMPLETE_FAIL: break; default: $allow = false; } } else { // Other values require exact match. if ($completiondata->completionstate != $this->expectedcompletion) { $allow = false; } } if ($not) { $allow = !$allow; } } return $allow; } /** * Return current item IDs (cmid and sectionid). * * @param info $info * @return int[] with [0] => cmid/null, [1] => sectionid/null */ public function get_selfids(info $info): array { if (isset($this->selfids)) { return $this->selfids; } if ($info instanceof info_module) { $cminfo = $info->get_course_module(); if (!empty($cminfo->id)) { $this->selfids = [$cminfo->id, null]; return $this->selfids; } } if ($info instanceof info_section) { $section = $info->get_section(); if (!empty($section->id)) { $this->selfids = [null, $section->id]; return $this->selfids; } } return [null, null]; } /** * Get the cmid referenced in the access restriction. * * @param stdClass $course course object * @param int|null $selfcmid current course-module ID or null * @param int|null $selfsectionid current course-section ID or null * @return int|null cmid or null if no referenced cm is found */ public function get_cmid(stdClass $course, ?int $selfcmid, ?int $selfsectionid): ?int { if ($this->cmid > 0) { return $this->cmid; } // If it's a relative completion, load fast browsing. if ($this->cmid == self::OPTION_PREVIOUS) { $prevcmid = $this->get_previous_cmid($course, $selfcmid, $selfsectionid); if ($prevcmid) { return $prevcmid; } } return null; } /** * Return the previous CM ID of an specific course-module or course-section. * * @param stdClass $course course object * @param int|null $selfcmid course-module ID or null * @param int|null $selfsectionid course-section ID or null * @return int|null */ private function get_previous_cmid(stdClass $course, ?int $selfcmid, ?int $selfsectionid): ?int { $this->load_course_structure($course); if (isset($this->modfastprevious[$selfcmid])) { return $this->modfastprevious[$selfcmid]; } if (isset($this->sectionfastprevious[$selfsectionid])) { return $this->sectionfastprevious[$selfsectionid]; } return null; } /** * Loads static information about a course elements previous activities. * * Populates two variables: * - $this->sectionprevious[] course-module previous to a cmid * - $this->sectionfastprevious[] course-section previous to a cmid * * @param stdClass $course course object */ private function load_course_structure(stdClass $course): void { // If already loaded we don't need to do anything. if (empty($this->modfastprevious)) { $previouscache = cache::make('availability_completion', 'previous_cache'); $this->modfastprevious = $previouscache->get("mod_{$course->id}"); $this->sectionfastprevious = $previouscache->get("sec_{$course->id}"); } if (!empty($this->modfastprevious)) { return; } if (empty($this->modfastprevious)) { $this->modfastprevious = []; $sectionprevious = []; $modinfo = get_fast_modinfo($course); $lastcmid = 0; foreach ($modinfo->cms as $othercm) { if ($othercm->deletioninprogress) { continue; } // Save first cm of every section. if (!isset($sectionprevious[$othercm->section])) { $sectionprevious[$othercm->section] = $lastcmid; } if ($lastcmid) { $this->modfastprevious[$othercm->id] = $lastcmid; } // Load previous to all cms with completion. if ($othercm->completion == COMPLETION_TRACKING_NONE) { continue; } $lastcmid = $othercm->id; } // Fill empty sections index. $isections = array_reverse($modinfo->get_section_info_all()); foreach ($isections as $section) { if (isset($sectionprevious[$section->id])) { $lastcmid = $sectionprevious[$section->id]; } else { $sectionprevious[$section->id] = $lastcmid; } } $this->sectionfastprevious = $sectionprevious; $previouscache->set("mod_{$course->id}", $this->modfastprevious); $previouscache->set("sec_{$course->id}", $this->sectionfastprevious); } } /** * Returns a more readable keyword corresponding to a completion state. * * Used to make lang strings easier to read. * * @param int $completionstate COMPLETION_xx constant * @return string Readable keyword */ protected static function get_lang_string_keyword(int $completionstate): string { switch($completionstate) { case COMPLETION_INCOMPLETE: return 'incomplete'; case COMPLETION_COMPLETE: return 'complete'; case COMPLETION_COMPLETE_PASS: return 'complete_pass'; case COMPLETION_COMPLETE_FAIL: return 'complete_fail'; default: throw new \coding_exception('Unexpected completion state: ' . $completionstate); } } /** * Obtains a string describing this restriction (whether or not * it actually applies). * * @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_description($full, $not, info $info): string { global $USER; $str = 'requires_'; $course = $info->get_course(); list($selfcmid, $selfsectionid) = $this->get_selfids($info); $modname = ''; // On ajax duplicate get_fast_modinfo is called before $PAGE->set_context // so we cannot use $PAGE->user_is_editing(). $coursecontext = \context_course::instance($course->id); $editing = !empty($USER->editing) && has_capability('moodle/course:manageactivities', $coursecontext); if ($this->cmid == self::OPTION_PREVIOUS && $editing) { // Previous activity name could be inconsistent when editing due to partial page loadings. $str .= 'previous_'; } else { // Get name for module. $cmid = $this->get_cmid($course, $selfcmid, $selfsectionid); $modinfo = $info->get_modinfo(); if (!array_key_exists($cmid, $modinfo->cms) || $modinfo->cms[$cmid]->deletioninprogress) { $modname = get_string('missing', 'availability_completion'); } else { $modname = self::description_cm_name($modinfo->cms[$cmid]->id); } } // Work out which lang string to use depending on required completion status. if ($not) { // Convert NOT strings to use the equivalent where possible. switch ($this->expectedcompletion) { case COMPLETION_INCOMPLETE: $str .= self::get_lang_string_keyword(COMPLETION_COMPLETE); break; case COMPLETION_COMPLETE: $str .= self::get_lang_string_keyword(COMPLETION_INCOMPLETE); break; default: // The other two cases do not have direct opposites. $str .= 'not_' . self::get_lang_string_keyword($this->expectedcompletion); break; } } else { $str .= self::get_lang_string_keyword($this->expectedcompletion); } return get_string($str, 'availability_completion', $modname); } /** * Obtains a representation of the options of this condition as a string, * for debugging. * * @return string Text representation of parameters */ protected function get_debug_string(): string { switch ($this->expectedcompletion) { case COMPLETION_COMPLETE : $type = 'COMPLETE'; break; case COMPLETION_INCOMPLETE : $type = 'INCOMPLETE'; break; case COMPLETION_COMPLETE_PASS: $type = 'COMPLETE_PASS'; break; case COMPLETION_COMPLETE_FAIL: $type = 'COMPLETE_FAIL'; break; default: throw new \coding_exception('Unexpected expected completion'); } $cm = $this->cmid; if ($this->cmid == self::OPTION_PREVIOUS) { $cm = 'opprevious'; } return 'cm' . $cm . ' ' . $type; } /** * Updates this node after restore, returning true if anything changed. * * @see \core_availability\tree_node\update_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) * @return bool True if there was any change */ public function update_after_restore($restoreid, $courseid, \base_logger $logger, $name): bool { global $DB; $res = false; // If we depend on the previous activity, no translation is needed. if ($this->cmid == self::OPTION_PREVIOUS) { return $res; } $rec = \restore_dbops::get_backup_ids_record($restoreid, 'course_module', $this->cmid); if (!$rec || !$rec->newitemid) { // If we are on the same course (e.g. duplicate) then we can just // use the existing one. if ($DB->record_exists('course_modules', ['id' => $this->cmid, 'course' => $courseid])) { return $res; } // Otherwise it's a warning. $this->cmid = 0; $logger->process('Restored item (' . $name . ') has availability condition on module that was not restored', \backup::LOG_WARNING); } else { $this->cmid = (int)$rec->newitemid; } return true; } /** * Used in course/lib.php because we need to disable the completion JS if * a completion value affects 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): bool { // Have we already worked out a list of required completion values // for this course? If so just use that. if (!array_key_exists($course->id, self::$modsusedincondition)) { // We don't have data for this course, build it. $modinfo = get_fast_modinfo($course); self::$modsusedincondition[$course->id] = []; // Activities. foreach ($modinfo->cms as $othercm) { if (is_null($othercm->availability)) { continue; } $ci = new \core_availability\info_module($othercm); $tree = $ci->get_availability_tree(); foreach ($tree->get_all_children('availability_completion\condition') as $cond) { $condcmid = $cond->get_cmid($course, $othercm->id, null); if (!empty($condcmid)) { self::$modsusedincondition[$course->id][$condcmid] = true; } } } // Sections. foreach ($modinfo->get_section_info_all() as $section) { if (is_null($section->availability)) { continue; } $ci = new \core_availability\info_section($section); $tree = $ci->get_availability_tree(); foreach ($tree->get_all_children('availability_completion\condition') as $cond) { $condcmid = $cond->get_cmid($course, null, $section->id); if (!empty($condcmid)) { self::$modsusedincondition[$course->id][$condcmid] = true; } } } } return array_key_exists($cmid, self::$modsusedincondition[$course->id]); } /** * Wipes the static cache of modules used in a condition (for unit testing). */ public static function wipe_static_cache() { self::$modsusedincondition = []; } public function update_dependency_id($table, $oldid, $newid) { if ($table === 'course_modules' && (int)$this->cmid === (int)$oldid) { $this->cmid = $newid; return true; } else { return false; } } } condition/completion/classes/frontend.php 0000644 00000010045 15215712063 0014672 0 ustar 00 <?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/>. /** * Front-end class. * * @package availability_completion * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace availability_completion; defined('MOODLE_INTERNAL') || die(); /** * Front-end class. * * @package availability_completion * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class frontend extends \core_availability\frontend { /** * @var array Cached init parameters */ protected $cacheinitparams = []; /** * @var string IDs of course, cm, and section for cache (if any) */ protected $cachekey = ''; protected function get_javascript_strings() { return ['option_complete', 'option_fail', 'option_incomplete', 'option_pass', 'label_cm', 'label_completion']; } protected function get_javascript_init_params($course, ?\cm_info $cm = null, ?\section_info $section = null) { // Use cached result if available. The cache is just because we call it // twice (once from allow_add) so it's nice to avoid doing all the // print_string calls twice. $cachekey = $course->id . ',' . ($cm ? $cm->id : '') . ($section ? $section->id : ''); if ($cachekey !== $this->cachekey) { // Get list of activities on course which have completion values, // to fill the dropdown. $context = \context_course::instance($course->id); $cms = []; $modinfo = get_fast_modinfo($course); $previouscm = false; foreach ($modinfo->cms as $id => $othercm) { // Add each course-module if it has completion turned on and is not // the one currently being edited. if ($othercm->completion && (empty($cm) || $cm->id != $id) && !$othercm->deletioninprogress) { $cms[] = (object)['id' => $id, 'name' => format_string($othercm->name, true, ['context' => $context]), 'completiongradeitemnumber' => $othercm->completiongradeitemnumber]; } if (count($cms) && (empty($cm) || $cm->id == $id)) { $previouscm = true; } } if ($previouscm) { $previous = (object)['id' => \availability_completion\condition::OPTION_PREVIOUS, 'name' => get_string('option_previous', 'availability_completion'), 'completiongradeitemnumber' => \availability_completion\condition::OPTION_PREVIOUS]; array_unshift($cms, $previous); } $this->cachekey = $cachekey; $this->cacheinitparams = [$cms]; } return $this->cacheinitparams; } protected function allow_add($course, ?\cm_info $cm = null, ?\section_info $section = null) { global $CFG; // Check if completion is enabled for the course. require_once($CFG->libdir . '/completionlib.php'); $info = new \completion_info($course); if (!$info->is_enabled()) { return false; } // Check if there's at least one other module with completion info. $params = $this->get_javascript_init_params($course, $cm, $section); return ((array)$params[0]) != false; } } condition/completion/lang/en/availability_completion.php 0000644 00000006260 15215712063 0017650 0 ustar 00 <?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/>. /** * Language strings. * * @package availability_completion * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['cachedef_previous_cache'] = 'Previous activity dependency information'; $string['description'] = 'Require students to complete (or not complete) another activity.'; $string['error_selectcmid'] = 'You must select an activity for the completion condition.'; $string['error_selectcmidpassfail'] = 'You must select an activity with "Require grade" completion condition set.'; $string['label_cm'] = 'Activity or resource'; $string['label_completion'] = 'Required completion status'; $string['missing'] = '(Missing activity)'; $string['option_complete'] = 'must be marked complete'; $string['option_fail'] = 'must be complete with fail grade'; $string['option_incomplete'] = 'must not be marked complete'; $string['option_pass'] = 'must be complete with pass grade'; $string['option_previous'] = 'Previous activity with completion'; $string['pluginname'] = 'Restriction by activity completion'; $string['requires_incomplete'] = 'The activity <strong>{$a}</strong> is incomplete'; $string['requires_complete'] = 'The activity <strong>{$a}</strong> is marked complete'; $string['requires_complete_pass'] = 'The activity <strong>{$a}</strong> is complete and passed'; $string['requires_complete_fail'] = 'The activity <strong>{$a}</strong> is complete and failed'; $string['requires_not_complete_pass'] = 'The activity <strong>{$a}</strong> is not complete and passed'; $string['requires_not_complete_fail'] = 'The activity <strong>{$a}</strong> is not complete and failed'; $string['requires_previous_incomplete'] = 'The <strong>previous activity with completion</strong> is incomplete'; $string['requires_previous_complete'] = 'The <strong>previous activity with completion</strong> is marked complete'; $string['requires_previous_complete_pass'] = 'The <strong>previous activity with completion</strong> is complete and passed'; $string['requires_previous_complete_fail'] = 'The <strong>previous activity with completion</strong> is complete and failed'; $string['requires_previous_not_complete_pass'] = 'The <strong>previous activity with completion</strong> is not complete and passed'; $string['requires_previous_not_complete_fail'] = 'The <strong>previous activity with completion</strong> is not complete and failed'; $string['title'] = 'Activity completion'; $string['privacy:metadata'] = 'The Restriction by activity completion plugin does not store any personal data.'; condition/completion/version.php 0000644 00000001764 15215712063 0013113 0 ustar 00 <?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/>. /** * Version info. * * @package availability_completion * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2024100700; $plugin->requires = 2024100100; $plugin->component = 'availability_completion'; condition/completion/db/caches.php 0000644 00000002226 15215712063 0013233 0 ustar 00 <?php // This file is part of Moodle - https://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/>. /** * Defined caches used internally by the plugin. * * @package availability_completion * @category cache * @copyright 2020 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $definitions = [ 'previous_cache' => [ 'mode' => cache_store::MODE_REQUEST, 'simplekeys' => true, 'simpledata' => true, 'staticacceleration' => true ], ]; condition/completion/tests/behat/conditional_bug.feature 0000644 00000004424 15215712063 0017653 0 ustar 00 @availability @availability_completion Feature: Confirm that conditions on completion no longer cause a bug In order to use completion conditions As a teacher I need it to not break when I set up certain conditions on some modules Background: Given the following "courses" exist: | fullname | shortname | format | enablecompletion | | Course 1 | C1 | topics | 1 | And the following "users" exist: | username | | teacher1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | @javascript Scenario: Multiple completion conditions on glossary # Add a couple of Pages with manual completion. And the following "activities" exist: | activity | course | name | completion | | page | C1 | Page1 | 1 | | page | C1 | Page2 | 1 | And I log in as "teacher1" # Add a Glossary. When I add a glossary activity to course "Course 1" section "1" And I set the following fields to these values: | Name | TestGlossary | And I expand all fieldsets # Add restrictions to the previous Pages being complete. And I press "Add restriction..." And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" And I set the field "Activity or resource" to "Page1" And I press "Add restriction..." And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" And I set the field with xpath "//div[contains(concat(' ', normalize-space(@class), ' '), ' availability-item ')][preceding-sibling::div]//select[@name='cm']" to "Page2" And I press "Save and return to course" And I click on "Show more" "button" in the "TestGlossary" "core_availability > Activity availability" Then I should see "Not available unless:" in the ".activity.glossary" "css_element" And I should see "The activity Page1 is marked complete" in the ".activity.glossary" "css_element" And I should see "The activity Page2 is marked complete" in the ".activity.glossary" "css_element" # Behat will automatically check there is no error on this page. And I am on the TestGlossary "glossary activity" page And I should see "TestGlossary" condition/completion/tests/behat/availability_completion.feature 0000644 00000016727 15215712063 0021427 0 ustar 00 @availability @availability_completion Feature: availability_completion In order to control student access to activities As a teacher I need to set completion conditions which prevent student access Background: Given the following "courses" exist: | fullname | shortname | format | enablecompletion | | Course 1 | C1 | topics | 1 | And the following "users" exist: | username | | teacher1 | | student1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | course | name | completion | | page | C1 | Page 1 | 1 | | page | C1 | Page 2 | | | page | C1 | Page 3 | 1 | | page | C1 | Page 4 | | @javascript Scenario: Test condition # Basic setup. Given I am on the "Page 2" "page activity editing" page logged in as "teacher1" And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" And I click on ".availability-item .availability-eye img" "css_element" And I set the field "Activity or resource" to "Page 1" And I press "Save and return to course" # Log back in as student. When I am on the "Course 1" "course" page logged in as "student1" # Page 2 should not appear yet. Then I should not see "Page 2" in the "region-main" "region" # Mark page 1 complete When I toggle the manual completion state of "Page 1" Then I should see "Page 2" in the "region-main" "region" @javascript Scenario: Test completion and course cache rebuild Given the following "activities" exist: | activity | name | course | idnumber | completion | completionview | completionpostsenabled | completionposts | | forum | forum 1 | C1 | forum1 | 2 | 1 | 1 | 2 | And the following "mod_forum > discussions" exist: | forum | subject | message | | forum1 | Forum post 1 | This is the body | And I am on the "Page 2" "page activity editing" page logged in as "teacher1" And I expand all fieldsets And I press "Add restriction..." And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" And I click on ".availability-item .availability-eye img" "css_element" And I set the following fields to these values: | Required completion status | must be marked complete | | cm | forum 1 | And I press "Save and return to course" When I am on the "Course 1" "course" page logged in as "student1" # Page 2 should not appear yet. Then I should not see "Page 2" in the "region-main" "region" And I click on "forum 1" "link" in the "region-main" "region" # Page 2 should not appear yet. And I should not see "Page 2" in the "region-main" "region" And I am on the "forum 1" "forum activity editing" page logged in as "teacher1" And I expand all fieldsets And I set the following fields to these values: | completionpostsenabled | 0 | And I press "Save and display" And I am on the "Course 1" "course" page logged in as "student1" And I click on "forum 1" "link" in the "region-main" "region" And I am on "Course 1" course homepage And I should see "Page 2" in the "region-main" "region" @javascript Scenario Outline: Restrict access for activity completion should display correctly Given the following "question categories" exist: | contextlevel | reference | name | | Course | C1 | Test questions | And the following "questions" exist: | questioncategory | qtype | name | questiontext | | Test questions | truefalse | First question | Answer the first question | And the following "activities" exist: | activity | name | course | idnumber | gradepass | completion | completionpassgrade | completionusegrade | | quiz | Test quiz name | C1 | quiz1 | 5.00 | 2 | 1 | 1 | And quiz "Test quiz name" contains the following questions: | question | page | | First question | 1 | And I am on the "Page 2" "page activity editing" page logged in as "teacher1" And I expand all fieldsets And I press "Add restriction..." And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" And I click on ".availability-item .availability-eye img" "css_element" And I set the following fields to these values: | Required completion status | <condition> | | cm | quiz | And I press "Save and return to course" And I am on the "Course 1" "course" page logged in as "student1" And I <shouldornot> see "Page 2" in the "region-main" "region" # Failed grade for quiz. When user "student1" has attempted "Test quiz name" with responses: | slot | response | | 1 | <answer1> | And I reload the page And I <shouldornotanswer1> see "Page 2" in the "region-main" "region" # Passing grade for quiz. But user "student1" has attempted "Test quiz name" with responses: | slot | response | | 1 | <answer2> | And I reload the page And I <shouldornotanswer2> see "Page 2" in the "region-main" "region" Examples: | condition | answer1 | answer2 | shouldornot | shouldornotanswer1 | shouldornotanswer2 | | must be marked complete | False | True | should not | should not | should | | must not be marked complete | False | True | should | should | should not | | must be complete with pass grade | False | True | should not | should not | should | | must be complete with fail grade | False | True | should not | should | should not | @javascript Scenario: Edit dependent activity name should also change the access restriction message Given I am on the "Page 2" "page activity editing" page logged in as "teacher1" And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" And I click on ".availability-item .availability-eye img" "css_element" And I set the field "Activity or resource" to "Page 1" And I press "Save and return to course" And I am on the "Page 4" "page activity editing" page And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" And I click on ".availability-item .availability-eye img" "css_element" And I set the field "Activity or resource" to "Page 3" And I press "Save and return to course" And I switch editing mode on And I set the field "Edit title" in the "Page 1" "activity" to "Page X" And I wait until the page is ready Then I should see "Not available unless: The activity Page X is marked complete" in the "Page 2" "activity" Then I should see "Not available unless: The activity Page 3 is marked complete" in the "Page 4" "activity" condition/completion/tests/behat/availability_completion_previous.feature 0000644 00000024735 15215712063 0023361 0 ustar 00 @availability @availability_completion Feature: Confirm that availability_completion works with previous activity setting In order to control student access to activities As a teacher I need to set completion conditions which prevent student access Background: Given the following "courses" exist: | fullname | shortname | format | enablecompletion | numsections | | Course 1 | C1 | topics | 1 | 5 | And the following "users" exist: | username | | teacher1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | Given the following "activities" exist: | activity | name | intro | course | idnumber | groupmode | completion | section | | page | Page1 | Page 1 description | C1 | page1 | 1 | 1 | 1 | | page | Page Ignored 1 | Page Ignored | C1 | pagei1 | 1 | 0 | 1 | | page | Page2 | Page 2 description | C1 | page2 | 1 | 1 | 3 | | page | Page3 | Page 3 description | C1 | page3 | 1 | 1 | 4 | @javascript Scenario: Test condition with previous activity on an activity Given I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on # Set Page3 restriction to Previous Activity with completion. When I open "Page3" actions menu And I click on "Edit settings" "link" in the "Page3" activity And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" And I click on "Displayed if student doesn't meet this condition • Click to hide" "link" And I set the field "Activity or resource" to "Previous activity with completion" And I press "Save and return to course" Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region" When I turn editing mode off Then I should see "Not available unless: The activity Page2 is marked complete" in the "region-main" "region" # Remove Page 2 and check Page3 depends now on Page1. When I turn editing mode on And I change window size to "large" And I delete "Page2" activity And I turn editing mode off Then I should see "Not available unless: The activity Page1 is marked complete" in the "region-main" "region" @javascript Scenario: Test previous activity availability when duplicate an activity Given I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on # Set Page3 restriction to Previous Activity with completion. When I open "Page3" actions menu And I click on "Edit settings" "link" in the "Page3" activity And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" And I click on "Displayed if student doesn't meet this condition • Click to hide" "link" And I set the field "Activity or resource" to "Previous activity with completion" And I press "Save and return to course" Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region" When I turn editing mode off Then I should see "Not available unless: The activity Page2 is marked complete" in the "region-main" "region" # Duplicate Page3. When I turn editing mode on And I duplicate "Page3" activity And I turn editing mode off Then I should see "Not available unless: The activity Page3 is marked complete" in the "region-main" "region" @javascript Scenario: Test previous activity availability when modify completion tacking Given I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on # Set Page3 restriction to Previous Activity with completion. When I open "Page3" actions menu And I click on "Edit settings" "link" in the "Page3" activity And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" And I click on "Displayed if student doesn't meet this condition • Click to hide" "link" And I set the field "Activity or resource" to "Previous activity with completion" And I press "Save and return to course" Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region" When I turn editing mode off Then I should see "Not available unless: The activity Page2 is marked complete" in the "region-main" "region" # Test if I disable completion tracking on Page2 section 5 depends on Page2. When I turn editing mode on And I change window size to "large" When I open "Page2" actions menu And I click on "Edit settings" "link" in the "Page2" activity And I set the following fields to these values: | None | 1 | And I press "Save and return to course" When I turn editing mode off Then I should see "Not available unless: The activity Page1 is marked complete" in the "region-main" "region" @javascript Scenario: Test condition with previous activity on a section Given I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on # Set section 4 restriction to Previous Activity with completion. When I edit the section "4" And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" And I click on "Displayed if student doesn't meet this condition • Click to hide" "link" And I set the field "Activity or resource" to "Previous activity with completion" And I press "Save changes" Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region" When I turn editing mode off Then I should see "Not available unless: The activity Page2 is marked complete" in the "region-main" "region" # Remove Page 2 and check Section 4 depends now on Page1. When I am on "Course 1" course homepage with editing mode on And I change window size to "large" And I delete "Page2" activity And I turn editing mode off Then I should see "Not available unless: The activity Page1 is marked complete" in the "region-main" "region" @javascript Scenario: Test condition with previous activity on the first activity of the course Given I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on # Try to set Page1 restriction to Previous Activity with completion. When I open "Page1" actions menu And I click on "Edit settings" "link" in the "Page1" activity And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" And I click on "Displayed if student doesn't meet this condition • Click to hide" "link" Then the "Activity or resource" select box should not contain "Previous activity with completion" # Set Page2 restriction to Previous Activity with completion and delete Page1. When I am on "Course 1" course homepage When I open "Page2" actions menu And I click on "Edit settings" "link" in the "Page2" activity And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" And I click on "Displayed if student doesn't meet this condition • Click to hide" "link" And I set the field "Activity or resource" to "Previous activity with completion" And I press "Save and return to course" Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region" # Delete Page 1 and check than Page2 now depends on a missing activity (no previous activity found). When I am on "Course 1" course homepage And I delete "Page1" activity And I turn editing mode off Then I should see "Not available unless: The activity (Missing activity)" in the "region-main" "region" @javascript Scenario: Test previous activities on empty sections Given I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And I change window size to "large" # Set section 2 restriction to Previous Activity with completion. When I edit the section "2" And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" And I click on "Displayed if student doesn't meet this condition • Click to hide" "link" And I set the field "Activity or resource" to "Previous activity with completion" And I press "Save changes" Then I should see "Not available unless: The previous activity with completion" in the "region-main" "region" And I turn editing mode off And I should see "Not available unless: The activity Page1 is marked complete" in the "region-main" "region" # Set section 5 restriction to Previous Activity with completion. And I am on "Course 1" course homepage with editing mode on And I edit the section "5" And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Activity completion" "button" in the "Add restriction..." "dialogue" And I click on "Displayed if student doesn't meet this condition • Click to hide" "link" And I set the field "Activity or resource" to "Previous activity with completion" And I press "Save changes" And I should see "Not available unless: The previous activity with completion" in the "region-main" "region" And I turn editing mode off Then I should see "Not available unless: The activity Page3 is marked complete" in the "region-main" "region" # Test if I disable completion tracking on Page3 section 5 depends on Page2. And I am on "Course 1" course homepage with editing mode on And I open "Page3" actions menu And I click on "Edit settings" "link" in the "Page3" activity And I set the following fields to these values: | None | 1 | And I press "Save and return to course" And I turn editing mode off And I should see "Not available unless: The activity Page2 is marked complete" in the "region-main" "region" condition/completion/tests/condition_test.php 0000644 00000114116 15215712063 0015611 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace availability_completion; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->libdir . '/completionlib.php'); /** * Unit tests for the completion condition. * * @package availability_completion * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class condition_test extends \advanced_testcase { /** * Setup to ensure that fixtures are loaded. */ public static function setupBeforeClass(): void { global $CFG; // Load the mock info class so that it can be used. require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info.php'); require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info_module.php'); require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info_section.php'); } /** * Load required classes. */ public function setUp(): void { parent::setUp(); condition::wipe_static_cache(); } /** * Tests constructing and using condition as part of tree. */ public function test_in_tree(): void { global $USER, $CFG; $this->resetAfterTest(); $this->setAdminUser(); // Create course with completion turned on and a Page. $CFG->enablecompletion = true; $CFG->enableavailability = true; $generator = $this->getDataGenerator(); $course = $generator->create_course(['enablecompletion' => 1]); $page = $generator->get_plugin_generator('mod_page')->create_instance( ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]); $selfpage = $generator->get_plugin_generator('mod_page')->create_instance( ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]); $modinfo = get_fast_modinfo($course); $cm = $modinfo->get_cm($page->cmid); $info = new \core_availability\mock_info($course, $USER->id); $structure = (object)[ 'op' => '|', 'show' => true, 'c' => [ (object)[ 'type' => 'completion', 'cm' => (int)$cm->id, 'e' => COMPLETION_COMPLETE ] ] ]; $tree = new \core_availability\tree($structure); // Initial check (user has not completed activity). $result = $tree->check_available(false, $info, true, $USER->id); $this->assertFalse($result->is_available()); // Mark activity complete. $completion = new \completion_info($course); $completion->update_state($cm, COMPLETION_COMPLETE); // Now it's true! $result = $tree->check_available(false, $info, true, $USER->id); $this->assertTrue($result->is_available()); } /** * Tests the constructor including error conditions. Also tests the * string conversion feature (intended for debugging only). */ public function test_constructor(): void { // No parameters. $structure = new \stdClass(); try { $cond = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Missing or invalid ->cm', $e->getMessage()); } // Invalid $cm. $structure->cm = 'hello'; try { $cond = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Missing or invalid ->cm', $e->getMessage()); } // Missing $e. $structure->cm = 42; try { $cond = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Missing or invalid ->e', $e->getMessage()); } // Invalid $e. $structure->e = 99; try { $cond = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Missing or invalid ->e', $e->getMessage()); } // Successful construct & display with all different expected values. $structure->e = COMPLETION_COMPLETE; $cond = new condition($structure); $this->assertEquals('{completion:cm42 COMPLETE}', (string)$cond); $structure->e = COMPLETION_COMPLETE_PASS; $cond = new condition($structure); $this->assertEquals('{completion:cm42 COMPLETE_PASS}', (string)$cond); $structure->e = COMPLETION_COMPLETE_FAIL; $cond = new condition($structure); $this->assertEquals('{completion:cm42 COMPLETE_FAIL}', (string)$cond); $structure->e = COMPLETION_INCOMPLETE; $cond = new condition($structure); $this->assertEquals('{completion:cm42 INCOMPLETE}', (string)$cond); // Successful contruct with previous activity. $structure->cm = condition::OPTION_PREVIOUS; $cond = new condition($structure); $this->assertEquals('{completion:cmopprevious INCOMPLETE}', (string)$cond); } /** * Tests the save() function. */ public function test_save(): void { $structure = (object)['cm' => 42, 'e' => COMPLETION_COMPLETE]; $cond = new condition($structure); $structure->type = 'completion'; $this->assertEquals($structure, $cond->save()); } /** * Tests the is_available and get_description functions. */ public function test_usage(): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/assign/locallib.php'); $this->resetAfterTest(); // Create course with completion turned on. $CFG->enablecompletion = true; $CFG->enableavailability = true; $generator = $this->getDataGenerator(); $course = $generator->create_course(['enablecompletion' => 1]); $user = $generator->create_user(); $generator->enrol_user($user->id, $course->id); $this->setUser($user); // Create a Page with manual completion for basic checks. $page = $generator->get_plugin_generator('mod_page')->create_instance( ['course' => $course->id, 'name' => 'Page!', 'completion' => COMPLETION_TRACKING_MANUAL]); // Create an assignment - we need to have something that can be graded // so as to test the PASS/FAIL states. Set it up to be completed based // on its grade item. $assignrow = $this->getDataGenerator()->create_module('assign', [ 'course' => $course->id, 'name' => 'Assign!', 'completion' => COMPLETION_TRACKING_AUTOMATIC]); $DB->set_field('course_modules', 'completiongradeitemnumber', 0, ['id' => $assignrow->cmid]); // As we manually set the field here, we are going to need to reset the modinfo cache. rebuild_course_cache($course->id, true); $assign = new \assign(\context_module::instance($assignrow->cmid), false, false); // Get basic details. $modinfo = get_fast_modinfo($course); $pagecm = $modinfo->get_cm($page->cmid); $assigncm = $assign->get_course_module(); $info = new \core_availability\mock_info($course, $user->id); // COMPLETE state (false), positive and NOT. $cond = new condition((object)[ 'cm' => (int)$pagecm->id, 'e' => COMPLETION_COMPLETE ]); $this->assertFalse($cond->is_available(false, $info, true, $user->id)); $information = $cond->get_description(false, false, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression('~Page!.*is marked complete~', $information); $this->assertTrue($cond->is_available(true, $info, true, $user->id)); // INCOMPLETE state (true). $cond = new condition((object)[ 'cm' => (int)$pagecm->id, 'e' => COMPLETION_INCOMPLETE ]); $this->assertTrue($cond->is_available(false, $info, true, $user->id)); $this->assertFalse($cond->is_available(true, $info, true, $user->id)); $information = $cond->get_description(false, true, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression('~Page!.*is marked complete~', $information); // Mark page complete. $completion = new \completion_info($course); $completion->update_state($pagecm, COMPLETION_COMPLETE); // COMPLETE state (true). $cond = new condition((object)[ 'cm' => (int)$pagecm->id, 'e' => COMPLETION_COMPLETE ]); $this->assertTrue($cond->is_available(false, $info, true, $user->id)); $this->assertFalse($cond->is_available(true, $info, true, $user->id)); $information = $cond->get_description(false, true, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression('~Page!.*is incomplete~', $information); // INCOMPLETE state (false). $cond = new condition((object)[ 'cm' => (int)$pagecm->id, 'e' => COMPLETION_INCOMPLETE ]); $this->assertFalse($cond->is_available(false, $info, true, $user->id)); $information = $cond->get_description(false, false, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression('~Page!.*is incomplete~', $information); $this->assertTrue($cond->is_available(true, $info, true, $user->id)); // We are going to need the grade item so that we can get pass/fails. $gradeitem = $assign->get_grade_item(); \grade_object::set_properties($gradeitem, ['gradepass' => 50.0]); $gradeitem->update(); // With no grade, it should return true for INCOMPLETE and false for // the other three. $cond = new condition((object)[ 'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE ]); $this->assertTrue($cond->is_available(false, $info, true, $user->id)); $this->assertFalse($cond->is_available(true, $info, true, $user->id)); $cond = new condition((object)[ 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE ]); $this->assertFalse($cond->is_available(false, $info, true, $user->id)); $this->assertTrue($cond->is_available(true, $info, true, $user->id)); // Check $information for COMPLETE_PASS and _FAIL as we haven't yet. $cond = new condition((object)[ 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS ]); $this->assertFalse($cond->is_available(false, $info, true, $user->id)); $information = $cond->get_description(false, false, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression('~Assign!.*is complete and passed~', $information); $this->assertTrue($cond->is_available(true, $info, true, $user->id)); $cond = new condition((object)[ 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL ]); $this->assertFalse($cond->is_available(false, $info, true, $user->id)); $information = $cond->get_description(false, false, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression('~Assign!.*is complete and failed~', $information); $this->assertTrue($cond->is_available(true, $info, true, $user->id)); // Change the grade to be complete and failed. self::set_grade($assignrow, $user->id, 40); $cond = new condition((object)[ 'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE ]); $this->assertTrue($cond->is_available(false, $info, true, $user->id)); $this->assertFalse($cond->is_available(true, $info, true, $user->id)); $cond = new condition((object)[ 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE ]); $this->assertFalse($cond->is_available(false, $info, true, $user->id)); $this->assertTrue($cond->is_available(true, $info, true, $user->id)); $cond = new condition((object)[ 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS ]); $this->assertFalse($cond->is_available(false, $info, true, $user->id)); $information = $cond->get_description(false, false, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression('~Assign!.*is complete and passed~', $information); $this->assertTrue($cond->is_available(true, $info, true, $user->id)); $cond = new condition((object)[ 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL ]); $this->assertTrue($cond->is_available(false, $info, true, $user->id)); $this->assertFalse($cond->is_available(true, $info, true, $user->id)); $information = $cond->get_description(false, true, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression('~Assign!.*is not complete and failed~', $information); // Now change it to pass. self::set_grade($assignrow, $user->id, 60); $cond = new condition((object)[ 'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE ]); $this->assertFalse($cond->is_available(false, $info, true, $user->id)); $this->assertTrue($cond->is_available(true, $info, true, $user->id)); $cond = new condition((object)[ 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE ]); $this->assertTrue($cond->is_available(false, $info, true, $user->id)); $this->assertFalse($cond->is_available(true, $info, true, $user->id)); $cond = new condition((object)[ 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS ]); $this->assertTrue($cond->is_available(false, $info, true, $user->id)); $this->assertFalse($cond->is_available(true, $info, true, $user->id)); $information = $cond->get_description(false, true, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression('~Assign!.*is not complete and passed~', $information); $cond = new condition((object)[ 'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_FAIL ]); $this->assertFalse($cond->is_available(false, $info, true, $user->id)); $information = $cond->get_description(false, false, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression('~Assign!.*is complete and failed~', $information); $this->assertTrue($cond->is_available(true, $info, true, $user->id)); // Simulate deletion of an activity by using an invalid cmid. These // conditions always fail, regardless of NOT flag or INCOMPLETE. $cond = new condition((object)[ 'cm' => ($assigncm->id + 100), 'e' => COMPLETION_COMPLETE ]); $this->assertFalse($cond->is_available(false, $info, true, $user->id)); $information = $cond->get_description(false, false, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression('~(Missing activity).*is marked complete~', $information); $this->assertFalse($cond->is_available(true, $info, true, $user->id)); $cond = new condition((object)[ 'cm' => ($assigncm->id + 100), 'e' => COMPLETION_INCOMPLETE ]); $this->assertFalse($cond->is_available(false, $info, true, $user->id)); } /** * Tests the is_available and get_description functions for previous activity option. * * @dataProvider previous_activity_data * @param int $grade the current assign grade (0 for none) * @param int $condition true for complete, false for incomplete * @param string $mark activity to mark as complete * @param string $activity activity name to test * @param bool $result if it must be available or not * @param bool $resultnot if it must be available when the condition is inverted * @param string $description the availabiklity text to check */ public function test_previous_activity(int $grade, int $condition, string $mark, string $activity, bool $result, bool $resultnot, string $description): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/assign/locallib.php'); $this->resetAfterTest(); // Create course with completion turned on. $CFG->enablecompletion = true; $CFG->enableavailability = true; $generator = $this->getDataGenerator(); $course = $generator->create_course(['enablecompletion' => 1]); $user = $generator->create_user(); $generator->enrol_user($user->id, $course->id); $this->setUser($user); // Page 1 (manual completion). $page1 = $generator->get_plugin_generator('mod_page')->create_instance( ['course' => $course->id, 'name' => 'Page1!', 'completion' => COMPLETION_TRACKING_MANUAL]); // Page 2 (manual completion). $page2 = $generator->get_plugin_generator('mod_page')->create_instance( ['course' => $course->id, 'name' => 'Page2!', 'completion' => COMPLETION_TRACKING_MANUAL]); // Page ignored (no completion). $pagenocompletion = $generator->get_plugin_generator('mod_page')->create_instance( ['course' => $course->id, 'name' => 'Page ignored!']); // Create an assignment - we need to have something that can be graded // so as to test the PASS/FAIL states. Set it up to be completed based // on its grade item. $assignrow = $this->getDataGenerator()->create_module('assign', [ 'course' => $course->id, 'name' => 'Assign!', 'completion' => COMPLETION_TRACKING_AUTOMATIC ]); $DB->set_field('course_modules', 'completiongradeitemnumber', 0, ['id' => $assignrow->cmid]); $assign = new \assign(\context_module::instance($assignrow->cmid), false, false); // Page 3 (manual completion). $page3 = $generator->get_plugin_generator('mod_page')->create_instance( ['course' => $course->id, 'name' => 'Page3!', 'completion' => COMPLETION_TRACKING_MANUAL]); // Get basic details. $activities = []; $modinfo = get_fast_modinfo($course); $activities['page1'] = $modinfo->get_cm($page1->cmid); $activities['page2'] = $modinfo->get_cm($page2->cmid); $activities['assign'] = $assign->get_course_module(); $activities['page3'] = $modinfo->get_cm($page3->cmid); $prevvalue = condition::OPTION_PREVIOUS; // Setup gradings and completion. if ($grade) { $gradeitem = $assign->get_grade_item(); \grade_object::set_properties($gradeitem, ['gradepass' => 50.0]); $gradeitem->update(); self::set_grade($assignrow, $user->id, $grade); } if ($mark) { $completion = new \completion_info($course); $completion->update_state($activities[$mark], COMPLETION_COMPLETE); } // Set opprevious WITH non existent previous activity. $info = new \core_availability\mock_info_module($user->id, $activities[$activity]); $cond = new condition((object)[ 'cm' => (int)$prevvalue, 'e' => $condition ]); // Do the checks. $this->assertEquals($result, $cond->is_available(false, $info, true, $user->id)); $this->assertEquals($resultnot, $cond->is_available(true, $info, true, $user->id)); $information = $cond->get_description(false, false, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression($description, $information); } public static function previous_activity_data(): array { // Assign grade, condition, activity to complete, activity to test, result, resultnot, description. return [ 'Missing previous activity complete' => [ 0, COMPLETION_COMPLETE, '', 'page1', false, false, '~Missing activity.*is marked complete~' ], 'Missing previous activity incomplete' => [ 0, COMPLETION_INCOMPLETE, '', 'page1', false, false, '~Missing activity.*is incomplete~' ], 'Previous complete condition with previous activity incompleted' => [ 0, COMPLETION_COMPLETE, '', 'page2', false, true, '~Page1!.*is marked complete~' ], 'Previous incomplete condition with previous activity incompleted' => [ 0, COMPLETION_INCOMPLETE, '', 'page2', true, false, '~Page1!.*is incomplete~' ], 'Previous complete condition with previous activity completed' => [ 0, COMPLETION_COMPLETE, 'page1', 'page2', true, false, '~Page1!.*is marked complete~' ], 'Previous incomplete condition with previous activity completed' => [ 0, COMPLETION_INCOMPLETE, 'page1', 'page2', false, true, '~Page1!.*is incomplete~' ], // Depenging on page pass fail (pages are not gradable). 'Previous complete pass condition with previous no gradable activity incompleted' => [ 0, COMPLETION_COMPLETE_PASS, '', 'page2', false, true, '~Page1!.*is complete and passed~' ], 'Previous complete fail condition with previous no gradable activity incompleted' => [ 0, COMPLETION_COMPLETE_FAIL, '', 'page2', false, true, '~Page1!.*is complete and failed~' ], 'Previous complete pass condition with previous no gradable activity completed' => [ 0, COMPLETION_COMPLETE_PASS, 'page1', 'page2', false, true, '~Page1!.*is complete and passed~' ], 'Previous complete fail condition with previous no gradable activity completed' => [ 0, COMPLETION_COMPLETE_FAIL, 'page1', 'page2', false, true, '~Page1!.*is complete and failed~' ], // There's an page without completion between page2 ans assign. 'Previous complete condition with sibling activity incompleted' => [ 0, COMPLETION_COMPLETE, '', 'assign', false, true, '~Page2!.*is marked complete~' ], 'Previous incomplete condition with sibling activity incompleted' => [ 0, COMPLETION_INCOMPLETE, '', 'assign', true, false, '~Page2!.*is incomplete~' ], 'Previous complete condition with sibling activity completed' => [ 0, COMPLETION_COMPLETE, 'page2', 'assign', true, false, '~Page2!.*is marked complete~' ], 'Previous incomplete condition with sibling activity completed' => [ 0, COMPLETION_INCOMPLETE, 'page2', 'assign', false, true, '~Page2!.*is incomplete~' ], // Depending on assign without grade. 'Previous complete condition with previous without grade' => [ 0, COMPLETION_COMPLETE, '', 'page3', false, true, '~Assign!.*is marked complete~' ], 'Previous incomplete condition with previous without grade' => [ 0, COMPLETION_INCOMPLETE, '', 'page3', true, false, '~Assign!.*is incomplete~' ], 'Previous complete pass condition with previous without grade' => [ 0, COMPLETION_COMPLETE_PASS, '', 'page3', false, true, '~Assign!.*is complete and passed~' ], 'Previous complete fail condition with previous without grade' => [ 0, COMPLETION_COMPLETE_FAIL, '', 'page3', false, true, '~Assign!.*is complete and failed~' ], // Depending on assign with grade. 'Previous complete condition with previous fail grade' => [ 40, COMPLETION_COMPLETE, '', 'page3', false, true, '~Assign!.*is marked complete~', ], 'Previous incomplete condition with previous fail grade' => [ 40, COMPLETION_INCOMPLETE, '', 'page3', true, false, '~Assign!.*is incomplete~', ], 'Previous complete pass condition with previous fail grade' => [ 40, COMPLETION_COMPLETE_PASS, '', 'page3', false, true, '~Assign!.*is complete and passed~' ], 'Previous complete fail condition with previous fail grade' => [ 40, COMPLETION_COMPLETE_FAIL, '', 'page3', true, false, '~Assign!.*is complete and failed~' ], 'Previous complete condition with previous pass grade' => [ 60, COMPLETION_COMPLETE, '', 'page3', true, false, '~Assign!.*is marked complete~' ], 'Previous incomplete condition with previous pass grade' => [ 60, COMPLETION_INCOMPLETE, '', 'page3', false, true, '~Assign!.*is incomplete~' ], 'Previous complete pass condition with previous pass grade' => [ 60, COMPLETION_COMPLETE_PASS, '', 'page3', true, false, '~Assign!.*is complete and passed~' ], 'Previous complete fail condition with previous pass grade' => [ 60, COMPLETION_COMPLETE_FAIL, '', 'page3', false, true, '~Assign!.*is complete and failed~' ], ]; } /** * Tests the is_available and get_description functions for * previous activity option in course sections. * * @dataProvider section_previous_activity_data * @param int $condition condition value * @param bool $mark if Page 1 must be mark as completed * @param string $section section to add the availability * @param bool $result expected result * @param bool $resultnot expected negated result * @param string $description description to match */ public function test_section_previous_activity(int $condition, bool $mark, string $section, bool $result, bool $resultnot, string $description): void { global $CFG, $DB; require_once($CFG->dirroot . '/mod/assign/locallib.php'); $this->resetAfterTest(); // Create course with completion turned on. $CFG->enablecompletion = true; $CFG->enableavailability = true; $generator = $this->getDataGenerator(); $course = $generator->create_course( ['numsections' => 4, 'enablecompletion' => 1], ['createsections' => true]); $user = $generator->create_user(); $generator->enrol_user($user->id, $course->id); $this->setUser($user); // Section 1 - page1 (manual completion). $page1 = $generator->get_plugin_generator('mod_page')->create_instance( ['course' => $course->id, 'name' => 'Page1!', 'section' => 1, 'completion' => COMPLETION_TRACKING_MANUAL]); // Section 1 - page ignored 1 (no completion). $pagenocompletion1 = $generator->get_plugin_generator('mod_page')->create_instance( ['course' => $course, 'name' => 'Page ignored!', 'section' => 1]); // Section 2 - page ignored 2 (no completion). $pagenocompletion2 = $generator->get_plugin_generator('mod_page')->create_instance( ['course' => $course, 'name' => 'Page ignored!', 'section' => 2]); // Section 3 - page2 (manual completion). $page2 = $generator->get_plugin_generator('mod_page')->create_instance( ['course' => $course->id, 'name' => 'Page2!', 'section' => 3, 'completion' => COMPLETION_TRACKING_MANUAL]); // Section 4 is empty. // Get basic details. get_fast_modinfo(0, 0, true); $modinfo = get_fast_modinfo($course); $sections['section1'] = $modinfo->get_section_info(1); $sections['section2'] = $modinfo->get_section_info(2); $sections['section3'] = $modinfo->get_section_info(3); $sections['section4'] = $modinfo->get_section_info(4); $page1cm = $modinfo->get_cm($page1->cmid); $prevvalue = condition::OPTION_PREVIOUS; if ($mark) { // Mark page1 complete. $completion = new \completion_info($course); $completion->update_state($page1cm, COMPLETION_COMPLETE); } $info = new \core_availability\mock_info_section($user->id, $sections[$section]); $cond = new condition((object)[ 'cm' => (int)$prevvalue, 'e' => $condition ]); $this->assertEquals($result, $cond->is_available(false, $info, true, $user->id)); $this->assertEquals($resultnot, $cond->is_available(true, $info, true, $user->id)); $information = $cond->get_description(false, false, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression($description, $information); } public static function section_previous_activity_data(): array { return [ // Condition, Activity completion, section to test, result, resultnot, description. 'Completion complete Section with no previous activity' => [ COMPLETION_COMPLETE, false, 'section1', false, false, '~Missing activity.*is marked complete~' ], 'Completion incomplete Section with no previous activity' => [ COMPLETION_INCOMPLETE, false, 'section1', false, false, '~Missing activity.*is incomplete~' ], // Section 2 depending on section 1 -> Page 1 (no grading). 'Completion complete Section with previous activity incompleted' => [ COMPLETION_COMPLETE, false, 'section2', false, true, '~Page1!.*is marked complete~' ], 'Completion incomplete Section with previous activity incompleted' => [ COMPLETION_INCOMPLETE, false, 'section2', true, false, '~Page1!.*is incomplete~' ], 'Completion complete Section with previous activity completed' => [ COMPLETION_COMPLETE, true, 'section2', true, false, '~Page1!.*is marked complete~' ], 'Completion incomplete Section with previous activity completed' => [ COMPLETION_INCOMPLETE, true, 'section2', false, true, '~Page1!.*is incomplete~' ], // Section 3 depending on section 1 -> Page 1 (no grading). 'Completion complete Section ignoring empty sections and activity incompleted' => [ COMPLETION_COMPLETE, false, 'section3', false, true, '~Page1!.*is marked complete~' ], 'Completion incomplete Section ignoring empty sections and activity incompleted' => [ COMPLETION_INCOMPLETE, false, 'section3', true, false, '~Page1!.*is incomplete~' ], 'Completion complete Section ignoring empty sections and activity completed' => [ COMPLETION_COMPLETE, true, 'section3', true, false, '~Page1!.*is marked complete~' ], 'Completion incomplete Section ignoring empty sections and activity completed' => [ COMPLETION_INCOMPLETE, true, 'section3', false, true, '~Page1!.*is incomplete~' ], // Section 4 depending on section 3 -> Page 2 (no grading). 'Completion complete Last section with previous activity incompleted' => [ COMPLETION_COMPLETE, false, 'section4', false, true, '~Page2!.*is marked complete~' ], 'Completion incomplete Last section with previous activity incompleted' => [ COMPLETION_INCOMPLETE, false, 'section4', true, false, '~Page2!.*is incomplete~' ], 'Completion complete Last section with previous activity completed' => [ COMPLETION_COMPLETE, true, 'section4', false, true, '~Page2!.*is marked complete~' ], 'Completion incomplete Last section with previous activity completed' => [ COMPLETION_INCOMPLETE, true, 'section4', true, false, '~Page2!.*is incomplete~' ], ]; } /** * Tests completion_value_used static function. */ public function test_completion_value_used(): void { global $CFG, $DB; $this->resetAfterTest(); $prevvalue = condition::OPTION_PREVIOUS; // Create course with completion turned on and some sections. $CFG->enablecompletion = true; $CFG->enableavailability = true; $generator = $this->getDataGenerator(); $course = $generator->create_course( ['numsections' => 1, 'enablecompletion' => 1], ['createsections' => true]); // Create six pages with manual completion. $page1 = $generator->get_plugin_generator('mod_page')->create_instance( ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]); $page2 = $generator->get_plugin_generator('mod_page')->create_instance( ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]); $page3 = $generator->get_plugin_generator('mod_page')->create_instance( ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]); $page4 = $generator->get_plugin_generator('mod_page')->create_instance( ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]); $page5 = $generator->get_plugin_generator('mod_page')->create_instance( ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]); $page6 = $generator->get_plugin_generator('mod_page')->create_instance( ['course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL]); // Set up page3 to depend on page1, and section1 to depend on page2. $DB->set_field('course_modules', 'availability', '{"op":"|","show":true,"c":[' . '{"type":"completion","e":1,"cm":' . $page1->cmid . '}]}', ['id' => $page3->cmid]); $DB->set_field('course_sections', 'availability', '{"op":"|","show":true,"c":[' . '{"type":"completion","e":1,"cm":' . $page2->cmid . '}]}', ['course' => $course->id, 'section' => 1]); // Set up page5 and page6 to depend on previous activity. $DB->set_field('course_modules', 'availability', '{"op":"|","show":true,"c":[' . '{"type":"completion","e":1,"cm":' . $prevvalue . '}]}', ['id' => $page5->cmid]); $DB->set_field('course_modules', 'availability', '{"op":"|","show":true,"c":[' . '{"type":"completion","e":1,"cm":' . $prevvalue . '}]}', ['id' => $page6->cmid]); // Check 1: nothing depends on page3 and page6 but something does on the others. $this->assertTrue(condition::completion_value_used( $course, $page1->cmid)); $this->assertTrue(condition::completion_value_used( $course, $page2->cmid)); $this->assertFalse(condition::completion_value_used( $course, $page3->cmid)); $this->assertTrue(condition::completion_value_used( $course, $page4->cmid)); $this->assertTrue(condition::completion_value_used( $course, $page5->cmid)); $this->assertFalse(condition::completion_value_used( $course, $page6->cmid)); } /** * Updates the grade of a user in the given assign module instance. * * @param \stdClass $assignrow Assignment row from database * @param int $userid User id * @param float $grade Grade */ protected static function set_grade($assignrow, $userid, $grade) { $grades = []; $grades[$userid] = (object)[ 'rawgrade' => $grade, 'userid' => $userid ]; $assignrow->cmidnumber = null; assign_grade_item_update($assignrow, $grades); } /** * Tests the update_dependency_id() function. */ public function test_update_dependency_id(): void { $cond = new condition((object)[ 'cm' => 42, 'e' => COMPLETION_COMPLETE, 'selfid' => 43 ]); $this->assertFalse($cond->update_dependency_id('frogs', 42, 540)); $this->assertFalse($cond->update_dependency_id('course_modules', 12, 34)); $this->assertTrue($cond->update_dependency_id('course_modules', 42, 456)); $after = $cond->save(); $this->assertEquals(456, $after->cm); // Test selfid updating. $cond = new condition((object)[ 'cm' => 42, 'e' => COMPLETION_COMPLETE ]); $this->assertFalse($cond->update_dependency_id('frogs', 43, 540)); $this->assertFalse($cond->update_dependency_id('course_modules', 12, 34)); $after = $cond->save(); $this->assertEquals(42, $after->cm); // Test on previous activity. $cond = new condition((object)[ 'cm' => condition::OPTION_PREVIOUS, 'e' => COMPLETION_COMPLETE ]); $this->assertFalse($cond->update_dependency_id('frogs', 43, 80)); $this->assertFalse($cond->update_dependency_id('course_modules', 12, 34)); $after = $cond->save(); $this->assertEquals(condition::OPTION_PREVIOUS, $after->cm); } } condition/completion/yui/src/form/js/form.js 0000644 00000007433 15215712063 0015171 0 ustar 00 /** * JavaScript for form editing completion conditions. * * @module moodle-availability_completion-form */ M.availability_completion = M.availability_completion || {}; /** * @class M.availability_completion.form * @extends M.core_availability.plugin */ M.availability_completion.form = Y.Object(M.core_availability.plugin); /** * Initialises this plugin. * * @method initInner * @param {Array} cms Array of objects containing cmid => name */ M.availability_completion.form.initInner = function(cms) { this.cms = cms; }; M.availability_completion.form.getNode = function(json) { // Create HTML structure. var html = '<span class="col-form-label pe-3"> ' + M.util.get_string('title', 'availability_completion') + '</span>' + ' <span class="availability-group mb-3"><label>' + '<span class="accesshide">' + M.util.get_string('label_cm', 'availability_completion') + ' </span>' + '<select class="custom-select" name="cm" title="' + M.util.get_string('label_cm', 'availability_completion') + '">' + '<option value="0">' + M.util.get_string('choosedots', 'moodle') + '</option>'; for (var i = 0; i < this.cms.length; i++) { var cm = this.cms[i]; // String has already been escaped using format_string. html += '<option value="' + cm.id + '">' + cm.name + '</option>'; } html += '</select></label> <label><span class="accesshide">' + M.util.get_string('label_completion', 'availability_completion') + ' </span><select class="custom-select" ' + 'name="e" title="' + M.util.get_string('label_completion', 'availability_completion') + '">' + '<option value="1">' + M.util.get_string('option_complete', 'availability_completion') + '</option>' + '<option value="0">' + M.util.get_string('option_incomplete', 'availability_completion') + '</option>' + '<option value="2">' + M.util.get_string('option_pass', 'availability_completion') + '</option>' + '<option value="3">' + M.util.get_string('option_fail', 'availability_completion') + '</option>' + '</select></label></span>'; var node = Y.Node.create('<span class="d-flex flex-wrap align-items-center">' + html + '</span>'); // Set initial values. if (json.cm !== undefined && node.one('select[name=cm] > option[value=' + json.cm + ']')) { node.one('select[name=cm]').set('value', '' + json.cm); } if (json.e !== undefined) { node.one('select[name=e]').set('value', '' + json.e); } // Add event handlers (first time only). if (!M.availability_completion.form.addedEvents) { M.availability_completion.form.addedEvents = true; var root = Y.one('.availability-field'); root.delegate('change', function() { // Whichever dropdown changed, just update the form. M.core_availability.form.update(); }, '.availability_completion select'); } return node; }; M.availability_completion.form.fillValue = function(value, node) { value.cm = parseInt(node.one('select[name=cm]').get('value'), 10); value.e = parseInt(node.one('select[name=e]').get('value'), 10); }; M.availability_completion.form.fillErrors = function(errors, node) { var cmid = parseInt(node.one('select[name=cm]').get('value'), 10); if (cmid === 0) { errors.push('availability_completion:error_selectcmid'); } var e = parseInt(node.one('select[name=e]').get('value'), 10); if (((e === 2) || (e === 3))) { this.cms.forEach(function(cm) { if (cm.id === cmid) { if (cm.completiongradeitemnumber === null) { errors.push('availability_completion:error_selectcmidpassfail'); } } }); } }; condition/completion/yui/src/form/build.json 0000644 00000000247 15215712063 0015242 0 ustar 00 { "name": "moodle-availability_completion-form", "builds": { "moodle-availability_completion-form": { "jsfiles": [ "form.js" ] } } } condition/completion/yui/src/form/meta/form.json 0000644 00000000244 15215712063 0016031 0 ustar 00 { "moodle-availability_completion-form": { "requires": [ "base", "node", "event", "moodle-core_availability-form" ] } } yui/build/moodle-availability_completion-form/moodle-availability_completion-form-debug.js 0000644 00000007675 15215712063 0032436 0 ustar 00 condition/completion YUI.add('moodle-availability_completion-form', function (Y, NAME) { /** * JavaScript for form editing completion conditions. * * @module moodle-availability_completion-form */ M.availability_completion = M.availability_completion || {}; /** * @class M.availability_completion.form * @extends M.core_availability.plugin */ M.availability_completion.form = Y.Object(M.core_availability.plugin); /** * Initialises this plugin. * * @method initInner * @param {Array} cms Array of objects containing cmid => name */ M.availability_completion.form.initInner = function(cms) { this.cms = cms; }; M.availability_completion.form.getNode = function(json) { // Create HTML structure. var html = '<span class="col-form-label pe-3"> ' + M.util.get_string('title', 'availability_completion') + '</span>' + ' <span class="availability-group mb-3"><label>' + '<span class="accesshide">' + M.util.get_string('label_cm', 'availability_completion') + ' </span>' + '<select class="custom-select" name="cm" title="' + M.util.get_string('label_cm', 'availability_completion') + '">' + '<option value="0">' + M.util.get_string('choosedots', 'moodle') + '</option>'; for (var i = 0; i < this.cms.length; i++) { var cm = this.cms[i]; // String has already been escaped using format_string. html += '<option value="' + cm.id + '">' + cm.name + '</option>'; } html += '</select></label> <label><span class="accesshide">' + M.util.get_string('label_completion', 'availability_completion') + ' </span><select class="custom-select" ' + 'name="e" title="' + M.util.get_string('label_completion', 'availability_completion') + '">' + '<option value="1">' + M.util.get_string('option_complete', 'availability_completion') + '</option>' + '<option value="0">' + M.util.get_string('option_incomplete', 'availability_completion') + '</option>' + '<option value="2">' + M.util.get_string('option_pass', 'availability_completion') + '</option>' + '<option value="3">' + M.util.get_string('option_fail', 'availability_completion') + '</option>' + '</select></label></span>'; var node = Y.Node.create('<span class="d-flex flex-wrap align-items-center">' + html + '</span>'); // Set initial values. if (json.cm !== undefined && node.one('select[name=cm] > option[value=' + json.cm + ']')) { node.one('select[name=cm]').set('value', '' + json.cm); } if (json.e !== undefined) { node.one('select[name=e]').set('value', '' + json.e); } // Add event handlers (first time only). if (!M.availability_completion.form.addedEvents) { M.availability_completion.form.addedEvents = true; var root = Y.one('.availability-field'); root.delegate('change', function() { // Whichever dropdown changed, just update the form. M.core_availability.form.update(); }, '.availability_completion select'); } return node; }; M.availability_completion.form.fillValue = function(value, node) { value.cm = parseInt(node.one('select[name=cm]').get('value'), 10); value.e = parseInt(node.one('select[name=e]').get('value'), 10); }; M.availability_completion.form.fillErrors = function(errors, node) { var cmid = parseInt(node.one('select[name=cm]').get('value'), 10); if (cmid === 0) { errors.push('availability_completion:error_selectcmid'); } var e = parseInt(node.one('select[name=e]').get('value'), 10); if (((e === 2) || (e === 3))) { this.cms.forEach(function(cm) { if (cm.id === cmid) { if (cm.completiongradeitemnumber === null) { errors.push('availability_completion:error_selectcmidpassfail'); } } }); } }; }, '@VERSION@', {"requires": ["base", "node", "event", "moodle-core_availability-form"]}); completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form.js 0000644 00000007675 15215712063 0031352 0 ustar 00 condition YUI.add('moodle-availability_completion-form', function (Y, NAME) { /** * JavaScript for form editing completion conditions. * * @module moodle-availability_completion-form */ M.availability_completion = M.availability_completion || {}; /** * @class M.availability_completion.form * @extends M.core_availability.plugin */ M.availability_completion.form = Y.Object(M.core_availability.plugin); /** * Initialises this plugin. * * @method initInner * @param {Array} cms Array of objects containing cmid => name */ M.availability_completion.form.initInner = function(cms) { this.cms = cms; }; M.availability_completion.form.getNode = function(json) { // Create HTML structure. var html = '<span class="col-form-label pe-3"> ' + M.util.get_string('title', 'availability_completion') + '</span>' + ' <span class="availability-group mb-3"><label>' + '<span class="accesshide">' + M.util.get_string('label_cm', 'availability_completion') + ' </span>' + '<select class="custom-select" name="cm" title="' + M.util.get_string('label_cm', 'availability_completion') + '">' + '<option value="0">' + M.util.get_string('choosedots', 'moodle') + '</option>'; for (var i = 0; i < this.cms.length; i++) { var cm = this.cms[i]; // String has already been escaped using format_string. html += '<option value="' + cm.id + '">' + cm.name + '</option>'; } html += '</select></label> <label><span class="accesshide">' + M.util.get_string('label_completion', 'availability_completion') + ' </span><select class="custom-select" ' + 'name="e" title="' + M.util.get_string('label_completion', 'availability_completion') + '">' + '<option value="1">' + M.util.get_string('option_complete', 'availability_completion') + '</option>' + '<option value="0">' + M.util.get_string('option_incomplete', 'availability_completion') + '</option>' + '<option value="2">' + M.util.get_string('option_pass', 'availability_completion') + '</option>' + '<option value="3">' + M.util.get_string('option_fail', 'availability_completion') + '</option>' + '</select></label></span>'; var node = Y.Node.create('<span class="d-flex flex-wrap align-items-center">' + html + '</span>'); // Set initial values. if (json.cm !== undefined && node.one('select[name=cm] > option[value=' + json.cm + ']')) { node.one('select[name=cm]').set('value', '' + json.cm); } if (json.e !== undefined) { node.one('select[name=e]').set('value', '' + json.e); } // Add event handlers (first time only). if (!M.availability_completion.form.addedEvents) { M.availability_completion.form.addedEvents = true; var root = Y.one('.availability-field'); root.delegate('change', function() { // Whichever dropdown changed, just update the form. M.core_availability.form.update(); }, '.availability_completion select'); } return node; }; M.availability_completion.form.fillValue = function(value, node) { value.cm = parseInt(node.one('select[name=cm]').get('value'), 10); value.e = parseInt(node.one('select[name=e]').get('value'), 10); }; M.availability_completion.form.fillErrors = function(errors, node) { var cmid = parseInt(node.one('select[name=cm]').get('value'), 10); if (cmid === 0) { errors.push('availability_completion:error_selectcmid'); } var e = parseInt(node.one('select[name=e]').get('value'), 10); if (((e === 2) || (e === 3))) { this.cms.forEach(function(cm) { if (cm.id === cmid) { if (cm.completiongradeitemnumber === null) { errors.push('availability_completion:error_selectcmidpassfail'); } } }); } }; }, '@VERSION@', {"requires": ["base", "node", "event", "moodle-core_availability-form"]}); completion/yui/build/moodle-availability_completion-form/moodle-availability_completion-form-min.js 0000644 00000005007 15215712063 0032116 0 ustar 00 condition YUI.add("moodle-availability_completion-form",function(o,e){M.availability_completion=M.availability_completion||{},M.availability_completion.form=o.Object(M.core_availability.plugin),M.availability_completion.form.initInner=function(e){this.cms=e},M.availability_completion.form.getNode=function(e){for(var i,l,t='<span class="col-form-label pe-3"> '+M.util.get_string("title","availability_completion")+'</span> <span class="availability-group mb-3"><label><span class="accesshide">'+M.util.get_string("label_cm","availability_completion")+' </span><select class="custom-select" name="cm" title="'+M.util.get_string("label_cm","availability_completion")+'"><option value="0">'+M.util.get_string("choosedots","moodle")+"</option>",a=0;a<this.cms.length;a++)t+='<option value="'+(i=this.cms[a]).id+'">'+i.name+"</option>";return t+='</select></label> <label><span class="accesshide">'+M.util.get_string("label_completion","availability_completion")+' </span><select class="custom-select" name="e" title="'+M.util.get_string("label_completion","availability_completion")+'"><option value="1">'+M.util.get_string("option_complete","availability_completion")+'</option><option value="0">'+M.util.get_string("option_incomplete","availability_completion")+'</option><option value="2">'+M.util.get_string("option_pass","availability_completion")+'</option><option value="3">'+M.util.get_string("option_fail","availability_completion")+"</option></select></label></span>",l=o.Node.create('<span class="d-flex flex-wrap align-items-center">'+t+"</span>"),e.cm!==undefined&&l.one("select[name=cm] > option[value="+e.cm+"]")&&l.one("select[name=cm]").set("value",""+e.cm),e.e!==undefined&&l.one("select[name=e]").set("value",""+e.e),M.availability_completion.form.addedEvents||(M.availability_completion.form.addedEvents=!0,o.one(".availability-field").delegate("change",function(){M.core_availability.form.update()},".availability_completion select")),l},M.availability_completion.form.fillValue=function(e,i){e.cm=parseInt(i.one("select[name=cm]").get("value"),10),e.e=parseInt(i.one("select[name=e]").get("value"),10)},M.availability_completion.form.fillErrors=function(i,e){var l=parseInt(e.one("select[name=cm]").get("value"),10);0===l&&i.push("availability_completion:error_selectcmid"),2!==(e=parseInt(e.one("select[name=e]").get("value"),10))&&3!==e||this.cms.forEach(function(e){e.id===l&&null===e.completiongradeitemnumber&&i.push("availability_completion:error_selectcmidpassfail")})}},"@VERSION@",{requires:["base","node","event","moodle-core_availability-form"]}); condition/group/classes/privacy/provider.php 0000644 00000003015 15215712063 0015344 0 ustar 00 <?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 availability_group. * * @package availability_group * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace availability_group\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for availability_group implementing null_provider. * * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @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'; } } condition/group/classes/condition.php 0000644 00000024660 15215712063 0014034 0 ustar 00 <?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/>. /** * Condition main class. * * @package availability_group * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace availability_group; defined('MOODLE_INTERNAL') || die(); /** * Condition main class. * * @package availability_group * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class condition extends \core_availability\condition { /** @var array Array from group id => name */ protected static $groupnames = array(); /** @var int ID of group that this condition requires, or 0 = any group */ protected $groupid; /** * Constructor. * * @param \stdClass $structure Data structure from JSON decode * @throws \coding_exception If invalid data structure. */ public function __construct($structure) { // Get group id. if (!property_exists($structure, 'id')) { $this->groupid = 0; } else if (is_int($structure->id)) { $this->groupid = $structure->id; } else { throw new \coding_exception('Invalid ->id for group condition'); } } public function save() { $result = (object)array('type' => 'group'); if ($this->groupid) { $result->id = $this->groupid; } return $result; } public function is_available($not, \core_availability\info $info, $grabthelot, $userid) { $course = $info->get_course(); $context = \context_course::instance($course->id); $allow = true; if (!has_capability('moodle/site:accessallgroups', $context, $userid)) { // Get all groups the user belongs to. $groups = $info->get_groups(0, $userid); if ($this->groupid) { $allow = in_array($this->groupid, $groups); } else { // No specific group. Allow if they belong to any group at all. $allow = $groups ? true : false; } // The NOT condition applies before accessallgroups (i.e. if you // set something to be available to those NOT in group X, // people with accessallgroups can still access it even if // they are in group X). if ($not) { $allow = !$allow; } } return $allow; } public function get_description($full, $not, \core_availability\info $info) { global $DB; if ($this->groupid) { // Need to get the name for the group. Unfortunately this requires // a database query. To save queries, get all groups for course at // once in a static cache. $course = $info->get_course(); if (!array_key_exists($this->groupid, self::$groupnames)) { $coursegroups = $DB->get_records( 'groups', array('courseid' => $course->id), '', 'id, name'); foreach ($coursegroups as $rec) { self::$groupnames[$rec->id] = $rec->name; } } // If it still doesn't exist, it must have been misplaced. if (!array_key_exists($this->groupid, self::$groupnames)) { $name = get_string('missing', 'availability_group'); } else { // Not safe to call format_string here; use the special function to call it later. $name = self::description_format_string(self::$groupnames[$this->groupid]); } } else { return get_string($not ? 'requires_notanygroup' : 'requires_anygroup', 'availability_group'); } return get_string($not ? 'requires_notgroup' : 'requires_group', 'availability_group', $name); } protected function get_debug_string() { return $this->groupid ? '#' . $this->groupid : 'any'; } /** * Include this condition only if we are including groups in restore, or * if it's a generic 'same activity' one. * * @param int $restoreid The restore Id. * @param int $courseid The ID of the course. * @param base_logger $logger The logger being used. * @param string $name Name of item being restored. * @param base_task $task The task being performed. * * @return Integer groupid */ public function include_after_restore($restoreid, $courseid, \base_logger $logger, $name, \base_task $task) { return !$this->groupid || $task->get_setting_value('groups'); } public function update_after_restore($restoreid, $courseid, \base_logger $logger, $name) { global $DB; if (!$this->groupid) { return false; } $rec = \restore_dbops::get_backup_ids_record($restoreid, 'group', $this->groupid); if (!$rec || !$rec->newitemid) { // If we are on the same course (e.g. duplicate) then we can just // use the existing one. if ($DB->record_exists('groups', array('id' => $this->groupid, 'courseid' => $courseid))) { return false; } // Otherwise it's a warning. $this->groupid = -1; $logger->process('Restored item (' . $name . ') has availability condition on group that was not restored', \backup::LOG_WARNING); } else { $this->groupid = (int)$rec->newitemid; } return true; } public function update_dependency_id($table, $oldid, $newid) { if ($table === 'groups' && (int)$this->groupid === (int)$oldid) { $this->groupid = $newid; return true; } else { return false; } } /** * Wipes the static cache used to store grouping names. */ public static function wipe_static_cache() { self::$groupnames = array(); } public function is_applied_to_user_lists() { // Group conditions are assumed to be 'permanent', so they affect the // display of user lists for activities. return true; } public function filter_user_list(array $users, $not, \core_availability\info $info, \core_availability\capability_checker $checker) { global $CFG, $DB; // If the array is empty already, just return it. if (!$users) { return $users; } require_once($CFG->libdir . '/grouplib.php'); $course = $info->get_course(); // List users for this course who match the condition. if ($this->groupid) { $groupusers = groups_get_members($this->groupid, 'u.id', 'u.id ASC'); } else { $groupusers = $DB->get_records_sql(" SELECT DISTINCT gm.userid FROM {groups} g JOIN {groups_members} gm ON gm.groupid = g.id WHERE g.courseid = ?", array($course->id)); } // List users who have access all groups. $aagusers = $checker->get_users_by_capability('moodle/site:accessallgroups'); // Filter the user list. $result = array(); foreach ($users as $id => $user) { // Always include users with access all groups. if (array_key_exists($id, $aagusers)) { $result[$id] = $user; continue; } // Other users are included or not based on group membership. $allow = array_key_exists($id, $groupusers); if ($not) { $allow = !$allow; } if ($allow) { $result[$id] = $user; } } return $result; } /** * Returns a JSON object which corresponds to a condition of this type. * * Intended for unit testing, as normally the JSON values are constructed * by JavaScript code. * * @param int $groupid Required group id (0 = any group) * @return stdClass Object representing condition */ public static function get_json($groupid = 0) { $result = (object)array('type' => 'group'); // Id is only included if set. if ($groupid) { $result->id = (int)$groupid; } return $result; } public function get_user_list_sql($not, \core_availability\info $info, $onlyactive) { global $DB; // Get enrolled users with access all groups. These always are allowed. list($aagsql, $aagparams) = get_enrolled_sql( $info->get_context(), 'moodle/site:accessallgroups', 0, $onlyactive); // Get all enrolled users. list ($enrolsql, $enrolparams) = get_enrolled_sql($info->get_context(), '', 0, $onlyactive); // Condition for specified or any group. $matchparams = array(); if ($this->groupid) { $matchsql = "SELECT 1 FROM {groups_members} gm WHERE gm.userid = userids.id AND gm.groupid = " . self::unique_sql_parameter($matchparams, $this->groupid); } else { $matchsql = "SELECT 1 FROM {groups_members} gm JOIN {groups} g ON g.id = gm.groupid WHERE gm.userid = userids.id AND g.courseid = " . self::unique_sql_parameter($matchparams, $info->get_course()->id); } // Overall query combines all this. $condition = $not ? 'NOT' : ''; $sql = "SELECT userids.id FROM ($enrolsql) userids WHERE (userids.id IN ($aagsql)) OR $condition EXISTS ($matchsql)"; return array($sql, array_merge($enrolparams, $aagparams, $matchparams)); } } condition/group/classes/frontend.php 0000644 00000005477 15215712063 0013672 0 ustar 00 <?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/>. /** * Front-end class. * * @package availability_group * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace availability_group; defined('MOODLE_INTERNAL') || die(); /** * Front-end class. * * @package availability_group * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class frontend extends \core_availability\frontend { /** @var array Array of group info for course */ protected $allgroups; /** @var int Course id that $allgroups is for */ protected $allgroupscourseid; protected function get_javascript_strings() { return array('anygroup'); } protected function get_javascript_init_params($course, ?\cm_info $cm = null, ?\section_info $section = null) { // Get all groups for course. $groups = $this->get_all_groups($course->id); // Change to JS array format and return. $jsarray = array(); $context = \context_course::instance($course->id); foreach ($groups as $rec) { $jsarray[] = (object)array( 'id' => $rec->id, 'name' => format_string($rec->name, true, array('context' => $context)), 'visibility' => $rec->visibility ); } return array($jsarray); } /** * Gets all groups for the given course. * * @param int $courseid Course id * @return array Array of all the group objects */ protected function get_all_groups($courseid) { global $CFG; require_once($CFG->libdir . '/grouplib.php'); if ($courseid != $this->allgroupscourseid) { $this->allgroups = groups_get_all_groups($courseid, 0, 0, 'g.id, g.name, g.visibility'); $this->allgroupscourseid = $courseid; } return $this->allgroups; } protected function allow_add($course, ?\cm_info $cm = null, ?\section_info $section = null) { global $CFG; // Only show this option if there are some groups. return count($this->get_all_groups($course->id)) > 0; } } condition/group/lang/en/availability_group.php 0000644 00000003003 15215712063 0015606 0 ustar 00 <?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/>. /** * Language strings. * * @package availability_group * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['anygroup'] = '(Any group)'; $string['description'] = 'Allow only students who belong to a specified group, or all groups.'; $string['missing'] = '(Missing group)'; $string['pluginname'] = 'Restriction by group'; $string['error_selectgroup'] = 'You must select a group.'; $string['requires_anygroup'] = 'You belong to any group'; $string['requires_group'] = 'You belong to <strong>{$a}</strong>'; $string['requires_notanygroup'] = 'You do not belong to any group'; $string['requires_notgroup'] = 'You do not belong to <strong>{$a}</strong>'; $string['title'] = 'Group'; $string['privacy:metadata'] = 'The Restriction by group plugin does not store any personal data.'; condition/group/version.php 0000644 00000001752 15215712063 0012073 0 ustar 00 <?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/>. /** * Version info. * * @package availability_group * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2024100700; $plugin->requires = 2024100100; $plugin->component = 'availability_group'; condition/group/tests/behat/availability_group.feature 0000644 00000015303 15215712063 0017362 0 ustar 00 @availability @availability_group Feature: availability_group In order to control student access to activities As a teacher I need to set group conditions which prevent student access Background: Given the following "courses" exist: | fullname | shortname | format | enablecompletion | numsections | | Course 1 | C1 | topics | 1 | 3 | And the following "users" exist: | username | | teacher1 | | student1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | course | name | | page | C1 | P1 | | page | C1 | P2 | | page | C1 | P3 | @javascript Scenario: Test condition # Basic setup. Given I am on the "P1" "page activity editing" page logged in as "teacher1" And I expand all fieldsets And I click on "Add restriction..." "button" Then "Group" "button" should not exist in the "Add restriction..." "dialogue" And I click on "Cancel" "button" in the "Add restriction..." "dialogue" # Back to course page but add groups. Given the following "groups" exist: | name | course | idnumber | | G1 | C1 | GI1 | | G2 | C1 | GI2 | # This step used to be 'And I follow "C1"', but Chrome thinks the breadcrumb # is not clickable, so we'll go via the home page instead. And I am on the "P1" "page activity editing" page And I expand all fieldsets And I click on "Add restriction..." "button" Then "Group" "button" should exist in the "Add restriction..." "dialogue" # Page P1 any group. Given I click on "Group" "button" in the "Add restriction..." "dialogue" And I set the field "Group" to "(Any group)" And I click on ".availability-item .availability-eye img" "css_element" And I click on "Save and return to course" "button" # Page P2 with group G1. And I am on the "P2" "page activity editing" page And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Group" "button" in the "Add restriction..." "dialogue" And I set the field "Group" to "G1" And I click on ".availability-item .availability-eye img" "css_element" And I click on "Save and return to course" "button" # Page P3 with group G2 And I am on the "P3" "page activity editing" page And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Group" "button" in the "Add restriction..." "dialogue" And I set the field "Group" to "G2" And I click on ".availability-item .availability-eye img" "css_element" And I click on "Save and return to course" "button" # Log back in as student. When I am on the "Course 1" "course" page logged in as "student1" # No pages should appear yet. Then I should not see "P1" in the "region-main" "region" And I should not see "P2" in the "region-main" "region" And I should not see "P3" in the "region-main" "region" # Add to groups and log out/in again. Given the following "group members" exist: | user | group | | student1 | GI1 | And I am on "Course 1" course homepage # P1 (any groups) and P2 should show but not P3. Then I should see "P1" in the "region-main" "region" And I should see "P2" in the "region-main" "region" And I should not see "P3" in the "region-main" "region" @javascript Scenario: Condition display with filters # Teacher sets up a restriction on group G1, using multilang filter. Given the following "groups" exist: | name | course | idnumber | | <span lang="en" class="multilang">G-One</span><span lang="fr" class="multilang">G-Un</span> | C1 | GI1 | And the "multilang" filter is "on" And the "multilang" filter applies to "content and headings" # The activity names filter is enabled because it triggered a bug in older versions. And the "activitynames" filter is "on" And the "activitynames" filter applies to "content and headings" And I am on the "P1" "page activity editing" page logged in as "teacher1" And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Group" "button" in the "Add restriction..." "dialogue" And I set the field "Group" to "G-One" And I click on "Save and return to course" "button" # Student sees information about no access to group, with group name in correct language. When I am on the "C1" "Course" page logged in as "student1" Then I should see "Not available unless: You belong to G-One" And I should not see "G-Un" @javascript Scenario: Condition using a hidden group Given the following "groups" exist: | name | course | idnumber | visibility | | Hidden Group | C1 | GA | 3 | And I log in as "teacher1" And I add a page activity to course "Course 1" section "1" And I expand all fieldsets # Page P1 any group. And I am on the "P1" "page activity editing" page And I expand all fieldsets And I click on "Add restriction..." "button" And "Group" "button" should exist in the "Add restriction..." "dialogue" And I click on "Group" "button" in the "Add restriction..." "dialogue" And I set the field "Group" to "(Any group)" And I click on ".availability-item .availability-eye img" "css_element" And I click on "Save and return to course" "button" # Page P2 with hidden group. And I am on the "P2" "page activity editing" page And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Group" "button" in the "Add restriction..." "dialogue" And I set the field "Group" to "Hidden Group" And I click on "Save and return to course" "button" # Log back in as student. When I am on the "Course 1" "course" page logged in as "student1" # No pages should appear yet. Then I should not see "P1" in the "region-main" "region" And I should not see "P2" in the "region-main" "region" And I should not see "Hidden Group" # Add to groups and log out/in again. And the following "group members" exist: | user | group | | student1 | GA | And I am on "Course 1" course homepage # P1 (any groups) and P2 should show. The user should not see the hidden group mentioned anywhere. And I should see "P1" in the "region-main" "region" And I should see "P2" in the "region-main" "region" And I should not see "Hidden Group" condition/group/tests/condition_test.php 0000644 00000025063 15215712063 0014576 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace availability_group; /** * Unit tests for the condition. * * @package availability_group * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class condition_test extends \advanced_testcase { /** * Load required classes. */ public function setUp(): void { // Load the mock info class so that it can be used. global $CFG; require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info.php'); parent::setUp(); } /** * Tests constructing and using condition. */ public function test_usage(): void { global $CFG, $USER; $this->resetAfterTest(); $CFG->enableavailability = true; // Erase static cache before test. condition::wipe_static_cache(); // Make a test course and user. $generator = $this->getDataGenerator(); $course = $generator->create_course(); $user = $generator->create_and_enrol($course); $usertwo = $generator->create_and_enrol($course); $info = new \core_availability\mock_info($course, $user->id); // Make 2 test groups, one in a grouping and one not. $grouping = $generator->create_grouping(array('courseid' => $course->id)); $group1 = $generator->create_group(array('courseid' => $course->id, 'name' => 'G1!')); groups_assign_grouping($grouping->id, $group1->id); $group2 = $generator->create_group(array('courseid' => $course->id, 'name' => 'G2!')); // Do test (not in group). $cond = new condition((object)array('id' => (int)$group1->id)); // Check if available (when not available). $this->assertFalse($cond->is_available(false, $info, true, $user->id)); $information = $cond->get_description(false, false, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression('~You belong to.*G1!~', $information); $this->assertTrue($cond->is_available(true, $info, true, $user->id)); // Add user to groups and refresh cache. groups_add_member($group1, $user); groups_add_member($group2, $user); $info = new \core_availability\mock_info($course, $user->id); // Recheck. $this->assertTrue($cond->is_available(false, $info, true, $user->id)); $this->assertFalse($cond->is_available(true, $info, true, $user->id)); $this->assertFalse($cond->is_available(false, $info, true, $usertwo->id)); $this->assertTrue($cond->is_available(true, $info, true, $usertwo->id)); $information = $cond->get_description(false, true, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression('~do not belong to.*G1!~', $information); // Check group 2 works also. $cond = new condition((object)array('id' => (int)$group2->id)); $this->assertTrue($cond->is_available(false, $info, true, $user->id)); $this->assertFalse($cond->is_available(false, $info, true, $usertwo->id)); // What about an 'any group' condition? $cond = new condition((object)array()); $this->assertTrue($cond->is_available(false, $info, true, $user->id)); $this->assertFalse($cond->is_available(true, $info, true, $user->id)); $this->assertFalse($cond->is_available(false, $info, true, $usertwo->id)); $this->assertTrue($cond->is_available(true, $info, true, $usertwo->id)); $information = $cond->get_description(false, true, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression('~do not belong to any~', $information); // Admin user doesn't belong to a group, but they can access it // either way (positive or NOT). $this->setAdminUser(); $this->assertTrue($cond->is_available(false, $info, true, $USER->id)); $this->assertTrue($cond->is_available(true, $info, true, $USER->id)); // Group that doesn't exist uses 'missing' text. $cond = new condition((object)array('id' => $group2->id + 1000)); $this->assertFalse($cond->is_available(false, $info, true, $user->id)); $information = $cond->get_description(false, false, $info); $information = \core_availability\info::format_info($information, $course); $this->assertMatchesRegularExpression('~You belong to.*\(Missing group\)~', $information); } /** * Tests the constructor including error conditions. Also tests the * string conversion feature (intended for debugging only). */ public function test_constructor(): void { // Invalid id (not int). $structure = (object)array('id' => 'bourne'); try { $cond = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Invalid ->id', $e->getMessage()); } // Valid (with id). $structure->id = 123; $cond = new condition($structure); $this->assertEquals('{group:#123}', (string)$cond); // Valid (no id). unset($structure->id); $cond = new condition($structure); $this->assertEquals('{group:any}', (string)$cond); } /** * Tests the save() function. */ public function test_save(): void { $structure = (object)array('id' => 123); $cond = new condition($structure); $structure->type = 'group'; $this->assertEquals($structure, $cond->save()); $structure = (object)array(); $cond = new condition($structure); $structure->type = 'group'; $this->assertEquals($structure, $cond->save()); } /** * Tests the update_dependency_id() function. */ public function test_update_dependency_id(): void { $cond = new condition((object)array('id' => 123)); $this->assertFalse($cond->update_dependency_id('frogs', 123, 456)); $this->assertFalse($cond->update_dependency_id('groups', 12, 34)); $this->assertTrue($cond->update_dependency_id('groups', 123, 456)); $after = $cond->save(); $this->assertEquals(456, $after->id); } /** * Tests the filter_users (bulk checking) function. Also tests the SQL * variant get_user_list_sql. */ public function test_filter_users(): void { global $DB; $this->resetAfterTest(); // Erase static cache before test. condition::wipe_static_cache(); // Make a test course and some users. $generator = $this->getDataGenerator(); $course = $generator->create_course(); $roleids = $DB->get_records_menu('role', null, '', 'shortname, id'); $teacher = $generator->create_user(); $generator->enrol_user($teacher->id, $course->id, $roleids['editingteacher']); $allusers = array($teacher->id => $teacher); $students = array(); for ($i = 0; $i < 3; $i++) { $student = $generator->create_user(); $students[$i] = $student; $generator->enrol_user($student->id, $course->id, $roleids['student']); $allusers[$student->id] = $student; } $info = new \core_availability\mock_info($course); // Make test groups. $group1 = $generator->create_group(array('courseid' => $course->id)); $group2 = $generator->create_group(array('courseid' => $course->id)); // Assign students to groups as follows (teacher is not in a group): // 0: no groups. // 1: in group 1. // 2: in group 2. groups_add_member($group1, $students[1]); groups_add_member($group2, $students[2]); // Test 'any group' condition. $checker = new \core_availability\capability_checker($info->get_context()); $cond = new condition((object)array()); $result = array_keys($cond->filter_user_list($allusers, false, $info, $checker)); ksort($result); $expected = array($teacher->id, $students[1]->id, $students[2]->id); $this->assertEquals($expected, $result); // Test it with get_user_list_sql. list ($sql, $params) = $cond->get_user_list_sql(false, $info, true); $result = $DB->get_fieldset_sql($sql, $params); sort($result); $this->assertEquals($expected, $result); // Test NOT version (note that teacher can still access because AAG works // both ways). $result = array_keys($cond->filter_user_list($allusers, true, $info, $checker)); ksort($result); $expected = array($teacher->id, $students[0]->id); $this->assertEquals($expected, $result); // Test with get_user_list_sql. list ($sql, $params) = $cond->get_user_list_sql(true, $info, true); $result = $DB->get_fieldset_sql($sql, $params); sort($result); $this->assertEquals($expected, $result); // Test specific group. $cond = new condition((object)array('id' => (int)$group1->id)); $result = array_keys($cond->filter_user_list($allusers, false, $info, $checker)); ksort($result); $expected = array($teacher->id, $students[1]->id); $this->assertEquals($expected, $result); list ($sql, $params) = $cond->get_user_list_sql(false, $info, true); $result = $DB->get_fieldset_sql($sql, $params); sort($result); $this->assertEquals($expected, $result); $result = array_keys($cond->filter_user_list($allusers, true, $info, $checker)); ksort($result); $expected = array($teacher->id, $students[0]->id, $students[2]->id); $this->assertEquals($expected, $result); list ($sql, $params) = $cond->get_user_list_sql(true, $info, true); $result = $DB->get_fieldset_sql($sql, $params); sort($result); $this->assertEquals($expected, $result); } } condition/group/yui/src/form/js/form.js 0000644 00000007440 15215712063 0014152 0 ustar 00 /** * JavaScript for form editing group conditions. * * @module moodle-availability_group-form */ M.availability_group = M.availability_group || {}; /** * @class M.availability_group.form * @extends M.core_availability.plugin */ M.availability_group.form = Y.Object(M.core_availability.plugin); /** * Groups available for selection (alphabetical order). * * @property groups * @type Array */ M.availability_group.form.groups = null; /** * Initialises this plugin. * * @method initInner * @param {Array} groups Array of objects containing groupid => name */ M.availability_group.form.initInner = function(groups) { this.groups = groups; }; M.availability_group.form.getNode = function(json) { // Create HTML structure. var html = '<label><span class="pe-3">' + M.util.get_string('title', 'availability_group') + '</span> ' + '<span class="availability-group">' + '<select name="id" class="custom-select">' + '<option value="choose">' + M.util.get_string('choosedots', 'moodle') + '</option>' + '<option value="any">' + M.util.get_string('anygroup', 'availability_group') + '</option>'; for (var i = 0; i < this.groups.length; i++) { var group = this.groups[i]; // String has already been escaped using format_string. html += '<option value="' + group.id + '" data-visibility="' + group.visibility + '">' + group.name + '</option>'; } html += '</select></span></label>'; var node = Y.Node.create('<span class="d-flex flex-wrap align-items-center">' + html + '</span>'); var select = node.one('select[name=id]'); select.on('change', function(e) { var value = e.target.get('value'); // Find the visibility of the selected group. var visibility = e.target.one('option[value=' + value + ']').get('dataset').visibility; var event; if (visibility > 0) { event = 'availability:privateRuleSet'; } else { event = 'availability:privateRuleUnset'; } node.fire(event, {plugin: 'group'}); }); // Set initial values (leave default 'choose' if creating afresh). if (json.creating === undefined) { if (json.id !== undefined) { var option = select.one('option[value=' + json.id + ']'); if (option) { select.set('value', '' + json.id); var visibility = option.get('dataset').visibility; if (visibility > 0) { // Defer firing the event, to allow event bubbling to be set up in M.core_availability.form. window.setTimeout(function() { node.fire('availability:privateRuleSet', {plugin: 'group'}); }, 0); } } } else if (json.id === undefined) { node.one('select[name=id]').set('value', 'any'); } } // Add event handlers (first time only). if (!M.availability_group.form.addedEvents) { M.availability_group.form.addedEvents = true; var root = Y.one('.availability-field'); root.delegate('change', function() { // Just update the form fields. M.core_availability.form.update(); }, '.availability_group select'); } return node; }; M.availability_group.form.fillValue = function(value, node) { var selected = node.one('select[name=id]').get('value'); if (selected === 'choose') { value.id = 'choose'; } else if (selected !== 'any') { value.id = parseInt(selected, 10); } }; M.availability_group.form.fillErrors = function(errors, node) { var value = {}; this.fillValue(value, node); // Check group item id. if (value.id && value.id === 'choose') { errors.push('availability_group:error_selectgroup'); } }; condition/group/yui/src/form/build.json 0000644 00000000235 15215712063 0014222 0 ustar 00 { "name": "moodle-availability_group-form", "builds": { "moodle-availability_group-form": { "jsfiles": [ "form.js" ] } } } condition/group/yui/src/form/meta/form.json 0000644 00000000237 15215712063 0015016 0 ustar 00 { "moodle-availability_group-form": { "requires": [ "base", "node", "event", "moodle-core_availability-form" ] } } condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form.js 0000644 00000007675 15215712063 0026362 0 ustar 00 YUI.add('moodle-availability_group-form', function (Y, NAME) { /** * JavaScript for form editing group conditions. * * @module moodle-availability_group-form */ M.availability_group = M.availability_group || {}; /** * @class M.availability_group.form * @extends M.core_availability.plugin */ M.availability_group.form = Y.Object(M.core_availability.plugin); /** * Groups available for selection (alphabetical order). * * @property groups * @type Array */ M.availability_group.form.groups = null; /** * Initialises this plugin. * * @method initInner * @param {Array} groups Array of objects containing groupid => name */ M.availability_group.form.initInner = function(groups) { this.groups = groups; }; M.availability_group.form.getNode = function(json) { // Create HTML structure. var html = '<label><span class="pe-3">' + M.util.get_string('title', 'availability_group') + '</span> ' + '<span class="availability-group">' + '<select name="id" class="custom-select">' + '<option value="choose">' + M.util.get_string('choosedots', 'moodle') + '</option>' + '<option value="any">' + M.util.get_string('anygroup', 'availability_group') + '</option>'; for (var i = 0; i < this.groups.length; i++) { var group = this.groups[i]; // String has already been escaped using format_string. html += '<option value="' + group.id + '" data-visibility="' + group.visibility + '">' + group.name + '</option>'; } html += '</select></span></label>'; var node = Y.Node.create('<span class="d-flex flex-wrap align-items-center">' + html + '</span>'); var select = node.one('select[name=id]'); select.on('change', function(e) { var value = e.target.get('value'); // Find the visibility of the selected group. var visibility = e.target.one('option[value=' + value + ']').get('dataset').visibility; var event; if (visibility > 0) { event = 'availability:privateRuleSet'; } else { event = 'availability:privateRuleUnset'; } node.fire(event, {plugin: 'group'}); }); // Set initial values (leave default 'choose' if creating afresh). if (json.creating === undefined) { if (json.id !== undefined) { var option = select.one('option[value=' + json.id + ']'); if (option) { select.set('value', '' + json.id); var visibility = option.get('dataset').visibility; if (visibility > 0) { // Defer firing the event, to allow event bubbling to be set up in M.core_availability.form. window.setTimeout(function() { node.fire('availability:privateRuleSet', {plugin: 'group'}); }, 0); } } } else if (json.id === undefined) { node.one('select[name=id]').set('value', 'any'); } } // Add event handlers (first time only). if (!M.availability_group.form.addedEvents) { M.availability_group.form.addedEvents = true; var root = Y.one('.availability-field'); root.delegate('change', function() { // Just update the form fields. M.core_availability.form.update(); }, '.availability_group select'); } return node; }; M.availability_group.form.fillValue = function(value, node) { var selected = node.one('select[name=id]').get('value'); if (selected === 'choose') { value.id = 'choose'; } else if (selected !== 'any') { value.id = parseInt(selected, 10); } }; M.availability_group.form.fillErrors = function(errors, node) { var value = {}; this.fillValue(value, node); // Check group item id. if (value.id && value.id === 'choose') { errors.push('availability_group:error_selectgroup'); } }; }, '@VERSION@', {"requires": ["base", "node", "event", "moodle-core_availability-form"]}); condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form-min.js 0000644 00000003741 15215712063 0027131 0 ustar 00 YUI.add("moodle-availability_group-form",function(r,i){M.availability_group=M.availability_group||{},M.availability_group.form=r.Object(M.core_availability.plugin),M.availability_group.form.groups=null,M.availability_group.form.initInner=function(i){this.groups=i},M.availability_group.form.getNode=function(i){for(var a,e,t,l,o='<label><span class="pe-3">'+M.util.get_string("title","availability_group")+'</span> <span class="availability-group"><select name="id" class="custom-select"><option value="choose">'+M.util.get_string("choosedots","moodle")+'</option><option value="any">'+M.util.get_string("anygroup","availability_group")+"</option>",n=0;n<this.groups.length;n++)o+='<option value="'+(a=this.groups[n]).id+'" data-visibility="'+a.visibility+'">'+a.name+"</option>";return(t=(e=r.Node.create('<span class="d-flex flex-wrap align-items-center">'+(o+="</select></span></label>")+"</span>")).one("select[name=id]")).on("change",function(i){var a=i.target.get("value"),a=i.target.one("option[value="+a+"]").get("dataset").visibility,i=0<a?"availability:privateRuleSet":"availability:privateRuleUnset";e.fire(i,{plugin:"group"})}),i.creating===undefined&&(i.id!==undefined?(l=t.one("option[value="+i.id+"]"))&&(t.set("value",""+i.id),0<l.get("dataset").visibility&&window.setTimeout(function(){e.fire("availability:privateRuleSet",{plugin:"group"})},0)):i.id===undefined&&e.one("select[name=id]").set("value","any")),M.availability_group.form.addedEvents||(M.availability_group.form.addedEvents=!0,r.one(".availability-field").delegate("change",function(){M.core_availability.form.update()},".availability_group select")),e},M.availability_group.form.fillValue=function(i,a){a=a.one("select[name=id]").get("value");"choose"===a?i.id="choose":"any"!==a&&(i.id=parseInt(a,10))},M.availability_group.form.fillErrors=function(i,a){var e={};this.fillValue(e,a),e.id&&"choose"===e.id&&i.push("availability_group:error_selectgroup")}},"@VERSION@",{requires:["base","node","event","moodle-core_availability-form"]}); condition/group/yui/build/moodle-availability_group-form/moodle-availability_group-form-debug.js 0000644 00000007675 15215712063 0027446 0 ustar 00 YUI.add('moodle-availability_group-form', function (Y, NAME) { /** * JavaScript for form editing group conditions. * * @module moodle-availability_group-form */ M.availability_group = M.availability_group || {}; /** * @class M.availability_group.form * @extends M.core_availability.plugin */ M.availability_group.form = Y.Object(M.core_availability.plugin); /** * Groups available for selection (alphabetical order). * * @property groups * @type Array */ M.availability_group.form.groups = null; /** * Initialises this plugin. * * @method initInner * @param {Array} groups Array of objects containing groupid => name */ M.availability_group.form.initInner = function(groups) { this.groups = groups; }; M.availability_group.form.getNode = function(json) { // Create HTML structure. var html = '<label><span class="pe-3">' + M.util.get_string('title', 'availability_group') + '</span> ' + '<span class="availability-group">' + '<select name="id" class="custom-select">' + '<option value="choose">' + M.util.get_string('choosedots', 'moodle') + '</option>' + '<option value="any">' + M.util.get_string('anygroup', 'availability_group') + '</option>'; for (var i = 0; i < this.groups.length; i++) { var group = this.groups[i]; // String has already been escaped using format_string. html += '<option value="' + group.id + '" data-visibility="' + group.visibility + '">' + group.name + '</option>'; } html += '</select></span></label>'; var node = Y.Node.create('<span class="d-flex flex-wrap align-items-center">' + html + '</span>'); var select = node.one('select[name=id]'); select.on('change', function(e) { var value = e.target.get('value'); // Find the visibility of the selected group. var visibility = e.target.one('option[value=' + value + ']').get('dataset').visibility; var event; if (visibility > 0) { event = 'availability:privateRuleSet'; } else { event = 'availability:privateRuleUnset'; } node.fire(event, {plugin: 'group'}); }); // Set initial values (leave default 'choose' if creating afresh). if (json.creating === undefined) { if (json.id !== undefined) { var option = select.one('option[value=' + json.id + ']'); if (option) { select.set('value', '' + json.id); var visibility = option.get('dataset').visibility; if (visibility > 0) { // Defer firing the event, to allow event bubbling to be set up in M.core_availability.form. window.setTimeout(function() { node.fire('availability:privateRuleSet', {plugin: 'group'}); }, 0); } } } else if (json.id === undefined) { node.one('select[name=id]').set('value', 'any'); } } // Add event handlers (first time only). if (!M.availability_group.form.addedEvents) { M.availability_group.form.addedEvents = true; var root = Y.one('.availability-field'); root.delegate('change', function() { // Just update the form fields. M.core_availability.form.update(); }, '.availability_group select'); } return node; }; M.availability_group.form.fillValue = function(value, node) { var selected = node.one('select[name=id]').get('value'); if (selected === 'choose') { value.id = 'choose'; } else if (selected !== 'any') { value.id = parseInt(selected, 10); } }; M.availability_group.form.fillErrors = function(errors, node) { var value = {}; this.fillValue(value, node); // Check group item id. if (value.id && value.id === 'choose') { errors.push('availability_group:error_selectgroup'); } }; }, '@VERSION@', {"requires": ["base", "node", "event", "moodle-core_availability-form"]}); condition/grouping/classes/privacy/provider.php 0000644 00000003031 15215712063 0016040 0 ustar 00 <?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 availability_grouping. * * @package availability_grouping * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace availability_grouping\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for availability_grouping implementing null_provider. * * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @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'; } } condition/grouping/classes/condition.php 0000644 00000026524 15215712063 0014533 0 ustar 00 <?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/>. /** * Condition main class. * * @package availability_grouping * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace availability_grouping; defined('MOODLE_INTERNAL') || die(); /** * Condition main class. * * @package availability_grouping * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class condition extends \core_availability\condition { /** @var array Array from grouping id => name */ protected static $groupingnames = array(); /** @var int ID of grouping that this condition requires */ protected $groupingid = 0; /** @var bool If true, indicates that activity $cm->grouping is used */ protected $activitygrouping = false; /** * Constructor. * * @param \stdClass $structure Data structure from JSON decode * @throws \coding_exception If invalid data structure. */ public function __construct($structure) { // Get grouping id. if (isset($structure->id)) { if (is_int($structure->id)) { $this->groupingid = $structure->id; } else { throw new \coding_exception('Invalid ->id for grouping condition'); } } else if (isset($structure->activity)) { if (is_bool($structure->activity) && $structure->activity) { $this->activitygrouping = true; } else { throw new \coding_exception('Invalid ->activity for grouping condition'); } } else { throw new \coding_exception('Missing ->id / ->activity for grouping condition'); } } public function save() { $result = (object)array('type' => 'grouping'); if ($this->groupingid) { $result->id = $this->groupingid; } else { $result->activity = true; } return $result; } public function is_available($not, \core_availability\info $info, $grabthelot, $userid) { $context = \context_course::instance($info->get_course()->id); $allow = true; if (!has_capability('moodle/site:accessallgroups', $context, $userid)) { // If the activity has 'group members only' and you don't have accessallgroups... $groups = $info->get_modinfo()->get_groups($this->get_grouping_id($info)); if (!$groups) { // ...and you don't belong to a group, then set it so you can't see/access it. $allow = false; } // The NOT condition applies before accessallgroups (i.e. if you // set something to be available to those NOT in grouping X, // people with accessallgroups can still access it even if // they are in grouping X). if ($not) { $allow = !$allow; } } return $allow; } /** * Gets the actual grouping id for the condition. This is either a specified * id, or a special flag indicating that we use the one for the current cm. * * @param \core_availability\info $info Info about context cm * @return int Grouping id * @throws \coding_exception If it's set to use a cm but there isn't grouping */ protected function get_grouping_id(\core_availability\info $info) { if ($this->activitygrouping) { $groupingid = $info->get_course_module()->groupingid; if (!$groupingid) { throw new \coding_exception( 'Not supposed to be able to turn on activitygrouping when no grouping'); } return $groupingid; } else { return $this->groupingid; } } public function get_description($full, $not, \core_availability\info $info) { global $DB; $course = $info->get_course(); // Need to get the name for the grouping. Unfortunately this requires // a database query. To save queries, get all groupings for course at // once in a static cache. $groupingid = $this->get_grouping_id($info); if (!array_key_exists($groupingid, self::$groupingnames)) { $coursegroupings = $DB->get_records( 'groupings', array('courseid' => $course->id), '', 'id, name'); foreach ($coursegroupings as $rec) { self::$groupingnames[$rec->id] = $rec->name; } } // If it still doesn't exist, it must have been misplaced. if (!array_key_exists($groupingid, self::$groupingnames)) { $name = get_string('missing', 'availability_grouping'); } else { // Not safe to call format_string here; use the special function to call it later. $name = self::description_format_string(self::$groupingnames[$groupingid]); } return get_string($not ? 'requires_notgrouping' : 'requires_grouping', 'availability_grouping', $name); } protected function get_debug_string() { if ($this->activitygrouping) { return 'CM'; } else { return '#' . $this->groupingid; } } /** * Include this condition only if we are including groups in restore, or * if it's a generic 'same activity' one. * * @param int $restoreid The restore Id. * @param int $courseid The ID of the course. * @param base_logger $logger The logger being used. * @param string $name Name of item being restored. * @param base_task $task The task being performed. * * @return Integer groupid */ public function include_after_restore($restoreid, $courseid, \base_logger $logger, $name, \base_task $task) { return !$this->groupingid || $task->get_setting_value('groups'); } public function update_after_restore($restoreid, $courseid, \base_logger $logger, $name) { global $DB; if (!$this->groupingid) { // If using 'same as activity' option, no need to change it. return false; } $rec = \restore_dbops::get_backup_ids_record($restoreid, 'grouping', $this->groupingid); if (!$rec || !$rec->newitemid) { // If we are on the same course (e.g. duplicate) then we can just // use the existing one. if ($DB->record_exists('groupings', array('id' => $this->groupingid, 'courseid' => $courseid))) { return false; } // Otherwise it's a warning. $this->groupingid = -1; $logger->process('Restored item (' . $name . ') has availability condition on grouping that was not restored', \backup::LOG_WARNING); } else { $this->groupingid = (int)$rec->newitemid; } return true; } public function update_dependency_id($table, $oldid, $newid) { if ($table === 'groupings' && (int)$this->groupingid === (int)$oldid) { $this->groupingid = $newid; return true; } else { return false; } } /** * Wipes the static cache used to store grouping names. */ public static function wipe_static_cache() { self::$groupingnames = array(); } public function is_applied_to_user_lists() { // Grouping conditions are assumed to be 'permanent', so they affect the // display of user lists for activities. return true; } public function filter_user_list(array $users, $not, \core_availability\info $info, \core_availability\capability_checker $checker) { global $CFG, $DB; // If the array is empty already, just return it. if (!$users) { return $users; } // List users for this course who match the condition. $groupingusers = $DB->get_records_sql(" SELECT DISTINCT gm.userid FROM {groupings_groups} gg JOIN {groups_members} gm ON gm.groupid = gg.groupid WHERE gg.groupingid = ?", array($this->get_grouping_id($info))); // List users who have access all groups. $aagusers = $checker->get_users_by_capability('moodle/site:accessallgroups'); // Filter the user list. $result = array(); foreach ($users as $id => $user) { // Always include users with access all groups. if (array_key_exists($id, $aagusers)) { $result[$id] = $user; continue; } // Other users are included or not based on grouping membership. $allow = array_key_exists($id, $groupingusers); if ($not) { $allow = !$allow; } if ($allow) { $result[$id] = $user; } } return $result; } /** * Returns a JSON object which corresponds to a condition of this type. * * Intended for unit testing, as normally the JSON values are constructed * by JavaScript code. * * @param int $groupingid Required grouping id (0 = grouping linked to activity) * @return stdClass Object representing condition */ public static function get_json($groupingid = 0) { $result = (object)array('type' => 'grouping'); if ($groupingid) { $result->id = (int)$groupingid; } else { $result->activity = true; } return $result; } public function get_user_list_sql($not, \core_availability\info $info, $onlyactive) { global $DB; // Get enrolled users with access all groups. These always are allowed. list($aagsql, $aagparams) = get_enrolled_sql( $info->get_context(), 'moodle/site:accessallgroups', 0, $onlyactive); // Get all enrolled users. list ($enrolsql, $enrolparams) = get_enrolled_sql($info->get_context(), '', 0, $onlyactive); // Condition for specified or any group. $matchparams = array(); $matchsql = "SELECT 1 FROM {groups_members} gm JOIN {groupings_groups} gg ON gg.groupid = gm.groupid WHERE gm.userid = userids.id AND gg.groupingid = " . self::unique_sql_parameter($matchparams, $this->get_grouping_id($info)); // Overall query combines all this. $condition = $not ? 'NOT' : ''; $sql = "SELECT userids.id FROM ($enrolsql) userids WHERE (userids.id IN ($aagsql)) OR $condition EXISTS ($matchsql)"; return array($sql, array_merge($enrolparams, $aagparams, $matchparams)); } } condition/grouping/classes/frontend.php 0000644 00000005533 15215712063 0014361 0 ustar 00 <?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/>. /** * Front-end class. * * @package availability_grouping * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace availability_grouping; defined('MOODLE_INTERNAL') || die(); /** * Front-end class. * * @package availability_grouping * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class frontend extends \core_availability\frontend { /** @var array Array of grouping info for course */ protected $allgroupings; /** @var int Course id that $allgroupings is for */ protected $allgroupingscourseid; protected function get_javascript_init_params($course, ?\cm_info $cm = null, ?\section_info $section = null) { // Get all groups for course. $groupings = $this->get_all_groupings($course->id); // Change to JS array format and return. $jsarray = array(); $context = \context_course::instance($course->id); foreach ($groupings as $rec) { $jsarray[] = (object)array('id' => $rec->id, 'name' => format_string($rec->name, true, array('context' => $context))); } return array($jsarray); } /** * Gets all the groupings on the course. * * @param int $courseid Course id * @return array Array of grouping objects */ protected function get_all_groupings($courseid) { global $DB; if ($courseid != $this->allgroupingscourseid) { $this->allgroupings = $DB->get_records('groupings', ['courseid' => $courseid], 'name'); $this->allgroupingscourseid = $courseid; } return $this->allgroupings; } protected function allow_add($course, ?\cm_info $cm = null, ?\section_info $section = null) { global $CFG, $DB; // Check if groupings are in use for the course. (Unlike the 'group' // condition there is no case where you might want to set up the // condition before you set a grouping - there is no 'any grouping' // option.) return count($this->get_all_groupings($course->id)) > 0; } } condition/grouping/lang/en/availability_grouping.php 0000644 00000002624 15215712063 0017012 0 ustar 00 <?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/>. /** * Language strings. * * @package availability_grouping * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['description'] = 'Allow only students who belong to a group within a specified grouping.'; $string['error_selectgrouping'] = 'You must select a grouping.'; $string['missing'] = '(Missing grouping)'; $string['pluginname'] = 'Restriction by grouping'; $string['requires_grouping'] = 'You belong to a group in <strong>{$a}</strong>'; $string['requires_notgrouping'] = 'You do not belong to a group in <strong>{$a}</strong>'; $string['title'] = 'Grouping'; $string['privacy:metadata'] = 'The Restriction by grouping plugin does not store any personal data.'; condition/grouping/version.php 0000644 00000001760 15215712063 0012570 0 ustar 00 <?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/>. /** * Version info. * * @package availability_grouping * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2024100700; $plugin->requires = 2024100100; $plugin->component = 'availability_grouping'; condition/grouping/tests/behat/availability_grouping.feature 0000644 00000013265 15215712063 0020563 0 ustar 00 @availability @availability_grouping Feature: availability_grouping In order to control student access to activities As a teacher I need to set grouping conditions which prevent student access Background: Given the following "courses" exist: | fullname | shortname | format | enablecompletion | | Course 1 | C1 | topics | 1 | And the following "users" exist: | username | | teacher1 | | student1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "groups" exist: | name | course | idnumber | | G1 | C1 | GI1 | And the following "group members" exist: | user | group | | student1 | GI1 | # Basic setup. And the following "activities" exist: | activity | course | name | | page | C1 | P1 | | page | C1 | P2 | @javascript Scenario: Test condition # Start to add a Page. If there aren't any groupings, there's no Grouping option. Given I am on the "P1" "page activity editing" page logged in as "teacher1" And I expand all fieldsets And I click on "Add restriction..." "button" Then "Grouping" "button" should not exist in the "Add restriction..." "dialogue" And I click on "Cancel" "button" in the "Add restriction..." "dialogue" # Back to course page but add groups. # This step used to be 'And I follow "C1"', but Chrome thinks the breadcrumb # is not clickable, so we'll go via the home page instead. And I am on "Course 1" course homepage And the following "groupings" exist: | name | course | idnumber | | GX1 | C1 | GXI1 | | GX2 | C1 | GXI2 | And I am on the "P1" "page activity editing" page And I expand all fieldsets And I click on "Add restriction..." "button" Then "Grouping" "button" should exist in the "Add restriction..." "dialogue" # Page P1 grouping GX1. Given I click on "Grouping" "button" And I set the field "Grouping" to "GX1" And I click on ".availability-item .availability-eye img" "css_element" And I click on "Save and return to course" "button" # Page P2 with grouping GX2. And I am on the "P2" "page activity editing" page And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Grouping" "button" And I set the field "Grouping" to "GX2" And I click on ".availability-item .availability-eye img" "css_element" And I click on "Save and return to course" "button" # Log back in as student. When I am on the "Course 1" "course" page logged in as "student1" # No pages should appear yet. Then I should not see "P1" in the "region-main" "region" And I should not see "P2" in the "region-main" "region" # Add group to grouping and log out/in again. And the following "grouping groups" exist: | grouping | group | | GXI1 | GI1 | And I am on the "Course 1" "course" page logged in as "student1" # P1 should show but not B2. Then I should see "P1" in the "region-main" "region" And I should not see "P2" in the "region-main" "region" @javascript Scenario: Check grouping access restriction message on course homepage Given the following "groupings" exist: | name | course | idnumber | | Grouping A | C1 | GA | And the following "grouping groups" exist: | grouping | group | | GA | GI1 | And the following "activities" exist: | activity | name | intro | course | idnumber | groupmode | grouping | | assign | Test assign | Assign description | C1 | assign1 | 1 | GA | And I log in as "teacher1" And I am on "Course 1" course homepage And I turn editing mode on And I open "Test assign" actions menu And I choose "Edit settings" in the open action menu And I expand all fieldsets And the field "groupingid" matches value "Grouping A" And I press "Add group/grouping access restriction" When I press "Save and return to course" Then I should see "Not available unless: You belong to a group in Grouping A" @javascript Scenario: Condition display with filters # Teacher sets up a restriction on group G1, using multilang filter. Given the following "groupings" exist: | name | course | idnumber | | <span lang="en" class="multilang">Gr-One</span><span lang="fr" class="multilang">Gr-Un</span> | C1 | GA | And the following "activities" exist: | activity | name | intro | course | idnumber | groupmode | grouping | | assign | Test assign | Assign description | C1 | assign1 | 1 | GA | And the "multilang" filter is "on" And the "multilang" filter applies to "content and headings" # The activity names filter is enabled because it triggered a bug in older versions. And the "activitynames" filter is "on" And the "activitynames" filter applies to "content and headings" And I am on the "Test assign" "assign activity editing" page logged in as "teacher1" And I expand all fieldsets And I press "Add group/grouping access restriction" And I press "Save and return to course" # Student sees information about no access to group, with group name in correct language. When I am on the "C1" "Course" page logged in as "student1" Then I should see "Not available unless: You belong to a group in Gr-One" And I should not see "Gr-Un" condition/grouping/tests/condition_test.php 0000644 00000027332 15215712063 0015275 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace availability_grouping; /** * Unit tests for the condition. * * @package availability_grouping * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class condition_test extends \advanced_testcase { /** * Load required classes. */ public function setUp(): void { // Load the mock info class so that it can be used. global $CFG; require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info.php'); parent::setUp(); } /** * Tests constructing and using condition. */ public function test_usage(): void { global $CFG, $USER; $this->resetAfterTest(); $CFG->enableavailability = true; // Erase static cache before test. condition::wipe_static_cache(); // Make a test course and user. $generator = $this->getDataGenerator(); $course = $generator->create_course(); $user = $generator->create_user(); $generator->enrol_user($user->id, $course->id); $info = new \core_availability\mock_info($course, $user->id); // Make a test grouping and group. $grouping = $generator->create_grouping(array('courseid' => $course->id, 'name' => 'Grouping!')); $group = $generator->create_group(array('courseid' => $course->id)); groups_assign_grouping($grouping->id, $group->id); // Do test (not in grouping). $structure = (object)array('type' => 'grouping', 'id' => (int)$grouping->id); $cond = new condition($structure); // Check if available (when not available). $this->assertFalse($cond->is_available(false, $info, true, $user->id)); $information = $cond->get_description(false, false, $info); $this->assertMatchesRegularExpression('~belong to a group in.*Grouping!~', $information); $this->assertTrue($cond->is_available(true, $info, true, $user->id)); // Add user to grouping and refresh cache. groups_add_member($group, $user); get_fast_modinfo($course->id, 0, true); // Recheck. $this->assertTrue($cond->is_available(false, $info, true, $user->id)); $this->assertFalse($cond->is_available(true, $info, true, $user->id)); $information = $cond->get_description(false, true, $info); $this->assertMatchesRegularExpression('~do not belong to a group in.*Grouping!~', $information); // Admin user doesn't belong to the grouping, but they can access it // either way (positive or NOT) because of accessallgroups. $this->setAdminUser(); $infoadmin = new \core_availability\mock_info($course, $USER->id); $this->assertTrue($cond->is_available(false, $infoadmin, true, $USER->id)); $this->assertTrue($cond->is_available(true, $infoadmin, true, $USER->id)); // Grouping that doesn't exist uses 'missing' text. $cond = new condition((object)array('id' => $grouping->id + 1000)); $this->assertFalse($cond->is_available(false, $info, true, $user->id)); $information = $cond->get_description(false, false, $info); $this->assertMatchesRegularExpression('~belong to a group in.*(Missing grouping)~', $information); // We need an actual cm object to test the 'grouping from cm' option. $pagegen = $generator->get_plugin_generator('mod_page'); $page = $pagegen->create_instance(array('course' => $course->id, 'groupingid' => $grouping->id, 'availability' => '{"op":"|","show":true,"c":[{"type":"grouping","activity":true}]}')); rebuild_course_cache($course->id, true); // Check if available using the 'from course-module' grouping option. $modinfo = get_fast_modinfo($course, $user->id); $cm = $modinfo->get_cm($page->cmid); $info = new \core_availability\info_module($cm); $information = ''; $this->assertTrue($info->is_available($information, false, $user->id)); // Remove user from grouping again and recheck. groups_remove_member($group, $user); get_fast_modinfo($course->id, 0, true); $this->assertFalse($info->is_available($information, false, $user->id)); $this->assertMatchesRegularExpression('~belong to a group in.*Grouping!~', $information); } /** * Tests the constructor including error conditions. Also tests the * string conversion feature (intended for debugging only). */ public function test_constructor(): void { // No parameters. $structure = new \stdClass(); try { $cond = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Missing ->id / ->activity', $e->getMessage()); } // Invalid id (not int). $structure->id = 'bourne'; try { $cond = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Invalid ->id', $e->getMessage()); } // Invalid activity option (not bool). unset($structure->id); $structure->activity = 42; try { $cond = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Invalid ->activity', $e->getMessage()); } // Invalid activity option (false). $structure->activity = false; try { $cond = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Invalid ->activity', $e->getMessage()); } // Valid with id. $structure->id = 123; $cond = new condition($structure); $this->assertEquals('{grouping:#123}', (string)$cond); // Valid with activity. unset($structure->id); $structure->activity = true; $cond = new condition($structure); $this->assertEquals('{grouping:CM}', (string)$cond); } /** * Tests the save() function. */ public function test_save(): void { $structure = (object)array('id' => 123); $cond = new condition($structure); $structure->type = 'grouping'; $this->assertEquals($structure, $cond->save()); $structure = (object)array('activity' => true); $cond = new condition($structure); $structure->type = 'grouping'; $this->assertEquals($structure, $cond->save()); } /** * Tests the update_dependency_id() function. */ public function test_update_dependency_id(): void { $cond = new condition((object)array('id' => 123)); $this->assertFalse($cond->update_dependency_id('frogs', 123, 456)); $this->assertFalse($cond->update_dependency_id('groupings', 12, 34)); $this->assertTrue($cond->update_dependency_id('groupings', 123, 456)); $after = $cond->save(); $this->assertEquals(456, $after->id); $cond = new condition((object)array('activity' => true)); $this->assertFalse($cond->update_dependency_id('frogs', 123, 456)); } /** * Tests the filter_users (bulk checking) function. Also tests the SQL * variant get_user_list_sql. */ public function test_filter_users(): void { global $DB, $CFG; $this->resetAfterTest(); $CFG->enableavailability = true; // Erase static cache before test. condition::wipe_static_cache(); // Make a test course and some users. $generator = $this->getDataGenerator(); $course = $generator->create_course(); $roleids = $DB->get_records_menu('role', null, '', 'shortname, id'); $teacher = $generator->create_user(); $generator->enrol_user($teacher->id, $course->id, $roleids['editingteacher']); $allusers = array($teacher->id => $teacher); $students = array(); for ($i = 0; $i < 3; $i++) { $student = $generator->create_user(); $students[$i] = $student; $generator->enrol_user($student->id, $course->id, $roleids['student']); $allusers[$student->id] = $student; } $info = new \core_availability\mock_info($course); $checker = new \core_availability\capability_checker($info->get_context()); // Make test groups. $group1 = $generator->create_group(array('courseid' => $course->id)); $group2 = $generator->create_group(array('courseid' => $course->id)); $grouping1 = $generator->create_grouping(array('courseid' => $course->id)); $grouping2 = $generator->create_grouping(array('courseid' => $course->id)); groups_assign_grouping($grouping1->id, $group1->id); groups_assign_grouping($grouping2->id, $group2->id); // Make page in grouping 2. $pagegen = $generator->get_plugin_generator('mod_page'); $page = $pagegen->create_instance(array('course' => $course->id, 'groupingid' => $grouping2->id, 'availability' => '{"op":"|","show":true,"c":[{"type":"grouping","activity":true}]}')); // Assign students to groups as follows (teacher is not in a group): // 0: no groups. // 1: in group 1/grouping 1. // 2: in group 2/grouping 2. groups_add_member($group1, $students[1]); groups_add_member($group2, $students[2]); // Test specific grouping. $cond = new condition((object)array('id' => (int)$grouping1->id)); $result = array_keys($cond->filter_user_list($allusers, false, $info, $checker)); ksort($result); $expected = array($teacher->id, $students[1]->id); $this->assertEquals($expected, $result); // Test it with get_user_list_sql. list ($sql, $params) = $cond->get_user_list_sql(false, $info, true); $result = $DB->get_fieldset_sql($sql, $params); sort($result); $this->assertEquals($expected, $result); // NOT test. $result = array_keys($cond->filter_user_list($allusers, true, $info, $checker)); ksort($result); $expected = array($teacher->id, $students[0]->id, $students[2]->id); $this->assertEquals($expected, $result); // NOT with get_user_list_sql. list ($sql, $params) = $cond->get_user_list_sql(true, $info, true); $result = $DB->get_fieldset_sql($sql, $params); sort($result); $this->assertEquals($expected, $result); // Test course-module grouping. $modinfo = get_fast_modinfo($course); $cm = $modinfo->get_cm($page->cmid); $info = new \core_availability\info_module($cm); $result = array_keys($info->filter_user_list($allusers, $course)); $expected = array($teacher->id, $students[2]->id); $this->assertEquals($expected, $result); // With get_user_list_sql. list ($sql, $params) = $info->get_user_list_sql(true); $result = $DB->get_fieldset_sql($sql, $params); sort($result); $this->assertEquals($expected, $result); } } condition/grouping/yui/src/form/js/form.js 0000644 00000005141 15215712063 0014644 0 ustar 00 /** * JavaScript for form editing grouping conditions. * * @module moodle-availability_grouping-form */ M.availability_grouping = M.availability_grouping || {}; /** * @class M.availability_grouping.form * @extends M.core_availability.plugin */ M.availability_grouping.form = Y.Object(M.core_availability.plugin); /** * Groupings available for selection (alphabetical order). * * @property groupings * @type Array */ M.availability_grouping.form.groupings = null; /** * Initialises this plugin. * * @method initInner * @param {Array} groupings Array of objects containing groupingid => name */ M.availability_grouping.form.initInner = function(groupings) { this.groupings = groupings; }; M.availability_grouping.form.getNode = function(json) { // Create HTML structure. var html = '<label><span class="pe-3">' + M.util.get_string('title', 'availability_grouping') + '</span> ' + '<span class="availability-group">' + '<select name="id" class="custom-select">' + '<option value="choose">' + M.util.get_string('choosedots', 'moodle') + '</option>'; for (var i = 0; i < this.groupings.length; i++) { var grouping = this.groupings[i]; // String has already been escaped using format_string. html += '<option value="' + grouping.id + '">' + grouping.name + '</option>'; } html += '</select></span></label>'; var node = Y.Node.create('<span class="d-flex flex-wrap align-items-center">' + html + '</span>'); // Set initial value if specified. if (json.id !== undefined && node.one('select[name=id] > option[value=' + json.id + ']')) { node.one('select[name=id]').set('value', '' + json.id); } // Add event handlers (first time only). if (!M.availability_grouping.form.addedEvents) { M.availability_grouping.form.addedEvents = true; var root = Y.one('.availability-field'); root.delegate('change', function() { // Just update the form fields. M.core_availability.form.update(); }, '.availability_grouping select'); } return node; }; M.availability_grouping.form.fillValue = function(value, node) { var selected = node.one('select[name=id]').get('value'); if (selected === 'choose') { value.id = 'choose'; } else { value.id = parseInt(selected, 10); } }; M.availability_grouping.form.fillErrors = function(errors, node) { var value = {}; this.fillValue(value, node); // Check grouping item id. if (value.id === 'choose') { errors.push('availability_grouping:error_selectgrouping'); } }; condition/grouping/yui/src/form/build.json 0000644 00000000243 15215712063 0014717 0 ustar 00 { "name": "moodle-availability_grouping-form", "builds": { "moodle-availability_grouping-form": { "jsfiles": [ "form.js" ] } } } condition/grouping/yui/src/form/meta/form.json 0000644 00000000242 15215712063 0015510 0 ustar 00 { "moodle-availability_grouping-form": { "requires": [ "base", "node", "event", "moodle-core_availability-form" ] } } grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form-min.js 0000644 00000002754 15215712063 0030747 0 ustar 00 condition YUI.add("moodle-availability_grouping-form",function(n,i){M.availability_grouping=M.availability_grouping||{},M.availability_grouping.form=n.Object(M.core_availability.plugin),M.availability_grouping.form.groupings=null,M.availability_grouping.form.initInner=function(i){this.groupings=i},M.availability_grouping.form.getNode=function(i){for(var a,e,l='<label><span class="pe-3">'+M.util.get_string("title","availability_grouping")+'</span> <span class="availability-group"><select name="id" class="custom-select"><option value="choose">'+M.util.get_string("choosedots","moodle")+"</option>",o=0;o<this.groupings.length;o++)l+='<option value="'+(a=this.groupings[o]).id+'">'+a.name+"</option>";return e=n.Node.create('<span class="d-flex flex-wrap align-items-center">'+(l+="</select></span></label>")+"</span>"),i.id!==undefined&&e.one("select[name=id] > option[value="+i.id+"]")&&e.one("select[name=id]").set("value",""+i.id),M.availability_grouping.form.addedEvents||(M.availability_grouping.form.addedEvents=!0,n.one(".availability-field").delegate("change",function(){M.core_availability.form.update()},".availability_grouping select")),e},M.availability_grouping.form.fillValue=function(i,a){a=a.one("select[name=id]").get("value");i.id="choose"===a?"choose":parseInt(a,10)},M.availability_grouping.form.fillErrors=function(i,a){var e={};this.fillValue(e,a),"choose"===e.id&&i.push("availability_grouping:error_selectgrouping")}},"@VERSION@",{requires:["base","node","event","moodle-core_availability-form"]}); grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form-debug.js 0000644 00000005401 15215712063 0031242 0 ustar 00 condition YUI.add('moodle-availability_grouping-form', function (Y, NAME) { /** * JavaScript for form editing grouping conditions. * * @module moodle-availability_grouping-form */ M.availability_grouping = M.availability_grouping || {}; /** * @class M.availability_grouping.form * @extends M.core_availability.plugin */ M.availability_grouping.form = Y.Object(M.core_availability.plugin); /** * Groupings available for selection (alphabetical order). * * @property groupings * @type Array */ M.availability_grouping.form.groupings = null; /** * Initialises this plugin. * * @method initInner * @param {Array} groupings Array of objects containing groupingid => name */ M.availability_grouping.form.initInner = function(groupings) { this.groupings = groupings; }; M.availability_grouping.form.getNode = function(json) { // Create HTML structure. var html = '<label><span class="pe-3">' + M.util.get_string('title', 'availability_grouping') + '</span> ' + '<span class="availability-group">' + '<select name="id" class="custom-select">' + '<option value="choose">' + M.util.get_string('choosedots', 'moodle') + '</option>'; for (var i = 0; i < this.groupings.length; i++) { var grouping = this.groupings[i]; // String has already been escaped using format_string. html += '<option value="' + grouping.id + '">' + grouping.name + '</option>'; } html += '</select></span></label>'; var node = Y.Node.create('<span class="d-flex flex-wrap align-items-center">' + html + '</span>'); // Set initial value if specified. if (json.id !== undefined && node.one('select[name=id] > option[value=' + json.id + ']')) { node.one('select[name=id]').set('value', '' + json.id); } // Add event handlers (first time only). if (!M.availability_grouping.form.addedEvents) { M.availability_grouping.form.addedEvents = true; var root = Y.one('.availability-field'); root.delegate('change', function() { // Just update the form fields. M.core_availability.form.update(); }, '.availability_grouping select'); } return node; }; M.availability_grouping.form.fillValue = function(value, node) { var selected = node.one('select[name=id]').get('value'); if (selected === 'choose') { value.id = 'choose'; } else { value.id = parseInt(selected, 10); } }; M.availability_grouping.form.fillErrors = function(errors, node) { var value = {}; this.fillValue(value, node); // Check grouping item id. if (value.id === 'choose') { errors.push('availability_grouping:error_selectgrouping'); } }; }, '@VERSION@', {"requires": ["base", "node", "event", "moodle-core_availability-form"]}); condition/grouping/yui/build/moodle-availability_grouping-form/moodle-availability_grouping-form.js 0000644 00000005401 15215712063 0030235 0 ustar 00 YUI.add('moodle-availability_grouping-form', function (Y, NAME) { /** * JavaScript for form editing grouping conditions. * * @module moodle-availability_grouping-form */ M.availability_grouping = M.availability_grouping || {}; /** * @class M.availability_grouping.form * @extends M.core_availability.plugin */ M.availability_grouping.form = Y.Object(M.core_availability.plugin); /** * Groupings available for selection (alphabetical order). * * @property groupings * @type Array */ M.availability_grouping.form.groupings = null; /** * Initialises this plugin. * * @method initInner * @param {Array} groupings Array of objects containing groupingid => name */ M.availability_grouping.form.initInner = function(groupings) { this.groupings = groupings; }; M.availability_grouping.form.getNode = function(json) { // Create HTML structure. var html = '<label><span class="pe-3">' + M.util.get_string('title', 'availability_grouping') + '</span> ' + '<span class="availability-group">' + '<select name="id" class="custom-select">' + '<option value="choose">' + M.util.get_string('choosedots', 'moodle') + '</option>'; for (var i = 0; i < this.groupings.length; i++) { var grouping = this.groupings[i]; // String has already been escaped using format_string. html += '<option value="' + grouping.id + '">' + grouping.name + '</option>'; } html += '</select></span></label>'; var node = Y.Node.create('<span class="d-flex flex-wrap align-items-center">' + html + '</span>'); // Set initial value if specified. if (json.id !== undefined && node.one('select[name=id] > option[value=' + json.id + ']')) { node.one('select[name=id]').set('value', '' + json.id); } // Add event handlers (first time only). if (!M.availability_grouping.form.addedEvents) { M.availability_grouping.form.addedEvents = true; var root = Y.one('.availability-field'); root.delegate('change', function() { // Just update the form fields. M.core_availability.form.update(); }, '.availability_grouping select'); } return node; }; M.availability_grouping.form.fillValue = function(value, node) { var selected = node.one('select[name=id]').get('value'); if (selected === 'choose') { value.id = 'choose'; } else { value.id = parseInt(selected, 10); } }; M.availability_grouping.form.fillErrors = function(errors, node) { var value = {}; this.fillValue(value, node); // Check grouping item id. if (value.id === 'choose') { errors.push('availability_grouping:error_selectgrouping'); } }; }, '@VERSION@', {"requires": ["base", "node", "event", "moodle-core_availability-form"]}); condition/profile/classes/privacy/provider.php 0000644 00000003025 15215712063 0015651 0 ustar 00 <?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 availability_profile. * * @package availability_profile * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace availability_profile\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for availability_profile implementing null_provider. * * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @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'; } } condition/profile/classes/condition.php 0000644 00000064031 15215712063 0014334 0 ustar 00 <?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/>. /** * User profile field condition. * * @package availability_profile * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace availability_profile; defined('MOODLE_INTERNAL') || die(); /** * User profile field condition. * * @package availability_profile * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class condition extends \core_availability\condition { /** @var string Operator: field contains value */ const OP_CONTAINS = 'contains'; /** @var string Operator: field does not contain value */ const OP_DOES_NOT_CONTAIN = 'doesnotcontain'; /** @var string Operator: field equals value */ const OP_IS_EQUAL_TO = 'isequalto'; /** @var string Operator: field starts with value */ const OP_STARTS_WITH = 'startswith'; /** @var string Operator: field ends with value */ const OP_ENDS_WITH = 'endswith'; /** @var string Operator: field is empty */ const OP_IS_EMPTY = 'isempty'; /** @var string Operator: field is not empty */ const OP_IS_NOT_EMPTY = 'isnotempty'; /** @var array|null Array of custom profile fields (static cache within request) */ protected static $customprofilefields = null; /** @var string Field name (for standard fields) or '' if custom field */ protected $standardfield = ''; /** @var int Field name (for custom fields) or '' if standard field */ protected $customfield = ''; /** @var string Operator type (OP_xx constant) */ protected $operator; /** @var string Expected value for field */ protected $value = ''; /** * Constructor. * * @param \stdClass $structure Data structure from JSON decode * @throws \coding_exception If invalid data structure. */ public function __construct($structure) { // Get operator. if (isset($structure->op) && in_array($structure->op, array(self::OP_CONTAINS, self::OP_DOES_NOT_CONTAIN, self::OP_IS_EQUAL_TO, self::OP_STARTS_WITH, self::OP_ENDS_WITH, self::OP_IS_EMPTY, self::OP_IS_NOT_EMPTY), true)) { $this->operator = $structure->op; } else { throw new \coding_exception('Missing or invalid ->op for profile condition'); } // For operators other than the empty/not empty ones, require value. switch($this->operator) { case self::OP_IS_EMPTY: case self::OP_IS_NOT_EMPTY: if (isset($structure->v)) { throw new \coding_exception('Unexpected ->v for non-value operator'); } break; default: if (isset($structure->v) && is_string($structure->v)) { $this->value = $structure->v; } else { throw new \coding_exception('Missing or invalid ->v for profile condition'); } break; } // Get field type. if (property_exists($structure, 'sf')) { if (property_exists($structure, 'cf')) { throw new \coding_exception('Both ->sf and ->cf for profile condition'); } if (is_string($structure->sf)) { $this->standardfield = $structure->sf; } else { throw new \coding_exception('Invalid ->sf for profile condition'); } } else if (property_exists($structure, 'cf')) { if (is_string($structure->cf)) { $this->customfield = $structure->cf; } else { throw new \coding_exception('Invalid ->cf for profile condition'); } } else { throw new \coding_exception('Missing ->sf or ->cf for profile condition'); } } public function save() { $result = (object)array('type' => 'profile', 'op' => $this->operator); if ($this->customfield) { $result->cf = $this->customfield; } else { $result->sf = $this->standardfield; } switch($this->operator) { case self::OP_IS_EMPTY: case self::OP_IS_NOT_EMPTY: break; default: $result->v = $this->value; break; } return $result; } /** * Returns a JSON object which corresponds to a condition of this type. * * Intended for unit testing, as normally the JSON values are constructed * by JavaScript code. * * @param bool $customfield True if this is a custom field * @param string $fieldname Field name * @param string $operator Operator name (OP_xx constant) * @param string|null $value Value (not required for some operator types) * @return stdClass Object representing condition */ public static function get_json($customfield, $fieldname, $operator, $value = null) { $result = (object)array('type' => 'profile', 'op' => $operator); if ($customfield) { $result->cf = $fieldname; } else { $result->sf = $fieldname; } switch ($operator) { case self::OP_IS_EMPTY: case self::OP_IS_NOT_EMPTY: break; default: if (is_null($value)) { throw new \coding_exception('Operator requires value'); } $result->v = $value; break; } return $result; } public function is_available($not, \core_availability\info $info, $grabthelot, $userid) { $uservalue = $this->get_cached_user_profile_field($userid); $allow = self::is_field_condition_met($this->operator, $uservalue, $this->value); if ($not) { $allow = !$allow; } return $allow; } public function get_description($full, $not, \core_availability\info $info) { $course = $info->get_course(); // Display the fieldname into current lang. if ($this->customfield) { // Is a custom profile field (will use multilang). $customfields = self::get_custom_profile_fields(); if (array_key_exists($this->customfield, $customfields)) { $translatedfieldname = $customfields[$this->customfield]->name; } else { $translatedfieldname = get_string('missing', 'availability_profile', $this->customfield); } } else { $standardfields = self::get_standard_profile_fields(); if (array_key_exists($this->standardfield, $standardfields)) { $translatedfieldname = $standardfields[$this->standardfield]; } else { $translatedfieldname = get_string('missing', 'availability_profile', $this->standardfield); } } $a = new \stdClass(); // Not safe to call format_string here; use the special function to call it later. $a->field = self::description_format_string($translatedfieldname); $a->value = s($this->value); if ($not) { // When doing NOT strings, we replace the operator with its inverse. // Some of them don't have inverses, so for those we use a new // identifier which is only used for this lang string. switch($this->operator) { case self::OP_CONTAINS: $opname = self::OP_DOES_NOT_CONTAIN; break; case self::OP_DOES_NOT_CONTAIN: $opname = self::OP_CONTAINS; break; case self::OP_ENDS_WITH: $opname = 'notendswith'; break; case self::OP_IS_EMPTY: $opname = self::OP_IS_NOT_EMPTY; break; case self::OP_IS_EQUAL_TO: $opname = 'notisequalto'; break; case self::OP_IS_NOT_EMPTY: $opname = self::OP_IS_EMPTY; break; case self::OP_STARTS_WITH: $opname = 'notstartswith'; break; default: throw new \coding_exception('Unexpected operator: ' . $this->operator); } } else { $opname = $this->operator; } return get_string('requires_' . $opname, 'availability_profile', $a); } protected function get_debug_string() { if ($this->customfield) { $out = '*' . $this->customfield; } else { $out = $this->standardfield; } $out .= ' ' . $this->operator; switch($this->operator) { case self::OP_IS_EMPTY: case self::OP_IS_NOT_EMPTY: break; default: $out .= ' ' . $this->value; break; } return $out; } /** * Returns true if a field meets the required conditions, false otherwise. * * @param string $operator the requirement/condition * @param string $uservalue the user's value * @param string $value the value required * @return boolean True if conditions are met */ protected static function is_field_condition_met($operator, $uservalue, $value) { if ($uservalue === false) { // If the user value is false this is an instant fail. // All user values come from the database as either data or the default. // They will always be a string. return false; } $fieldconditionmet = true; // Just to be doubly sure it is a string. $uservalue = (string)$uservalue; switch($operator) { case self::OP_CONTAINS: $pos = strpos($uservalue, $value); if ($pos === false) { $fieldconditionmet = false; } break; case self::OP_DOES_NOT_CONTAIN: if (!empty($value)) { $pos = strpos($uservalue, $value); if ($pos !== false) { $fieldconditionmet = false; } } break; case self::OP_IS_EQUAL_TO: if ($value !== $uservalue) { $fieldconditionmet = false; } break; case self::OP_STARTS_WITH: $length = strlen($value); if ((substr($uservalue, 0, $length) !== $value)) { $fieldconditionmet = false; } break; case self::OP_ENDS_WITH: $length = strlen($value); $start = $length * -1; if (substr($uservalue, $start) !== $value) { $fieldconditionmet = false; } break; case self::OP_IS_EMPTY: if (!empty($uservalue)) { $fieldconditionmet = false; } break; case self::OP_IS_NOT_EMPTY: if (empty($uservalue)) { $fieldconditionmet = false; } break; } return $fieldconditionmet; } /** * Return list of standard user profile fields used by the condition * * @return string[] */ public static function get_standard_profile_fields(): array { return [ 'firstname' => \core_user\fields::get_display_name('firstname'), 'lastname' => \core_user\fields::get_display_name('lastname'), 'email' => \core_user\fields::get_display_name('email'), 'city' => \core_user\fields::get_display_name('city'), 'country' => \core_user\fields::get_display_name('country'), 'idnumber' => \core_user\fields::get_display_name('idnumber'), 'institution' => \core_user\fields::get_display_name('institution'), 'department' => \core_user\fields::get_display_name('department'), 'phone1' => \core_user\fields::get_display_name('phone1'), 'phone2' => \core_user\fields::get_display_name('phone2'), 'address' => \core_user\fields::get_display_name('address'), ]; } /** * Gets data about custom profile fields. Cached statically in current * request. * * This only includes fields which can be tested by the system (those whose * data is cached in $USER object) - basically doesn't include textarea type * fields. * * @return array Array of records indexed by shortname */ public static function get_custom_profile_fields() { global $DB, $CFG; if (self::$customprofilefields === null) { // Get fields and store them indexed by shortname. require_once($CFG->dirroot . '/user/profile/lib.php'); $fields = profile_get_custom_fields(true); self::$customprofilefields = array(); foreach ($fields as $field) { self::$customprofilefields[$field->shortname] = $field; } } return self::$customprofilefields; } /** * Wipes the static cache (for use in unit tests). */ public static function wipe_static_cache() { self::$customprofilefields = null; } /** * Return the value for a user's profile field * * @param int $userid User ID * @return string|bool Value, or false if user does not have a value for this field */ protected function get_cached_user_profile_field($userid) { global $USER, $DB, $CFG; $iscurrentuser = $USER->id == $userid; if (isguestuser($userid) || ($iscurrentuser && !isloggedin())) { // Must be logged in and can't be the guest. return false; } // Custom profile fields will be numeric, there are no numeric standard profile fields so this is not a problem. $iscustomprofilefield = $this->customfield ? true : false; if ($iscustomprofilefield) { // As its a custom profile field we need to map the id back to the actual field. // We'll also preload all of the other custom profile fields just in case and ensure we have the // default value available as well. if (!array_key_exists($this->customfield, self::get_custom_profile_fields())) { // No such field exists. // This shouldn't normally happen but occur if things go wrong when deleting a custom profile field // or when restoring a backup of a course with user profile field conditions. return false; } $field = $this->customfield; } else { $field = $this->standardfield; } // If its the current user than most likely we will be able to get this information from $USER. // If its a regular profile field then it should already be available, if not then we have a mega problem. // If its a custom profile field then it should be available but may not be. If it is then we use the value // available, otherwise we load all custom profile fields into a temp object and refer to that. // Noting its not going be great for performance if we have to use the temp object as it involves loading the // custom profile field API and classes. if ($iscurrentuser) { if (!$iscustomprofilefield) { if (property_exists($USER, $field)) { return $USER->{$field}; } else { // Unknown user field. This should not happen. throw new \coding_exception('Requested user profile field does not exist'); } } // Checking if the custom profile fields are already available. if (!isset($USER->profile)) { // Drat! they're not. We need to use a temp object and load them. // We don't use $USER as the profile fields are loaded into the object. $user = new \stdClass; $user->id = $USER->id; // This should ALWAYS be set, but just in case we check. require_once($CFG->dirroot . '/user/profile/lib.php'); profile_load_custom_fields($user); if (array_key_exists($field, $user->profile)) { return $user->profile[$field]; } } else if (array_key_exists($field, $USER->profile)) { // Hurrah they're available, this is easy. return $USER->profile[$field]; } // The profile field doesn't exist. return false; } else { // Loading for another user. if ($iscustomprofilefield) { // Fetch the data for the field. Noting we keep this query simple so that Database caching takes care of performance // for us (this will likely be hit again). // We are able to do this because we've already pre-loaded the custom fields. $data = $DB->get_field('user_info_data', 'data', array('userid' => $userid, 'fieldid' => self::$customprofilefields[$field]->id), IGNORE_MISSING); // If we have data return that, otherwise return the default. if ($data !== false) { return $data; } else { return self::$customprofilefields[$field]->defaultdata; } } else { // Its a standard field, retrieve it from the user. return $DB->get_field('user', $field, array('id' => $userid), MUST_EXIST); } } return false; } public function is_applied_to_user_lists() { // Profile conditions are assumed to be 'permanent', so they affect the // display of user lists for activities. return true; } public function filter_user_list(array $users, $not, \core_availability\info $info, \core_availability\capability_checker $checker) { global $CFG, $DB; // If the array is empty already, just return it. if (!$users) { return $users; } // Get all users from the list who match the condition. list ($sql, $params) = $DB->get_in_or_equal(array_keys($users)); if ($this->customfield) { $customfields = self::get_custom_profile_fields(); if (!array_key_exists($this->customfield, $customfields)) { // If the field isn't found, nobody matches. return array(); } $customfield = $customfields[$this->customfield]; // Fetch custom field value for all users. $values = $DB->get_records_select('user_info_data', 'fieldid = ? AND userid ' . $sql, array_merge(array($customfield->id), $params), '', 'userid, data'); $valuefield = 'data'; $default = $customfield->defaultdata; } else { $standardfields = self::get_standard_profile_fields(); if (!array_key_exists($this->standardfield, $standardfields)) { // If the field isn't found, nobody matches. return []; } $values = $DB->get_records_select('user', 'id ' . $sql, $params, '', 'id, '. $this->standardfield); $valuefield = $this->standardfield; $default = ''; } // Filter the user list. $result = array(); foreach ($users as $id => $user) { // Get value for user. if (array_key_exists($id, $values)) { $value = $values[$id]->{$valuefield}; } else { $value = $default; } // Check value. $allow = $this->is_field_condition_met($this->operator, $value, $this->value); if ($not) { $allow = !$allow; } if ($allow) { $result[$id] = $user; } } return $result; } /** * Gets SQL to match a field against this condition. The second copy of the * field is in case you're using variables for the field so that it needs * to be two different ones. * * @param string $field Field name * @param string $field2 Second copy of field name (default same). * @param boolean $istext Any of the fields correspond to a TEXT column in database (true) or not (false). * @return array Array of SQL and parameters */ private function get_condition_sql($field, $field2 = null, $istext = false) { global $DB; if (is_null($field2)) { $field2 = $field; } $params = array(); switch($this->operator) { case self::OP_CONTAINS: $sql = $DB->sql_like($field, self::unique_sql_parameter( $params, '%' . $this->value . '%')); break; case self::OP_DOES_NOT_CONTAIN: if (empty($this->value)) { // The 'does not contain nothing' expression matches everyone. return null; } $sql = $DB->sql_like($field, self::unique_sql_parameter( $params, '%' . $this->value . '%'), true, true, true); break; case self::OP_IS_EQUAL_TO: if ($istext) { $sql = $DB->sql_compare_text($field) . ' = ' . $DB->sql_compare_text( self::unique_sql_parameter($params, $this->value)); } else { $sql = $field . ' = ' . self::unique_sql_parameter( $params, $this->value); } break; case self::OP_STARTS_WITH: $sql = $DB->sql_like($field, self::unique_sql_parameter( $params, $this->value . '%')); break; case self::OP_ENDS_WITH: $sql = $DB->sql_like($field, self::unique_sql_parameter( $params, '%' . $this->value)); break; case self::OP_IS_EMPTY: // Mimic PHP empty() behaviour for strings, '0' or ''. $emptystring = self::unique_sql_parameter($params, ''); if ($istext) { $sql = '(' . $DB->sql_compare_text($field) . " IN ('0', $emptystring) OR $field2 IS NULL)"; } else { $sql = '(' . $field . " IN ('0', $emptystring) OR $field2 IS NULL)"; } break; case self::OP_IS_NOT_EMPTY: $emptystring = self::unique_sql_parameter($params, ''); if ($istext) { $sql = '(' . $DB->sql_compare_text($field) . " NOT IN ('0', $emptystring) AND $field2 IS NOT NULL)"; } else { $sql = '(' . $field . " NOT IN ('0', $emptystring) AND $field2 IS NOT NULL)"; } break; } return array($sql, $params); } public function get_user_list_sql($not, \core_availability\info $info, $onlyactive) { global $DB; // Build suitable SQL depending on custom or standard field. if ($this->customfield) { $customfields = self::get_custom_profile_fields(); if (!array_key_exists($this->customfield, $customfields)) { // If the field isn't found, nobody matches. return array('SELECT id FROM {user} WHERE 0 = 1', array()); } $customfield = $customfields[$this->customfield]; $mainparams = array(); $tablesql = "LEFT JOIN {user_info_data} ud ON ud.fieldid = " . self::unique_sql_parameter($mainparams, $customfield->id) . " AND ud.userid = userids.id"; list ($condition, $conditionparams) = $this->get_condition_sql('ud.data', null, true); $mainparams = array_merge($mainparams, $conditionparams); // If default is true, then allow that too. if ($this->is_field_condition_met( $this->operator, $customfield->defaultdata, $this->value)) { $where = "((ud.data IS NOT NULL AND $condition) OR (ud.data IS NULL))"; } else { $where = "(ud.data IS NOT NULL AND $condition)"; } } else { $standardfields = self::get_standard_profile_fields(); if (!array_key_exists($this->standardfield, $standardfields)) { // If the field isn't found, nobody matches. return ['SELECT id FROM {user} WHERE 0 = 1', []]; } $tablesql = "JOIN {user} u ON u.id = userids.id"; list ($where, $mainparams) = $this->get_condition_sql( 'u.' . $this->standardfield); } // Handle NOT. if ($not) { $where = 'NOT (' . $where . ')'; } // Get enrolled user SQL and combine with this query. list ($enrolsql, $enrolparams) = get_enrolled_sql($info->get_context(), '', 0, $onlyactive); $sql = "SELECT userids.id FROM ($enrolsql) userids $tablesql WHERE $where"; $params = array_merge($enrolparams, $mainparams); return array($sql, $params); } } condition/profile/classes/frontend.php 0000644 00000004461 15215712063 0014166 0 ustar 00 <?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/>. /** * Front-end class. * * @package availability_profile * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace availability_profile; defined('MOODLE_INTERNAL') || die(); /** * Front-end class. * * @package availability_profile * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class frontend extends \core_availability\frontend { protected function get_javascript_strings() { return array('op_contains', 'op_doesnotcontain', 'op_endswith', 'op_isempty', 'op_isequalto', 'op_isnotempty', 'op_startswith', 'conditiontitle', 'label_operator', 'label_value'); } protected function get_javascript_init_params($course, ?\cm_info $cm = null, ?\section_info $section = null) { // Standard user fields. $standardfields = condition::get_standard_profile_fields(); \core_collator::asort($standardfields); // Custom fields. $customfields = array(); $options = array('context' => \context_course::instance($course->id)); foreach (condition::get_custom_profile_fields() as $field) { $customfields[$field->shortname] = format_string($field->name, true, $options); } \core_collator::asort($customfields); // Make arrays into JavaScript format (non-associative, ordered) and return. return array(self::convert_associative_array_for_js($standardfields, 'field', 'display'), self::convert_associative_array_for_js($customfields, 'field', 'display')); } } condition/profile/lang/en/availability_profile.php 0000644 00000005362 15215712063 0016430 0 ustar 00 <?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/>. /** * Language strings. * * @package availability_profile * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['conditiontitle'] = 'User profile field'; $string['description'] = 'Control access based on fields within the student\'s profile.'; $string['error_selectfield'] = 'You must select a profile field.'; $string['error_setvalue'] = 'You must type a value.'; $string['label_operator'] = 'Method of comparison'; $string['label_value'] = 'Value to compare against'; $string['pluginname'] = 'Restriction by profile'; $string['requires_contains'] = 'Your <strong>{$a->field}</strong> contains <strong>{$a->value}</strong>'; $string['requires_doesnotcontain'] = 'Your <strong>{$a->field}</strong> does not contain <strong>{$a->value}</strong>'; $string['requires_endswith'] = 'Your <strong>{$a->field}</strong> ends with <strong>{$a->value}</strong>'; $string['requires_isempty'] = 'Your <strong>{$a->field}</strong> is empty'; $string['requires_isequalto'] = 'Your <strong>{$a->field}</strong> is <strong>{$a->value}</strong>'; $string['requires_isnotempty'] = 'Your <strong>{$a->field}</strong> is not empty'; $string['requires_notendswith'] = 'Your <strong>{$a->field}</strong> does not end with <strong>{$a->value}</strong>'; $string['requires_notisequalto'] = 'Your <strong>{$a->field}</strong> is not <strong>{$a->value}</strong>'; $string['requires_notstartswith'] = 'Your <strong>{$a->field}</strong> does not start with <strong>{$a->value}</strong>'; $string['requires_startswith'] = 'Your <strong>{$a->field}</strong> starts with <strong>{$a->value}</strong>'; $string['missing'] = '(Missing field: {$a})'; $string['title'] = 'User profile'; $string['op_contains'] = 'contains'; $string['op_doesnotcontain'] = 'doesn\'t contain'; $string['op_endswith'] = 'ends with'; $string['op_isempty'] = 'is empty'; $string['op_isequalto'] = 'is equal to'; $string['op_isnotempty'] = 'is not empty'; $string['op_startswith'] = 'starts with'; $string['privacy:metadata'] = 'The Restriction by profile plugin does not store any personal data.'; condition/profile/version.php 0000644 00000001756 15215712063 0012403 0 ustar 00 <?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/>. /** * Version info. * * @package availability_profile * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2024100700; $plugin->requires = 2024100100; $plugin->component = 'availability_profile'; condition/profile/tests/behat/availability_profile.feature 0000644 00000011572 15215712063 0020176 0 ustar 00 @availability @availability_profile Feature: availability_profile In order to control student access to activities As a teacher I need to set profile conditions which prevent student access Background: Given the following "courses" exist: | fullname | shortname | format | enablecompletion | | Course 1 | C1 | topics | 1 | And the following "users" exist: | username | email | | teacher1 | t@example.com | | student1 | s@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | course | name | | page | C1 | P1 | | page | C1 | P2 | @javascript Scenario: Test condition # Basic setup. Given I am on the "P1" "page activity editing" page logged in as "teacher1" And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "User profile" "button" And I set the field "User profile field" to "Email address" And I set the field "Value to compare against" to "s@example.com" And I click on ".availability-item .availability-eye img" "css_element" And I click on "Save and return to course" "button" # Add And I am on the "P2" "page activity editing" page And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "User profile" "button" And I set the field "User profile field" to "Email address" And I set the field "Value to compare against" to "q@example.com" And I click on ".availability-item .availability-eye img" "css_element" And I click on "Save and return to course" "button" # Log back in as student. When I am on the "Course 1" "course" page logged in as "student1" # I see P1 but not P2. Then I should see "P1" in the "region-main" "region" And I should not see "P2" in the "region-main" "region" @javascript Scenario: Test with custom user profile field Given the following "custom profile fields" exist: | datatype | shortname | name | | text | superfield | Super field | # Set field value for user. And I am on the "s@example.com" "user > editing" page logged in as "admin" And I expand all fieldsets And I set the field "Super field" to "Bananaman" And I click on "Update profile" "button" # Set Page activity which has requirement on this field. And I am on the "P1" "page activity editing" page And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "User profile" "button" And I set the following fields to these values: | User profile field | Super field | | Value to compare against | Bananaman | And I click on ".availability-item .availability-eye img" "css_element" And I click on "Save and return to course" "button" # Edit it again and check the setting still works. When I am on the P1 "page activity editing" page And I expand all fieldsets Then the field "User profile field" matches value "Super field" And the field "Value to compare against" matches value "Bananaman" # Log out and back in as student. Should be able to see activity. And I am on the "Course 1" "course" page logged in as "student1" Then I should see "P1" in the "region-main" "region" @javascript Scenario: Condition display with filters # Teacher sets up a restriction on group G1, using multilang filter. Given the following "custom profile fields" exist: | datatype | shortname | name | param2 | | text | frog | <span lang="en" class="multilang">F-One</span><span lang="fr" class="multilang">F-Un</span> | 100 | And the "multilang" filter is "on" And the "multilang" filter applies to "content and headings" # The activity names filter is enabled because it triggered a bug in older versions. And the "activitynames" filter is "on" And the "activitynames" filter applies to "content and headings" And I am on the "P1" "page activity editing" page logged in as "teacher1" And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "User profile" "button" in the "Add restriction..." "dialogue" And I set the following fields to these values: | User profile field | F-One | | Value to compare against | 111 | And I click on "Save and return to course" "button" And I log out # Student sees information about no access to group, with group name in correct language. When I am on the "C1" "Course" page logged in as "student1" Then I should see "Not available unless: Your F-One is 111" And I should not see "F-Un" condition/profile/tests/condition_test.php 0000644 00000055255 15215712063 0015110 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace availability_profile; /** * Unit tests for the user profile condition. * * @package availability_profile * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class condition_test extends \advanced_testcase { /** @var profile_define_text Profile field for testing */ protected $profilefield; /** @var array Array of user IDs for whome we already set the profile field */ protected $setusers = array(); /** @var condition Current condition */ private $cond; /** @var \core_availability\info Current info */ private $info; public function setUp(): void { global $DB, $CFG; parent::setUp(); $this->resetAfterTest(); // Add a custom profile field type. $this->profilefield = $this->getDataGenerator()->create_custom_profile_field(array( 'shortname' => 'frogtype', 'name' => 'Type of frog', 'datatype' => 'text')); // Clear static cache. \availability_profile\condition::wipe_static_cache(); // Load the mock info class so that it can be used. require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info.php'); } /** * Tests constructing and using date condition as part of tree. */ public function test_in_tree(): void { global $USER; $this->setAdminUser(); $info = new \core_availability\mock_info(); $structure = (object)array('op' => '|', 'show' => true, 'c' => array( (object)array('type' => 'profile', 'op' => condition::OP_IS_EQUAL_TO, 'cf' => 'frogtype', 'v' => 'tree'))); $tree = new \core_availability\tree($structure); // Initial check (user does not have custom field). $result = $tree->check_available(false, $info, true, $USER->id); $this->assertFalse($result->is_available()); // Set field. $this->set_field($USER->id, 'tree'); // Now it's true! $result = $tree->check_available(false, $info, true, $USER->id); $this->assertTrue($result->is_available()); } /** * Tests the constructor including error conditions. Also tests the * string conversion feature (intended for debugging only). */ public function test_constructor(): void { // No parameters. $structure = new \stdClass(); try { $cond = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Missing or invalid ->op', $e->getMessage()); } // Invalid op. $structure->op = 'isklingonfor'; try { $cond = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Missing or invalid ->op', $e->getMessage()); } // Missing value. $structure->op = condition::OP_IS_EQUAL_TO; try { $cond = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Missing or invalid ->v', $e->getMessage()); } // Invalid value (not string). $structure->v = false; try { $cond = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Missing or invalid ->v', $e->getMessage()); } // Unexpected value. $structure->op = condition::OP_IS_EMPTY; try { $cond = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Unexpected ->v', $e->getMessage()); } // Missing field. $structure->op = condition::OP_IS_EQUAL_TO; $structure->v = 'flying'; try { $cond = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Missing ->sf or ->cf', $e->getMessage()); } // Invalid field (not string). $structure->sf = 42; try { $cond = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Invalid ->sf', $e->getMessage()); } // Both fields. $structure->sf = 'department'; $structure->cf = 'frogtype'; try { $cond = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Both ->sf and ->cf', $e->getMessage()); } // Invalid ->cf field (not string). unset($structure->sf); $structure->cf = false; try { $cond = new condition($structure); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Invalid ->cf', $e->getMessage()); } // Valid examples (checks values are correctly included). $structure->cf = 'frogtype'; $cond = new condition($structure); $this->assertEquals('{profile:*frogtype isequalto flying}', (string)$cond); unset($structure->v); $structure->op = condition::OP_IS_EMPTY; $cond = new condition($structure); $this->assertEquals('{profile:*frogtype isempty}', (string)$cond); unset($structure->cf); $structure->sf = 'department'; $cond = new condition($structure); $this->assertEquals('{profile:department isempty}', (string)$cond); } /** * Tests the save() function. */ public function test_save(): void { $structure = (object)array('cf' => 'frogtype', 'op' => condition::OP_IS_EMPTY); $cond = new condition($structure); $structure->type = 'profile'; $this->assertEquals($structure, $cond->save()); $structure = (object)array('cf' => 'frogtype', 'op' => condition::OP_ENDS_WITH, 'v' => 'bouncy'); $cond = new condition($structure); $structure->type = 'profile'; $this->assertEquals($structure, $cond->save()); } /** * Tests the is_available function. There is no separate test for * get_full_information because that function is called from is_available * and we test its values here. */ public function test_is_available(): void { global $USER, $SITE, $DB; $this->setAdminUser(); $info = new \core_availability\mock_info(); // Prepare to test with all operators against custom field using all // combinations of NOT and true/false states.. $information = 'x'; $structure = (object)array('cf' => 'frogtype'); $structure->op = condition::OP_IS_NOT_EMPTY; $cond = new condition($structure); $this->assert_is_available_result(false, '~Type of frog.*is not empty~', $cond, $info, $USER->id); $this->set_field($USER->id, 'poison dart'); $this->assert_is_available_result(true, '~Type of frog.*is empty~', $cond, $info, $USER->id); $structure->op = condition::OP_IS_EMPTY; $cond = new condition($structure); $this->assert_is_available_result(false, '~.*Type of frog.*is empty~', $cond, $info, $USER->id); $this->set_field($USER->id, null); $this->assert_is_available_result(true, '~.*Type of frog.*is not empty~', $cond, $info, $USER->id); $this->set_field($USER->id, ''); $this->assert_is_available_result(true, '~.*Type of frog.*is not empty~', $cond, $info, $USER->id); $structure->op = condition::OP_CONTAINS; $structure->v = 'llf'; $cond = new condition($structure); $this->assert_is_available_result(false, '~Type of frog.*contains.*llf~', $cond, $info, $USER->id); $this->set_field($USER->id, 'bullfrog'); $this->assert_is_available_result(true, '~Type of frog.*does not contain.*llf~', $cond, $info, $USER->id); $structure->op = condition::OP_DOES_NOT_CONTAIN; $cond = new condition($structure); $this->assert_is_available_result(false, '~Type of frog.*does not contain.*llf~', $cond, $info, $USER->id); $this->set_field($USER->id, 'goliath'); $this->assert_is_available_result(true, '~Type of frog.*contains.*llf~', $cond, $info, $USER->id); $structure->op = condition::OP_IS_EQUAL_TO; $structure->v = 'Kermit'; $cond = new condition($structure); $this->assert_is_available_result(false, '~Type of frog.*is <.*Kermit~', $cond, $info, $USER->id); $this->set_field($USER->id, 'Kermit'); $this->assert_is_available_result(true, '~Type of frog.*is not.*Kermit~', $cond, $info, $USER->id); $structure->op = condition::OP_STARTS_WITH; $structure->v = 'Kerm'; $cond = new condition($structure); $this->assert_is_available_result(true, '~Type of frog.*does not start.*Kerm~', $cond, $info, $USER->id); $this->set_field($USER->id, 'Keroppi'); $this->assert_is_available_result(false, '~Type of frog.*starts.*Kerm~', $cond, $info, $USER->id); $structure->op = condition::OP_ENDS_WITH; $structure->v = 'ppi'; $cond = new condition($structure); $this->assert_is_available_result(true, '~Type of frog.*does not end.*ppi~', $cond, $info, $USER->id); $this->set_field($USER->id, 'Kermit'); $this->assert_is_available_result(false, '~Type of frog.*ends.*ppi~', $cond, $info, $USER->id); // Also test is_available for a different (not current) user. $generator = $this->getDataGenerator(); $user = $generator->create_user(); $structure->op = condition::OP_CONTAINS; $structure->v = 'rne'; $cond = new condition($structure); $this->assertFalse($cond->is_available(false, $info, true, $user->id)); $this->set_field($user->id, 'horned'); $this->assertTrue($cond->is_available(false, $info, true, $user->id)); // Now check with a standard field (department). $structure = (object)array('op' => condition::OP_IS_EQUAL_TO, 'sf' => 'department', 'v' => 'Cheese Studies'); $cond = new condition($structure); $this->assertFalse($cond->is_available(false, $info, true, $USER->id)); $this->assertFalse($cond->is_available(false, $info, true, $user->id)); // Check the message (should be using lang string with capital, which // is evidence that it called the right function to get the name). $information = $cond->get_description(false, false, $info); $information = \core_availability\info::format_info($information, $info->get_course()); $this->assertMatchesRegularExpression('~Department~', $information); // Set the field to true for both users and retry. $DB->set_field('user', 'department', 'Cheese Studies', array('id' => $user->id)); $USER->department = 'Cheese Studies'; $this->assertTrue($cond->is_available(false, $info, true, $USER->id)); $this->assertTrue($cond->is_available(false, $info, true, $user->id)); } /** * Tests what happens with custom fields that are text areas. These should * not be offered in the menu because their data is not included in user * object */ public function test_custom_textarea_field(): void { global $USER, $SITE, $DB; $this->setAdminUser(); $info = new \core_availability\mock_info(); // Add custom textarea type. $customfield = $this->getDataGenerator()->create_custom_profile_field(array( 'shortname' => 'longtext', 'name' => 'Long text', 'datatype' => 'textarea')); // The list of fields should include the text field added in setUp(), // but should not include the textarea field added just now. $fields = condition::get_custom_profile_fields(); $this->assertArrayHasKey('frogtype', $fields); $this->assertArrayNotHasKey('longtext', $fields); } /** * Sets the custom profile field used for testing. * * @param int $userid User id * @param string|null $value Field value or null to clear * @param int $fieldid Field id or 0 to use default one */ protected function set_field($userid, $value, $fieldid = 0) { global $DB, $USER; if (!$fieldid) { $fieldid = $this->profilefield->id; } $alreadyset = array_key_exists($userid, $this->setusers); if (is_null($value)) { $DB->delete_records('user_info_data', array('userid' => $userid, 'fieldid' => $fieldid)); unset($this->setusers[$userid]); } else if ($alreadyset) { $DB->set_field('user_info_data', 'data', $value, array('userid' => $userid, 'fieldid' => $fieldid)); } else { $DB->insert_record('user_info_data', array('userid' => $userid, 'fieldid' => $fieldid, 'data' => $value)); $this->setusers[$userid] = true; } } /** * Checks the result of is_available. This function is to save duplicated * code; it does two checks (the normal is_available with $not set to true * and set to false). Whichever result is expected to be true, it checks * $information ends up as empty string for that one, and as a regex match * for another one. * * @param bool $yes If the positive test is expected to return true * @param string $failpattern Regex pattern to match text when it returns false * @param condition $cond Condition * @param \core_availability\info $info Information about current context * @param int $userid User id */ protected function assert_is_available_result($yes, $failpattern, condition $cond, \core_availability\info $info, $userid) { // Positive (normal) test. $this->assertEquals($yes, $cond->is_available(false, $info, true, $userid), 'Failed checking normal (positive) result'); if (!$yes) { $information = $cond->get_description(false, false, $info); $information = \core_availability\info::format_info($information, $info->get_course()); $this->assertMatchesRegularExpression($failpattern, $information); } // Negative (NOT) test. $this->assertEquals(!$yes, $cond->is_available(true, $info, true, $userid), 'Failed checking NOT (negative) result'); if ($yes) { $information = $cond->get_description(false, true, $info); $information = \core_availability\info::format_info($information, $info->get_course()); $this->assertMatchesRegularExpression($failpattern, $information); } } /** * Tests the filter_users (bulk checking) function. */ public function test_filter_users(): void { global $DB, $CFG; $this->resetAfterTest(); $CFG->enableavailability = true; // Erase static cache before test. condition::wipe_static_cache(); // Make a test course and some users. $generator = $this->getDataGenerator(); $course = $generator->create_course(); $student1 = $generator->create_user(array('institution' => 'Unseen University')); $student2 = $generator->create_user(array('institution' => 'Hogwarts')); $student3 = $generator->create_user(array('institution' => 'Unseen University')); $allusers = array(); foreach (array($student1, $student2, $student3) as $student) { $generator->enrol_user($student->id, $course->id); $allusers[$student->id] = $student; } $this->set_field($student1->id, 'poison dart'); $this->set_field($student2->id, 'poison dart'); $info = new \core_availability\mock_info($course); $checker = new \core_availability\capability_checker($info->get_context()); // Test standard field condition (positive and negative). $cond = new condition((object)array('sf' => 'institution', 'op' => 'contains', 'v' => 'Unseen')); $result = array_keys($cond->filter_user_list($allusers, false, $info, $checker)); ksort($result); $this->assertEquals(array($student1->id, $student3->id), $result); $result = array_keys($cond->filter_user_list($allusers, true, $info, $checker)); ksort($result); $this->assertEquals(array($student2->id), $result); // Test custom field condition. $cond = new condition((object)array('cf' => 'frogtype', 'op' => 'contains', 'v' => 'poison')); $result = array_keys($cond->filter_user_list($allusers, false, $info, $checker)); ksort($result); $this->assertEquals(array($student1->id, $student2->id), $result); $result = array_keys($cond->filter_user_list($allusers, true, $info, $checker)); ksort($result); $this->assertEquals(array($student3->id), $result); } /** * Tests getting user list SQL. This is a different test from the above because * there is some additional code in this function so more variants need testing. */ public function test_get_user_list_sql(): void { global $DB, $CFG; $this->resetAfterTest(); $CFG->enableavailability = true; // Erase static cache before test. condition::wipe_static_cache(); // For testing, make another info field with default value. $otherprofilefield = $this->getDataGenerator()->create_custom_profile_field(array( 'shortname' => 'tonguestyle', 'name' => 'Tongue style', 'datatype' => 'text', 'defaultdata' => 'Slimy')); // Make a test course and some users. $generator = $this->getDataGenerator(); $course = $generator->create_course(); $student1 = $generator->create_user(array('institution' => 'Unseen University')); $student2 = $generator->create_user(array('institution' => 'Hogwarts')); $student3 = $generator->create_user(array('institution' => 'Unseen University')); $student4 = $generator->create_user(array('institution' => '0')); $allusers = array(); foreach (array($student1, $student2, $student3, $student4) as $student) { $generator->enrol_user($student->id, $course->id); $allusers[$student->id] = $student; } $this->set_field($student1->id, 'poison dart'); $this->set_field($student2->id, 'poison dart'); $this->set_field($student3->id, 'Rough', $otherprofilefield->id); $this->info = new \core_availability\mock_info($course); // Test standard field condition (positive). $this->cond = new condition((object)array('sf' => 'institution', 'op' => condition::OP_CONTAINS, 'v' => 'Univ')); $this->assert_user_list_sql_results(array($student1->id, $student3->id)); // Now try it negative. $this->assert_user_list_sql_results(array($student2->id, $student4->id), true); // Try all the other condition types. $this->cond = new condition((object)array('sf' => 'institution', 'op' => condition::OP_DOES_NOT_CONTAIN, 'v' => 's')); $this->assert_user_list_sql_results(array($student4->id)); $this->cond = new condition((object)array('sf' => 'institution', 'op' => condition::OP_IS_EQUAL_TO, 'v' => 'Hogwarts')); $this->assert_user_list_sql_results(array($student2->id)); $this->cond = new condition((object)array('sf' => 'institution', 'op' => condition::OP_STARTS_WITH, 'v' => 'U')); $this->assert_user_list_sql_results(array($student1->id, $student3->id)); $this->cond = new condition((object)array('sf' => 'institution', 'op' => condition::OP_ENDS_WITH, 'v' => 'rts')); $this->assert_user_list_sql_results(array($student2->id)); $this->cond = new condition((object)array('sf' => 'institution', 'op' => condition::OP_IS_EMPTY)); $this->assert_user_list_sql_results(array($student4->id)); $this->cond = new condition((object)array('sf' => 'institution', 'op' => condition::OP_IS_NOT_EMPTY)); $this->assert_user_list_sql_results(array($student1->id, $student2->id, $student3->id)); // Try with a custom field condition that doesn't have a default. $this->cond = new condition((object)array('cf' => 'frogtype', 'op' => condition::OP_CONTAINS, 'v' => 'poison')); $this->assert_user_list_sql_results(array($student1->id, $student2->id)); $this->cond = new condition((object)array('cf' => 'frogtype', 'op' => condition::OP_IS_EMPTY)); $this->assert_user_list_sql_results(array($student3->id, $student4->id)); // Try with one that does have a default. $this->cond = new condition((object)array('cf' => 'tonguestyle', 'op' => condition::OP_STARTS_WITH, 'v' => 'Sli')); $this->assert_user_list_sql_results(array($student1->id, $student2->id, $student4->id)); $this->cond = new condition((object)array('cf' => 'tonguestyle', 'op' => condition::OP_IS_EMPTY)); $this->assert_user_list_sql_results(array()); } /** * Convenience function. Gets the user list SQL and runs it, then checks * results. * * @param array $expected Array of expected user ids * @param bool $not True if using NOT condition */ private function assert_user_list_sql_results(array $expected, $not = false) { global $DB; list ($sql, $params) = $this->cond->get_user_list_sql($not, $this->info, true); $result = $DB->get_fieldset_sql($sql, $params); sort($result); $this->assertEquals($expected, $result); } } condition/profile/yui/src/form/js/form.js 0000644 00000012570 15215712063 0014456 0 ustar 00 /** * JavaScript for form editing profile conditions. * * @module moodle-availability_profile-form */ M.availability_profile = M.availability_profile || {}; /** * @class M.availability_profile.form * @extends M.core_availability.plugin */ M.availability_profile.form = Y.Object(M.core_availability.plugin); /** * Groupings available for selection (alphabetical order). * * @property profiles * @type Array */ M.availability_profile.form.profiles = null; /** * Initialises this plugin. * * @method initInner * @param {Array} standardFields Array of objects with .field, .display * @param {Array} customFields Array of objects with .field, .display */ M.availability_profile.form.initInner = function(standardFields, customFields) { this.standardFields = standardFields; this.customFields = customFields; }; M.availability_profile.form.getNode = function(json) { // Create HTML structure. var html = '<span class="availability-group"><label><span class="pe-3">' + M.util.get_string('conditiontitle', 'availability_profile') + '</span> ' + '<select name="field" class="custom-select">' + '<option value="choose">' + M.util.get_string('choosedots', 'moodle') + '</option>'; var fieldInfo; for (var i = 0; i < this.standardFields.length; i++) { fieldInfo = this.standardFields[i]; // String has already been escaped using format_string. html += '<option value="sf_' + fieldInfo.field + '">' + fieldInfo.display + '</option>'; } for (i = 0; i < this.customFields.length; i++) { fieldInfo = this.customFields[i]; // String has already been escaped using format_string. html += '<option value="cf_' + fieldInfo.field + '">' + fieldInfo.display + '</option>'; } html += '</select></label> <label><span class="accesshide">' + M.util.get_string('label_operator', 'availability_profile') + ' </span><select name="op" title="' + M.util.get_string('label_operator', 'availability_profile') + '"' + ' class="custom-select">'; var operators = ['isequalto', 'contains', 'doesnotcontain', 'startswith', 'endswith', 'isempty', 'isnotempty']; for (i = 0; i < operators.length; i++) { html += '<option value="' + operators[i] + '">' + M.util.get_string('op_' + operators[i], 'availability_profile') + '</option>'; } html += '</select></label> <label><span class="accesshide">' + M.util.get_string('label_value', 'availability_profile') + '</span><input name="value" type="text" class="form-control" style="width: 10em" title="' + M.util.get_string('label_value', 'availability_profile') + '"/></label></span>'; var node = Y.Node.create('<span class="d-flex flex-wrap align-items-center">' + html + '</span>'); // Set initial values if specified. if (json.sf !== undefined && node.one('select[name=field] > option[value=sf_' + json.sf + ']')) { node.one('select[name=field]').set('value', 'sf_' + json.sf); } else if (json.cf !== undefined && node.one('select[name=field] > option[value=cf_' + json.cf + ']')) { node.one('select[name=field]').set('value', 'cf_' + json.cf); } if (json.op !== undefined && node.one('select[name=op] > option[value=' + json.op + ']')) { node.one('select[name=op]').set('value', json.op); if (json.op === 'isempty' || json.op === 'isnotempty') { node.one('input[name=value]').set('disabled', true); } } if (json.v !== undefined) { node.one('input').set('value', json.v); } // Add event handlers (first time only). if (!M.availability_profile.form.addedEvents) { M.availability_profile.form.addedEvents = true; var updateForm = function(input) { var ancestorNode = input.ancestor('span.availability_profile'); var op = ancestorNode.one('select[name=op]'); var novalue = (op.get('value') === 'isempty' || op.get('value') === 'isnotempty'); ancestorNode.one('input[name=value]').set('disabled', novalue); M.core_availability.form.update(); }; var root = Y.one('.availability-field'); root.delegate('change', function() { updateForm(this); }, '.availability_profile select'); root.delegate('change', function() { updateForm(this); }, '.availability_profile input[name=value]'); } return node; }; M.availability_profile.form.fillValue = function(value, node) { // Set field. var field = node.one('select[name=field]').get('value'); if (field.substr(0, 3) === 'sf_') { value.sf = field.substr(3); } else if (field.substr(0, 3) === 'cf_') { value.cf = field.substr(3); } // Operator and value value.op = node.one('select[name=op]').get('value'); var valueNode = node.one('input[name=value]'); if (!valueNode.get('disabled')) { value.v = valueNode.get('value'); } }; M.availability_profile.form.fillErrors = function(errors, node) { var value = {}; this.fillValue(value, node); // Check profile item id. if (value.sf === undefined && value.cf === undefined) { errors.push('availability_profile:error_selectfield'); } if (value.v !== undefined && /^\s*$/.test(value.v)) { errors.push('availability_profile:error_setvalue'); } }; condition/profile/yui/src/form/build.json 0000644 00000000241 15215712063 0014523 0 ustar 00 { "name": "moodle-availability_profile-form", "builds": { "moodle-availability_profile-form": { "jsfiles": [ "form.js" ] } } } condition/profile/yui/src/form/meta/form.json 0000644 00000000241 15215712063 0015315 0 ustar 00 { "moodle-availability_profile-form": { "requires": [ "base", "node", "event", "moodle-core_availability-form" ] } } condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form-min.js0000644 00000006356 15215712063 0030252 0 ustar 00 YUI.add("moodle-availability_profile-form",function(n,e){M.availability_profile=M.availability_profile||{},M.availability_profile.form=n.Object(M.core_availability.plugin),M.availability_profile.form.profiles=null,M.availability_profile.form.initInner=function(e,i){this.standardFields=e,this.customFields=i},M.availability_profile.form.getNode=function(e){for(var i,l,a,t,o='<span class="availability-group"><label><span class="pe-3">'+M.util.get_string("conditiontitle","availability_profile")+'</span> <select name="field" class="custom-select"><option value="choose">'+M.util.get_string("choosedots","moodle")+"</option>",s=0;s<this.standardFields.length;s++)o+='<option value="sf_'+(i=this.standardFields[s]).field+'">'+i.display+"</option>";for(s=0;s<this.customFields.length;s++)o+='<option value="cf_'+(i=this.customFields[s]).field+'">'+i.display+"</option>";for(o+='</select></label> <label><span class="accesshide">'+M.util.get_string("label_operator","availability_profile")+' </span><select name="op" title="'+M.util.get_string("label_operator","availability_profile")+'" class="custom-select">',l=["isequalto","contains","doesnotcontain","startswith","endswith","isempty","isnotempty"],s=0;s<l.length;s++)o+='<option value="'+l[s]+'">'+M.util.get_string("op_"+l[s],"availability_profile")+"</option>";return o+='</select></label> <label><span class="accesshide">'+M.util.get_string("label_value","availability_profile")+'</span><input name="value" type="text" class="form-control" style="width: 10em" title="'+M.util.get_string("label_value","availability_profile")+'"/></label></span>',a=n.Node.create('<span class="d-flex flex-wrap align-items-center">'+o+"</span>"),e.sf!==undefined&&a.one("select[name=field] > option[value=sf_"+e.sf+"]")?a.one("select[name=field]").set("value","sf_"+e.sf):e.cf!==undefined&&a.one("select[name=field] > option[value=cf_"+e.cf+"]")&&a.one("select[name=field]").set("value","cf_"+e.cf),e.op!==undefined&&a.one("select[name=op] > option[value="+e.op+"]")&&(a.one("select[name=op]").set("value",e.op),"isempty"!==e.op&&"isnotempty"!==e.op||a.one("input[name=value]").set("disabled",!0)),e.v!==undefined&&a.one("input").set("value",e.v),M.availability_profile.form.addedEvents||(M.availability_profile.form.addedEvents=!0,t=function(e){var e=e.ancestor("span.availability_profile"),i=e.one("select[name=op]"),i="isempty"===i.get("value")||"isnotempty"===i.get("value");e.one("input[name=value]").set("disabled",i),M.core_availability.form.update()},(e=n.one(".availability-field")).delegate("change",function(){t(this)},".availability_profile select"),e.delegate("change",function(){t(this)},".availability_profile input[name=value]")),a},M.availability_profile.form.fillValue=function(e,i){var l=i.one("select[name=field]").get("value");"sf_"===l.substr(0,3)?e.sf=l.substr(3):"cf_"===l.substr(0,3)&&(e.cf=l.substr(3)),e.op=i.one("select[name=op]").get("value"),(l=i.one("input[name=value]")).get("disabled")||(e.v=l.get("value"))},M.availability_profile.form.fillErrors=function(e,i){var l={};this.fillValue(l,i),l.sf===undefined&&l.cf===undefined&&e.push("availability_profile:error_selectfield"),l.v!==undefined&&/^\s*$/.test(l.v)&&e.push("availability_profile:error_setvalue")}},"@VERSION@",{requires:["base","node","event","moodle-core_availability-form"]}); profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form-debug.js 0000644 00000013027 15215712063 0030467 0 ustar 00 condition YUI.add('moodle-availability_profile-form', function (Y, NAME) { /** * JavaScript for form editing profile conditions. * * @module moodle-availability_profile-form */ M.availability_profile = M.availability_profile || {}; /** * @class M.availability_profile.form * @extends M.core_availability.plugin */ M.availability_profile.form = Y.Object(M.core_availability.plugin); /** * Groupings available for selection (alphabetical order). * * @property profiles * @type Array */ M.availability_profile.form.profiles = null; /** * Initialises this plugin. * * @method initInner * @param {Array} standardFields Array of objects with .field, .display * @param {Array} customFields Array of objects with .field, .display */ M.availability_profile.form.initInner = function(standardFields, customFields) { this.standardFields = standardFields; this.customFields = customFields; }; M.availability_profile.form.getNode = function(json) { // Create HTML structure. var html = '<span class="availability-group"><label><span class="pe-3">' + M.util.get_string('conditiontitle', 'availability_profile') + '</span> ' + '<select name="field" class="custom-select">' + '<option value="choose">' + M.util.get_string('choosedots', 'moodle') + '</option>'; var fieldInfo; for (var i = 0; i < this.standardFields.length; i++) { fieldInfo = this.standardFields[i]; // String has already been escaped using format_string. html += '<option value="sf_' + fieldInfo.field + '">' + fieldInfo.display + '</option>'; } for (i = 0; i < this.customFields.length; i++) { fieldInfo = this.customFields[i]; // String has already been escaped using format_string. html += '<option value="cf_' + fieldInfo.field + '">' + fieldInfo.display + '</option>'; } html += '</select></label> <label><span class="accesshide">' + M.util.get_string('label_operator', 'availability_profile') + ' </span><select name="op" title="' + M.util.get_string('label_operator', 'availability_profile') + '"' + ' class="custom-select">'; var operators = ['isequalto', 'contains', 'doesnotcontain', 'startswith', 'endswith', 'isempty', 'isnotempty']; for (i = 0; i < operators.length; i++) { html += '<option value="' + operators[i] + '">' + M.util.get_string('op_' + operators[i], 'availability_profile') + '</option>'; } html += '</select></label> <label><span class="accesshide">' + M.util.get_string('label_value', 'availability_profile') + '</span><input name="value" type="text" class="form-control" style="width: 10em" title="' + M.util.get_string('label_value', 'availability_profile') + '"/></label></span>'; var node = Y.Node.create('<span class="d-flex flex-wrap align-items-center">' + html + '</span>'); // Set initial values if specified. if (json.sf !== undefined && node.one('select[name=field] > option[value=sf_' + json.sf + ']')) { node.one('select[name=field]').set('value', 'sf_' + json.sf); } else if (json.cf !== undefined && node.one('select[name=field] > option[value=cf_' + json.cf + ']')) { node.one('select[name=field]').set('value', 'cf_' + json.cf); } if (json.op !== undefined && node.one('select[name=op] > option[value=' + json.op + ']')) { node.one('select[name=op]').set('value', json.op); if (json.op === 'isempty' || json.op === 'isnotempty') { node.one('input[name=value]').set('disabled', true); } } if (json.v !== undefined) { node.one('input').set('value', json.v); } // Add event handlers (first time only). if (!M.availability_profile.form.addedEvents) { M.availability_profile.form.addedEvents = true; var updateForm = function(input) { var ancestorNode = input.ancestor('span.availability_profile'); var op = ancestorNode.one('select[name=op]'); var novalue = (op.get('value') === 'isempty' || op.get('value') === 'isnotempty'); ancestorNode.one('input[name=value]').set('disabled', novalue); M.core_availability.form.update(); }; var root = Y.one('.availability-field'); root.delegate('change', function() { updateForm(this); }, '.availability_profile select'); root.delegate('change', function() { updateForm(this); }, '.availability_profile input[name=value]'); } return node; }; M.availability_profile.form.fillValue = function(value, node) { // Set field. var field = node.one('select[name=field]').get('value'); if (field.substr(0, 3) === 'sf_') { value.sf = field.substr(3); } else if (field.substr(0, 3) === 'cf_') { value.cf = field.substr(3); } // Operator and value value.op = node.one('select[name=op]').get('value'); var valueNode = node.one('input[name=value]'); if (!valueNode.get('disabled')) { value.v = valueNode.get('value'); } }; M.availability_profile.form.fillErrors = function(errors, node) { var value = {}; this.fillValue(value, node); // Check profile item id. if (value.sf === undefined && value.cf === undefined) { errors.push('availability_profile:error_selectfield'); } if (value.v !== undefined && /^\s*$/.test(value.v)) { errors.push('availability_profile:error_setvalue'); } }; }, '@VERSION@', {"requires": ["base", "node", "event", "moodle-core_availability-form"]}); condition/profile/yui/build/moodle-availability_profile-form/moodle-availability_profile-form.js 0000644 00000013027 15215712063 0027462 0 ustar 00 YUI.add('moodle-availability_profile-form', function (Y, NAME) { /** * JavaScript for form editing profile conditions. * * @module moodle-availability_profile-form */ M.availability_profile = M.availability_profile || {}; /** * @class M.availability_profile.form * @extends M.core_availability.plugin */ M.availability_profile.form = Y.Object(M.core_availability.plugin); /** * Groupings available for selection (alphabetical order). * * @property profiles * @type Array */ M.availability_profile.form.profiles = null; /** * Initialises this plugin. * * @method initInner * @param {Array} standardFields Array of objects with .field, .display * @param {Array} customFields Array of objects with .field, .display */ M.availability_profile.form.initInner = function(standardFields, customFields) { this.standardFields = standardFields; this.customFields = customFields; }; M.availability_profile.form.getNode = function(json) { // Create HTML structure. var html = '<span class="availability-group"><label><span class="pe-3">' + M.util.get_string('conditiontitle', 'availability_profile') + '</span> ' + '<select name="field" class="custom-select">' + '<option value="choose">' + M.util.get_string('choosedots', 'moodle') + '</option>'; var fieldInfo; for (var i = 0; i < this.standardFields.length; i++) { fieldInfo = this.standardFields[i]; // String has already been escaped using format_string. html += '<option value="sf_' + fieldInfo.field + '">' + fieldInfo.display + '</option>'; } for (i = 0; i < this.customFields.length; i++) { fieldInfo = this.customFields[i]; // String has already been escaped using format_string. html += '<option value="cf_' + fieldInfo.field + '">' + fieldInfo.display + '</option>'; } html += '</select></label> <label><span class="accesshide">' + M.util.get_string('label_operator', 'availability_profile') + ' </span><select name="op" title="' + M.util.get_string('label_operator', 'availability_profile') + '"' + ' class="custom-select">'; var operators = ['isequalto', 'contains', 'doesnotcontain', 'startswith', 'endswith', 'isempty', 'isnotempty']; for (i = 0; i < operators.length; i++) { html += '<option value="' + operators[i] + '">' + M.util.get_string('op_' + operators[i], 'availability_profile') + '</option>'; } html += '</select></label> <label><span class="accesshide">' + M.util.get_string('label_value', 'availability_profile') + '</span><input name="value" type="text" class="form-control" style="width: 10em" title="' + M.util.get_string('label_value', 'availability_profile') + '"/></label></span>'; var node = Y.Node.create('<span class="d-flex flex-wrap align-items-center">' + html + '</span>'); // Set initial values if specified. if (json.sf !== undefined && node.one('select[name=field] > option[value=sf_' + json.sf + ']')) { node.one('select[name=field]').set('value', 'sf_' + json.sf); } else if (json.cf !== undefined && node.one('select[name=field] > option[value=cf_' + json.cf + ']')) { node.one('select[name=field]').set('value', 'cf_' + json.cf); } if (json.op !== undefined && node.one('select[name=op] > option[value=' + json.op + ']')) { node.one('select[name=op]').set('value', json.op); if (json.op === 'isempty' || json.op === 'isnotempty') { node.one('input[name=value]').set('disabled', true); } } if (json.v !== undefined) { node.one('input').set('value', json.v); } // Add event handlers (first time only). if (!M.availability_profile.form.addedEvents) { M.availability_profile.form.addedEvents = true; var updateForm = function(input) { var ancestorNode = input.ancestor('span.availability_profile'); var op = ancestorNode.one('select[name=op]'); var novalue = (op.get('value') === 'isempty' || op.get('value') === 'isnotempty'); ancestorNode.one('input[name=value]').set('disabled', novalue); M.core_availability.form.update(); }; var root = Y.one('.availability-field'); root.delegate('change', function() { updateForm(this); }, '.availability_profile select'); root.delegate('change', function() { updateForm(this); }, '.availability_profile input[name=value]'); } return node; }; M.availability_profile.form.fillValue = function(value, node) { // Set field. var field = node.one('select[name=field]').get('value'); if (field.substr(0, 3) === 'sf_') { value.sf = field.substr(3); } else if (field.substr(0, 3) === 'cf_') { value.cf = field.substr(3); } // Operator and value value.op = node.one('select[name=op]').get('value'); var valueNode = node.one('input[name=value]'); if (!valueNode.get('disabled')) { value.v = valueNode.get('value'); } }; M.availability_profile.form.fillErrors = function(errors, node) { var value = {}; this.fillValue(value, node); // Check profile item id. if (value.sf === undefined && value.cf === undefined) { errors.push('availability_profile:error_selectfield'); } if (value.v !== undefined && /^\s*$/.test(value.v)) { errors.push('availability_profile:error_setvalue'); } }; }, '@VERSION@', {"requires": ["base", "node", "event", "moodle-core_availability-form"]}); UPGRADING.md 0000644 00000000404 15215712063 0006406 0 ustar 00 # core_availability (subsystem) Upgrade notes ## 4.5 ### Removed - The previously deprecated renderer `render_core_availability_multiple_messages` method has been removed. For more information see [MDL-82223](https://tracker.moodle.org/browse/MDL-82223) renderer.php 0000644 00000002563 15215712063 0007073 0 ustar 00 <?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/>. use core\output\plugin_renderer_base; use core_availability\output\availability_info; /** * Renderer for availability display. * * @package core_availability * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_availability_renderer extends plugin_renderer_base { /** * @deprecated since Moodle 4.0 MDL-71691 - please do not use this function any more. */ #[\core\attribute\deprecated(availability_info::class, since: '4.0', mdl: 'MDL-71691', final: true)] public function render_core_availability_multiple_messages(): void { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); } } templates/availability_info.mustache 0000644 00000003365 15215712063 0013773 0 ustar 00 {{! 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/>. }} {{! @template core_availability/availability_info Renders the availability tree for course activties Example context (json): { "id" : "123456", "header": "Not available unless:", "items" : [ { "hasitems": 0, "header": "You belong to Green Group" }, { "hasitems": 0, "header": "You belong to Red Group" }, { "hasitems": 0, "header": "You belong to Orange Group" }, { "hasitems": 0, "header": "You belong to Pink Group" }, { "hasitems": 0, "header": "You belong to Red Group" } ], "hasitems": 1 } }} {{{header}}} {{#hasitems}} <ul {{#id}} id="availability-tree-{{id}}" {{/id}} data-region="availability-multiple"> {{#items}} <li> {{> core_availability/availability_info }} </li> {{/items}} </ul> {{/hasitems}} tests/behat/display_availability.feature 0000644 00000022252 15215712063 0014552 0 ustar 00 @core @core_availability Feature: Display availability for activities and sections In order to know which activities are available As a user I need to see appropriate availability restrictions for activities and sections # PURPOSE OF THIS TEST FEATURE: # # This test is to do a basic check of the user interface relating to display # of availability conditions - i.e. if there's a condition, does it show up; # are we doing the HTML correctly; does it correctly hide an activity where # the options are set to not show it at all. # # Things this test is not: # - It is not a test of the date condition specifically. The date condition is # only used as an example in order to get the availability information to # display. (The date condition has its own Behat test in # /availability/condition/date/tests/behat.) # - It is not a complete test of the logic. This is supposed to be a shallow # check of the user interface parts and doesn't cover all logical # possibilities. The logic is tested in PHPUnit tests instead, which are # much more efficient. (Again there are unit tests for the overall system # and for each condition type.) Background: Given the following "course" exists: | fullname | Course 1 | | shortname | C1 | | format | topics | | initsections | 1 | And the following "users" exist: | username | | teacher1 | | student1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | course | section | name | | page | C1 | 1 | Page 1 | | page | C1 | 2 | Page 2 | | page | C1 | 3 | Page 3 | @javascript Scenario: Activity availability display # Set up. Given I am on the "Page 1" "page activity editing" page logged in as "teacher1" And I expand all fieldsets And I press "Add restriction..." And I click on "Date" "button" in the "Add restriction..." "dialogue" And I set the field "direction" to "until" And I set the field "x[year]" to "2013" And I set the field "x[month]" to "March" And I press "Save and return to course" # Add a Page with 2 restrictions - one is set to hide from students if failed. And I am on the "Page 2" "page activity editing" page And I expand all fieldsets And I press "Add restriction..." And I click on "Date" "button" in the "Add restriction..." "dialogue" And I set the field "direction" to "until" And I set the field "x[year]" to "2013" And I set the field "x[month]" to "March" And I click on ".availability-item .availability-eye img" "css_element" And I press "Add restriction..." And I click on "User profile" "button" in the "Add restriction..." "dialogue" And I set the field "User profile field" to "Email address" And I set the field "Value to compare against" to "email@example.com" And I set the field "Method of comparison" to "is equal to" And I press "Save and return to course" # Page 1 should show in single-line format, showing the date Then I should see "Available until" in the "Page 1" "core_availability > Activity availability" And I should see "2013" in the "Page 1" "core_availability > Activity availability" And I should see "2013" in the "Page 1" "core_availability > Activity availability" And "li" "css_element" should not exist in the "Page 1" "core_availability > Activity availability" And "Show more" "button" should not exist in the "Page 1" "core_availability > Activity availability" # Page 2 should show in list format. And "li" "css_element" should exist in the "Page 2" "core_availability > Activity availability" And I should see "Not available unless:" in the "Page 2" "core_availability > Activity availability" And I should see "It is before" in the "Page 2" "core_availability > Activity availability" And I should see "hidden otherwise" in the "Page 2" "core_availability > Activity availability" And I click on "Show more" "button" in the "Page 2" "activity" And I should see "Email address" in the "Page 2" "core_availability > Activity availability" And I click on "Show less" "button" in the "Page 2" "core_availability > Activity availability" And I should not see "Email address" in the "Page 2" "core_availability > Activity availability" # Page 3 should not have available info. And "Page 3" "core_availability > Activity availability" should not exist # Change to student view. Given I am on the "C1" "Course" page logged in as "student1" # Page 1 display still there but should not be a link. Then I should see "Page 1" in the "#section-1" "css_element" And ".activity-instance a" "css_element" should not exist in the "Section 1" "section" # Date display should be present. And I should see "Available until" in the "Section 1" "section" # Page 2 display not there at all And I should not see "Page 2" in the "region-main" "region" # Page 3 display and link And I should see "Page 3" in the "region-main" "region" And ".activity-instance a" "css_element" should exist in the "Section 3" "section" @javascript Scenario: Section availability display # Set up. Given I am on the "C1" "Course" page logged in as "teacher1" And I turn editing mode on # Add a restriction to section 1 (visible to students). When I edit the section "1" And I expand all fieldsets And I press "Add restriction..." And I click on "Date" "button" in the "Add restriction..." "dialogue" And I set the field "direction" to "until" And I set the field "x[year]" to "2013" And I press "Add restriction..." And I click on "User profile" "button" in the "Add restriction..." "dialogue" And I set the field "User profile field" to "Email address" And I set the field "Value to compare against" to "email@example.com" And I set the field "Method of comparison" to "is equal to" And I press "Save changes" # Section 2 is the same but hidden from students And I am on "Course 1" course homepage And I edit the section "2" And I expand all fieldsets And I press "Add restriction..." And I click on "Date" "button" in the "Add restriction..." "dialogue" And I set the field "direction" to "until" And I set the field "x[year]" to "2013" And I click on ".availability-item .availability-eye img" "css_element" And I press "Save changes" # This is necessary because otherwise it fails in Chrome, see MDL-44959 And I am on "Course 1" course homepage # Check display Then I should see "Not available unless" in the "section-1" "core_availability > Section availability" And I should see "Available until" in the "section-2" "core_availability > Section availability" And I should see "hidden otherwise" in the "section-2" "core_availability > Section availability" # Change to student view. Given I am on the "Course 1" "Course" page logged in as "student1" # The contents of both sections should be hidden. Then I should not see "Page 1" in the "region-main" "region" And I should not see "Page 2" in the "region-main" "region" And I should see "Page 3" in the "region-main" "region" # Section 1 should be visible and show info. And I should see "Section 1" in the "region-main" "region" And I should see "Not available unless" in the "section-1" "core_availability > Section availability" And I click on "Show more" "button" in the "section-1" "core_availability > Section availability" And I should see "Email address" in the "section-1" "core_availability > Section availability" And I click on "Show less" "button" in the "section-1" "core_availability > Section availability" And I should not see "Email address" in the "section-1" "core_availability > Section availability" # Section 2 should not be available at all And I should not see "Section 2" in the "region-main" "region" @javascript Scenario: Change default display for in manage restriction, then check eye icon by add restriction access to activity. Given I log in as "admin" When I navigate to "Plugins > Availability restrictions > Manage restrictions" in site administration # Change value for display eye. And I click on "Hide" "icon" in the ".display-mode-date" "css_element" And I am on the "Page 1" "page activity editing" page logged in as "teacher1" And I expand all fieldsets And I press "Add restriction..." And I click on "Date" "button" in the "Add restriction..." "dialogue" Then the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Hidden entirely" And I click on ".availability-item .availability-delete img" "css_element" And I should not see "Date" in the "Restrict access" "fieldset" And I press "Add restriction..." And I click on "Grade" "button" in the "Add restriction..." "dialogue" And the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Displayed if student" tests/behat/edit_availability.feature 0000644 00000033102 15215712063 0014026 0 ustar 00 @core @core_availability Feature: edit_availability In order to control which students can see activities As a teacher I need to set up availability options for activities and sections # PURPOSE OF THIS TEST FEATURE: # # This test covers the user interface around editing availability conditions, # especially the JavaScript code which is not tested elsewhere (e.g. does the # 'Add restriction' dialog work). It tests both forms and also the admin # setting interface. # # This test does not check the detailed behaviour of the availability system, # which is mainly covered in PHPUnit (and, from the user interface # perspective, in the other Behat tests for each type of condition). Background: Given the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | And the following "users" exist: | username | | teacher1 | | student1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activity" exists: | activity | forum | | course | C1 | | name | MyForum | Scenario: Confirm the 'enable availability' option is working Given the following config values are set as admin: | enableavailability | 0 | When I log in as "teacher1" And the following "activity" exists: | activity | page | | course | C1 | | idnumber | 0001 | | section | 1 | | name | Page1 | | intro | pageintro | And I am on "Course 1" course homepage with editing mode on And I follow "Page1" And I navigate to "Settings" in current page administration Then "Restrict access" "fieldset" should not exist Given I am on "Course 1" course homepage When I edit the section "1" Then "Restrict access" "fieldset" should not exist And the following config values are set as admin: | enableavailability | 1 | And the following "activity" exists: | activity | page | | course | C1 | | idnumber | 0002 | | name | Page2 | And I am on the "Page2" "page activity editing" page Then "Restrict access" "fieldset" should exist Given I am on "Course 1" course homepage When I edit the section "1" Then "Restrict access" "fieldset" should exist @javascript Scenario: Edit availability using settings in activity form # Set up. Given the following "activity" exists: | activity | page | | course | C1 | | section | 1 | | name | P1 | And I am on the "P1" "page activity editing" page logged in as "teacher1" And I expand all fieldsets Then I should see "None" in the "Restrict access" "fieldset" # Add a Date restriction and check it appears. When I click on "Add restriction..." "button" Then "Add restriction..." "dialogue" should be visible When I click on "Date" "button" in the "Add restriction..." "dialogue" Then "Add restriction..." "dialogue" should not exist And I should not see "None" in the "Restrict access" "fieldset" And "Restriction type" "select" should be visible And I should see "Date" in the "Restrict access" "fieldset" And ".availability-item .availability-eye img" "css_element" should be visible And ".availability-item .availability-delete img" "css_element" should be visible And the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Displayed if student" # Toggle the eye icon. When I click on ".availability-item .availability-eye img" "css_element" Then the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Hidden entirely" When I click on ".availability-item .availability-eye img" "css_element" Then the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Displayed if student" # Click the delete button. When I click on ".availability-item .availability-delete img" "css_element" Then I should not see "Date" in the "Restrict access" "fieldset" # Add a nested restriction set and check it appears. When I click on "Add restriction..." "button" And I click on "Restriction set" "button" in the "Add restriction..." "dialogue" Then ".availability-children .availability-list" "css_element" should be visible And I should see "None" in the ".availability-children .availability-list" "css_element" And I should see "Please set" in the ".availability-children .availability-list" "css_element" And I should see "Add restriction" in the ".availability-children .availability-list" "css_element" # Click on the button to add a restriction inside the nested set. When I click on "Add restriction..." "button" in the ".availability-children .availability-list" "css_element" And I click on "Date" "button" in the "Add restriction..." "dialogue" Then I should not see "None" in the ".availability-children .availability-list" "css_element" And I should not see "Please set" in the ".availability-children .availability-list" "css_element" And I should see "Date" in the ".availability-children .availability-list" "css_element" # OK, let's delete the date inside the nested set... When I click on ".availability-item .availability-delete img" "css_element" in the ".availability-item" "css_element" Then I should not see "Date" in the ".availability-children .availability-list" "css_element" And I should see "None" in the ".availability-children .availability-list" "css_element" # ...and the nested set itself. When I click on ".availability-none .availability-delete img" "css_element" Then ".availability-children .availability-list" "css_element" should not exist # Add two dates so we can check the connectors. When I click on "Add restriction..." "button" And I click on "Date" "button" in the "Add restriction..." "dialogue" And I click on "Add restriction..." "button" And I click on "Date" "button" in the "Add restriction..." "dialogue" Then I should see "and" in the "Restrict access" "fieldset" And "Required restrictions" "select" should be visible # Try changing the connector type. When I set the field "Required restrictions" to "any" Then I should not see "and" in the "Restrict access" "fieldset" And I should see "or" in the "Restrict access" "fieldset" # Now delete one of the dates and check the connector goes away. When I click on ".availability-item .availability-delete img" "css_element" Then I should not see "or" in the "Restrict access" "fieldset" # Add a nested restriction set with two dates so there will be inner connector. When I click on "Add restriction..." "button" And I click on "Restriction set" "button" in the "Add restriction..." "dialogue" And I click on "Add restriction..." "button" in the ".availability-children .availability-list" "css_element" And I click on "Date" "button" in the "Add restriction..." "dialogue" And I click on "Add restriction..." "button" in the ".availability-children .availability-list" "css_element" And I click on "Date" "button" in the "Add restriction..." "dialogue" Then I should see "and" in the ".availability-children .availability-list .availability-connector" "css_element" # Check changing the outer one does not affect the inner one. When I set the field "Required restrictions" to "all" Then I should not see "or" in the "Restrict access" "fieldset" When I set the field "Required restrictions" to "any" Then I should see "or" in the "Restrict access" "fieldset" And I should not see "or" in the ".availability-children .availability-list .availability-connector" "css_element" @javascript Scenario: Edit availability using settings in section form # Set up. Given I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on # Edit a section When I edit the section "1" And I expand all fieldsets Then I should see "None" in the "Restrict access" "fieldset" # Add a Date restriction and check it appears. When I click on "Add restriction..." "button" When I click on "Date" "button" in the "Add restriction..." "dialogue" And I should not see "None" in the "Restrict access" "fieldset" And "Restriction type" "select" should be visible And I should see "Date" in the "Restrict access" "fieldset" @javascript Scenario: 'Add group/grouping access restriction' button unavailable # Button does not exist when conditional access restrictions are turned off. Given the following config values are set as admin: | enableavailability | 0 | And I am on the "MyForum" "forum activity editing" page logged in as admin When I expand all fieldsets Then "Add group/grouping access restriction" "button" should not exist @javascript Scenario: Use the 'Add group/grouping access restriction' button # Button should initially be disabled. Given the following "groupings" exist: | name | course | idnumber | | GX1 | C1 | GXI1 | And I am on the "MyForum" "forum activity editing" page logged in as admin When I expand all fieldsets Then the "Add group/grouping access restriction" "button" should be disabled # Turn on separate groups. And I set the field "Group mode" to "Separate groups" And the "Add group/grouping access restriction" "button" should be enabled # Press the button and check it adds a restriction and disables itself. And I should see "None" in the "Restrict access" "fieldset" And I press "Add group/grouping access restriction" And I should see "Group" in the "Restrict access" "fieldset" And the "Add group/grouping access restriction" "button" should be disabled # Delete the restriction and check it is enabled again. And I click on "Delete" "link" in the "Restrict access" "fieldset" And the "Add group/grouping access restriction" "button" should be enabled # Try a grouping instead. And I set the field "Grouping" to "GX1" And I press "Add group/grouping access restriction" And I should see "Grouping" in the "Restrict access" "fieldset" # Check the button still works after saving and editing. And I press "Save and display" And I navigate to "Settings" in current page administration And I expand all fieldsets And the "Add group/grouping access restriction" "button" should be disabled And I should see "Grouping" in the "Restrict access" "fieldset" # And check it's still active if I delete the condition. And I click on "Delete" "link" in the "Restrict access" "fieldset" And the "Add group/grouping access restriction" "button" should be enabled @javascript Scenario: Edit section availability using course page link # Setting a restriction up Given I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And I edit the section "1" And I expand all fieldsets And I press "Add restriction..." And I click on "Date" "button" in the "Add restriction..." "dialogue" And I press "Save changes" # Testing edit restrictions link And "Edit restrictions" "link" should exist in the "section-1" "core_availability > Section availability" When I click on "Edit restrictions" "link" in the "section-1" "core_availability > Section availability" Then I should see "Restrict access" And I should not see "Summary of General" And I should see "Collapse all" And I should not see "Expand all" And I click on "Cancel" "button" And I am on "Course 1" course homepage with editing mode off And I should not see "Edit restrictions" @javascript Scenario: Edit activity availability using course page link # Setting a restriction up Given I am on the "MyForum" "forum activity editing" page logged in as teacher1 And I expand all fieldsets And I press "Add restriction..." And I click on "Date" "button" in the "Add restriction..." "dialogue" When I press "Save and return to course" # Edit restrictions link not displayed when editing mode is off. Then "Edit restrictions" "link" should not exist in the "MyForum" "core_availability > Activity availability" # Testing edit restrictions link But I am on "Course 1" course homepage with editing mode on And "Edit restrictions" "link" should exist in the "MyForum" "core_availability > Activity availability" And I click on "Edit restrictions" "link" in the "MyForum" "core_availability > Activity availability" And I should see "Restrict access" And I should not see "Content" And I should see "Collapse all" And I should not see "Expand all" @javascript Scenario: Edit activity availability button is shown after duplicating an activity # Setting a restriction up Given I am on the "MyForum" "forum activity editing" page logged in as teacher1 And I expand all fieldsets And I press "Add restriction..." And I click on "Date" "button" in the "Add restriction..." "dialogue" When I press "Save and return to course" And I turn editing mode on And I duplicate "MyForum" activity # Testing edit restrictions link Then "Edit restrictions" "link" should exist in the "MyForum (copy)" "core_availability > Activity availability" tests/behat/private_ruleset.feature 0000644 00000021417 15215712063 0013572 0 ustar 00 @core @core_availability @javascript Feature: Private rule sets In order to prevent private data being leaked in restriction sets As a teacher I want to have restrictions hidden when a private condition is selected Background: Given the following "courses" exist: | fullname | shortname | format | enablecompletion | numsections | | Course 1 | C1 | topics | 1 | 3 | And the following "users" exist: | username | | teacher1 | | student1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "groups" exist: | name | course | idnumber | visibility | | Group A | C1 | GA | 0 | | Group B | C1 | GB | 1 | And I log in as "teacher1" And I add a page activity to course "Course 1" section "1" And I expand all fieldsets Scenario: Add restriction with visible condition (must match), display option should be active When I click on "Add restriction..." "button" And I click on "Date" "button" in the "Add restriction..." "dialogue" Then ".availability-children .availability-eye" "css_element" should be visible And ".availability-children .availability-eye-disabled" "css_element" should not be visible And the "title" attribute of ".availability-eye" "css_element" should contain "Click to hide" Scenario: Add restriction with private condition (must match), display option should be disabled When I click on "Add restriction..." "button" And I click on "Group" "button" in the "Add restriction..." "dialogue" And I set the field "Group" to "Group B" Then ".availability-children .availability-eye" "css_element" should not be visible And ".availability-children .availability-eye-disabled" "css_element" should be visible And the "title" attribute of ".availability-eye-disabled" "css_element" should contain "Cannot be changed as ruleset includes a rule containing private data." Scenario: Add restrictions with a visible and a private condition (must match all), display option should be disabled When I click on "Add restriction..." "button" And I click on "Group" "button" in the "Add restriction..." "dialogue" And I set the field "Group" to "Group B" When I click on "Add restriction..." "button" And I click on "Date" "button" in the "Add restriction..." "dialogue" Then ".availability-children .availability-eye" "css_element" should not be visible And ".availability-children .availability-eye-disabled" "css_element" should be visible Scenario: Remove private condition (must match), display option should be active When I click on "Add restriction..." "button" And I click on "Group" "button" in the "Add restriction..." "dialogue" And I set the field "Group" to "Group B" And I click on "Add restriction..." "button" And I click on "Date" "button" in the "Add restriction..." "dialogue" Then ".availability-children .availability-eye" "css_element" should not be visible And ".availability-children .availability-eye-disabled" "css_element" should be visible # Should pick the first one (Group B) When I click on ".availability-item .availability-delete img" "css_element" Then ".availability-children .availability-eye" "css_element" should be visible And ".availability-children .availability-eye-disabled" "css_element" should not be visible Scenario: Set a private condition to a visible value (must match), display option should be active When I click on "Add restriction..." "button" And I click on "Group" "button" in the "Add restriction..." "dialogue" And I set the field "Group" to "Group B" Then ".availability-children .availability-eye" "css_element" should not be visible And ".availability-children .availability-eye-disabled" "css_element" should be visible # Should pick the first one (Group B) When I set the field "Group" to "Group A" Then ".availability-children .availability-eye" "css_element" should be visible And ".availability-children .availability-eye-disabled" "css_element" should not be visible Scenario: Add restrictions with a visible and a private condition (must match any), display option should be disabled When I click on "Add restriction..." "button" And I click on "Group" "button" in the "Add restriction..." "dialogue" And I set the field "Group" to "Group B" And I click on "Add restriction..." "button" And I click on "Date" "button" in the "Add restriction..." "dialogue" And I set the field "Required restrictions" to "any" # "Hidden" icon should be shown in header. And ".availability-children .availability-eye" "css_element" should not be visible And ".availability-children .availability-eye-disabled" "css_element" should not be visible And ".availability-header .availability-eye" "css_element" should not be visible And ".availability-header .availability-eye-disabled" "css_element" should be visible Scenario: Add restriction with private condition (must not match), display option should be disabled When I click on "Add restriction..." "button" And I click on "Group" "button" in the "Add restriction..." "dialogue" And I set the field "Group" to "Group B" And I set the field "Restriction type" to "must not" # "Hidden" icon should be shown in header. And ".availability-children .availability-eye" "css_element" should not be visible And ".availability-children .availability-eye-disabled" "css_element" should not be visible And ".availability-header .availability-eye" "css_element" should not be visible And ".availability-header .availability-eye-disabled" "css_element" should be visible Scenario: Add restrictions with a visible and a private condition (must not match all), display option should be disabled When I click on "Add restriction..." "button" And I click on "Group" "button" in the "Add restriction..." "dialogue" And I set the field "Group" to "Group B" And I click on "Add restriction..." "button" And I click on "Date" "button" in the "Add restriction..." "dialogue" And I set the field "Restriction type" to "must not" # "Hidden" icon should be shown in header. And ".availability-children .availability-eye" "css_element" should not be visible And ".availability-children .availability-eye-disabled" "css_element" should not be visible And ".availability-header .availability-eye" "css_element" should not be visible And ".availability-header .availability-eye-disabled" "css_element" should be visible Scenario: Add restrictions with a visible and a private condition (must not match any), display option should be disabled When I click on "Add restriction..." "button" And I click on "Group" "button" in the "Add restriction..." "dialogue" And I set the field "Group" to "Group B" And I click on "Add restriction..." "button" And I click on "Date" "button" in the "Add restriction..." "dialogue" And I set the field "Restriction type" to "must not" And I set the field "Required restrictions" to "any" # "Hidden" icon should be shown in conditions, but not in the header. And ".availability-header .availability-eye" "css_element" should not be visible And ".availability-header .availability-eye-disabled" "css_element" should not be visible And ".availability-children .availability-eye" "css_element" should not be visible And ".availability-children .availability-eye-disabled" "css_element" should be visible Scenario: Private conditions should not show to unprivileged users Given I set the field "Name" to "Test page" And I set the field "Page content" to "test" And I click on "Add restriction..." "button" And I click on "Group" "button" in the "Add restriction..." "dialogue" And I set the field "Group" to "Group B" And I press "Save and return to course" And I log out And I log in as "student1" When I am on "Course 1" course homepage Then I should not see "Test page" And I should not see "Not available unless: You belong to Group B" Scenario: Loading a rule set containing private conditions should disable display option Given I set the field "Name" to "Test page" And I set the field "Page content" to "test" And I click on "Add restriction..." "button" And I click on "Group" "button" in the "Add restriction..." "dialogue" And I set the field "Group" to "Group B" And I press "Save and display" When I follow "Settings" And I expand all fieldsets Then ".availability-children .availability-eye" "css_element" should not be visible And ".availability-children .availability-eye-disabled" "css_element" should be visible tests/behat/behat_availability.php 0000644 00000004650 15215712063 0013326 0 ustar 00 <?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/>. require_once(__DIR__ . '/../../../lib/behat/behat_base.php'); /** * Availability related behat steps and selectors definitions. * * @package core_availability * @category test * @copyright 2023 Amaia Anabitarte <amaia@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_availability extends behat_base { /** * Return the list of partial named selectors. * * @return array */ public static function get_partial_named_selectors(): array { return [ new behat_component_named_selector( 'Activity availability', [ ".//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')]" . "[descendant::*[contains(normalize-space(.), %locator%)]]//div[@data-region='availabilityinfo']", ] ), new behat_component_named_selector( 'Section availability', [".//li[@id = %locator%]//div[@data-region='availabilityinfo']"], ), new behat_component_named_selector( 'Set Of Restrictions', ["//div[h3[@data-restriction-order=%locator% and contains(text(), 'Set of')]]"], ), ]; } /** * Return the list of exact named selectors * * @return array */ public static function get_exact_named_selectors(): array { return [ new behat_component_named_selector( 'Availability Button Area', [ "//h3[@data-restriction-order=%locator%]/following-sibling::div[contains(@class,'availability-inner')]/" . "div[contains(@class,'availability-button')]", ], ), ]; } } tests/fixtures/mock_info.php 0000644 00000004635 15215712063 0012246 0 ustar 00 <?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/>. /** * For use in unit tests that require an info object which isn't really used. * * @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(); /** * For use in unit tests that require an info object which isn't really used. * * @package core_availability * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mock_info extends info { /** @var int User id for modinfo */ protected $userid; /** * Constructs with item details. * * @param \stdClass $course Optional course param (otherwise uses $SITE) * @param int $userid Userid for modinfo (if used) */ public function __construct($course = null, $userid = 0) { global $SITE; if (!$course) { $course = $SITE; } parent::__construct($course, true, null); $this->userid = $userid; } protected function get_thing_name() { return 'Mock'; } 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) { } public function get_modinfo() { // Allow modinfo usage outside is_available etc., so we can use this // to directly call into condition is_available. if (!$this->userid) { throw new \coding_exception('Need to set mock_info userid'); } return get_fast_modinfo($this->course, $this->userid); } } tests/fixtures/mock_info_module.php 0000644 00000006521 15215712063 0013607 0 ustar 00 <?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/>. /** * For use in unit tests that require an info module which isn't really used. * * @package core_availability * @copyright 2019 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_availability; defined('MOODLE_INTERNAL') || die(); /** * For use in unit tests that require an info module which isn't really used. * * @package core_availability * @copyright 2019 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mock_info_module extends info_module { /** @var int User id for modinfo */ protected $userid; /** @var \cm_info Activity. */ protected $cm; /** * Constructs with item details. * * @param int $userid Userid for modinfo (if used) * @param \cm_info $cm Course-module object */ public function __construct($userid = 0, ?\cm_info $cm = null) { parent::__construct($cm); $this->userid = $userid; $this->cm = $cm; } /** * Just returns a mock name. * * @return string Name of item */ protected function get_thing_name() { return 'Mock Module'; } /** * Returns the current context. * * @return \context Context for this item */ public function get_context() { return \context_course::instance($this->get_course()->id); } /** * Returns the cappability used to ignore access restrictions. * * @return string Name of capability used to view hidden items of this type */ protected function get_view_hidden_capability() { return 'moodle/course:ignoreavailabilityrestrictions'; } /** * Mocks don't need to save anything into DB. * * @param string $availability New JSON value */ protected function set_in_database($availability) { } /** * 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() { // Allow modinfo usage outside is_available etc., so we can use this // to directly call into condition is_available. if (!$this->userid) { throw new \coding_exception('Need to set mock_info userid'); } return get_fast_modinfo($this->course, $this->userid); } /** * Override course-module info. * @param \cm_info $cm */ public function set_cm(\cm_info $cm) { $this->cm = $cm; } } tests/fixtures/mock_condition.php 0000644 00000012041 15215712063 0013267 0 ustar 00 <?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/>. /** * Mock condition. * * @package core_availability * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace availability_mock; defined('MOODLE_INTERNAL') || die(); /** * Mock condition. * * @package core_availability * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class condition extends \core_availability\condition { /** @var bool True if available */ protected $available; /** @var string Message if not available */ protected $message; /** @var bool True if available for all (normal state) */ protected $forall; /** @var bool True if available for all (NOT state) */ protected $forallnot; /** @var string Dependency table (empty if none) */ protected $dependtable; /** @var id Dependency id (0 if none) */ protected $dependid; /** @var array Array of user ids for filter results, empty if no filter support */ protected $filter; /** * Constructs a mock condition with given structure. * * @param \stdClass $structure Structure object */ public function __construct($structure) { $this->available = isset($structure->a) ? $structure->a : false; $this->message = isset($structure->m) ? $structure->m : ''; $this->forall = isset($structure->all) ? $structure->all : false; $this->forallnot = isset($structure->allnot) ? $structure->allnot : false; $this->dependtable = isset($structure->table) ? $structure->table : ''; $this->dependid = isset($structure->id) ? $structure->id : 0; $this->filter = isset($structure->filter) ? $structure->filter : array(); } public function save() { return (object)array('a' => $this->available, 'm' => $this->message, 'all' => $this->forall, 'allnot' => $this->forallnot, 'table' => $this->dependtable, 'id' => $this->dependid, 'filter' => $this->filter); } public function is_available($not, \core_availability\info $info, $grabthelot, $userid) { return $not ? !$this->available : $this->available; } public function is_available_for_all($not = false) { return $not ? $this->forallnot : $this->forall; } public function get_description($full, $not, \core_availability\info $info) { $fulltext = $full ? '[FULL]' : ''; $nottext = $not ? '!' : ''; return $nottext . $fulltext . $this->message; } public function get_standalone_description( $full, $not, \core_availability\info $info) { // Override so that we can spot that this function is used. return 'SA: ' . $this->get_description($full, $not, $info); } public function update_dependency_id($table, $oldid, $newid) { if ($table === $this->dependtable && (int)$oldid === (int)$this->dependid) { $this->dependid = $newid; return true; } else { return false; } } protected function get_debug_string() { return ($this->available ? 'y' : 'n') . ',' . $this->message; } public function is_applied_to_user_lists() { return $this->filter; } public function filter_user_list(array $users, $not, \core_availability\info $info, \core_availability\capability_checker $checker) { $result = array(); foreach ($users as $id => $user) { $match = in_array($id, $this->filter); if ($not) { $match = !$match; } if ($match) { $result[$id] = $user; } } return $result; } public function get_user_list_sql($not, \core_availability\info $info, $onlyactive) { global $DB; // The data for this condition is not really stored in the database, // so we return SQL that contains the hard-coded user list. list ($enrolsql, $enrolparams) = get_enrolled_sql($info->get_context(), '', 0, $onlyactive); $condition = $not ? 'NOT' : ''; list ($matchsql, $matchparams) = $DB->get_in_or_equal($this->filter, SQL_PARAMS_NAMED); $sql = "SELECT userids.id FROM ($enrolsql) userids WHERE $condition (userids.id $matchsql)"; return array($sql, array_merge($enrolparams, $matchparams)); } } tests/fixtures/mock_info_section.php 0000644 00000006640 15215712063 0013770 0 ustar 00 <?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/>. /** * For use in unit tests that require an info section which isn't really used. * * @package core_availability * @copyright 2019 Ferran Recio <ferran@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_availability; defined('MOODLE_INTERNAL') || die(); /** * For use in unit tests that require an info section which isn't really used. * * @package core_availability * @copyright 2019 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mock_info_section extends info_section { /** @var int User id for modinfo */ protected $userid; /** @var \section_info Section. */ protected $section; /** * Constructs with item details. * * @param int $userid Userid for modinfo (if used) * @param \section_info $section Section object */ public function __construct($userid = 0, ?\section_info $section = null) { parent::__construct($section); $this->userid = $userid; $this->section = $section; } /** * Just returns a mock name. * * @return string Name of item */ protected function get_thing_name() { return 'Mock Section'; } /** * Returns the current context. * * @return \context Context for this item */ public function get_context() { return \context_course::instance($this->get_course()->id); } /** * Returns the cappability used to ignore access restrictions. * * @return string Name of capability used to view hidden items of this type */ protected function get_view_hidden_capability() { return 'moodle/course:ignoreavailabilityrestrictions'; } /** * Mocks don't need to save anything into DB. * * @param string $availability New JSON value */ protected function set_in_database($availability) { } /** * 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() { // Allow modinfo usage outside is_available etc., so we can use this // to directly call into condition is_available. if (!$this->userid) { throw new \coding_exception('Need to set mock_info userid'); } return get_fast_modinfo($this->course, $this->userid); } /** * Override section info. * * @param \section_info $section */ public function set_section(\section_info $section) { $this->section = $section; } } tests/info_test.php 0000644 00000067665 15215712063 0010437 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_availability; /** * Unit tests for info and subclasses. * * @package core_availability * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class info_test extends \advanced_testcase { public function setUp(): void { // Load the mock condition so that it can be used. require_once(__DIR__ . '/fixtures/mock_condition.php'); parent::setUp(); } /** * Tests the info_module class (is_available, get_full_information). */ public function test_info_module(): void { global $DB, $CFG; // Create a course and pages. $CFG->enableavailability = 0; $this->setAdminUser(); $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); $rec = array('course' => $course); $page1 = $generator->get_plugin_generator('mod_page')->create_instance($rec); $page2 = $generator->get_plugin_generator('mod_page')->create_instance($rec); $page3 = $generator->get_plugin_generator('mod_page')->create_instance($rec); $page4 = $generator->get_plugin_generator('mod_page')->create_instance($rec); // Set up the availability option for the pages to mock options. $DB->set_field('course_modules', 'availability', '{"op":"|","show":true,"c":[' . '{"type":"mock","a":false,"m":"grandmaster flash"}]}', array('id' => $page1->cmid)); $DB->set_field('course_modules', 'availability', '{"op":"|","show":true,"c":[' . '{"type":"mock","a":true,"m":"the furious five"}]}', array('id' => $page2->cmid)); // Third page is invalid. (Fourth has no availability settings.) $DB->set_field('course_modules', 'availability', '{{{', array('id' => $page3->cmid)); $modinfo = get_fast_modinfo($course); $cm1 = $modinfo->get_cm($page1->cmid); $cm2 = $modinfo->get_cm($page2->cmid); $cm3 = $modinfo->get_cm($page3->cmid); $cm4 = $modinfo->get_cm($page4->cmid); // Do availability and full information checks. $info = new info_module($cm1); $information = ''; $this->assertFalse($info->is_available($information)); $this->assertEquals('SA: grandmaster flash', $information); $this->assertEquals('SA: [FULL]grandmaster flash', $info->get_full_information()); $info = new info_module($cm2); $this->assertTrue($info->is_available($information)); $this->assertEquals('', $information); $this->assertEquals('SA: [FULL]the furious five', $info->get_full_information()); // Check invalid one. $info = new info_module($cm3); $this->assertFalse($info->is_available($information)); $debugging = $this->getDebuggingMessages(); $this->resetDebugging(); $this->assertEquals(1, count($debugging)); $this->assertStringContainsString('Invalid availability', $debugging[0]->message); // Check empty one. $info = new info_module($cm4); $this->assertTrue($info->is_available($information)); $this->assertEquals('', $information); $this->assertEquals('', $info->get_full_information()); } /** * Tests the info_section class (is_available, get_full_information). */ public function test_info_section(): void { global $DB; // Create a course. $this->setAdminUser(); $this->resetAfterTest(); $generator = $this->getDataGenerator(); $course = $generator->create_course( array('numsections' => 4), array('createsections' => true)); // Set up the availability option for the sections to mock options. $DB->set_field('course_sections', 'availability', '{"op":"|","show":true,"c":[' . '{"type":"mock","a":false,"m":"public"}]}', array('course' => $course->id, 'section' => 1)); $DB->set_field('course_sections', 'availability', '{"op":"|","show":true,"c":[' . '{"type":"mock","a":true,"m":"enemy"}]}', array('course' => $course->id, 'section' => 2)); // Third section is invalid. (Fourth has no availability setting.) $DB->set_field('course_sections', 'availability', '{{{', array('course' => $course->id, 'section' => 3)); $modinfo = get_fast_modinfo($course); $sections = $modinfo->get_section_info_all(); // Do availability and full information checks. $info = new info_section($sections[1]); $information = ''; $this->assertFalse($info->is_available($information)); $this->assertEquals('SA: public', $information); $this->assertEquals('SA: [FULL]public', $info->get_full_information()); $info = new info_section($sections[2]); $this->assertTrue($info->is_available($information)); $this->assertEquals('', $information); $this->assertEquals('SA: [FULL]enemy', $info->get_full_information()); // Check invalid one. $info = new info_section($sections[3]); $this->assertFalse($info->is_available($information)); $debugging = $this->getDebuggingMessages(); $this->resetDebugging(); $this->assertEquals(1, count($debugging)); $this->assertStringContainsString('Invalid availability', $debugging[0]->message); // Check empty one. $info = new info_section($sections[4]); $this->assertTrue($info->is_available($information)); $this->assertEquals('', $information); $this->assertEquals('', $info->get_full_information()); } /** * Tests the is_user_visible() static function in info_module. */ public function test_is_user_visible(): void { global $CFG, $DB; require_once($CFG->dirroot . '/course/lib.php'); $this->resetAfterTest(); $CFG->enableavailability = 0; // Create a course and some pages: // 0. Invisible due to visible=0. // 1. Availability restriction (mock, set to fail). // 2. Availability restriction on section (mock, set to fail). // 3. Actually visible. $generator = $this->getDataGenerator(); $course = $generator->create_course( array('numsections' => 1), array('createsections' => true)); $rec = array('course' => $course, ); $pages = array(); $pagegen = $generator->get_plugin_generator('mod_page'); $pages[0] = $pagegen->create_instance($rec, array('visible' => 0)); $pages[1] = $pagegen->create_instance($rec); $pages[2] = $pagegen->create_instance($rec); $pages[3] = $pagegen->create_instance($rec); $modinfo = get_fast_modinfo($course); $section = $modinfo->get_section_info(1); $cm = $modinfo->get_cm($pages[2]->cmid); moveto_module($cm, $section); // Set the availability restrictions in database. The enableavailability // setting is off so these do not take effect yet. $notavailable = '{"op":"|","show":true,"c":[{"type":"mock","a":false}]}'; $DB->set_field('course_sections', 'availability', $notavailable, array('id' => $section->id)); $DB->set_field('course_modules', 'availability', $notavailable, array('id' => $pages[1]->cmid)); get_fast_modinfo($course, 0, true); // Set up 4 users - a teacher and student plus somebody who isn't even // on the course. Also going to use admin user and a spare student to // avoid cache problems. $roleids = $DB->get_records_menu('role', null, '', 'shortname, id'); $teacher = $generator->create_user(); $student = $generator->create_user(); $student2 = $generator->create_user(); $other = $generator->create_user(); $admin = $DB->get_record('user', array('username' => 'admin')); $generator->enrol_user($teacher->id, $course->id, $roleids['teacher']); $generator->enrol_user($student->id, $course->id, $roleids['student']); $generator->enrol_user($student2->id, $course->id, $roleids['student']); // Basic case when availability disabled, for visible item. $this->assertTrue(info_module::is_user_visible($pages[3]->cmid, $student->id, false)); // Specifying as an object should not make any queries. $cm = $DB->get_record('course_modules', array('id' => $pages[3]->cmid)); $beforequeries = $DB->perf_get_queries(); $this->assertTrue(info_module::is_user_visible($cm, $student->id, false)); $this->assertEquals($beforequeries, $DB->perf_get_queries()); // Specifying as cm_info for correct user should not make any more queries // if we have already obtained dynamic data. $modinfo = get_fast_modinfo($course, $student->id); $cminfo = $modinfo->get_cm($cm->id); // This will obtain dynamic data. $name = $cminfo->name; $beforequeries = $DB->perf_get_queries(); $this->assertTrue(info_module::is_user_visible($cminfo, $student->id, false)); $this->assertEquals($beforequeries, $DB->perf_get_queries()); // Function does not care if you are in the course (unless $checkcourse). $this->assertTrue(info_module::is_user_visible($cm, $other->id, false)); // With $checkcourse, check for enrolled, not enrolled, and admin user. $this->assertTrue(info_module::is_user_visible($cm, $student->id, true)); $this->assertFalse(info_module::is_user_visible($cm, $other->id, true)); $this->assertTrue(info_module::is_user_visible($cm, $admin->id, true)); // With availability off, the student can access all except the // visible=0 one. $this->assertFalse(info_module::is_user_visible($pages[0]->cmid, $student->id, false)); $this->assertTrue(info_module::is_user_visible($pages[1]->cmid, $student->id, false)); $this->assertTrue(info_module::is_user_visible($pages[2]->cmid, $student->id, false)); // Teacher and admin can even access the visible=0 one. $this->assertTrue(info_module::is_user_visible($pages[0]->cmid, $teacher->id, false)); $this->assertTrue(info_module::is_user_visible($pages[0]->cmid, $admin->id, false)); // Now enable availability (and clear cache). $CFG->enableavailability = true; get_fast_modinfo($course, 0, true); // Student cannot access the activity restricted by its own or by the // section's availability. $this->assertFalse(info_module::is_user_visible($pages[1]->cmid, $student->id, false)); $this->assertFalse(info_module::is_user_visible($pages[2]->cmid, $student->id, false)); } /** * Tests the convert_legacy_fields function used in restore. */ public function test_convert_legacy_fields(): void { // Check with no availability conditions first. $rec = (object)array('availablefrom' => 0, 'availableuntil' => 0, 'groupingid' => 7, 'showavailability' => 1); $this->assertNull(info::convert_legacy_fields($rec, false)); // Check same list for a section. $this->assertEquals( '{"op":"&","showc":[false],"c":[{"type":"grouping","id":7}]}', info::convert_legacy_fields($rec, true)); // Check groupmembersonly with grouping. $rec->groupmembersonly = 1; $this->assertEquals( '{"op":"&","showc":[false],"c":[{"type":"grouping","id":7}]}', info::convert_legacy_fields($rec, false)); // Check groupmembersonly without grouping. $rec->groupingid = 0; $this->assertEquals( '{"op":"&","showc":[false],"c":[{"type":"group"}]}', info::convert_legacy_fields($rec, false)); // Check start date. $rec->groupmembersonly = 0; $rec->availablefrom = 123; $this->assertEquals( '{"op":"&","showc":[true],"c":[{"type":"date","d":">=","t":123}]}', info::convert_legacy_fields($rec, false)); // Start date with show = false. $rec->showavailability = 0; $this->assertEquals( '{"op":"&","showc":[false],"c":[{"type":"date","d":">=","t":123}]}', info::convert_legacy_fields($rec, false)); // End date. $rec->showavailability = 1; $rec->availablefrom = 0; $rec->availableuntil = 456; $this->assertEquals( '{"op":"&","showc":[false],"c":[{"type":"date","d":"<","t":456}]}', info::convert_legacy_fields($rec, false)); // All together now. $rec->groupingid = 7; $rec->groupmembersonly = 1; $rec->availablefrom = 123; $this->assertEquals( '{"op":"&","showc":[false,true,false],"c":[' . '{"type":"grouping","id":7},' . '{"type":"date","d":">=","t":123},' . '{"type":"date","d":"<","t":456}' . ']}', info::convert_legacy_fields($rec, false)); $this->assertEquals( '{"op":"&","showc":[false,true,false],"c":[' . '{"type":"grouping","id":7},' . '{"type":"date","d":">=","t":123},' . '{"type":"date","d":"<","t":456}' . ']}', info::convert_legacy_fields($rec, false, true)); } /** * Tests the add_legacy_availability_condition function used in restore. */ public function test_add_legacy_availability_condition(): void { // Completion condition tests. $rec = (object)array('sourcecmid' => 7, 'requiredcompletion' => 1); // No previous availability, show = true. $this->assertEquals( '{"op":"&","showc":[true],"c":[{"type":"completion","cm":7,"e":1}]}', info::add_legacy_availability_condition(null, $rec, true)); // No previous availability, show = false. $this->assertEquals( '{"op":"&","showc":[false],"c":[{"type":"completion","cm":7,"e":1}]}', info::add_legacy_availability_condition(null, $rec, false)); // Existing availability. $before = '{"op":"&","showc":[true],"c":[{"type":"date","d":">=","t":70}]}'; $this->assertEquals( '{"op":"&","showc":[true,true],"c":['. '{"type":"date","d":">=","t":70},' . '{"type":"completion","cm":7,"e":1}' . ']}', info::add_legacy_availability_condition($before, $rec, true)); // Grade condition tests. $rec = (object)array('gradeitemid' => 3, 'grademin' => 7, 'grademax' => null); $this->assertEquals( '{"op":"&","showc":[true],"c":[{"type":"grade","id":3,"min":7.00000}]}', info::add_legacy_availability_condition(null, $rec, true)); $rec->grademax = 8; $this->assertEquals( '{"op":"&","showc":[true],"c":[{"type":"grade","id":3,"min":7.00000,"max":8.00000}]}', info::add_legacy_availability_condition(null, $rec, true)); unset($rec->grademax); unset($rec->grademin); $this->assertEquals( '{"op":"&","showc":[true],"c":[{"type":"grade","id":3}]}', info::add_legacy_availability_condition(null, $rec, true)); // Note: There is no need to test the grade condition with show // true/false and existing availability, because this uses the same // function. } /** * Tests the add_legacy_availability_field_condition function used in restore. */ public function test_add_legacy_availability_field_condition(): void { // User field, normal operator. $rec = (object)array('userfield' => 'email', 'shortname' => null, 'operator' => 'contains', 'value' => '@'); $this->assertEquals( '{"op":"&","showc":[true],"c":[' . '{"type":"profile","op":"contains","sf":"email","v":"@"}]}', info::add_legacy_availability_field_condition(null, $rec, true)); // User field, non-value operator. $rec = (object)array('userfield' => 'email', 'shortname' => null, 'operator' => 'isempty', 'value' => ''); $this->assertEquals( '{"op":"&","showc":[true],"c":[' . '{"type":"profile","op":"isempty","sf":"email"}]}', info::add_legacy_availability_field_condition(null, $rec, true)); // Custom field. $rec = (object)array('userfield' => null, 'shortname' => 'frogtype', 'operator' => 'isempty', 'value' => ''); $this->assertEquals( '{"op":"&","showc":[true],"c":[' . '{"type":"profile","op":"isempty","cf":"frogtype"}]}', info::add_legacy_availability_field_condition(null, $rec, true)); } /** * Tests the filter_user_list() and get_user_list_sql() functions. */ public function test_filter_user_list(): void { global $CFG, $DB; require_once($CFG->dirroot . '/course/lib.php'); $this->resetAfterTest(); $CFG->enableavailability = true; // Create a course with 2 sections and 2 pages and 3 users. // Availability is set up initially on the 'page/section 2' items. $generator = $this->getDataGenerator(); $course = $generator->create_course( array('numsections' => 2), array('createsections' => true)); $u1 = $generator->create_user(); $u2 = $generator->create_user(); $u3 = $generator->create_user(); $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'), MUST_EXIST); $allusers = array($u1->id => $u1, $u2->id => $u2, $u3->id => $u3); $generator->enrol_user($u1->id, $course->id, $studentroleid); $generator->enrol_user($u2->id, $course->id, $studentroleid); $generator->enrol_user($u3->id, $course->id, $studentroleid); // Page 2 allows access to users 2 and 3, while section 2 allows access // to users 1 and 2. $pagegen = $generator->get_plugin_generator('mod_page'); $page = $pagegen->create_instance(array('course' => $course)); $page2 = $pagegen->create_instance(array('course' => $course, 'availability' => '{"op":"|","show":true,"c":[{"type":"mock","filter":[' . $u2->id . ',' . $u3->id . ']}]}')); $modinfo = get_fast_modinfo($course); $section = $modinfo->get_section_info(1); $section2 = $modinfo->get_section_info(2); $DB->set_field('course_sections', 'availability', '{"op":"|","show":true,"c":[{"type":"mock","filter":[' . $u1->id . ',' . $u2->id .']}]}', array('id' => $section2->id)); moveto_module($modinfo->get_cm($page2->cmid), $section2); // With no restrictions, returns full list. $info = new info_module($modinfo->get_cm($page->cmid)); $this->assertEquals(array($u1->id, $u2->id, $u3->id), array_keys($info->filter_user_list($allusers))); $this->assertEquals(array('', array()), $info->get_user_list_sql(true)); // Set an availability restriction in database for section 1. // For the section we set it so it doesn't support filters; for the // module we have a filter. $DB->set_field('course_sections', 'availability', '{"op":"|","show":true,"c":[{"type":"mock","a":false}]}', array('id' => $section->id)); $DB->set_field('course_modules', 'availability', '{"op":"|","show":true,"c":[{"type":"mock","filter":[' . $u3->id .']}]}', array('id' => $page->cmid)); rebuild_course_cache($course->id, true); $modinfo = get_fast_modinfo($course); // Now it should work (for the module). $info = new info_module($modinfo->get_cm($page->cmid)); $expected = array($u3->id); $this->assertEquals($expected, array_keys($info->filter_user_list($allusers))); list ($sql, $params) = $info->get_user_list_sql(); $result = $DB->get_fieldset_sql($sql, $params); sort($result); $this->assertEquals($expected, $result); $info = new info_section($modinfo->get_section_info(1)); $this->assertEquals(array($u1->id, $u2->id, $u3->id), array_keys($info->filter_user_list($allusers))); $this->assertEquals(array('', array()), $info->get_user_list_sql(true)); // With availability disabled, module returns full list too. $CFG->enableavailability = false; $info = new info_module($modinfo->get_cm($page->cmid)); $this->assertEquals(array($u1->id, $u2->id, $u3->id), array_keys($info->filter_user_list($allusers))); $this->assertEquals(array('', array()), $info->get_user_list_sql(true)); // Check the other section... $CFG->enableavailability = true; $info = new info_section($modinfo->get_section_info(2)); $expected = array($u1->id, $u2->id); $this->assertEquals($expected, array_keys($info->filter_user_list($allusers))); list ($sql, $params) = $info->get_user_list_sql(true); $result = $DB->get_fieldset_sql($sql, $params); sort($result); $this->assertEquals($expected, $result); // And the module in that section - which has combined the section and // module restrictions. $info = new info_module($modinfo->get_cm($page2->cmid)); $expected = array($u2->id); $this->assertEquals($expected, array_keys($info->filter_user_list($allusers))); list ($sql, $params) = $info->get_user_list_sql(true); $result = $DB->get_fieldset_sql($sql, $params); sort($result); $this->assertEquals($expected, $result); // If the students have viewhiddenactivities, they get past the module // restriction. role_change_permission($studentroleid, \context_module::instance($page2->cmid), 'moodle/course:ignoreavailabilityrestrictions', CAP_ALLOW); $expected = array($u1->id, $u2->id); $this->assertEquals($expected, array_keys($info->filter_user_list($allusers))); list ($sql, $params) = $info->get_user_list_sql(true); $result = $DB->get_fieldset_sql($sql, $params); sort($result); $this->assertEquals($expected, $result); // If they have viewhiddensections, they also get past the section // restriction. role_change_permission($studentroleid, \context_course::instance($course->id), 'moodle/course:ignoreavailabilityrestrictions', CAP_ALLOW); $expected = array($u1->id, $u2->id, $u3->id); $this->assertEquals($expected, array_keys($info->filter_user_list($allusers))); list ($sql, $params) = $info->get_user_list_sql(true); $result = $DB->get_fieldset_sql($sql, $params); sort($result); $this->assertEquals($expected, $result); } /** * Tests the info_module class when involved in a recursive call to $cm->name. */ public function test_info_recursive_name_call(): void { global $DB; $this->resetAfterTest(); // Create a course and page. $generator = $this->getDataGenerator(); $course = $generator->create_course(); $page1 = $generator->create_module('page', ['course' => $course->id, 'name' => 'Page1']); // Set invalid availability. $DB->set_field('course_modules', 'availability', 'not valid', ['id' => $page1->cmid]); // Get the cm_info object. $this->setAdminUser(); $modinfo = get_fast_modinfo($course); $cm1 = $modinfo->get_cm($page1->cmid); // At this point we will generate dynamic data for $cm1, which will cause the debugging // call below. $this->assertEquals('Page1', $cm1->name); $this->assertDebuggingCalled('Error processing availability data for ' . '‘Page1’: Invalid availability text'); } /** * Test for the is_available_for_all() method of the info base class. * @covers \core_availability\info_module::is_available_for_all */ public function test_is_available_for_all(): void { global $CFG, $DB; $this->resetAfterTest(); $CFG->enableavailability = 0; $generator = $this->getDataGenerator(); $course = $generator->create_course(); $page = $generator->get_plugin_generator('mod_page')->create_instance(['course' => $course]); // Set an availability restriction and reset the modinfo cache. // The enableavailability setting is disabled so this does not take effect yet. $notavailable = '{"op":"|","show":true,"c":[{"type":"mock","a":false}]}'; $DB->set_field('course_modules', 'availability', $notavailable, ['id' => $page->cmid]); get_fast_modinfo($course, 0, true); // Availability is disabled, so we expect this module to be available for everyone. $modinfo = get_fast_modinfo($course); $info = new info_module($modinfo->get_cm($page->cmid)); $this->assertTrue($info->is_available_for_all()); // Now, enable availability restrictions, and check again. // This time, we expect it to return false, because of the access restriction. $CFG->enableavailability = 1; $this->assertFalse($info->is_available_for_all()); } /** * Test update_display_mode function. * * @covers \core\plugininfo\availability::update_display_mode * @dataProvider update_display_mode_provider * * @param string $plugin The plugin name. * @param string $expected The expected data. */ public function test_update_display_mode(string $plugin, string $expected): void { global $DB; $this->resetAfterTest(); // Get default value for default display mode. $availabilityvalue = $DB->get_field('config_plugins', 'value', ['name' => 'defaultdisplaymode', 'plugin' => "availability_$plugin"]); $updatedisplaymode = \core\plugininfo\availability::update_display_mode($plugin, true); // The default value is not inserted into the table. // Or the display is updated but the display mode is the same value as the default. $this->assertFalse($availabilityvalue); $this->assertFalse($updatedisplaymode); // Update display mode for plugins. $updatedisplaymode = \core\plugininfo\availability::update_display_mode($plugin, false); // The function should return true because the display mode value has changed. $this->assertTrue($updatedisplaymode); // Get the updated value for the default display mode. $availabilityvalue = $DB->get_field('config_plugins', 'value', ['name' => 'defaultdisplaymode', 'plugin' => "availability_$plugin"]); $this->assertEquals($expected, $availabilityvalue); } /** * Data provider for test_update_display_mode(). * * @return array */ public static function update_display_mode_provider(): array { return [ 'Update display mode for completion' => [ 'completion', '1', ], 'Update display mode for grade' => [ 'grade', '1', ], 'Update display mode for group' => [ 'group', '1', ], ]; } } tests/capability_checker_test.php 0000644 00000004702 15215712063 0013270 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_availability; /** * Unit tests for the capability checker class. * * @package core_availability * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class capability_checker_test extends \advanced_testcase { /** * Tests loading a class from /availability/classes. */ public function test_capability_checker(): void { global $CFG, $DB; $this->resetAfterTest(); // Create a course with teacher and student. $generator = $this->getDataGenerator(); $course = $generator->create_course(); $roleids = $DB->get_records_menu('role', null, '', 'shortname, id'); $teacher = $generator->create_user(); $student = $generator->create_user(); $generator->enrol_user($teacher->id, $course->id, $roleids['teacher']); $generator->enrol_user($student->id, $course->id, $roleids['student']); // Check a capability which they both have. $context = \context_course::instance($course->id); $checker = new capability_checker($context); $result = array_keys($checker->get_users_by_capability('mod/forum:replypost')); sort($result); $this->assertEquals(array($teacher->id, $student->id), $result); // And one that only teachers have. $result = array_keys($checker->get_users_by_capability('mod/forum:deleteanypost')); $this->assertEquals(array($teacher->id), $result); // Check the caching is working. $before = $DB->perf_get_queries(); $result = array_keys($checker->get_users_by_capability('mod/forum:deleteanypost')); $this->assertEquals(array($teacher->id), $result); $this->assertEquals($before, $DB->perf_get_queries()); } } tests/tree_test.php 0000644 00000103405 15215712063 0010422 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_availability; /** * Unit tests for the condition tree class and related logic. * * @package core_availability * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class tree_test extends \advanced_testcase { public function setUp(): void { // Load the mock classes so they can be used. require_once(__DIR__ . '/fixtures/mock_condition.php'); require_once(__DIR__ . '/fixtures/mock_info.php'); parent::setUp(); } /** * Tests constructing a tree with errors. */ public function test_construct_errors(): void { try { new tree('frog'); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('not object', $e->getMessage()); } try { new tree((object)array()); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('missing ->op', $e->getMessage()); } try { new tree((object)array('op' => '*')); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('unknown ->op', $e->getMessage()); } try { new tree((object)array('op' => '|')); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('missing ->show', $e->getMessage()); } try { new tree((object)array('op' => '|', 'show' => 0)); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('->show not bool', $e->getMessage()); } try { new tree((object)array('op' => '&')); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('missing ->showc', $e->getMessage()); } try { new tree((object)array('op' => '&', 'showc' => 0)); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('->showc not array', $e->getMessage()); } try { new tree((object)array('op' => '&', 'showc' => array(0))); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('->showc value not bool', $e->getMessage()); } try { new tree((object)array('op' => '|', 'show' => true)); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('missing ->c', $e->getMessage()); } try { new tree((object)array('op' => '|', 'show' => true, 'c' => 'side')); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('->c not array', $e->getMessage()); } try { new tree((object)array('op' => '|', 'show' => true, 'c' => array(3))); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('child not object', $e->getMessage()); } try { new tree((object)array('op' => '|', 'show' => true, 'c' => array((object)array('type' => 'doesnotexist')))); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('Unknown condition type: doesnotexist', $e->getMessage()); } try { new tree((object)array('op' => '|', 'show' => true, 'c' => array((object)array()))); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('missing ->op', $e->getMessage()); } try { new tree((object)array('op' => '&', 'c' => array((object)array('op' => '&', 'c' => array())), 'showc' => array(true, true) )); $this->fail(); } catch (\coding_exception $e) { $this->assertStringContainsString('->c, ->showc mismatch', $e->getMessage()); } } /** * Tests constructing a tree with plugin that does not exist (ignored). */ public function test_construct_ignore_missing_plugin(): void { // Construct a tree with & combination of one condition that doesn't exist. $tree = new tree(tree::get_root_json(array( (object)array('type' => 'doesnotexist')), tree::OP_OR), true); // Expected result is an empty tree with | condition, shown. $this->assertEquals('+|()', (string)$tree); } /** * Tests constructing a tree with subtrees using all available operators. */ public function test_construct_just_trees(): void { $structure = tree::get_root_json(array( tree::get_nested_json(array(), tree::OP_OR), tree::get_nested_json(array( tree::get_nested_json(array(), tree::OP_NOT_OR)), tree::OP_NOT_AND)), tree::OP_AND, array(true, true)); $tree = new tree($structure); $this->assertEquals('&(+|(),+!&(!|()))', (string)$tree); } /** * Tests constructing tree using the mock plugin. */ public function test_construct_with_mock_plugin(): void { $structure = tree::get_root_json(array( self::mock(array('a' => true, 'm' => ''))), tree::OP_OR); $tree = new tree($structure); $this->assertEquals('+|({mock:y,})', (string)$tree); } /** * Tests the check_available and get_result_information functions. */ public function test_check_available(): void { global $USER; // Setup. $this->resetAfterTest(); $info = new \core_availability\mock_info(); $this->setAdminUser(); $information = ''; // No conditions. $structure = tree::get_root_json(array(), tree::OP_OR); list ($available, $information) = $this->get_available_results( $structure, $info, $USER->id); $this->assertTrue($available); // One condition set to yes. $structure->c = array( self::mock(array('a' => true))); list ($available, $information) = $this->get_available_results( $structure, $info, $USER->id); $this->assertTrue($available); // One condition set to no. $structure->c = array( self::mock(array('a' => false, 'm' => 'no'))); list ($available, $information) = $this->get_available_results( $structure, $info, $USER->id); $this->assertFalse($available); $this->assertEquals('SA: no', $information); // Two conditions, OR, resolving as true. $structure->c = array( self::mock(array('a' => false, 'm' => 'no')), self::mock(array('a' => true))); list ($available, $information) = $this->get_available_results( $structure, $info, $USER->id); $this->assertTrue($available); $this->assertEquals('', $information); // Two conditions, OR, resolving as false. $structure->c = array( self::mock(array('a' => false, 'm' => 'no')), self::mock(array('a' => false, 'm' => 'way'))); list ($available, $information) = $this->get_available_results( $structure, $info, $USER->id); $this->assertFalse($available); $this->assertMatchesRegularExpression('~any of.*no.*way~', $information); // Two conditions, OR, resolving as false, no display. $structure->show = false; list ($available, $information) = $this->get_available_results( $structure, $info, $USER->id); $this->assertFalse($available); $this->assertEquals('', $information); // Two conditions, AND, resolving as true. $structure->op = '&'; unset($structure->show); $structure->showc = array(true, true); $structure->c = array( self::mock(array('a' => true)), self::mock(array('a' => true))); list ($available, $information) = $this->get_available_results( $structure, $info, $USER->id); $this->assertTrue($available); // Two conditions, AND, one false. $structure->c = array( self::mock(array('a' => false, 'm' => 'wom')), self::mock(array('a' => true, 'm' => ''))); list ($available, $information) = $this->get_available_results( $structure, $info, $USER->id); $this->assertFalse($available); $this->assertEquals('SA: wom', $information); // Two conditions, AND, both false. $structure->c = array( self::mock(array('a' => false, 'm' => 'wom')), self::mock(array('a' => false, 'm' => 'bat'))); list ($available, $information) = $this->get_available_results( $structure, $info, $USER->id); $this->assertFalse($available); $this->assertMatchesRegularExpression('~wom.*bat~', $information); // Two conditions, AND, both false, show turned off for one. When // show is turned off, that means if you don't have that condition // you don't get to see anything at all. $structure->showc[0] = false; list ($available, $information) = $this->get_available_results( $structure, $info, $USER->id); $this->assertFalse($available); $this->assertEquals('', $information); $structure->showc[0] = true; // Two conditions, NOT OR, both false. $structure->op = '!|'; list ($available, $information) = $this->get_available_results( $structure, $info, $USER->id); $this->assertTrue($available); // Two conditions, NOT OR, one true. $structure->c[0]->a = true; list ($available, $information) = $this->get_available_results( $structure, $info, $USER->id); $this->assertFalse($available); $this->assertEquals('SA: !wom', $information); // Two conditions, NOT OR, both true. $structure->c[1]->a = true; list ($available, $information) = $this->get_available_results( $structure, $info, $USER->id); $this->assertFalse($available); $this->assertMatchesRegularExpression('~!wom.*!bat~', $information); // Two conditions, NOT AND, both true. $structure->op = '!&'; unset($structure->showc); $structure->show = true; list ($available, $information) = $this->get_available_results( $structure, $info, $USER->id); $this->assertFalse($available); $this->assertMatchesRegularExpression('~any of.*!wom.*!bat~', $information); // Two conditions, NOT AND, one true. $structure->c[1]->a = false; list ($available, $information) = $this->get_available_results( $structure, $info, $USER->id); $this->assertTrue($available); // Nested NOT conditions; true. $structure->c = array( tree::get_nested_json(array( self::mock(array('a' => true, 'm' => 'no'))), tree::OP_NOT_AND)); list ($available, $information) = $this->get_available_results( $structure, $info, $USER->id); $this->assertTrue($available); // Nested NOT conditions; false (note no ! in message). $structure->c[0]->c[0]->a = false; list ($available, $information) = $this->get_available_results( $structure, $info, $USER->id); $this->assertFalse($available); $this->assertEquals('SA: no', $information); // Nested condition groups, message test. $structure->op = '|'; $structure->c = array( tree::get_nested_json(array( self::mock(array('a' => false, 'm' => '1')), self::mock(array('a' => false, 'm' => '2')) ), tree::OP_AND), self::mock(array('a' => false, 'm' => 3))); list ($available, $information) = $this->get_available_results( $structure, $info, $USER->id); $this->assertFalse($available); $this->assertMatchesRegularExpression('~<ul.*<ul.*<li.*1.*<li.*2.*</ul>.*<li.*3~', $information); } /** * Shortcut function to check availability and also get information. * * @param \stdClass $structure Tree structure * @param \core_availability\info $info Location info * @param int $userid User id */ protected function get_available_results($structure, \core_availability\info $info, $userid) { global $PAGE, $OUTPUT; $tree = new tree($structure); $result = $tree->check_available(false, $info, true, $userid); $information = $tree->get_result_information($info, $result); if (!is_string($information)) { $renderable = new \core_availability\output\availability_info($information); $information = str_replace(array("\r", "\n"), '', $OUTPUT->render($renderable)); } return array($result->is_available(), $information); } /** * Shortcut function to render the full availability information. * * @param \stdClass $structure Tree structure * @param \core_availability\info $info Location info */ protected function render_full_information($structure, \core_availability\info $info) { global $OUTPUT; $tree = new tree($structure); $information = $tree->get_full_information($info); $renderable = new \core_availability\output\availability_info($information); $html = $OUTPUT->render($renderable); return str_replace(array("\r", "\n"), '', $html); } /** * Tests the is_available_for_all() function. */ public function test_is_available_for_all(): void { // Empty tree is always available. $structure = tree::get_root_json(array(), tree::OP_OR); $tree = new tree($structure); $this->assertTrue($tree->is_available_for_all()); // Tree with normal item in it, not always available. $structure->c[0] = (object)array('type' => 'mock'); $tree = new tree($structure); $this->assertFalse($tree->is_available_for_all()); // OR tree with one always-available item. $structure->c[1] = self::mock(array('all' => true)); $tree = new tree($structure); $this->assertTrue($tree->is_available_for_all()); // AND tree with one always-available and one not. $structure->op = '&'; $structure->showc = array(true, true); unset($structure->show); $tree = new tree($structure); $this->assertFalse($tree->is_available_for_all()); // Test NOT conditions (items not always-available). $structure->op = '!&'; $structure->show = true; unset($structure->showc); $tree = new tree($structure); $this->assertFalse($tree->is_available_for_all()); // Test again with one item always-available for NOT mode. $structure->c[1]->allnot = true; $tree = new tree($structure); $this->assertTrue($tree->is_available_for_all()); } /** * Tests the get_full_information() function. */ public function test_get_full_information(): void { global $PAGE; // Setup. $info = new \core_availability\mock_info(); // No conditions. $structure = tree::get_root_json(array(), tree::OP_OR); $tree = new tree($structure); $this->assertEquals('', $tree->get_full_information($info)); // Condition (normal and NOT). $structure->c = array( self::mock(array('m' => 'thing'))); $tree = new tree($structure); $this->assertEquals('SA: [FULL]thing', $tree->get_full_information($info)); $structure->op = '!&'; $tree = new tree($structure); $this->assertEquals('SA: ![FULL]thing', $tree->get_full_information($info)); // Complex structure. $structure->op = '|'; $structure->c = array( tree::get_nested_json(array( self::mock(array('m' => '1')), self::mock(array('m' => '2'))), tree::OP_AND), self::mock(array('m' => 3))); $this->assertMatchesRegularExpression('~<ul.*<ul.*<li.*1.*<li.*2.*</ul>.*<li.*3~', $this->render_full_information($structure, $info)); // Test intro messages before list. First, OR message. $structure->c = array( self::mock(array('m' => '1')), self::mock(array('m' => '2')) ); $this->assertMatchesRegularExpression('~Not available unless any of:.*<ul~', $this->render_full_information($structure, $info)); // Now, OR message when not shown. $structure->show = false; $this->assertMatchesRegularExpression('~hidden.*<ul~', $this->render_full_information($structure, $info)); // AND message. $structure->op = '&'; unset($structure->show); $structure->showc = array(false, false); $this->assertMatchesRegularExpression('~Not available unless:.*<ul~', $this->render_full_information($structure, $info)); // Hidden markers on items. $this->assertMatchesRegularExpression('~1.*hidden.*2.*hidden~', $this->render_full_information($structure, $info)); // Hidden markers on child tree and items. $structure->c[1] = tree::get_nested_json(array( self::mock(array('m' => '2')), self::mock(array('m' => '3'))), tree::OP_AND); $this->assertMatchesRegularExpression('~1.*hidden.*All of \(hidden.*2.*3~', $this->render_full_information($structure, $info)); $structure->c[1]->op = '|'; $this->assertMatchesRegularExpression('~1.*hidden.*Any of \(hidden.*2.*3~', $this->render_full_information($structure, $info)); // Hidden markers on single-item display, AND and OR. $structure->showc = array(false); $structure->c = array( self::mock(array('m' => '1')) ); $tree = new tree($structure); $this->assertMatchesRegularExpression('~1.*hidden~', $tree->get_full_information($info)); unset($structure->showc); $structure->show = false; $structure->op = '|'; $tree = new tree($structure); $this->assertMatchesRegularExpression('~1.*hidden~', $tree->get_full_information($info)); // Hidden marker if single item is tree. $structure->c[0] = tree::get_nested_json(array( self::mock(array('m' => '1')), self::mock(array('m' => '2'))), tree::OP_AND); $this->assertMatchesRegularExpression('~Not available \(hidden.*1.*2~', $this->render_full_information($structure, $info)); // Single item tree containing single item. unset($structure->c[0]->c[1]); $tree = new tree($structure); $this->assertMatchesRegularExpression('~SA.*1.*hidden~', $tree->get_full_information($info)); } /** * Tests the is_empty() function. */ public function test_is_empty(): void { // Tree with nothing in should be empty. $structure = tree::get_root_json(array(), tree::OP_OR); $tree = new tree($structure); $this->assertTrue($tree->is_empty()); // Tree with something in is not empty. $structure = tree::get_root_json(array(self::mock(array('m' => '1'))), tree::OP_OR); $tree = new tree($structure); $this->assertFalse($tree->is_empty()); } /** * Tests the get_all_children() function. */ public function test_get_all_children(): void { // Create a tree with nothing in. $structure = tree::get_root_json(array(), tree::OP_OR); $tree1 = new tree($structure); // Create second tree with complex structure. $structure->c = array( tree::get_nested_json(array( self::mock(array('m' => '1')), self::mock(array('m' => '2')) ), tree::OP_OR), self::mock(array('m' => 3))); $tree2 = new tree($structure); // Check list of conditions from both trees. $this->assertEquals(array(), $tree1->get_all_children('core_availability\condition')); $result = $tree2->get_all_children('core_availability\condition'); $this->assertEquals(3, count($result)); $this->assertEquals('{mock:n,1}', (string)$result[0]); $this->assertEquals('{mock:n,2}', (string)$result[1]); $this->assertEquals('{mock:n,3}', (string)$result[2]); // Check specific type, should give same results. $result2 = $tree2->get_all_children('availability_mock\condition'); $this->assertEquals($result, $result2); } /** * Tests the update_dependency_id() function. */ public function test_update_dependency_id(): void { // Create tree with structure of 3 mocks. $structure = tree::get_root_json(array( tree::get_nested_json(array( self::mock(array('table' => 'frogs', 'id' => 9)), self::mock(array('table' => 'zombies', 'id' => 9)) )), self::mock(array('table' => 'frogs', 'id' => 9)))); // Get 'before' value. $tree = new tree($structure); $before = $tree->save(); // Try replacing a table or id that isn't used. $this->assertFalse($tree->update_dependency_id('toads', 9, 13)); $this->assertFalse($tree->update_dependency_id('frogs', 7, 8)); $this->assertEquals($before, $tree->save()); // Replace the zombies one. $this->assertTrue($tree->update_dependency_id('zombies', 9, 666)); $after = $tree->save(); $this->assertEquals(666, $after->c[0]->c[1]->id); // And the frogs one. $this->assertTrue($tree->update_dependency_id('frogs', 9, 3)); $after = $tree->save(); $this->assertEquals(3, $after->c[0]->c[0]->id); $this->assertEquals(3, $after->c[1]->id); } /** * Tests the filter_users function. */ public function test_filter_users(): void { $info = new \core_availability\mock_info(); $checker = new capability_checker($info->get_context()); // Don't need to create real users in database, just use these ids. $users = array(1 => null, 2 => null, 3 => null); // Test basic tree with one condition that doesn't filter. $structure = tree::get_root_json(array(self::mock(array()))); $tree = new tree($structure); $result = $tree->filter_user_list($users, false, $info, $checker); ksort($result); $this->assertEquals(array(1, 2, 3), array_keys($result)); // Now a tree with one condition that filters. $structure = tree::get_root_json(array(self::mock(array('filter' => array(2, 3))))); $tree = new tree($structure); $result = $tree->filter_user_list($users, false, $info, $checker); ksort($result); $this->assertEquals(array(2, 3), array_keys($result)); // Tree with two conditions that both filter (|). $structure = tree::get_root_json(array( self::mock(array('filter' => array(3))), self::mock(array('filter' => array(1)))), tree::OP_OR); $tree = new tree($structure); $result = $tree->filter_user_list($users, false, $info, $checker); ksort($result); $this->assertEquals(array(1, 3), array_keys($result)); // Tree with OR condition one of which doesn't filter. $structure = tree::get_root_json(array( self::mock(array('filter' => array(3))), self::mock(array())), tree::OP_OR); $tree = new tree($structure); $result = $tree->filter_user_list($users, false, $info, $checker); ksort($result); $this->assertEquals(array(1, 2, 3), array_keys($result)); // Tree with two condition that both filter (&). $structure = tree::get_root_json(array( self::mock(array('filter' => array(2, 3))), self::mock(array('filter' => array(1, 2))))); $tree = new tree($structure); $result = $tree->filter_user_list($users, false, $info, $checker); ksort($result); $this->assertEquals(array(2), array_keys($result)); // Tree with child tree with NOT condition. $structure = tree::get_root_json(array( tree::get_nested_json(array( self::mock(array('filter' => array(1)))), tree::OP_NOT_AND))); $tree = new tree($structure); $result = $tree->filter_user_list($users, false, $info, $checker); ksort($result); $this->assertEquals(array(2, 3), array_keys($result)); } /** * Tests the get_json methods in tree (which are mainly for use in testing * but might be used elsewhere). */ public function test_get_json(): void { // Create a simple child object (fake). $child = (object)array('type' => 'fake'); $childstr = json_encode($child); // Minimal case. $this->assertEquals( (object)array('op' => '&', 'c' => array()), tree::get_nested_json(array())); // Children and different operator. $this->assertEquals( (object)array('op' => '|', 'c' => array($child, $child)), tree::get_nested_json(array($child, $child), tree::OP_OR)); // Root empty. $this->assertEquals('{"op":"&","c":[],"showc":[]}', json_encode(tree::get_root_json(array(), tree::OP_AND))); // Root with children (multi-show operator). $this->assertEquals('{"op":"&","c":[' . $childstr . ',' . $childstr . '],"showc":[true,true]}', json_encode(tree::get_root_json(array($child, $child), tree::OP_AND))); // Root with children (single-show operator). $this->assertEquals('{"op":"|","c":[' . $childstr . ',' . $childstr . '],"show":true}', json_encode(tree::get_root_json(array($child, $child), tree::OP_OR))); // Root with children (specified show boolean). $this->assertEquals('{"op":"&","c":[' . $childstr . ',' . $childstr . '],"showc":[false,false]}', json_encode(tree::get_root_json(array($child, $child), tree::OP_AND, false))); // Root with children (specified show array). $this->assertEquals('{"op":"&","c":[' . $childstr . ',' . $childstr . '],"showc":[true,false]}', json_encode(tree::get_root_json(array($child, $child), tree::OP_AND, array(true, false)))); } /** * Tests the behaviour of the counter in unique_sql_parameter(). * * There was a problem with static counters used to implement a sequence of * parameter placeholders (MDL-53481). As always with static variables, it * is a bit tricky to unit test the behaviour reliably as it depends on the * actual tests executed and also their order. * * To minimise risk of false expected behaviour, this test method should be * first one where {@link core_availability\tree::get_user_list_sql()} is * used. We also use higher number of condition instances to increase the * risk of the counter collision, should there remain a problem. */ public function test_unique_sql_parameter_behaviour(): void { global $DB; $this->resetAfterTest(); $generator = $this->getDataGenerator(); // Create a test course with multiple groupings and groups and a student in each of them. $course = $generator->create_course(); $user = $generator->create_user(); $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student')); $generator->enrol_user($user->id, $course->id, $studentroleid); // The total number of groupings and groups must not be greater than 61. // There is a limit in MySQL on the max number of joined tables. $groups = []; for ($i = 0; $i < 25; $i++) { $group = $generator->create_group(array('courseid' => $course->id)); groups_add_member($group, $user); $groups[] = $group; } $groupings = []; for ($i = 0; $i < 25; $i++) { $groupings[] = $generator->create_grouping(array('courseid' => $course->id)); } foreach ($groupings as $grouping) { foreach ($groups as $group) { groups_assign_grouping($grouping->id, $group->id); } } $info = new \core_availability\mock_info($course); // Make a huge tree with 'AND' of all groups and groupings conditions. $conditions = []; foreach ($groups as $group) { $conditions[] = \availability_group\condition::get_json($group->id); } foreach ($groupings as $groupingid) { $conditions[] = \availability_grouping\condition::get_json($grouping->id); } shuffle($conditions); $tree = new tree(tree::get_root_json($conditions)); list($sql, $params) = $tree->get_user_list_sql(false, $info, false); // This must not throw exception. $DB->fix_sql_params($sql, $params); } /** * Tests get_user_list_sql. */ public function test_get_user_list_sql(): void { global $DB; $this->resetAfterTest(); $generator = $this->getDataGenerator(); // Create a test course with 2 groups and users in each combination of them. $course = $generator->create_course(); $group1 = $generator->create_group(array('courseid' => $course->id)); $group2 = $generator->create_group(array('courseid' => $course->id)); $userin1 = $generator->create_user(); $userin2 = $generator->create_user(); $userinboth = $generator->create_user(); $userinneither = $generator->create_user(); $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student')); foreach (array($userin1, $userin2, $userinboth, $userinneither) as $user) { $generator->enrol_user($user->id, $course->id, $studentroleid); } groups_add_member($group1, $userin1); groups_add_member($group2, $userin2); groups_add_member($group1, $userinboth); groups_add_member($group2, $userinboth); $info = new \core_availability\mock_info($course); // Tree with single group condition. $tree = new tree(tree::get_root_json(array( \availability_group\condition::get_json($group1->id) ))); list($sql, $params) = $tree->get_user_list_sql(false, $info, false); $result = $DB->get_fieldset_sql($sql, $params); sort($result); $this->assertEquals(array($userin1->id, $userinboth->id), $result); // Tree with 'AND' of both group conditions. $tree = new tree(tree::get_root_json(array( \availability_group\condition::get_json($group1->id), \availability_group\condition::get_json($group2->id) ))); list($sql, $params) = $tree->get_user_list_sql(false, $info, false); $result = $DB->get_fieldset_sql($sql, $params); sort($result); $this->assertEquals(array($userinboth->id), $result); // Tree with 'AND' of both group conditions. $tree = new tree(tree::get_root_json(array( \availability_group\condition::get_json($group1->id), \availability_group\condition::get_json($group2->id) ), tree::OP_OR)); list($sql, $params) = $tree->get_user_list_sql(false, $info, false); $result = $DB->get_fieldset_sql($sql, $params); sort($result); $this->assertEquals(array($userin1->id, $userin2->id, $userinboth->id), $result); // Check with flipped logic (NOT above level of tree). list($sql, $params) = $tree->get_user_list_sql(true, $info, false); $result = $DB->get_fieldset_sql($sql, $params); sort($result); $this->assertEquals(array($userinneither->id), $result); // Tree with 'OR' of group conditions and a non-filtering condition. // The non-filtering condition should mean that ALL users are included. $tree = new tree(tree::get_root_json(array( \availability_group\condition::get_json($group1->id), \availability_date\condition::get_json(\availability_date\condition::DIRECTION_UNTIL, 3) ), tree::OP_OR)); list($sql, $params) = $tree->get_user_list_sql(false, $info, false); $this->assertEquals('', $sql); $this->assertEquals(array(), $params); } /** * Utility function to build the PHP structure representing a mock condition. * * @param array $params Mock parameters * @return \stdClass Structure object */ protected static function mock(array $params) { $params['type'] = 'mock'; return (object)$params; } } tests/component_test.php 0000644 00000003746 15215712063 0011474 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core_availability; /** * Unit tests for the component and plugin definitions for availability system. * * @package core_availability * @copyright 2014 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class component_test extends \advanced_testcase { /** * Tests loading a class from /availability/classes. */ public function test_load_class(): void { $result = get_class_methods('\core_availability\info'); $this->assertTrue(is_array($result)); } /** * Tests the plugininfo class is present and working. */ public function test_plugin_info(): void { // This code will throw debugging information if the plugininfo class // is missing. Unfortunately it doesn't actually cause the test to // fail, but it's obvious when running test at least. $pluginmanager = \core_plugin_manager::instance(); $list = $pluginmanager->get_enabled_plugins('availability'); $this->assertArrayHasKey('completion', $list); $this->assertArrayHasKey('date', $list); $this->assertArrayHasKey('grade', $list); $this->assertArrayHasKey('group', $list); $this->assertArrayHasKey('grouping', $list); $this->assertArrayHasKey('profile', $list); } } amd/src/availability_more.js 0000644 00000004571 15215712063 0012137 0 ustar 00 // 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/>. /** * Show more action for availablity information. * * @deprecated since 4.3 MDL-78204. * @todo MDL-78489 This will be deleted in Moodle 4.7. * @module core_availability/availability_more * @copyright 2021 Bas Brands <bas@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Availability info selectors. */ const Selectors = { regions: { availability: '[data-region="availability-multiple"]', }, actions: { showmorelink: '[data-action="showmore"]' }, classes: { hidden: 'd-none', visible: 'd-block', } }; /** * Displays all the availability information in case part of it is hidden. * * @param {Event} event the triggered event */ const showMoreHandler = (event) => { const triggerElement = event.target.closest(Selectors.actions.showmorelink); if (triggerElement === null) { return; } const container = triggerElement.closest(Selectors.regions.availability); container.querySelectorAll('.' + Selectors.classes.hidden).forEach(function(node) { node.classList.remove(Selectors.classes.hidden); }); container.querySelectorAll('.' + Selectors.classes.visible).forEach(function(node) { node.classList.remove(Selectors.classes.visible); node.classList.add(Selectors.classes.hidden); }); event.preventDefault(); }; /** * Initialise the eventlister for the showmore action on availability information. * * @method init */ export const init = () => { const body = document.querySelector('body'); if (!body.dataset.showmoreactive) { document.addEventListener('click', showMoreHandler); body.dataset.showmoreactive = 1; } }; amd/build/availability_more.min.js 0000644 00000002660 15215712063 0013226 0 ustar 00 define("core_availability/availability_more",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0; /** * Show more action for availablity information. * * @deprecated since 4.3 MDL-78204. * @todo MDL-78489 This will be deleted in Moodle 4.7. * @module core_availability/availability_more * @copyright 2021 Bas Brands <bas@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ const Selectors_regions={availability:'[data-region="availability-multiple"]'},Selectors_actions={showmorelink:'[data-action="showmore"]'},Selectors_classes={hidden:"d-none",visible:"d-block"},showMoreHandler=event=>{const triggerElement=event.target.closest(Selectors_actions.showmorelink);if(null===triggerElement)return;const container=triggerElement.closest(Selectors_regions.availability);container.querySelectorAll("."+Selectors_classes.hidden).forEach((function(node){node.classList.remove(Selectors_classes.hidden)})),container.querySelectorAll("."+Selectors_classes.visible).forEach((function(node){node.classList.remove(Selectors_classes.visible),node.classList.add(Selectors_classes.hidden)})),event.preventDefault()};_exports.init=()=>{const body=document.querySelector("body");body.dataset.showmoreactive||(document.addEventListener("click",showMoreHandler),body.dataset.showmoreactive=1)}})); //# sourceMappingURL=availability_more.min.js.map amd/build/availability_more.min.js.map 0000644 00000006564 15215712063 0014011 0 ustar 00 {"version":3,"file":"availability_more.min.js","sources":["../src/availability_more.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Show more action for availablity information.\n *\n * @deprecated since 4.3 MDL-78204.\n * @todo MDL-78489 This will be deleted in Moodle 4.7.\n * @module core_availability/availability_more\n * @copyright 2021 Bas Brands <bas@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * Availability info selectors.\n */\nconst Selectors = {\n regions: {\n availability: '[data-region=\"availability-multiple\"]',\n },\n actions: {\n showmorelink: '[data-action=\"showmore\"]'\n },\n classes: {\n hidden: 'd-none',\n visible: 'd-block',\n\n }\n};\n\n/**\n * Displays all the availability information in case part of it is hidden.\n *\n * @param {Event} event the triggered event\n */\nconst showMoreHandler = (event) => {\n const triggerElement = event.target.closest(Selectors.actions.showmorelink);\n if (triggerElement === null) {\n return;\n }\n const container = triggerElement.closest(Selectors.regions.availability);\n container.querySelectorAll('.' + Selectors.classes.hidden).forEach(function(node) {\n node.classList.remove(Selectors.classes.hidden);\n });\n container.querySelectorAll('.' + Selectors.classes.visible).forEach(function(node) {\n node.classList.remove(Selectors.classes.visible);\n node.classList.add(Selectors.classes.hidden);\n });\n event.preventDefault();\n};\n\n/**\n * Initialise the eventlister for the showmore action on availability information.\n *\n * @method init\n */\nexport const init = () => {\n const body = document.querySelector('body');\n if (!body.dataset.showmoreactive) {\n document.addEventListener('click', showMoreHandler);\n body.dataset.showmoreactive = 1;\n }\n};\n"],"names":["Selectors","availability","showmorelink","hidden","visible","showMoreHandler","event","triggerElement","target","closest","container","querySelectorAll","forEach","node","classList","remove","add","preventDefault","body","document","querySelector","dataset","showmoreactive","addEventListener"],"mappings":";;;;;;;;;;MA4BMA,kBACO,CACLC,aAAc,yCAFhBD,kBAIO,CACLE,aAAc,4BALhBF,kBAOO,CACLG,OAAQ,SACRC,QAAS,WAUXC,gBAAmBC,cACfC,eAAiBD,MAAME,OAAOC,QAAQT,kBAAkBE,iBACvC,OAAnBK,4BAGEG,UAAYH,eAAeE,QAAQT,kBAAkBC,cAC3DS,UAAUC,iBAAiB,IAAMX,kBAAkBG,QAAQS,SAAQ,SAASC,MACxEA,KAAKC,UAAUC,OAAOf,kBAAkBG,WAE5CO,UAAUC,iBAAiB,IAAMX,kBAAkBI,SAASQ,SAAQ,SAASC,MACzEA,KAAKC,UAAUC,OAAOf,kBAAkBI,SACxCS,KAAKC,UAAUE,IAAIhB,kBAAkBG,WAEzCG,MAAMW,gCAQU,WACVC,KAAOC,SAASC,cAAc,QAC/BF,KAAKG,QAAQC,iBACdH,SAASI,iBAAiB,QAASlB,iBACnCa,KAAKG,QAAQC,eAAiB"} yui/src/form/js/form.js 0000644 00000132710 15215712063 0011027 0 ustar 00 /** * Provides interface for users to edit availability settings on the * module/section editing form. * * The system works using this JavaScript plus form.js files inside each * condition plugin. * * The overall concept is that data is held in a textarea in the form in JSON * format. This JavaScript converts the textarea into a set of controls * generated here and by the relevant plugins. * * (Almost) all data is held directly by the state of the HTML controls, and * can be updated to the form field by calling the 'update' method, which * this code and the plugins call if any HTML control changes. * * @module moodle-core_availability-form */ M.core_availability = M.core_availability || {}; /** * Core static functions for availability settings in editing form. * * @class M.core_availability.form * @static */ M.core_availability.form = { /** * Object containing installed plugins. They are indexed by plugin name. * * @property plugins * @type Object */ plugins: {}, /** * Availability field (textarea). * * @property field * @type Y.Node */ field: null, /** * Main div that replaces the availability field. * * @property mainDiv * @type Y.Node */ mainDiv: null, /** * Object that represents the root of the tree. * * @property rootList * @type M.core_availability.List */ rootList: null, /** * Counter used when creating anything that needs an id. * * @property idCounter * @type Number */ idCounter: 0, /** * The 'Restrict by group' button if present. * * @property restrictByGroup * @type Y.Node */ restrictByGroup: null, /** * Called to initialise the system when the page loads. This method will * also call the init method for each plugin. * * @method init */ init: function(pluginParams) { // Init all plugins. for (var plugin in pluginParams) { var params = pluginParams[plugin]; var pluginClass = M[params[0]].form; pluginClass.init.apply(pluginClass, params); } // Get the availability field, hide it, and replace with the main div. this.field = Y.one('#id_availabilityconditionsjson'); this.field.setAttribute('aria-hidden', 'true'); // The fcontainer class here is inappropriate, but is necessary // because otherwise it is impossible to make Behat work correctly on // these controls as Behat incorrectly decides they're a moodleform // textarea. IMO Behat should not know about moodleforms at all and // should look purely at HTML elements on the page, but until it is // fixed to do this or fixed in some other way to only detect moodleform // elements that specifically match what those elements should look like, // then there is no good solution. this.mainDiv = Y.Node.create('<div class="availability-field fcontainer"></div>'); this.field.insert(this.mainDiv, 'after'); // Get top-level tree as JSON. var value = this.field.get('value'); var data = null; if (value !== '') { try { data = Y.JSON.parse(value); } catch (x) { // If the JSON data is not valid, treat it as empty. this.field.set('value', ''); } } this.rootList = new M.core_availability.List(data, true); this.mainDiv.appendChild(this.rootList.node); // Update JSON value after loading (to reflect any changes that need // to be made to make it valid). this.update(); this.rootList.renumber(); // Mark main area as dynamically updated. this.mainDiv.setAttribute('aria-live', 'polite'); // Listen for form submission - to avoid having our made-up fields // submitted, we need to disable them all before submit. this.field.ancestor('form').on('submit', function() { this.mainDiv.all('input,textarea,select').set('disabled', true); }, this); // If the form has group mode and/or grouping options, there is a // 'add restriction' button there. this.restrictByGroup = Y.one('#restrictbygroup'); if (this.restrictByGroup) { this.restrictByGroup.on('click', this.addRestrictByGroup, this); var groupmode = Y.one('#id_groupmode'); var groupingid = Y.one('#id_groupingid'); if (groupmode) { groupmode.on('change', this.updateRestrictByGroup, this); } if (groupingid) { groupingid.on('change', this.updateRestrictByGroup, this); } this.updateRestrictByGroup(); } // Everything is ready. Make sure the div is visible and hide the loading indicator. this.parent = Y.one('#fitem_id_availabilityconditionsjson'); this.parent.removeClass('d-none'); document.getElementById('availabilityconditions-loading').remove(); }, /** * Called at any time to update the hidden field value. * * This should be called whenever any value changes in the form settings. * * @method update */ update: function() { // Convert tree to value. var jsValue = this.rootList.getValue(); // Store any errors (for form reporting) in 'errors' value if present. var errors = []; this.rootList.fillErrors(errors); if (errors.length !== 0) { jsValue.errors = errors; } // Set into hidden form field, JS-encoded. this.field.set('value', Y.JSON.stringify(jsValue)); // Also update the restrict by group button if present. this.updateRestrictByGroup(); }, /** * Updates the status of the 'restrict by group' button (enables or disables * it) based on current availability restrictions and group/grouping settings. */ updateRestrictByGroup: function() { if (!this.restrictByGroup) { return; } // If the root list is anything other than the default 'and' type, disable. if (this.rootList.getValue().op !== '&') { this.restrictByGroup.set('disabled', true); return; } // If there's already a group restriction, disable it. var alreadyGot = this.rootList.hasItemOfType('group') || this.rootList.hasItemOfType('grouping'); if (alreadyGot) { this.restrictByGroup.set('disabled', true); return; } // If the groupmode and grouping id aren't set, disable it. var groupmode = Y.one('#id_groupmode'); var groupingid = Y.one('#id_groupingid'); var groupavailability = Number(this.restrictByGroup.getData('groupavailability')) === 1; var groupingavailability = Number(this.restrictByGroup.getData('groupingavailability')) === 1; if ((!groupmode || Number(groupmode.get('value')) === 0 || !groupavailability) && (!groupingid || Number(groupingid.get('value')) === 0 || !groupingavailability)) { this.restrictByGroup.set('disabled', true); return; } this.restrictByGroup.set('disabled', false); }, /** * Called when the user clicks on the 'restrict by group' button. This is * a special case that adds a group or grouping restriction. * * By default this restriction is not shown which makes it similar to the * * @param e Button click event */ addRestrictByGroup: function(e) { // If you don't prevent default, it submits the form for some reason. e.preventDefault(); // Add the condition. var groupmode = Y.one('#id_groupmode'); var groupingid = Y.one('#id_groupingid'); var groupavailability = Number(this.restrictByGroup.getData('groupavailability')) === 1; var groupingavailability = Number(this.restrictByGroup.getData('groupingavailability')) === 1; var newChild; if (groupingid && Number(groupingid.get('value')) !== 0 && groupingavailability) { // Add a grouping restriction if one is specified. newChild = new M.core_availability.Item( {type: 'grouping', id: Number(groupingid.get('value'))}, true); } else if (groupmode && groupavailability) { // Otherwise just add a group restriction. newChild = new M.core_availability.Item({type: 'group'}, true); } // Refresh HTML. if (newChild !== null) { this.rootList.addChild(newChild); this.update(); this.rootList.renumber(); this.rootList.updateHtml(); } } }; /** * Base object for plugins. Plugins should use Y.Object to extend this class. * * @class M.core_availability.plugin * @static */ M.core_availability.plugin = { /** * True if users are allowed to add items of this plugin at the moment. * * @property allowAdd * @type Boolean */ allowAdd: false, /** * Called (from PHP) to initialise the plugin. Should usually not be * overridden by child plugin. * * @method init * @param {String} component Component name e.g. 'availability_date'. * @param {boolean} allowAdd Indicates whether adding new instances of the plugin is permitted. * @param {Object} params Additional parameters. * @param {boolean} displayMode Whether the eye icon is show or hide. True for "Hide", false for "Show". */ init: function(component, allowAdd, params, displayMode) { var name = component.replace(/^availability_/, ''); this.allowAdd = allowAdd; M.core_availability.form.plugins[name] = this; this.initInner.apply(this, params); this.displayMode = displayMode; }, /** * Init method for plugin to override. (Default does nothing.) * * This method will receive any parameters defined in frontend.php * get_javascript_init_params. * * @method initInner * @protected */ initInner: function() { // Can be overriden. }, /** * Gets a YUI node representing the controls for this plugin on the form. * * Must be implemented by sub-object; default throws an exception. * * @method getNode * @return {Y.Node} YUI node */ getNode: function() { throw 'getNode not implemented'; }, /** * Fills in the value from this plugin's controls into a value object, * which will later be converted to JSON and stored in the form field. * * Must be implemented by sub-object; default throws an exception. * * @method fillValue * @param {Object} value Value object (to be written to) * @param {Y.Node} node YUI node (same one returned from getNode) */ fillValue: function() { throw 'fillValue not implemented'; }, /** * Fills in any errors from this plugin's controls. If there are any * errors, push them into the supplied array. * * Errors are Moodle language strings in format component:string, e.g. * 'availability_date:error_date_past_end_of_world'. * * The default implementation does nothing. * * @method fillErrors * @param {Array} errors Array of errors (push new errors here) * @param {Y.Node} node YUI node (same one returned from getNode) */ fillErrors: function() { // Can be overriden. }, /** * Focuses the first thing in the plugin after it has been added. * * The default implementation uses a simple algorithm to identify the * first focusable input/select and then focuses it. */ focusAfterAdd: function(node) { var target = node.one('input:not([disabled]),select:not([disabled])'); target.focus(); } }; /** * Maintains a list of children and settings for how they are combined. * * @class M.core_availability.List * @constructor * @param {Object} json Decoded JSON value * @param {Boolean} [false] root True if this is root level list * @param {Boolean} [false] root True if parent is root level list */ M.core_availability.List = function(json, root, parentRoot) { // Set default value for children. (You can't do this in the prototype // definition, or it ends up sharing the same array between all of them.) this.children = []; if (root !== undefined) { this.root = root; } // Create DIV structure (without kids). this.node = Y.Node.create('<div class="availability-list"><h3 class="accesshide"></h3>' + '<div class="availability-inner">' + '<div class="availability-header mb-1"><span>' + M.util.get_string('listheader_sign_before', 'availability') + '</span>' + ' <label><span class="accesshide">' + M.util.get_string('label_sign', 'availability') + ' </span><select class="availability-neg custom-select mx-1"' + ' title="' + M.util.get_string('label_sign', 'availability') + '">' + '<option value="">' + M.util.get_string('listheader_sign_pos', 'availability') + '</option>' + '<option value="!">' + M.util.get_string('listheader_sign_neg', 'availability') + '</option></select></label> ' + '<span class="availability-single">' + M.util.get_string('listheader_single', 'availability') + '</span>' + '<span class="availability-multi">' + M.util.get_string('listheader_multi_before', 'availability') + ' <label><span class="accesshide">' + M.util.get_string('label_multi', 'availability') + ' </span>' + '<select class="availability-op custom-select mx-1"' + ' title="' + M.util.get_string('label_multi', 'availability') + '"><option value="&">' + M.util.get_string('listheader_multi_and', 'availability') + '</option>' + '<option value="|">' + M.util.get_string('listheader_multi_or', 'availability') + '</option></select></label> ' + M.util.get_string('listheader_multi_after', 'availability') + '</span></div>' + '<div class="availability-children"></div>' + '<div class="availability-none"><span class="px-3">' + M.util.get_string('none', 'moodle') + '</span></div>' + '<div class="clearfix mt-1"></div>' + '<div class="availability-button"></div></div><div class="clearfix"></div></div>'); if (!root) { this.node.addClass('availability-childlist d-sm-flex align-items-center'); } this.inner = this.node.one('> .availability-inner'); var shown = true; if (root) { // If it's the root, add an eye icon as first thing in header. if (json && json.show !== undefined) { shown = json.show; } this.eyeIcon = new M.core_availability.EyeIcon(false, shown); this.node.one('.availability-header').get('firstChild').insert( this.eyeIcon.span, 'before'); this.node.one('.availability-header').get('firstChild').insert( this.eyeIcon.disabledSpan, 'before'); this.on('availability:privateRuleSet', function(e) { e.target.getDOMNode().dataset.private = true; this.updatePrivateStatus(); }); this.on('availability:privateRuleUnset', function(e) { delete e.target.getDOMNode().dataset.private; this.updatePrivateStatus(); }); } else if (parentRoot) { // When the parent is root, add an eye icon before the main list div. if (json && json.showc !== undefined) { shown = json.showc; } this.eyeIcon = new M.core_availability.EyeIcon(false, shown); this.inner.insert(this.eyeIcon.span, 'before'); this.inner.insert(this.eyeIcon.disabledSpan, 'before'); } if (!root) { // If it's not the root, add a delete button to the 'none' option. // You can only delete lists when they have no children so this will // automatically appear at the correct time. var deleteIcon = new M.core_availability.DeleteIcon(this); var noneNode = this.node.one('.availability-none'); noneNode.appendChild(document.createTextNode(' ')); noneNode.appendChild(deleteIcon.span); // Also if it's not the root, none is actually invalid, so add a label. noneNode.appendChild(Y.Node.create('<span class="mt-1 badge bg-warning text-dark">' + M.util.get_string('invalid', 'availability') + '</span>')); } // Create the button and add it. var button = Y.Node.create('<button type="button" class="btn btn-secondary mt-1">' + M.util.get_string('addrestriction', 'availability') + '</button>'); button.on("click", function() { this.clickAdd(); }, this); this.node.one('div.availability-button').appendChild(button); if (json) { // Set operator from JSON data. switch (json.op) { case '&' : case '|' : this.node.one('.availability-neg').set('value', ''); break; case '!&' : case '!|' : this.node.one('.availability-neg').set('value', '!'); break; } switch (json.op) { case '&' : case '!&' : this.node.one('.availability-op').set('value', '&'); break; case '|' : case '!|' : this.node.one('.availability-op').set('value', '|'); break; } // Construct children. for (var i = 0; i < json.c.length; i++) { var child = json.c[i]; if (this.root && json && json.showc !== undefined) { child.showc = json.showc[i]; } var newItem; if (child.type !== undefined) { // Plugin type. newItem = new M.core_availability.Item(child, this.root); } else { // List type. newItem = new M.core_availability.List(child, false, this.root); } this.addChild(newItem); } } // Add update listeners to the dropdowns. this.node.one('.availability-neg').on('change', function() { // Update hidden field and HTML. M.util.js_pending('availability-neg-change'); M.core_availability.form.update(); this.updateHtml(); M.util.js_complete('availability-neg-change'); }, this); this.node.one('.availability-op').on('change', function() { // Update hidden field. M.util.js_pending('availability-op-change'); M.core_availability.form.update(); this.updateHtml(); M.util.js_complete('availability-op-change'); }, this); // Update HTML to hide unnecessary parts. this.updateHtml(); }; Y.augment(M.core_availability.List, Y.EventTarget, true, null, {emitFacade: true}); /** * Adds a child to the end of the list (in HTML and stored data). * * @method addChild * @private * @param {M.core_availability.Item|M.core_availability.List} newItem Child to add */ M.core_availability.List.prototype.addChild = function(newItem) { if (this.children.length > 0) { // Create connecting label (text will be filled in later by updateHtml). this.inner.one('.availability-children').appendChild(Y.Node.create( '<div class="availability-connector">' + '<span class="label"></span>' + '</div>')); } // Add item to array and to HTML. this.children.push(newItem); // Allow events from child Items and Lists to bubble up to this list. newItem.addTarget(this); this.inner.one('.availability-children').appendChild(newItem.node); }; /** * Focuses something after a new list is added. * * @method focusAfterAdd */ M.core_availability.List.prototype.focusAfterAdd = function() { this.inner.one('button').focus(); }; /** * Checks whether this list uses the individual show icons or the single one. * * (Basically, AND and the equivalent NOT OR list can have individual show icons * so that you hide the activity entirely if a user fails one condition, but * may display it with information about the condition if they fail a different * one. That isn't possible with OR and NOT AND because for those types, there * is not really a concept of which single condition caused the user to fail * it.) * * Method can only be called on the root list. * * @method isIndividualShowIcons * @return {Boolean} True if using the individual icons */ M.core_availability.List.prototype.isIndividualShowIcons = function() { if (!this.root) { throw 'Can only call this on root list'; } var neg = this.node.one('.availability-neg').get('value') === '!'; var isor = this.node.one('.availability-op').get('value') === '|'; return (!neg && !isor) || (neg && isor); }; /** * Renumbers the list and all children. * * @method renumber * @param {String} parentNumber Number to use in heading for this list */ M.core_availability.List.prototype.renumber = function(parentNumber) { // Update heading for list. var headingParams = {count: this.children.length}; var prefix; if (parentNumber === undefined) { headingParams.number = ''; prefix = ''; } else { headingParams.number = parentNumber + ':'; prefix = parentNumber + '.'; } var heading = M.util.get_string('setheading', 'availability', headingParams); this.node.one('> h3').set('innerHTML', heading); this.node.one('> h3').getDOMNode().dataset.restrictionOrder = parentNumber ? parentNumber : 'root'; // Do children. for (var i = 0; i < this.children.length; i++) { var child = this.children[i]; child.renumber(prefix + (i + 1)); } }; /** * Updates HTML for the list based on the current values, for example showing * the 'None' text if there are no children. * * @method updateHtml */ M.core_availability.List.prototype.updateHtml = function() { // Control children appearing or not appearing. if (this.children.length > 0) { this.inner.one('> .availability-children').removeAttribute('aria-hidden'); this.inner.one('> .availability-none').setAttribute('aria-hidden', 'true'); this.inner.one('> .availability-header').removeAttribute('aria-hidden'); if (this.children.length > 1) { this.inner.one('.availability-single').setAttribute('aria-hidden', 'true'); this.inner.one('.availability-multi').removeAttribute('aria-hidden'); } else { this.inner.one('.availability-single').removeAttribute('aria-hidden'); this.inner.one('.availability-multi').setAttribute('aria-hidden', 'true'); } } else { this.inner.one('> .availability-children').setAttribute('aria-hidden', 'true'); this.inner.one('> .availability-none').removeAttribute('aria-hidden'); this.inner.one('> .availability-header').setAttribute('aria-hidden', 'true'); } // For root list, control eye icons. if (this.root) { var showEyes = this.isIndividualShowIcons(); // Individual icons. for (var i = 0; i < this.children.length; i++) { var child = this.children[i]; if (showEyes) { child.eyeIcon.span.removeAttribute('aria-hidden'); child.eyeIcon.disabledSpan.removeAttribute('aria-hidden'); } else { child.eyeIcon.span.setAttribute('aria-hidden', 'true'); child.eyeIcon.disabledSpan.setAttribute('aria-hidden', 'true'); } } // Single icon is the inverse. if (showEyes) { this.eyeIcon.span.setAttribute('aria-hidden', 'true'); this.eyeIcon.disabledSpan.setAttribute('aria-hidden', 'true'); } else { this.eyeIcon.span.removeAttribute('aria-hidden'); this.eyeIcon.disabledSpan.removeAttribute('aria-hidden'); } this.updatePrivateStatus(); } // Update connector text. var connectorText; if (this.inner.one('.availability-op').get('value') === '&') { connectorText = M.util.get_string('and', 'availability'); } else { connectorText = M.util.get_string('or', 'availability'); } this.inner.all('> .availability-children > .availability-connector span.label').each(function(span) { span.set('innerHTML', connectorText); }); }; /** * Deletes a descendant item (Item or List). Called when the user clicks a * delete icon. * * This is a recursive function. * * @method deleteDescendant * @param {M.core_availability.Item|M.core_availability.List} descendant Item to delete * @return {Boolean} True if it was deleted */ M.core_availability.List.prototype.deleteDescendant = function(descendant) { // Loop through children. for (var i = 0; i < this.children.length; i++) { var child = this.children[i]; if (child === descendant) { // Remove from internal array. this.children.splice(i, 1); var target = child.node; // Remove one of the connector nodes around target (if any left). if (this.children.length > 0) { if (target.previous('.availability-connector')) { target.previous('.availability-connector').remove(); } else { target.next('.availability-connector').remove(); } } // Remove target itself. this.inner.one('> .availability-children').removeChild(target); // Update the form and the list HTML. M.core_availability.form.update(); this.updateHtml(); // Focus add button for this list. this.inner.one('> .availability-button').one('button').focus(); return true; } else if (child instanceof M.core_availability.List) { // Recursive call. var found = child.deleteDescendant(descendant); if (found) { return true; } } } return false; }; /** * Shows the 'add restriction' dialogue box. * * @method clickAdd */ M.core_availability.List.prototype.clickAdd = function() { var content = Y.Node.create('<div>' + '<ul class="list-unstyled container-fluid"></ul>' + '<div class="availability-buttons mdl-align">' + '<button type="button" class="btn btn-secondary">' + M.util.get_string('cancel', 'moodle') + '</button></div></div>'); var cancel = content.one('button'); // Make a list of all the dialog options. var dialogRef = {dialog: null}; var ul = content.one('ul'); var li, id, button, label; for (var type in M.core_availability.form.plugins) { // Plugins might decide not to display their add button. if (!M.core_availability.form.plugins[type].allowAdd) { continue; } // Add entry for plugin. li = Y.Node.create('<li class="clearfix row"></li>'); id = 'availability_addrestriction_' + type; button = Y.Node.create('<div class="col-6"><button type="button" class="btn btn-secondary w-100"' + 'id="' + id + '">' + M.util.get_string('title', 'availability_' + type) + '</button></div>'); button.on('click', this.getAddHandler(type, dialogRef), this); li.appendChild(button); label = Y.Node.create('<div class="col-6"><label for="' + id + '">' + M.util.get_string('description', 'availability_' + type) + '</label></div>'); li.appendChild(label); ul.appendChild(li); } // Extra entry for lists. li = Y.Node.create('<li class="clearfix row"></li>'); id = 'availability_addrestriction_list_'; button = Y.Node.create('<div class="col-6"><button type="button" class="btn btn-secondary w-100"' + 'id="' + id + '">' + M.util.get_string('condition_group', 'availability') + '</button></div>'); button.on('click', this.getAddHandler(null, dialogRef), this); li.appendChild(button); label = Y.Node.create('<div class="col-6"><label for="' + id + '">' + M.util.get_string('condition_group_info', 'availability') + '</label></div>'); li.appendChild(label); ul.appendChild(li); var config = { headerContent: M.util.get_string('addrestriction', 'availability'), bodyContent: content, additionalBaseClass: 'availability-dialogue', draggable: true, modal: true, closeButton: false, width: '450px' }; dialogRef.dialog = new M.core.dialogue(config); dialogRef.dialog.show(); cancel.on('click', function() { dialogRef.dialog.hide(); // Focus the button they clicked originally. this.inner.one('> .availability-button').one('button').focus(); }, this); }; /** * Gets an add handler function used by the dialogue to add a particular item. * * @method getAddHandler * @param {String|Null} type Type name of plugin or null to add lists * @param {Object} dialogRef Reference to object that contains dialog * @param {M.core.dialogue} dialogRef.dialog Dialog object * @return {Function} Add handler function to call when adding that thing */ M.core_availability.List.prototype.getAddHandler = function(type, dialogRef) { return function() { var newItem; var displayMode = true; // Check if we have changed the eye icon in the manage restriction to hidden. if (type && M.core_availability.form.plugins[type].displayMode) { displayMode = false; } if (type) { // Create an Item object to represent the child. newItem = new M.core_availability.Item({type: type, creating: true, showc: displayMode}, this.root); } else { // Create a new List object to represent the child. newItem = new M.core_availability.List({c: [], showc: displayMode}, false, this.root); } // Add to list. this.addChild(newItem); // Update the form and list HTML. M.core_availability.form.update(); M.core_availability.form.rootList.renumber(); this.updateHtml(); // Hide dialog. dialogRef.dialog.hide(); newItem.focusAfterAdd(); }; }; /** * Gets the value of the list ready to convert to JSON and fill form field. * * @method getValue * @return {Object} Value of list suitable for use in JSON */ M.core_availability.List.prototype.getValue = function() { // Work out operator from selects. var value = {}; value.op = this.node.one('.availability-neg').get('value') + this.node.one('.availability-op').get('value'); // Work out children from list. value.c = []; var i; for (i = 0; i < this.children.length; i++) { value.c.push(this.children[i].getValue()); } // Work out show/showc for root level. if (this.root) { if (this.isIndividualShowIcons()) { value.showc = []; for (i = 0; i < this.children.length; i++) { var eyeIcon = this.children[i].eyeIcon; value.showc.push(!eyeIcon.isHidden() && !eyeIcon.isDisabled()); } } else { value.show = !this.eyeIcon.isHidden() && !this.eyeIcon.isDisabled(); } } return value; }; /** * Checks whether this list has any errors (incorrect user input). If so, * an error string identifier in the form langfile:langstring should be pushed * into the errors array. * * @method fillErrors * @param {Array} errors Array of errors so far */ M.core_availability.List.prototype.fillErrors = function(errors) { // List with no items is an error (except root). if (this.children.length === 0 && !this.root) { errors.push('availability:error_list_nochildren'); } // Pass to children. for (var i = 0; i < this.children.length; i++) { this.children[i].fillErrors(errors); } }; /** * Checks whether the list contains any items of the given type name. * * @method hasItemOfType * @param {String} pluginType Required plugin type (name) * @return {Boolean} True if there is one */ M.core_availability.List.prototype.hasItemOfType = function(pluginType) { // Check each item. for (var i = 0; i < this.children.length; i++) { var child = this.children[i]; if (child instanceof M.core_availability.List) { // Recursive call. if (child.hasItemOfType(pluginType)) { return true; } } else { if (child.pluginType === pluginType) { return true; } } } return false; }; M.core_availability.List.prototype.getEyeIcons = function() { // Check each item. var eyeIcons = []; eyeIcons.push(this.eyeIcon); for (var i = 0; i < this.children.length; i++) { var child = this.children[i]; if (child.eyeIcon !== null) { eyeIcons.push(child.eyeIcon); } if (child instanceof M.core_availability.List) { eyeIcons.concat(child.getEyeIcons()); } } return eyeIcons; }; /** * Find all eye icons in the list and children, and disable or enable them if needed. */ M.core_availability.List.prototype.updatePrivateStatus = function() { if (!this.root) { throw new Error('Can only call this on root list'); } var shouldDisable = !this.node.all('[data-private]').isEmpty(); var eyeIcons = this.getEyeIcons(); for (var i = 0, j = eyeIcons.length; i < j; i++) { if (shouldDisable) { eyeIcons[i].setDisabled(); } else { eyeIcons[i].setEnabled(); } } }; /** * Eye icon for this list (null if none). * * @property eyeIcon * @type M.core_availability.EyeIcon */ M.core_availability.List.prototype.eyeIcon = null; /** * True if list is special root level list. * * @property root * @type Boolean */ M.core_availability.List.prototype.root = false; /** * Array containing children (Lists or Items). * * @property children * @type M.core_availability.List[]|M.core_availability.Item[] */ M.core_availability.List.prototype.children = null; /** * HTML outer node for list. * * @property node * @type Y.Node */ M.core_availability.List.prototype.node = null; /** * HTML node for inner div that actually is the displayed list. * * @property node * @type Y.Node */ M.core_availability.List.prototype.inner = null; /** * Represents a single condition. * * @class M.core_availability.Item * @constructor * @param {Object} json Decoded JSON value * @param {Boolean} root True if this item is a child of the root list. */ M.core_availability.Item = function(json, root) { this.pluginType = json.type; if (M.core_availability.form.plugins[json.type] === undefined) { // Handle undefined plugins. this.plugin = null; this.pluginNode = Y.Node.create('<div class="availability-warning">' + M.util.get_string('missingplugin', 'availability') + '</div>'); } else { // Plugin is known. this.plugin = M.core_availability.form.plugins[json.type]; this.pluginNode = this.plugin.getNode(json); // Add a class with the plugin Frankenstyle name to make CSS easier in plugin. this.pluginNode.addClass('availability_' + json.type); } // Allow events from pluginNode to bubble up to the Item. Y.augment(this.pluginNode, Y.EventTarget, true, null, {emitFacade: true}); this.pluginNode.addTarget(this); this.node = Y.Node.create('<div class="availability-item d-sm-flex align-items-center"><h3 class="accesshide"></h3></div>'); // Add eye icon if required. This icon is added for root items, but may be // hidden depending on the selected list operator. if (root) { var shown = true; if (json.showc !== undefined) { shown = json.showc; } this.eyeIcon = new M.core_availability.EyeIcon(true, shown); this.node.appendChild(this.eyeIcon.span); this.node.appendChild(this.eyeIcon.disabledSpan); } // Add plugin controls. this.pluginNode.addClass('availability-plugincontrols'); this.node.appendChild(this.pluginNode); // Add delete button for node. var deleteIcon = new M.core_availability.DeleteIcon(this); this.node.appendChild(deleteIcon.span); // Add the invalid marker (empty). this.node.appendChild(document.createTextNode(' ')); this.node.appendChild(Y.Node.create('<span class="badge bg-warning text-dark"/>')); }; Y.augment(M.core_availability.Item, Y.EventTarget, true, null, {emitFacade: true}); /** * Obtains the value of this condition, which will be serialized into JSON * format and stored in the form. * * @method getValue * @return {Object} JavaScript object containing value of this item */ M.core_availability.Item.prototype.getValue = function() { var value = {'type': this.pluginType}; if (this.plugin) { this.plugin.fillValue(value, this.pluginNode); } return value; }; /** * Checks whether this condition has any errors (incorrect user input). If so, * an error string identifier in the form langfile:langstring should be pushed * into the errors array. * * @method fillErrors * @param {Array} errors Array of errors so far */ M.core_availability.Item.prototype.fillErrors = function(errors) { var before = errors.length; if (this.plugin) { // Pass to plugin. this.plugin.fillErrors(errors, this.pluginNode); } else { // Unknown plugin is an error errors.push('core_availability:item_unknowntype'); } // If any errors were added, add the marker to this item. var errorLabel = this.node.one('> .bg-warning'); if (errors.length !== before && !errorLabel.get('firstChild')) { var errorString = ''; // Fetch the last error code from the array of errors and split using the ':' delimiter. var langString = errors[errors.length - 1].split(':'); var component = langString[0]; var identifier = langString[1]; // If get_string can't find the string, it will return the string in this format. var undefinedString = '[[' + identifier + ',' + component + ']]'; // Get the lang string. errorString = M.util.get_string(identifier, component); if (errorString === undefinedString) { // Use a generic invalid input message when the error lang string cannot be loaded. errorString = M.util.get_string('invalid', 'availability'); } // Show the error string. errorLabel.appendChild(document.createTextNode(errorString)); } else if (errors.length === before && errorLabel.get('firstChild')) { errorLabel.get('firstChild').remove(); } }; /** * Renumbers the item. * * @method renumber * @param {String} number Number to use in heading for this item */ M.core_availability.Item.prototype.renumber = function(number) { // Update heading for item. var headingParams = {number: number}; if (this.plugin) { headingParams.type = M.util.get_string('title', 'availability_' + this.pluginType); } else { headingParams.type = '[' + this.pluginType + ']'; } headingParams.number = number + ':'; var heading = M.util.get_string('itemheading', 'availability', headingParams); this.node.one('> h3').set('innerHTML', heading); this.node.one('> h3').getDOMNode().dataset.restrictionOrder = number ? number : 'root'; }; /** * Focuses something after a new item is added. * * @method focusAfterAdd */ M.core_availability.Item.prototype.focusAfterAdd = function() { this.plugin.focusAfterAdd(this.pluginNode); }; /** * Name of plugin. * * @property pluginType * @type String */ M.core_availability.Item.prototype.pluginType = null; /** * Object representing plugin form controls. * * @property plugin * @type Object */ M.core_availability.Item.prototype.plugin = null; /** * Eye icon for item. * * @property eyeIcon * @type M.core_availability.EyeIcon */ M.core_availability.Item.prototype.eyeIcon = null; /** * HTML node for item. * * @property node * @type Y.Node */ M.core_availability.Item.prototype.node = null; /** * Inner part of node that is owned by plugin. * * @property pluginNode * @type Y.Node */ M.core_availability.Item.prototype.pluginNode = null; /** * Eye icon (to control show/hide of the activity if the user fails a condition). * * There are individual eye icons (show/hide control for a single condition) and * 'all' eye icons (show/hide control that applies to the entire item, whatever * reason it fails for). This is necessary because the individual conditions * don't make sense for OR and AND NOT lists. * * @class M.core_availability.EyeIcon * @constructor * @param {Boolean} individual True if the icon is controlling a single condition * @param {Boolean} shown True if icon is initially in shown state */ M.core_availability.EyeIcon = function(individual, shown) { this.individual = individual; this.span = Y.Node.create('<a class="availability-eye col-form-label" href="#" role="button">'); var icon = Y.Node.create('<img />'); this.span.appendChild(icon); // Set up button text and icon. var suffix = individual ? '_individual' : '_all', setHidden = function() { var hiddenStr = M.util.get_string('hidden' + suffix, 'availability'); icon.set('src', M.util.image_url('i/show', 'core')); icon.set('alt', hiddenStr); this.span.set('title', hiddenStr + ' \u2022 ' + M.util.get_string('show_verb', 'availability')); }, setShown = function() { var shownStr = M.util.get_string('shown' + suffix, 'availability'); icon.set('src', M.util.image_url('i/hide', 'core')); icon.set('alt', shownStr); this.span.set('title', shownStr + ' \u2022 ' + M.util.get_string('hide_verb', 'availability')); }; if (shown) { setShown.call(this); } else { setHidden.call(this); } // Update when button is clicked. var click = function(e) { e.preventDefault(); if (this.isHidden()) { setShown.call(this); } else { setHidden.call(this); } M.core_availability.form.update(); }; this.span.on('click', click, this); this.span.on('key', click, 'up:32', this); this.span.on('key', function(e) { e.preventDefault(); }, 'down:32', this); this.disabledSpan = Y.Node.create('<span class="availability-eye-disabled col-form-label" href="#">'); var disabledIcon = Y.Node.create('<img />'); var disabledStr = M.util.get_string('hidden' + suffix, 'availability'); disabledIcon.set('src', M.util.image_url('i/show', 'core')); disabledIcon.set('alt', disabledStr); this.disabledSpan.set('title', disabledStr + ' \u2022 ' + M.util.get_string('disabled_verb', 'availability')); this.disabledSpan.appendChild(disabledIcon); this.disabledSpan.hide(); }; /** * True if this eye icon is an individual one (see above). * * @property individual * @type Boolean */ M.core_availability.EyeIcon.prototype.individual = false; /** * YUI node for the span that contains this icon. * * @property span * @type Y.Node */ M.core_availability.EyeIcon.prototype.span = null; /** * YUI node for the span that contains the "disabled" state of the icon. * * @property span * @type Y.Node */ M.core_availability.EyeIcon.prototype.disabledSpan = null; /** * Checks the current state of the icon. * * @method isHidden * @return {Boolean} True if this icon is set to 'hidden' */ M.core_availability.EyeIcon.prototype.isHidden = function() { var suffix = this.individual ? '_individual' : '_all', compare = M.util.get_string('hidden' + suffix, 'availability'); return this.span.one('img').get('alt') === compare; }; /** * Checks whether the eye icon is disabled, and a dummy "hidden" icon displayed instead. * * @method isDisabled * @return {Boolean} True if this icon is disabled */ M.core_availability.EyeIcon.prototype.isDisabled = function() { return this.span.hasAttribute('hidden'); }; /** * Locks the state of the icon. * * @method setLocked */ M.core_availability.EyeIcon.prototype.setDisabled = function() { if (!this.isDisabled()) { this.span.hide(); this.disabledSpan.show(); } }; /** * Unlocks the icon so it can be changed. * * @method setUnlocked */ M.core_availability.EyeIcon.prototype.setEnabled = function() { if (this.isDisabled()) { this.span.show(); this.disabledSpan.hide(); } }; /** * Delete icon (to delete an Item or List). * * @class M.core_availability.DeleteIcon * @constructor * @param {M.core_availability.Item|M.core_availability.List} toDelete Thing to delete */ M.core_availability.DeleteIcon = function(toDelete) { this.span = Y.Node.create('<a class="d-inline-block col-form-label availability-delete px-3" href="#" title="' + M.util.get_string('delete', 'moodle') + '" role="button">'); var img = Y.Node.create('<img src="' + M.util.image_url('t/delete', 'core') + '" alt="' + M.util.get_string('delete', 'moodle') + '" />'); this.span.appendChild(img); var click = function(e) { e.preventDefault(); M.core_availability.form.rootList.deleteDescendant(toDelete); M.core_availability.form.rootList.renumber(); }; this.span.on('click', click, this); this.span.on('key', click, 'up:32', this); this.span.on('key', function(e) { e.preventDefault(); }, 'down:32', this); }; /** * YUI node for the span that contains this icon. * * @property span * @type Y.Node */ M.core_availability.DeleteIcon.prototype.span = null; yui/src/form/build.json 0000644 00000000233 15215712063 0011076 0 ustar 00 { "name": "moodle-core_availability-form", "builds": { "moodle-core_availability-form": { "jsfiles": [ "form.js" ] } } } yui/src/form/meta/form.json 0000644 00000000335 15215712063 0011673 0 ustar 00 { "moodle-core_availability-form": { "requires": [ "base", "node", "event", "event-delegate", "panel", "moodle-core-notification-dialogue", "json" ] } } yui/build/moodle-core_availability-form/moodle-core_availability-form-debug.js 0000644 00000133317 15215712063 0023705 0 ustar 00 YUI.add('moodle-core_availability-form', function (Y, NAME) { /** * Provides interface for users to edit availability settings on the * module/section editing form. * * The system works using this JavaScript plus form.js files inside each * condition plugin. * * The overall concept is that data is held in a textarea in the form in JSON * format. This JavaScript converts the textarea into a set of controls * generated here and by the relevant plugins. * * (Almost) all data is held directly by the state of the HTML controls, and * can be updated to the form field by calling the 'update' method, which * this code and the plugins call if any HTML control changes. * * @module moodle-core_availability-form */ M.core_availability = M.core_availability || {}; /** * Core static functions for availability settings in editing form. * * @class M.core_availability.form * @static */ M.core_availability.form = { /** * Object containing installed plugins. They are indexed by plugin name. * * @property plugins * @type Object */ plugins: {}, /** * Availability field (textarea). * * @property field * @type Y.Node */ field: null, /** * Main div that replaces the availability field. * * @property mainDiv * @type Y.Node */ mainDiv: null, /** * Object that represents the root of the tree. * * @property rootList * @type M.core_availability.List */ rootList: null, /** * Counter used when creating anything that needs an id. * * @property idCounter * @type Number */ idCounter: 0, /** * The 'Restrict by group' button if present. * * @property restrictByGroup * @type Y.Node */ restrictByGroup: null, /** * Called to initialise the system when the page loads. This method will * also call the init method for each plugin. * * @method init */ init: function(pluginParams) { // Init all plugins. for (var plugin in pluginParams) { var params = pluginParams[plugin]; var pluginClass = M[params[0]].form; pluginClass.init.apply(pluginClass, params); } // Get the availability field, hide it, and replace with the main div. this.field = Y.one('#id_availabilityconditionsjson'); this.field.setAttribute('aria-hidden', 'true'); // The fcontainer class here is inappropriate, but is necessary // because otherwise it is impossible to make Behat work correctly on // these controls as Behat incorrectly decides they're a moodleform // textarea. IMO Behat should not know about moodleforms at all and // should look purely at HTML elements on the page, but until it is // fixed to do this or fixed in some other way to only detect moodleform // elements that specifically match what those elements should look like, // then there is no good solution. this.mainDiv = Y.Node.create('<div class="availability-field fcontainer"></div>'); this.field.insert(this.mainDiv, 'after'); // Get top-level tree as JSON. var value = this.field.get('value'); var data = null; if (value !== '') { try { data = Y.JSON.parse(value); } catch (x) { // If the JSON data is not valid, treat it as empty. this.field.set('value', ''); } } this.rootList = new M.core_availability.List(data, true); this.mainDiv.appendChild(this.rootList.node); // Update JSON value after loading (to reflect any changes that need // to be made to make it valid). this.update(); this.rootList.renumber(); // Mark main area as dynamically updated. this.mainDiv.setAttribute('aria-live', 'polite'); // Listen for form submission - to avoid having our made-up fields // submitted, we need to disable them all before submit. this.field.ancestor('form').on('submit', function() { this.mainDiv.all('input,textarea,select').set('disabled', true); }, this); // If the form has group mode and/or grouping options, there is a // 'add restriction' button there. this.restrictByGroup = Y.one('#restrictbygroup'); if (this.restrictByGroup) { this.restrictByGroup.on('click', this.addRestrictByGroup, this); var groupmode = Y.one('#id_groupmode'); var groupingid = Y.one('#id_groupingid'); if (groupmode) { groupmode.on('change', this.updateRestrictByGroup, this); } if (groupingid) { groupingid.on('change', this.updateRestrictByGroup, this); } this.updateRestrictByGroup(); } // Everything is ready. Make sure the div is visible and hide the loading indicator. this.parent = Y.one('#fitem_id_availabilityconditionsjson'); this.parent.removeClass('d-none'); document.getElementById('availabilityconditions-loading').remove(); }, /** * Called at any time to update the hidden field value. * * This should be called whenever any value changes in the form settings. * * @method update */ update: function() { // Convert tree to value. var jsValue = this.rootList.getValue(); // Store any errors (for form reporting) in 'errors' value if present. var errors = []; this.rootList.fillErrors(errors); if (errors.length !== 0) { jsValue.errors = errors; } // Set into hidden form field, JS-encoded. this.field.set('value', Y.JSON.stringify(jsValue)); // Also update the restrict by group button if present. this.updateRestrictByGroup(); }, /** * Updates the status of the 'restrict by group' button (enables or disables * it) based on current availability restrictions and group/grouping settings. */ updateRestrictByGroup: function() { if (!this.restrictByGroup) { return; } // If the root list is anything other than the default 'and' type, disable. if (this.rootList.getValue().op !== '&') { this.restrictByGroup.set('disabled', true); return; } // If there's already a group restriction, disable it. var alreadyGot = this.rootList.hasItemOfType('group') || this.rootList.hasItemOfType('grouping'); if (alreadyGot) { this.restrictByGroup.set('disabled', true); return; } // If the groupmode and grouping id aren't set, disable it. var groupmode = Y.one('#id_groupmode'); var groupingid = Y.one('#id_groupingid'); var groupavailability = Number(this.restrictByGroup.getData('groupavailability')) === 1; var groupingavailability = Number(this.restrictByGroup.getData('groupingavailability')) === 1; if ((!groupmode || Number(groupmode.get('value')) === 0 || !groupavailability) && (!groupingid || Number(groupingid.get('value')) === 0 || !groupingavailability)) { this.restrictByGroup.set('disabled', true); return; } this.restrictByGroup.set('disabled', false); }, /** * Called when the user clicks on the 'restrict by group' button. This is * a special case that adds a group or grouping restriction. * * By default this restriction is not shown which makes it similar to the * * @param e Button click event */ addRestrictByGroup: function(e) { // If you don't prevent default, it submits the form for some reason. e.preventDefault(); // Add the condition. var groupmode = Y.one('#id_groupmode'); var groupingid = Y.one('#id_groupingid'); var groupavailability = Number(this.restrictByGroup.getData('groupavailability')) === 1; var groupingavailability = Number(this.restrictByGroup.getData('groupingavailability')) === 1; var newChild; if (groupingid && Number(groupingid.get('value')) !== 0 && groupingavailability) { // Add a grouping restriction if one is specified. newChild = new M.core_availability.Item( {type: 'grouping', id: Number(groupingid.get('value'))}, true); } else if (groupmode && groupavailability) { // Otherwise just add a group restriction. newChild = new M.core_availability.Item({type: 'group'}, true); } // Refresh HTML. if (newChild !== null) { this.rootList.addChild(newChild); this.update(); this.rootList.renumber(); this.rootList.updateHtml(); } } }; /** * Base object for plugins. Plugins should use Y.Object to extend this class. * * @class M.core_availability.plugin * @static */ M.core_availability.plugin = { /** * True if users are allowed to add items of this plugin at the moment. * * @property allowAdd * @type Boolean */ allowAdd: false, /** * Called (from PHP) to initialise the plugin. Should usually not be * overridden by child plugin. * * @method init * @param {String} component Component name e.g. 'availability_date'. * @param {boolean} allowAdd Indicates whether adding new instances of the plugin is permitted. * @param {Object} params Additional parameters. * @param {boolean} displayMode Whether the eye icon is show or hide. True for "Hide", false for "Show". */ init: function(component, allowAdd, params, displayMode) { var name = component.replace(/^availability_/, ''); this.allowAdd = allowAdd; M.core_availability.form.plugins[name] = this; this.initInner.apply(this, params); this.displayMode = displayMode; }, /** * Init method for plugin to override. (Default does nothing.) * * This method will receive any parameters defined in frontend.php * get_javascript_init_params. * * @method initInner * @protected */ initInner: function() { // Can be overriden. }, /** * Gets a YUI node representing the controls for this plugin on the form. * * Must be implemented by sub-object; default throws an exception. * * @method getNode * @return {Y.Node} YUI node */ getNode: function() { throw 'getNode not implemented'; }, /** * Fills in the value from this plugin's controls into a value object, * which will later be converted to JSON and stored in the form field. * * Must be implemented by sub-object; default throws an exception. * * @method fillValue * @param {Object} value Value object (to be written to) * @param {Y.Node} node YUI node (same one returned from getNode) */ fillValue: function() { throw 'fillValue not implemented'; }, /** * Fills in any errors from this plugin's controls. If there are any * errors, push them into the supplied array. * * Errors are Moodle language strings in format component:string, e.g. * 'availability_date:error_date_past_end_of_world'. * * The default implementation does nothing. * * @method fillErrors * @param {Array} errors Array of errors (push new errors here) * @param {Y.Node} node YUI node (same one returned from getNode) */ fillErrors: function() { // Can be overriden. }, /** * Focuses the first thing in the plugin after it has been added. * * The default implementation uses a simple algorithm to identify the * first focusable input/select and then focuses it. */ focusAfterAdd: function(node) { var target = node.one('input:not([disabled]),select:not([disabled])'); target.focus(); } }; /** * Maintains a list of children and settings for how they are combined. * * @class M.core_availability.List * @constructor * @param {Object} json Decoded JSON value * @param {Boolean} [false] root True if this is root level list * @param {Boolean} [false] root True if parent is root level list */ M.core_availability.List = function(json, root, parentRoot) { // Set default value for children. (You can't do this in the prototype // definition, or it ends up sharing the same array between all of them.) this.children = []; if (root !== undefined) { this.root = root; } // Create DIV structure (without kids). this.node = Y.Node.create('<div class="availability-list"><h3 class="accesshide"></h3>' + '<div class="availability-inner">' + '<div class="availability-header mb-1"><span>' + M.util.get_string('listheader_sign_before', 'availability') + '</span>' + ' <label><span class="accesshide">' + M.util.get_string('label_sign', 'availability') + ' </span><select class="availability-neg custom-select mx-1"' + ' title="' + M.util.get_string('label_sign', 'availability') + '">' + '<option value="">' + M.util.get_string('listheader_sign_pos', 'availability') + '</option>' + '<option value="!">' + M.util.get_string('listheader_sign_neg', 'availability') + '</option></select></label> ' + '<span class="availability-single">' + M.util.get_string('listheader_single', 'availability') + '</span>' + '<span class="availability-multi">' + M.util.get_string('listheader_multi_before', 'availability') + ' <label><span class="accesshide">' + M.util.get_string('label_multi', 'availability') + ' </span>' + '<select class="availability-op custom-select mx-1"' + ' title="' + M.util.get_string('label_multi', 'availability') + '"><option value="&">' + M.util.get_string('listheader_multi_and', 'availability') + '</option>' + '<option value="|">' + M.util.get_string('listheader_multi_or', 'availability') + '</option></select></label> ' + M.util.get_string('listheader_multi_after', 'availability') + '</span></div>' + '<div class="availability-children"></div>' + '<div class="availability-none"><span class="px-3">' + M.util.get_string('none', 'moodle') + '</span></div>' + '<div class="clearfix mt-1"></div>' + '<div class="availability-button"></div></div><div class="clearfix"></div></div>'); if (!root) { this.node.addClass('availability-childlist d-sm-flex align-items-center'); } this.inner = this.node.one('> .availability-inner'); var shown = true; if (root) { // If it's the root, add an eye icon as first thing in header. if (json && json.show !== undefined) { shown = json.show; } this.eyeIcon = new M.core_availability.EyeIcon(false, shown); this.node.one('.availability-header').get('firstChild').insert( this.eyeIcon.span, 'before'); this.node.one('.availability-header').get('firstChild').insert( this.eyeIcon.disabledSpan, 'before'); this.on('availability:privateRuleSet', function(e) { e.target.getDOMNode().dataset.private = true; this.updatePrivateStatus(); }); this.on('availability:privateRuleUnset', function(e) { delete e.target.getDOMNode().dataset.private; this.updatePrivateStatus(); }); } else if (parentRoot) { // When the parent is root, add an eye icon before the main list div. if (json && json.showc !== undefined) { shown = json.showc; } this.eyeIcon = new M.core_availability.EyeIcon(false, shown); this.inner.insert(this.eyeIcon.span, 'before'); this.inner.insert(this.eyeIcon.disabledSpan, 'before'); } if (!root) { // If it's not the root, add a delete button to the 'none' option. // You can only delete lists when they have no children so this will // automatically appear at the correct time. var deleteIcon = new M.core_availability.DeleteIcon(this); var noneNode = this.node.one('.availability-none'); noneNode.appendChild(document.createTextNode(' ')); noneNode.appendChild(deleteIcon.span); // Also if it's not the root, none is actually invalid, so add a label. noneNode.appendChild(Y.Node.create('<span class="mt-1 badge bg-warning text-dark">' + M.util.get_string('invalid', 'availability') + '</span>')); } // Create the button and add it. var button = Y.Node.create('<button type="button" class="btn btn-secondary mt-1">' + M.util.get_string('addrestriction', 'availability') + '</button>'); button.on("click", function() { this.clickAdd(); }, this); this.node.one('div.availability-button').appendChild(button); if (json) { // Set operator from JSON data. switch (json.op) { case '&' : case '|' : this.node.one('.availability-neg').set('value', ''); break; case '!&' : case '!|' : this.node.one('.availability-neg').set('value', '!'); break; } switch (json.op) { case '&' : case '!&' : this.node.one('.availability-op').set('value', '&'); break; case '|' : case '!|' : this.node.one('.availability-op').set('value', '|'); break; } // Construct children. for (var i = 0; i < json.c.length; i++) { var child = json.c[i]; if (this.root && json && json.showc !== undefined) { child.showc = json.showc[i]; } var newItem; if (child.type !== undefined) { // Plugin type. newItem = new M.core_availability.Item(child, this.root); } else { // List type. newItem = new M.core_availability.List(child, false, this.root); } this.addChild(newItem); } } // Add update listeners to the dropdowns. this.node.one('.availability-neg').on('change', function() { // Update hidden field and HTML. M.util.js_pending('availability-neg-change'); M.core_availability.form.update(); this.updateHtml(); M.util.js_complete('availability-neg-change'); }, this); this.node.one('.availability-op').on('change', function() { // Update hidden field. M.util.js_pending('availability-op-change'); M.core_availability.form.update(); this.updateHtml(); M.util.js_complete('availability-op-change'); }, this); // Update HTML to hide unnecessary parts. this.updateHtml(); }; Y.augment(M.core_availability.List, Y.EventTarget, true, null, {emitFacade: true}); /** * Adds a child to the end of the list (in HTML and stored data). * * @method addChild * @private * @param {M.core_availability.Item|M.core_availability.List} newItem Child to add */ M.core_availability.List.prototype.addChild = function(newItem) { if (this.children.length > 0) { // Create connecting label (text will be filled in later by updateHtml). this.inner.one('.availability-children').appendChild(Y.Node.create( '<div class="availability-connector">' + '<span class="label"></span>' + '</div>')); } // Add item to array and to HTML. this.children.push(newItem); // Allow events from child Items and Lists to bubble up to this list. newItem.addTarget(this); this.inner.one('.availability-children').appendChild(newItem.node); }; /** * Focuses something after a new list is added. * * @method focusAfterAdd */ M.core_availability.List.prototype.focusAfterAdd = function() { this.inner.one('button').focus(); }; /** * Checks whether this list uses the individual show icons or the single one. * * (Basically, AND and the equivalent NOT OR list can have individual show icons * so that you hide the activity entirely if a user fails one condition, but * may display it with information about the condition if they fail a different * one. That isn't possible with OR and NOT AND because for those types, there * is not really a concept of which single condition caused the user to fail * it.) * * Method can only be called on the root list. * * @method isIndividualShowIcons * @return {Boolean} True if using the individual icons */ M.core_availability.List.prototype.isIndividualShowIcons = function() { if (!this.root) { throw 'Can only call this on root list'; } var neg = this.node.one('.availability-neg').get('value') === '!'; var isor = this.node.one('.availability-op').get('value') === '|'; return (!neg && !isor) || (neg && isor); }; /** * Renumbers the list and all children. * * @method renumber * @param {String} parentNumber Number to use in heading for this list */ M.core_availability.List.prototype.renumber = function(parentNumber) { // Update heading for list. var headingParams = {count: this.children.length}; var prefix; if (parentNumber === undefined) { headingParams.number = ''; prefix = ''; } else { headingParams.number = parentNumber + ':'; prefix = parentNumber + '.'; } var heading = M.util.get_string('setheading', 'availability', headingParams); this.node.one('> h3').set('innerHTML', heading); this.node.one('> h3').getDOMNode().dataset.restrictionOrder = parentNumber ? parentNumber : 'root'; // Do children. for (var i = 0; i < this.children.length; i++) { var child = this.children[i]; child.renumber(prefix + (i + 1)); } }; /** * Updates HTML for the list based on the current values, for example showing * the 'None' text if there are no children. * * @method updateHtml */ M.core_availability.List.prototype.updateHtml = function() { // Control children appearing or not appearing. if (this.children.length > 0) { this.inner.one('> .availability-children').removeAttribute('aria-hidden'); this.inner.one('> .availability-none').setAttribute('aria-hidden', 'true'); this.inner.one('> .availability-header').removeAttribute('aria-hidden'); if (this.children.length > 1) { this.inner.one('.availability-single').setAttribute('aria-hidden', 'true'); this.inner.one('.availability-multi').removeAttribute('aria-hidden'); } else { this.inner.one('.availability-single').removeAttribute('aria-hidden'); this.inner.one('.availability-multi').setAttribute('aria-hidden', 'true'); } } else { this.inner.one('> .availability-children').setAttribute('aria-hidden', 'true'); this.inner.one('> .availability-none').removeAttribute('aria-hidden'); this.inner.one('> .availability-header').setAttribute('aria-hidden', 'true'); } // For root list, control eye icons. if (this.root) { var showEyes = this.isIndividualShowIcons(); // Individual icons. for (var i = 0; i < this.children.length; i++) { var child = this.children[i]; if (showEyes) { child.eyeIcon.span.removeAttribute('aria-hidden'); child.eyeIcon.disabledSpan.removeAttribute('aria-hidden'); } else { child.eyeIcon.span.setAttribute('aria-hidden', 'true'); child.eyeIcon.disabledSpan.setAttribute('aria-hidden', 'true'); } } // Single icon is the inverse. if (showEyes) { this.eyeIcon.span.setAttribute('aria-hidden', 'true'); this.eyeIcon.disabledSpan.setAttribute('aria-hidden', 'true'); } else { this.eyeIcon.span.removeAttribute('aria-hidden'); this.eyeIcon.disabledSpan.removeAttribute('aria-hidden'); } this.updatePrivateStatus(); } // Update connector text. var connectorText; if (this.inner.one('.availability-op').get('value') === '&') { connectorText = M.util.get_string('and', 'availability'); } else { connectorText = M.util.get_string('or', 'availability'); } this.inner.all('> .availability-children > .availability-connector span.label').each(function(span) { span.set('innerHTML', connectorText); }); }; /** * Deletes a descendant item (Item or List). Called when the user clicks a * delete icon. * * This is a recursive function. * * @method deleteDescendant * @param {M.core_availability.Item|M.core_availability.List} descendant Item to delete * @return {Boolean} True if it was deleted */ M.core_availability.List.prototype.deleteDescendant = function(descendant) { // Loop through children. for (var i = 0; i < this.children.length; i++) { var child = this.children[i]; if (child === descendant) { // Remove from internal array. this.children.splice(i, 1); var target = child.node; // Remove one of the connector nodes around target (if any left). if (this.children.length > 0) { if (target.previous('.availability-connector')) { target.previous('.availability-connector').remove(); } else { target.next('.availability-connector').remove(); } } // Remove target itself. this.inner.one('> .availability-children').removeChild(target); // Update the form and the list HTML. M.core_availability.form.update(); this.updateHtml(); // Focus add button for this list. this.inner.one('> .availability-button').one('button').focus(); return true; } else if (child instanceof M.core_availability.List) { // Recursive call. var found = child.deleteDescendant(descendant); if (found) { return true; } } } return false; }; /** * Shows the 'add restriction' dialogue box. * * @method clickAdd */ M.core_availability.List.prototype.clickAdd = function() { var content = Y.Node.create('<div>' + '<ul class="list-unstyled container-fluid"></ul>' + '<div class="availability-buttons mdl-align">' + '<button type="button" class="btn btn-secondary">' + M.util.get_string('cancel', 'moodle') + '</button></div></div>'); var cancel = content.one('button'); // Make a list of all the dialog options. var dialogRef = {dialog: null}; var ul = content.one('ul'); var li, id, button, label; for (var type in M.core_availability.form.plugins) { // Plugins might decide not to display their add button. if (!M.core_availability.form.plugins[type].allowAdd) { continue; } // Add entry for plugin. li = Y.Node.create('<li class="clearfix row"></li>'); id = 'availability_addrestriction_' + type; button = Y.Node.create('<div class="col-6"><button type="button" class="btn btn-secondary w-100"' + 'id="' + id + '">' + M.util.get_string('title', 'availability_' + type) + '</button></div>'); button.on('click', this.getAddHandler(type, dialogRef), this); li.appendChild(button); label = Y.Node.create('<div class="col-6"><label for="' + id + '">' + M.util.get_string('description', 'availability_' + type) + '</label></div>'); li.appendChild(label); ul.appendChild(li); } // Extra entry for lists. li = Y.Node.create('<li class="clearfix row"></li>'); id = 'availability_addrestriction_list_'; button = Y.Node.create('<div class="col-6"><button type="button" class="btn btn-secondary w-100"' + 'id="' + id + '">' + M.util.get_string('condition_group', 'availability') + '</button></div>'); button.on('click', this.getAddHandler(null, dialogRef), this); li.appendChild(button); label = Y.Node.create('<div class="col-6"><label for="' + id + '">' + M.util.get_string('condition_group_info', 'availability') + '</label></div>'); li.appendChild(label); ul.appendChild(li); var config = { headerContent: M.util.get_string('addrestriction', 'availability'), bodyContent: content, additionalBaseClass: 'availability-dialogue', draggable: true, modal: true, closeButton: false, width: '450px' }; dialogRef.dialog = new M.core.dialogue(config); dialogRef.dialog.show(); cancel.on('click', function() { dialogRef.dialog.hide(); // Focus the button they clicked originally. this.inner.one('> .availability-button').one('button').focus(); }, this); }; /** * Gets an add handler function used by the dialogue to add a particular item. * * @method getAddHandler * @param {String|Null} type Type name of plugin or null to add lists * @param {Object} dialogRef Reference to object that contains dialog * @param {M.core.dialogue} dialogRef.dialog Dialog object * @return {Function} Add handler function to call when adding that thing */ M.core_availability.List.prototype.getAddHandler = function(type, dialogRef) { return function() { var newItem; var displayMode = true; // Check if we have changed the eye icon in the manage restriction to hidden. if (type && M.core_availability.form.plugins[type].displayMode) { displayMode = false; } if (type) { // Create an Item object to represent the child. newItem = new M.core_availability.Item({type: type, creating: true, showc: displayMode}, this.root); } else { // Create a new List object to represent the child. newItem = new M.core_availability.List({c: [], showc: displayMode}, false, this.root); } // Add to list. this.addChild(newItem); // Update the form and list HTML. M.core_availability.form.update(); M.core_availability.form.rootList.renumber(); this.updateHtml(); // Hide dialog. dialogRef.dialog.hide(); newItem.focusAfterAdd(); }; }; /** * Gets the value of the list ready to convert to JSON and fill form field. * * @method getValue * @return {Object} Value of list suitable for use in JSON */ M.core_availability.List.prototype.getValue = function() { // Work out operator from selects. var value = {}; value.op = this.node.one('.availability-neg').get('value') + this.node.one('.availability-op').get('value'); // Work out children from list. value.c = []; var i; for (i = 0; i < this.children.length; i++) { value.c.push(this.children[i].getValue()); } // Work out show/showc for root level. if (this.root) { if (this.isIndividualShowIcons()) { value.showc = []; for (i = 0; i < this.children.length; i++) { var eyeIcon = this.children[i].eyeIcon; value.showc.push(!eyeIcon.isHidden() && !eyeIcon.isDisabled()); } } else { value.show = !this.eyeIcon.isHidden() && !this.eyeIcon.isDisabled(); } } return value; }; /** * Checks whether this list has any errors (incorrect user input). If so, * an error string identifier in the form langfile:langstring should be pushed * into the errors array. * * @method fillErrors * @param {Array} errors Array of errors so far */ M.core_availability.List.prototype.fillErrors = function(errors) { // List with no items is an error (except root). if (this.children.length === 0 && !this.root) { errors.push('availability:error_list_nochildren'); } // Pass to children. for (var i = 0; i < this.children.length; i++) { this.children[i].fillErrors(errors); } }; /** * Checks whether the list contains any items of the given type name. * * @method hasItemOfType * @param {String} pluginType Required plugin type (name) * @return {Boolean} True if there is one */ M.core_availability.List.prototype.hasItemOfType = function(pluginType) { // Check each item. for (var i = 0; i < this.children.length; i++) { var child = this.children[i]; if (child instanceof M.core_availability.List) { // Recursive call. if (child.hasItemOfType(pluginType)) { return true; } } else { if (child.pluginType === pluginType) { return true; } } } return false; }; M.core_availability.List.prototype.getEyeIcons = function() { // Check each item. var eyeIcons = []; eyeIcons.push(this.eyeIcon); for (var i = 0; i < this.children.length; i++) { var child = this.children[i]; if (child.eyeIcon !== null) { eyeIcons.push(child.eyeIcon); } if (child instanceof M.core_availability.List) { eyeIcons.concat(child.getEyeIcons()); } } return eyeIcons; }; /** * Find all eye icons in the list and children, and disable or enable them if needed. */ M.core_availability.List.prototype.updatePrivateStatus = function() { if (!this.root) { throw new Error('Can only call this on root list'); } var shouldDisable = !this.node.all('[data-private]').isEmpty(); var eyeIcons = this.getEyeIcons(); for (var i = 0, j = eyeIcons.length; i < j; i++) { if (shouldDisable) { eyeIcons[i].setDisabled(); } else { eyeIcons[i].setEnabled(); } } }; /** * Eye icon for this list (null if none). * * @property eyeIcon * @type M.core_availability.EyeIcon */ M.core_availability.List.prototype.eyeIcon = null; /** * True if list is special root level list. * * @property root * @type Boolean */ M.core_availability.List.prototype.root = false; /** * Array containing children (Lists or Items). * * @property children * @type M.core_availability.List[]|M.core_availability.Item[] */ M.core_availability.List.prototype.children = null; /** * HTML outer node for list. * * @property node * @type Y.Node */ M.core_availability.List.prototype.node = null; /** * HTML node for inner div that actually is the displayed list. * * @property node * @type Y.Node */ M.core_availability.List.prototype.inner = null; /** * Represents a single condition. * * @class M.core_availability.Item * @constructor * @param {Object} json Decoded JSON value * @param {Boolean} root True if this item is a child of the root list. */ M.core_availability.Item = function(json, root) { this.pluginType = json.type; if (M.core_availability.form.plugins[json.type] === undefined) { // Handle undefined plugins. this.plugin = null; this.pluginNode = Y.Node.create('<div class="availability-warning">' + M.util.get_string('missingplugin', 'availability') + '</div>'); } else { // Plugin is known. this.plugin = M.core_availability.form.plugins[json.type]; this.pluginNode = this.plugin.getNode(json); // Add a class with the plugin Frankenstyle name to make CSS easier in plugin. this.pluginNode.addClass('availability_' + json.type); } // Allow events from pluginNode to bubble up to the Item. Y.augment(this.pluginNode, Y.EventTarget, true, null, {emitFacade: true}); this.pluginNode.addTarget(this); this.node = Y.Node.create('<div class="availability-item d-sm-flex align-items-center"><h3 class="accesshide"></h3></div>'); // Add eye icon if required. This icon is added for root items, but may be // hidden depending on the selected list operator. if (root) { var shown = true; if (json.showc !== undefined) { shown = json.showc; } this.eyeIcon = new M.core_availability.EyeIcon(true, shown); this.node.appendChild(this.eyeIcon.span); this.node.appendChild(this.eyeIcon.disabledSpan); } // Add plugin controls. this.pluginNode.addClass('availability-plugincontrols'); this.node.appendChild(this.pluginNode); // Add delete button for node. var deleteIcon = new M.core_availability.DeleteIcon(this); this.node.appendChild(deleteIcon.span); // Add the invalid marker (empty). this.node.appendChild(document.createTextNode(' ')); this.node.appendChild(Y.Node.create('<span class="badge bg-warning text-dark"/>')); }; Y.augment(M.core_availability.Item, Y.EventTarget, true, null, {emitFacade: true}); /** * Obtains the value of this condition, which will be serialized into JSON * format and stored in the form. * * @method getValue * @return {Object} JavaScript object containing value of this item */ M.core_availability.Item.prototype.getValue = function() { var value = {'type': this.pluginType}; if (this.plugin) { this.plugin.fillValue(value, this.pluginNode); } return value; }; /** * Checks whether this condition has any errors (incorrect user input). If so, * an error string identifier in the form langfile:langstring should be pushed * into the errors array. * * @method fillErrors * @param {Array} errors Array of errors so far */ M.core_availability.Item.prototype.fillErrors = function(errors) { var before = errors.length; if (this.plugin) { // Pass to plugin. this.plugin.fillErrors(errors, this.pluginNode); } else { // Unknown plugin is an error errors.push('core_availability:item_unknowntype'); } // If any errors were added, add the marker to this item. var errorLabel = this.node.one('> .bg-warning'); if (errors.length !== before && !errorLabel.get('firstChild')) { var errorString = ''; // Fetch the last error code from the array of errors and split using the ':' delimiter. var langString = errors[errors.length - 1].split(':'); var component = langString[0]; var identifier = langString[1]; // If get_string can't find the string, it will return the string in this format. var undefinedString = '[[' + identifier + ',' + component + ']]'; // Get the lang string. errorString = M.util.get_string(identifier, component); if (errorString === undefinedString) { // Use a generic invalid input message when the error lang string cannot be loaded. errorString = M.util.get_string('invalid', 'availability'); } // Show the error string. errorLabel.appendChild(document.createTextNode(errorString)); } else if (errors.length === before && errorLabel.get('firstChild')) { errorLabel.get('firstChild').remove(); } }; /** * Renumbers the item. * * @method renumber * @param {String} number Number to use in heading for this item */ M.core_availability.Item.prototype.renumber = function(number) { // Update heading for item. var headingParams = {number: number}; if (this.plugin) { headingParams.type = M.util.get_string('title', 'availability_' + this.pluginType); } else { headingParams.type = '[' + this.pluginType + ']'; } headingParams.number = number + ':'; var heading = M.util.get_string('itemheading', 'availability', headingParams); this.node.one('> h3').set('innerHTML', heading); this.node.one('> h3').getDOMNode().dataset.restrictionOrder = number ? number : 'root'; }; /** * Focuses something after a new item is added. * * @method focusAfterAdd */ M.core_availability.Item.prototype.focusAfterAdd = function() { this.plugin.focusAfterAdd(this.pluginNode); }; /** * Name of plugin. * * @property pluginType * @type String */ M.core_availability.Item.prototype.pluginType = null; /** * Object representing plugin form controls. * * @property plugin * @type Object */ M.core_availability.Item.prototype.plugin = null; /** * Eye icon for item. * * @property eyeIcon * @type M.core_availability.EyeIcon */ M.core_availability.Item.prototype.eyeIcon = null; /** * HTML node for item. * * @property node * @type Y.Node */ M.core_availability.Item.prototype.node = null; /** * Inner part of node that is owned by plugin. * * @property pluginNode * @type Y.Node */ M.core_availability.Item.prototype.pluginNode = null; /** * Eye icon (to control show/hide of the activity if the user fails a condition). * * There are individual eye icons (show/hide control for a single condition) and * 'all' eye icons (show/hide control that applies to the entire item, whatever * reason it fails for). This is necessary because the individual conditions * don't make sense for OR and AND NOT lists. * * @class M.core_availability.EyeIcon * @constructor * @param {Boolean} individual True if the icon is controlling a single condition * @param {Boolean} shown True if icon is initially in shown state */ M.core_availability.EyeIcon = function(individual, shown) { this.individual = individual; this.span = Y.Node.create('<a class="availability-eye col-form-label" href="#" role="button">'); var icon = Y.Node.create('<img />'); this.span.appendChild(icon); // Set up button text and icon. var suffix = individual ? '_individual' : '_all', setHidden = function() { var hiddenStr = M.util.get_string('hidden' + suffix, 'availability'); icon.set('src', M.util.image_url('i/show', 'core')); icon.set('alt', hiddenStr); this.span.set('title', hiddenStr + ' \u2022 ' + M.util.get_string('show_verb', 'availability')); }, setShown = function() { var shownStr = M.util.get_string('shown' + suffix, 'availability'); icon.set('src', M.util.image_url('i/hide', 'core')); icon.set('alt', shownStr); this.span.set('title', shownStr + ' \u2022 ' + M.util.get_string('hide_verb', 'availability')); }; if (shown) { setShown.call(this); } else { setHidden.call(this); } // Update when button is clicked. var click = function(e) { e.preventDefault(); if (this.isHidden()) { setShown.call(this); } else { setHidden.call(this); } M.core_availability.form.update(); }; this.span.on('click', click, this); this.span.on('key', click, 'up:32', this); this.span.on('key', function(e) { e.preventDefault(); }, 'down:32', this); this.disabledSpan = Y.Node.create('<span class="availability-eye-disabled col-form-label" href="#">'); var disabledIcon = Y.Node.create('<img />'); var disabledStr = M.util.get_string('hidden' + suffix, 'availability'); disabledIcon.set('src', M.util.image_url('i/show', 'core')); disabledIcon.set('alt', disabledStr); this.disabledSpan.set('title', disabledStr + ' \u2022 ' + M.util.get_string('disabled_verb', 'availability')); this.disabledSpan.appendChild(disabledIcon); this.disabledSpan.hide(); }; /** * True if this eye icon is an individual one (see above). * * @property individual * @type Boolean */ M.core_availability.EyeIcon.prototype.individual = false; /** * YUI node for the span that contains this icon. * * @property span * @type Y.Node */ M.core_availability.EyeIcon.prototype.span = null; /** * YUI node for the span that contains the "disabled" state of the icon. * * @property span * @type Y.Node */ M.core_availability.EyeIcon.prototype.disabledSpan = null; /** * Checks the current state of the icon. * * @method isHidden * @return {Boolean} True if this icon is set to 'hidden' */ M.core_availability.EyeIcon.prototype.isHidden = function() { var suffix = this.individual ? '_individual' : '_all', compare = M.util.get_string('hidden' + suffix, 'availability'); return this.span.one('img').get('alt') === compare; }; /** * Checks whether the eye icon is disabled, and a dummy "hidden" icon displayed instead. * * @method isDisabled * @return {Boolean} True if this icon is disabled */ M.core_availability.EyeIcon.prototype.isDisabled = function() { return this.span.hasAttribute('hidden'); }; /** * Locks the state of the icon. * * @method setLocked */ M.core_availability.EyeIcon.prototype.setDisabled = function() { if (!this.isDisabled()) { this.span.hide(); this.disabledSpan.show(); } }; /** * Unlocks the icon so it can be changed. * * @method setUnlocked */ M.core_availability.EyeIcon.prototype.setEnabled = function() { if (this.isDisabled()) { this.span.show(); this.disabledSpan.hide(); } }; /** * Delete icon (to delete an Item or List). * * @class M.core_availability.DeleteIcon * @constructor * @param {M.core_availability.Item|M.core_availability.List} toDelete Thing to delete */ M.core_availability.DeleteIcon = function(toDelete) { this.span = Y.Node.create('<a class="d-inline-block col-form-label availability-delete px-3" href="#" title="' + M.util.get_string('delete', 'moodle') + '" role="button">'); var img = Y.Node.create('<img src="' + M.util.image_url('t/delete', 'core') + '" alt="' + M.util.get_string('delete', 'moodle') + '" />'); this.span.appendChild(img); var click = function(e) { e.preventDefault(); M.core_availability.form.rootList.deleteDescendant(toDelete); M.core_availability.form.rootList.renumber(); }; this.span.on('click', click, this); this.span.on('key', click, 'up:32', this); this.span.on('key', function(e) { e.preventDefault(); }, 'down:32', this); }; /** * YUI node for the span that contains this icon. * * @property span * @type Y.Node */ M.core_availability.DeleteIcon.prototype.span = null; }, '@VERSION@', { "requires": [ "base", "node", "event", "event-delegate", "panel", "moodle-core-notification-dialogue", "json" ] }); yui/build/moodle-core_availability-form/moodle-core_availability-form.js 0000644 00000133317 15215712063 0022621 0 ustar 00 YUI.add('moodle-core_availability-form', function (Y, NAME) { /** * Provides interface for users to edit availability settings on the * module/section editing form. * * The system works using this JavaScript plus form.js files inside each * condition plugin. * * The overall concept is that data is held in a textarea in the form in JSON * format. This JavaScript converts the textarea into a set of controls * generated here and by the relevant plugins. * * (Almost) all data is held directly by the state of the HTML controls, and * can be updated to the form field by calling the 'update' method, which * this code and the plugins call if any HTML control changes. * * @module moodle-core_availability-form */ M.core_availability = M.core_availability || {}; /** * Core static functions for availability settings in editing form. * * @class M.core_availability.form * @static */ M.core_availability.form = { /** * Object containing installed plugins. They are indexed by plugin name. * * @property plugins * @type Object */ plugins: {}, /** * Availability field (textarea). * * @property field * @type Y.Node */ field: null, /** * Main div that replaces the availability field. * * @property mainDiv * @type Y.Node */ mainDiv: null, /** * Object that represents the root of the tree. * * @property rootList * @type M.core_availability.List */ rootList: null, /** * Counter used when creating anything that needs an id. * * @property idCounter * @type Number */ idCounter: 0, /** * The 'Restrict by group' button if present. * * @property restrictByGroup * @type Y.Node */ restrictByGroup: null, /** * Called to initialise the system when the page loads. This method will * also call the init method for each plugin. * * @method init */ init: function(pluginParams) { // Init all plugins. for (var plugin in pluginParams) { var params = pluginParams[plugin]; var pluginClass = M[params[0]].form; pluginClass.init.apply(pluginClass, params); } // Get the availability field, hide it, and replace with the main div. this.field = Y.one('#id_availabilityconditionsjson'); this.field.setAttribute('aria-hidden', 'true'); // The fcontainer class here is inappropriate, but is necessary // because otherwise it is impossible to make Behat work correctly on // these controls as Behat incorrectly decides they're a moodleform // textarea. IMO Behat should not know about moodleforms at all and // should look purely at HTML elements on the page, but until it is // fixed to do this or fixed in some other way to only detect moodleform // elements that specifically match what those elements should look like, // then there is no good solution. this.mainDiv = Y.Node.create('<div class="availability-field fcontainer"></div>'); this.field.insert(this.mainDiv, 'after'); // Get top-level tree as JSON. var value = this.field.get('value'); var data = null; if (value !== '') { try { data = Y.JSON.parse(value); } catch (x) { // If the JSON data is not valid, treat it as empty. this.field.set('value', ''); } } this.rootList = new M.core_availability.List(data, true); this.mainDiv.appendChild(this.rootList.node); // Update JSON value after loading (to reflect any changes that need // to be made to make it valid). this.update(); this.rootList.renumber(); // Mark main area as dynamically updated. this.mainDiv.setAttribute('aria-live', 'polite'); // Listen for form submission - to avoid having our made-up fields // submitted, we need to disable them all before submit. this.field.ancestor('form').on('submit', function() { this.mainDiv.all('input,textarea,select').set('disabled', true); }, this); // If the form has group mode and/or grouping options, there is a // 'add restriction' button there. this.restrictByGroup = Y.one('#restrictbygroup'); if (this.restrictByGroup) { this.restrictByGroup.on('click', this.addRestrictByGroup, this); var groupmode = Y.one('#id_groupmode'); var groupingid = Y.one('#id_groupingid'); if (groupmode) { groupmode.on('change', this.updateRestrictByGroup, this); } if (groupingid) { groupingid.on('change', this.updateRestrictByGroup, this); } this.updateRestrictByGroup(); } // Everything is ready. Make sure the div is visible and hide the loading indicator. this.parent = Y.one('#fitem_id_availabilityconditionsjson'); this.parent.removeClass('d-none'); document.getElementById('availabilityconditions-loading').remove(); }, /** * Called at any time to update the hidden field value. * * This should be called whenever any value changes in the form settings. * * @method update */ update: function() { // Convert tree to value. var jsValue = this.rootList.getValue(); // Store any errors (for form reporting) in 'errors' value if present. var errors = []; this.rootList.fillErrors(errors); if (errors.length !== 0) { jsValue.errors = errors; } // Set into hidden form field, JS-encoded. this.field.set('value', Y.JSON.stringify(jsValue)); // Also update the restrict by group button if present. this.updateRestrictByGroup(); }, /** * Updates the status of the 'restrict by group' button (enables or disables * it) based on current availability restrictions and group/grouping settings. */ updateRestrictByGroup: function() { if (!this.restrictByGroup) { return; } // If the root list is anything other than the default 'and' type, disable. if (this.rootList.getValue().op !== '&') { this.restrictByGroup.set('disabled', true); return; } // If there's already a group restriction, disable it. var alreadyGot = this.rootList.hasItemOfType('group') || this.rootList.hasItemOfType('grouping'); if (alreadyGot) { this.restrictByGroup.set('disabled', true); return; } // If the groupmode and grouping id aren't set, disable it. var groupmode = Y.one('#id_groupmode'); var groupingid = Y.one('#id_groupingid'); var groupavailability = Number(this.restrictByGroup.getData('groupavailability')) === 1; var groupingavailability = Number(this.restrictByGroup.getData('groupingavailability')) === 1; if ((!groupmode || Number(groupmode.get('value')) === 0 || !groupavailability) && (!groupingid || Number(groupingid.get('value')) === 0 || !groupingavailability)) { this.restrictByGroup.set('disabled', true); return; } this.restrictByGroup.set('disabled', false); }, /** * Called when the user clicks on the 'restrict by group' button. This is * a special case that adds a group or grouping restriction. * * By default this restriction is not shown which makes it similar to the * * @param e Button click event */ addRestrictByGroup: function(e) { // If you don't prevent default, it submits the form for some reason. e.preventDefault(); // Add the condition. var groupmode = Y.one('#id_groupmode'); var groupingid = Y.one('#id_groupingid'); var groupavailability = Number(this.restrictByGroup.getData('groupavailability')) === 1; var groupingavailability = Number(this.restrictByGroup.getData('groupingavailability')) === 1; var newChild; if (groupingid && Number(groupingid.get('value')) !== 0 && groupingavailability) { // Add a grouping restriction if one is specified. newChild = new M.core_availability.Item( {type: 'grouping', id: Number(groupingid.get('value'))}, true); } else if (groupmode && groupavailability) { // Otherwise just add a group restriction. newChild = new M.core_availability.Item({type: 'group'}, true); } // Refresh HTML. if (newChild !== null) { this.rootList.addChild(newChild); this.update(); this.rootList.renumber(); this.rootList.updateHtml(); } } }; /** * Base object for plugins. Plugins should use Y.Object to extend this class. * * @class M.core_availability.plugin * @static */ M.core_availability.plugin = { /** * True if users are allowed to add items of this plugin at the moment. * * @property allowAdd * @type Boolean */ allowAdd: false, /** * Called (from PHP) to initialise the plugin. Should usually not be * overridden by child plugin. * * @method init * @param {String} component Component name e.g. 'availability_date'. * @param {boolean} allowAdd Indicates whether adding new instances of the plugin is permitted. * @param {Object} params Additional parameters. * @param {boolean} displayMode Whether the eye icon is show or hide. True for "Hide", false for "Show". */ init: function(component, allowAdd, params, displayMode) { var name = component.replace(/^availability_/, ''); this.allowAdd = allowAdd; M.core_availability.form.plugins[name] = this; this.initInner.apply(this, params); this.displayMode = displayMode; }, /** * Init method for plugin to override. (Default does nothing.) * * This method will receive any parameters defined in frontend.php * get_javascript_init_params. * * @method initInner * @protected */ initInner: function() { // Can be overriden. }, /** * Gets a YUI node representing the controls for this plugin on the form. * * Must be implemented by sub-object; default throws an exception. * * @method getNode * @return {Y.Node} YUI node */ getNode: function() { throw 'getNode not implemented'; }, /** * Fills in the value from this plugin's controls into a value object, * which will later be converted to JSON and stored in the form field. * * Must be implemented by sub-object; default throws an exception. * * @method fillValue * @param {Object} value Value object (to be written to) * @param {Y.Node} node YUI node (same one returned from getNode) */ fillValue: function() { throw 'fillValue not implemented'; }, /** * Fills in any errors from this plugin's controls. If there are any * errors, push them into the supplied array. * * Errors are Moodle language strings in format component:string, e.g. * 'availability_date:error_date_past_end_of_world'. * * The default implementation does nothing. * * @method fillErrors * @param {Array} errors Array of errors (push new errors here) * @param {Y.Node} node YUI node (same one returned from getNode) */ fillErrors: function() { // Can be overriden. }, /** * Focuses the first thing in the plugin after it has been added. * * The default implementation uses a simple algorithm to identify the * first focusable input/select and then focuses it. */ focusAfterAdd: function(node) { var target = node.one('input:not([disabled]),select:not([disabled])'); target.focus(); } }; /** * Maintains a list of children and settings for how they are combined. * * @class M.core_availability.List * @constructor * @param {Object} json Decoded JSON value * @param {Boolean} [false] root True if this is root level list * @param {Boolean} [false] root True if parent is root level list */ M.core_availability.List = function(json, root, parentRoot) { // Set default value for children. (You can't do this in the prototype // definition, or it ends up sharing the same array between all of them.) this.children = []; if (root !== undefined) { this.root = root; } // Create DIV structure (without kids). this.node = Y.Node.create('<div class="availability-list"><h3 class="accesshide"></h3>' + '<div class="availability-inner">' + '<div class="availability-header mb-1"><span>' + M.util.get_string('listheader_sign_before', 'availability') + '</span>' + ' <label><span class="accesshide">' + M.util.get_string('label_sign', 'availability') + ' </span><select class="availability-neg custom-select mx-1"' + ' title="' + M.util.get_string('label_sign', 'availability') + '">' + '<option value="">' + M.util.get_string('listheader_sign_pos', 'availability') + '</option>' + '<option value="!">' + M.util.get_string('listheader_sign_neg', 'availability') + '</option></select></label> ' + '<span class="availability-single">' + M.util.get_string('listheader_single', 'availability') + '</span>' + '<span class="availability-multi">' + M.util.get_string('listheader_multi_before', 'availability') + ' <label><span class="accesshide">' + M.util.get_string('label_multi', 'availability') + ' </span>' + '<select class="availability-op custom-select mx-1"' + ' title="' + M.util.get_string('label_multi', 'availability') + '"><option value="&">' + M.util.get_string('listheader_multi_and', 'availability') + '</option>' + '<option value="|">' + M.util.get_string('listheader_multi_or', 'availability') + '</option></select></label> ' + M.util.get_string('listheader_multi_after', 'availability') + '</span></div>' + '<div class="availability-children"></div>' + '<div class="availability-none"><span class="px-3">' + M.util.get_string('none', 'moodle') + '</span></div>' + '<div class="clearfix mt-1"></div>' + '<div class="availability-button"></div></div><div class="clearfix"></div></div>'); if (!root) { this.node.addClass('availability-childlist d-sm-flex align-items-center'); } this.inner = this.node.one('> .availability-inner'); var shown = true; if (root) { // If it's the root, add an eye icon as first thing in header. if (json && json.show !== undefined) { shown = json.show; } this.eyeIcon = new M.core_availability.EyeIcon(false, shown); this.node.one('.availability-header').get('firstChild').insert( this.eyeIcon.span, 'before'); this.node.one('.availability-header').get('firstChild').insert( this.eyeIcon.disabledSpan, 'before'); this.on('availability:privateRuleSet', function(e) { e.target.getDOMNode().dataset.private = true; this.updatePrivateStatus(); }); this.on('availability:privateRuleUnset', function(e) { delete e.target.getDOMNode().dataset.private; this.updatePrivateStatus(); }); } else if (parentRoot) { // When the parent is root, add an eye icon before the main list div. if (json && json.showc !== undefined) { shown = json.showc; } this.eyeIcon = new M.core_availability.EyeIcon(false, shown); this.inner.insert(this.eyeIcon.span, 'before'); this.inner.insert(this.eyeIcon.disabledSpan, 'before'); } if (!root) { // If it's not the root, add a delete button to the 'none' option. // You can only delete lists when they have no children so this will // automatically appear at the correct time. var deleteIcon = new M.core_availability.DeleteIcon(this); var noneNode = this.node.one('.availability-none'); noneNode.appendChild(document.createTextNode(' ')); noneNode.appendChild(deleteIcon.span); // Also if it's not the root, none is actually invalid, so add a label. noneNode.appendChild(Y.Node.create('<span class="mt-1 badge bg-warning text-dark">' + M.util.get_string('invalid', 'availability') + '</span>')); } // Create the button and add it. var button = Y.Node.create('<button type="button" class="btn btn-secondary mt-1">' + M.util.get_string('addrestriction', 'availability') + '</button>'); button.on("click", function() { this.clickAdd(); }, this); this.node.one('div.availability-button').appendChild(button); if (json) { // Set operator from JSON data. switch (json.op) { case '&' : case '|' : this.node.one('.availability-neg').set('value', ''); break; case '!&' : case '!|' : this.node.one('.availability-neg').set('value', '!'); break; } switch (json.op) { case '&' : case '!&' : this.node.one('.availability-op').set('value', '&'); break; case '|' : case '!|' : this.node.one('.availability-op').set('value', '|'); break; } // Construct children. for (var i = 0; i < json.c.length; i++) { var child = json.c[i]; if (this.root && json && json.showc !== undefined) { child.showc = json.showc[i]; } var newItem; if (child.type !== undefined) { // Plugin type. newItem = new M.core_availability.Item(child, this.root); } else { // List type. newItem = new M.core_availability.List(child, false, this.root); } this.addChild(newItem); } } // Add update listeners to the dropdowns. this.node.one('.availability-neg').on('change', function() { // Update hidden field and HTML. M.util.js_pending('availability-neg-change'); M.core_availability.form.update(); this.updateHtml(); M.util.js_complete('availability-neg-change'); }, this); this.node.one('.availability-op').on('change', function() { // Update hidden field. M.util.js_pending('availability-op-change'); M.core_availability.form.update(); this.updateHtml(); M.util.js_complete('availability-op-change'); }, this); // Update HTML to hide unnecessary parts. this.updateHtml(); }; Y.augment(M.core_availability.List, Y.EventTarget, true, null, {emitFacade: true}); /** * Adds a child to the end of the list (in HTML and stored data). * * @method addChild * @private * @param {M.core_availability.Item|M.core_availability.List} newItem Child to add */ M.core_availability.List.prototype.addChild = function(newItem) { if (this.children.length > 0) { // Create connecting label (text will be filled in later by updateHtml). this.inner.one('.availability-children').appendChild(Y.Node.create( '<div class="availability-connector">' + '<span class="label"></span>' + '</div>')); } // Add item to array and to HTML. this.children.push(newItem); // Allow events from child Items and Lists to bubble up to this list. newItem.addTarget(this); this.inner.one('.availability-children').appendChild(newItem.node); }; /** * Focuses something after a new list is added. * * @method focusAfterAdd */ M.core_availability.List.prototype.focusAfterAdd = function() { this.inner.one('button').focus(); }; /** * Checks whether this list uses the individual show icons or the single one. * * (Basically, AND and the equivalent NOT OR list can have individual show icons * so that you hide the activity entirely if a user fails one condition, but * may display it with information about the condition if they fail a different * one. That isn't possible with OR and NOT AND because for those types, there * is not really a concept of which single condition caused the user to fail * it.) * * Method can only be called on the root list. * * @method isIndividualShowIcons * @return {Boolean} True if using the individual icons */ M.core_availability.List.prototype.isIndividualShowIcons = function() { if (!this.root) { throw 'Can only call this on root list'; } var neg = this.node.one('.availability-neg').get('value') === '!'; var isor = this.node.one('.availability-op').get('value') === '|'; return (!neg && !isor) || (neg && isor); }; /** * Renumbers the list and all children. * * @method renumber * @param {String} parentNumber Number to use in heading for this list */ M.core_availability.List.prototype.renumber = function(parentNumber) { // Update heading for list. var headingParams = {count: this.children.length}; var prefix; if (parentNumber === undefined) { headingParams.number = ''; prefix = ''; } else { headingParams.number = parentNumber + ':'; prefix = parentNumber + '.'; } var heading = M.util.get_string('setheading', 'availability', headingParams); this.node.one('> h3').set('innerHTML', heading); this.node.one('> h3').getDOMNode().dataset.restrictionOrder = parentNumber ? parentNumber : 'root'; // Do children. for (var i = 0; i < this.children.length; i++) { var child = this.children[i]; child.renumber(prefix + (i + 1)); } }; /** * Updates HTML for the list based on the current values, for example showing * the 'None' text if there are no children. * * @method updateHtml */ M.core_availability.List.prototype.updateHtml = function() { // Control children appearing or not appearing. if (this.children.length > 0) { this.inner.one('> .availability-children').removeAttribute('aria-hidden'); this.inner.one('> .availability-none').setAttribute('aria-hidden', 'true'); this.inner.one('> .availability-header').removeAttribute('aria-hidden'); if (this.children.length > 1) { this.inner.one('.availability-single').setAttribute('aria-hidden', 'true'); this.inner.one('.availability-multi').removeAttribute('aria-hidden'); } else { this.inner.one('.availability-single').removeAttribute('aria-hidden'); this.inner.one('.availability-multi').setAttribute('aria-hidden', 'true'); } } else { this.inner.one('> .availability-children').setAttribute('aria-hidden', 'true'); this.inner.one('> .availability-none').removeAttribute('aria-hidden'); this.inner.one('> .availability-header').setAttribute('aria-hidden', 'true'); } // For root list, control eye icons. if (this.root) { var showEyes = this.isIndividualShowIcons(); // Individual icons. for (var i = 0; i < this.children.length; i++) { var child = this.children[i]; if (showEyes) { child.eyeIcon.span.removeAttribute('aria-hidden'); child.eyeIcon.disabledSpan.removeAttribute('aria-hidden'); } else { child.eyeIcon.span.setAttribute('aria-hidden', 'true'); child.eyeIcon.disabledSpan.setAttribute('aria-hidden', 'true'); } } // Single icon is the inverse. if (showEyes) { this.eyeIcon.span.setAttribute('aria-hidden', 'true'); this.eyeIcon.disabledSpan.setAttribute('aria-hidden', 'true'); } else { this.eyeIcon.span.removeAttribute('aria-hidden'); this.eyeIcon.disabledSpan.removeAttribute('aria-hidden'); } this.updatePrivateStatus(); } // Update connector text. var connectorText; if (this.inner.one('.availability-op').get('value') === '&') { connectorText = M.util.get_string('and', 'availability'); } else { connectorText = M.util.get_string('or', 'availability'); } this.inner.all('> .availability-children > .availability-connector span.label').each(function(span) { span.set('innerHTML', connectorText); }); }; /** * Deletes a descendant item (Item or List). Called when the user clicks a * delete icon. * * This is a recursive function. * * @method deleteDescendant * @param {M.core_availability.Item|M.core_availability.List} descendant Item to delete * @return {Boolean} True if it was deleted */ M.core_availability.List.prototype.deleteDescendant = function(descendant) { // Loop through children. for (var i = 0; i < this.children.length; i++) { var child = this.children[i]; if (child === descendant) { // Remove from internal array. this.children.splice(i, 1); var target = child.node; // Remove one of the connector nodes around target (if any left). if (this.children.length > 0) { if (target.previous('.availability-connector')) { target.previous('.availability-connector').remove(); } else { target.next('.availability-connector').remove(); } } // Remove target itself. this.inner.one('> .availability-children').removeChild(target); // Update the form and the list HTML. M.core_availability.form.update(); this.updateHtml(); // Focus add button for this list. this.inner.one('> .availability-button').one('button').focus(); return true; } else if (child instanceof M.core_availability.List) { // Recursive call. var found = child.deleteDescendant(descendant); if (found) { return true; } } } return false; }; /** * Shows the 'add restriction' dialogue box. * * @method clickAdd */ M.core_availability.List.prototype.clickAdd = function() { var content = Y.Node.create('<div>' + '<ul class="list-unstyled container-fluid"></ul>' + '<div class="availability-buttons mdl-align">' + '<button type="button" class="btn btn-secondary">' + M.util.get_string('cancel', 'moodle') + '</button></div></div>'); var cancel = content.one('button'); // Make a list of all the dialog options. var dialogRef = {dialog: null}; var ul = content.one('ul'); var li, id, button, label; for (var type in M.core_availability.form.plugins) { // Plugins might decide not to display their add button. if (!M.core_availability.form.plugins[type].allowAdd) { continue; } // Add entry for plugin. li = Y.Node.create('<li class="clearfix row"></li>'); id = 'availability_addrestriction_' + type; button = Y.Node.create('<div class="col-6"><button type="button" class="btn btn-secondary w-100"' + 'id="' + id + '">' + M.util.get_string('title', 'availability_' + type) + '</button></div>'); button.on('click', this.getAddHandler(type, dialogRef), this); li.appendChild(button); label = Y.Node.create('<div class="col-6"><label for="' + id + '">' + M.util.get_string('description', 'availability_' + type) + '</label></div>'); li.appendChild(label); ul.appendChild(li); } // Extra entry for lists. li = Y.Node.create('<li class="clearfix row"></li>'); id = 'availability_addrestriction_list_'; button = Y.Node.create('<div class="col-6"><button type="button" class="btn btn-secondary w-100"' + 'id="' + id + '">' + M.util.get_string('condition_group', 'availability') + '</button></div>'); button.on('click', this.getAddHandler(null, dialogRef), this); li.appendChild(button); label = Y.Node.create('<div class="col-6"><label for="' + id + '">' + M.util.get_string('condition_group_info', 'availability') + '</label></div>'); li.appendChild(label); ul.appendChild(li); var config = { headerContent: M.util.get_string('addrestriction', 'availability'), bodyContent: content, additionalBaseClass: 'availability-dialogue', draggable: true, modal: true, closeButton: false, width: '450px' }; dialogRef.dialog = new M.core.dialogue(config); dialogRef.dialog.show(); cancel.on('click', function() { dialogRef.dialog.hide(); // Focus the button they clicked originally. this.inner.one('> .availability-button').one('button').focus(); }, this); }; /** * Gets an add handler function used by the dialogue to add a particular item. * * @method getAddHandler * @param {String|Null} type Type name of plugin or null to add lists * @param {Object} dialogRef Reference to object that contains dialog * @param {M.core.dialogue} dialogRef.dialog Dialog object * @return {Function} Add handler function to call when adding that thing */ M.core_availability.List.prototype.getAddHandler = function(type, dialogRef) { return function() { var newItem; var displayMode = true; // Check if we have changed the eye icon in the manage restriction to hidden. if (type && M.core_availability.form.plugins[type].displayMode) { displayMode = false; } if (type) { // Create an Item object to represent the child. newItem = new M.core_availability.Item({type: type, creating: true, showc: displayMode}, this.root); } else { // Create a new List object to represent the child. newItem = new M.core_availability.List({c: [], showc: displayMode}, false, this.root); } // Add to list. this.addChild(newItem); // Update the form and list HTML. M.core_availability.form.update(); M.core_availability.form.rootList.renumber(); this.updateHtml(); // Hide dialog. dialogRef.dialog.hide(); newItem.focusAfterAdd(); }; }; /** * Gets the value of the list ready to convert to JSON and fill form field. * * @method getValue * @return {Object} Value of list suitable for use in JSON */ M.core_availability.List.prototype.getValue = function() { // Work out operator from selects. var value = {}; value.op = this.node.one('.availability-neg').get('value') + this.node.one('.availability-op').get('value'); // Work out children from list. value.c = []; var i; for (i = 0; i < this.children.length; i++) { value.c.push(this.children[i].getValue()); } // Work out show/showc for root level. if (this.root) { if (this.isIndividualShowIcons()) { value.showc = []; for (i = 0; i < this.children.length; i++) { var eyeIcon = this.children[i].eyeIcon; value.showc.push(!eyeIcon.isHidden() && !eyeIcon.isDisabled()); } } else { value.show = !this.eyeIcon.isHidden() && !this.eyeIcon.isDisabled(); } } return value; }; /** * Checks whether this list has any errors (incorrect user input). If so, * an error string identifier in the form langfile:langstring should be pushed * into the errors array. * * @method fillErrors * @param {Array} errors Array of errors so far */ M.core_availability.List.prototype.fillErrors = function(errors) { // List with no items is an error (except root). if (this.children.length === 0 && !this.root) { errors.push('availability:error_list_nochildren'); } // Pass to children. for (var i = 0; i < this.children.length; i++) { this.children[i].fillErrors(errors); } }; /** * Checks whether the list contains any items of the given type name. * * @method hasItemOfType * @param {String} pluginType Required plugin type (name) * @return {Boolean} True if there is one */ M.core_availability.List.prototype.hasItemOfType = function(pluginType) { // Check each item. for (var i = 0; i < this.children.length; i++) { var child = this.children[i]; if (child instanceof M.core_availability.List) { // Recursive call. if (child.hasItemOfType(pluginType)) { return true; } } else { if (child.pluginType === pluginType) { return true; } } } return false; }; M.core_availability.List.prototype.getEyeIcons = function() { // Check each item. var eyeIcons = []; eyeIcons.push(this.eyeIcon); for (var i = 0; i < this.children.length; i++) { var child = this.children[i]; if (child.eyeIcon !== null) { eyeIcons.push(child.eyeIcon); } if (child instanceof M.core_availability.List) { eyeIcons.concat(child.getEyeIcons()); } } return eyeIcons; }; /** * Find all eye icons in the list and children, and disable or enable them if needed. */ M.core_availability.List.prototype.updatePrivateStatus = function() { if (!this.root) { throw new Error('Can only call this on root list'); } var shouldDisable = !this.node.all('[data-private]').isEmpty(); var eyeIcons = this.getEyeIcons(); for (var i = 0, j = eyeIcons.length; i < j; i++) { if (shouldDisable) { eyeIcons[i].setDisabled(); } else { eyeIcons[i].setEnabled(); } } }; /** * Eye icon for this list (null if none). * * @property eyeIcon * @type M.core_availability.EyeIcon */ M.core_availability.List.prototype.eyeIcon = null; /** * True if list is special root level list. * * @property root * @type Boolean */ M.core_availability.List.prototype.root = false; /** * Array containing children (Lists or Items). * * @property children * @type M.core_availability.List[]|M.core_availability.Item[] */ M.core_availability.List.prototype.children = null; /** * HTML outer node for list. * * @property node * @type Y.Node */ M.core_availability.List.prototype.node = null; /** * HTML node for inner div that actually is the displayed list. * * @property node * @type Y.Node */ M.core_availability.List.prototype.inner = null; /** * Represents a single condition. * * @class M.core_availability.Item * @constructor * @param {Object} json Decoded JSON value * @param {Boolean} root True if this item is a child of the root list. */ M.core_availability.Item = function(json, root) { this.pluginType = json.type; if (M.core_availability.form.plugins[json.type] === undefined) { // Handle undefined plugins. this.plugin = null; this.pluginNode = Y.Node.create('<div class="availability-warning">' + M.util.get_string('missingplugin', 'availability') + '</div>'); } else { // Plugin is known. this.plugin = M.core_availability.form.plugins[json.type]; this.pluginNode = this.plugin.getNode(json); // Add a class with the plugin Frankenstyle name to make CSS easier in plugin. this.pluginNode.addClass('availability_' + json.type); } // Allow events from pluginNode to bubble up to the Item. Y.augment(this.pluginNode, Y.EventTarget, true, null, {emitFacade: true}); this.pluginNode.addTarget(this); this.node = Y.Node.create('<div class="availability-item d-sm-flex align-items-center"><h3 class="accesshide"></h3></div>'); // Add eye icon if required. This icon is added for root items, but may be // hidden depending on the selected list operator. if (root) { var shown = true; if (json.showc !== undefined) { shown = json.showc; } this.eyeIcon = new M.core_availability.EyeIcon(true, shown); this.node.appendChild(this.eyeIcon.span); this.node.appendChild(this.eyeIcon.disabledSpan); } // Add plugin controls. this.pluginNode.addClass('availability-plugincontrols'); this.node.appendChild(this.pluginNode); // Add delete button for node. var deleteIcon = new M.core_availability.DeleteIcon(this); this.node.appendChild(deleteIcon.span); // Add the invalid marker (empty). this.node.appendChild(document.createTextNode(' ')); this.node.appendChild(Y.Node.create('<span class="badge bg-warning text-dark"/>')); }; Y.augment(M.core_availability.Item, Y.EventTarget, true, null, {emitFacade: true}); /** * Obtains the value of this condition, which will be serialized into JSON * format and stored in the form. * * @method getValue * @return {Object} JavaScript object containing value of this item */ M.core_availability.Item.prototype.getValue = function() { var value = {'type': this.pluginType}; if (this.plugin) { this.plugin.fillValue(value, this.pluginNode); } return value; }; /** * Checks whether this condition has any errors (incorrect user input). If so, * an error string identifier in the form langfile:langstring should be pushed * into the errors array. * * @method fillErrors * @param {Array} errors Array of errors so far */ M.core_availability.Item.prototype.fillErrors = function(errors) { var before = errors.length; if (this.plugin) { // Pass to plugin. this.plugin.fillErrors(errors, this.pluginNode); } else { // Unknown plugin is an error errors.push('core_availability:item_unknowntype'); } // If any errors were added, add the marker to this item. var errorLabel = this.node.one('> .bg-warning'); if (errors.length !== before && !errorLabel.get('firstChild')) { var errorString = ''; // Fetch the last error code from the array of errors and split using the ':' delimiter. var langString = errors[errors.length - 1].split(':'); var component = langString[0]; var identifier = langString[1]; // If get_string can't find the string, it will return the string in this format. var undefinedString = '[[' + identifier + ',' + component + ']]'; // Get the lang string. errorString = M.util.get_string(identifier, component); if (errorString === undefinedString) { // Use a generic invalid input message when the error lang string cannot be loaded. errorString = M.util.get_string('invalid', 'availability'); } // Show the error string. errorLabel.appendChild(document.createTextNode(errorString)); } else if (errors.length === before && errorLabel.get('firstChild')) { errorLabel.get('firstChild').remove(); } }; /** * Renumbers the item. * * @method renumber * @param {String} number Number to use in heading for this item */ M.core_availability.Item.prototype.renumber = function(number) { // Update heading for item. var headingParams = {number: number}; if (this.plugin) { headingParams.type = M.util.get_string('title', 'availability_' + this.pluginType); } else { headingParams.type = '[' + this.pluginType + ']'; } headingParams.number = number + ':'; var heading = M.util.get_string('itemheading', 'availability', headingParams); this.node.one('> h3').set('innerHTML', heading); this.node.one('> h3').getDOMNode().dataset.restrictionOrder = number ? number : 'root'; }; /** * Focuses something after a new item is added. * * @method focusAfterAdd */ M.core_availability.Item.prototype.focusAfterAdd = function() { this.plugin.focusAfterAdd(this.pluginNode); }; /** * Name of plugin. * * @property pluginType * @type String */ M.core_availability.Item.prototype.pluginType = null; /** * Object representing plugin form controls. * * @property plugin * @type Object */ M.core_availability.Item.prototype.plugin = null; /** * Eye icon for item. * * @property eyeIcon * @type M.core_availability.EyeIcon */ M.core_availability.Item.prototype.eyeIcon = null; /** * HTML node for item. * * @property node * @type Y.Node */ M.core_availability.Item.prototype.node = null; /** * Inner part of node that is owned by plugin. * * @property pluginNode * @type Y.Node */ M.core_availability.Item.prototype.pluginNode = null; /** * Eye icon (to control show/hide of the activity if the user fails a condition). * * There are individual eye icons (show/hide control for a single condition) and * 'all' eye icons (show/hide control that applies to the entire item, whatever * reason it fails for). This is necessary because the individual conditions * don't make sense for OR and AND NOT lists. * * @class M.core_availability.EyeIcon * @constructor * @param {Boolean} individual True if the icon is controlling a single condition * @param {Boolean} shown True if icon is initially in shown state */ M.core_availability.EyeIcon = function(individual, shown) { this.individual = individual; this.span = Y.Node.create('<a class="availability-eye col-form-label" href="#" role="button">'); var icon = Y.Node.create('<img />'); this.span.appendChild(icon); // Set up button text and icon. var suffix = individual ? '_individual' : '_all', setHidden = function() { var hiddenStr = M.util.get_string('hidden' + suffix, 'availability'); icon.set('src', M.util.image_url('i/show', 'core')); icon.set('alt', hiddenStr); this.span.set('title', hiddenStr + ' \u2022 ' + M.util.get_string('show_verb', 'availability')); }, setShown = function() { var shownStr = M.util.get_string('shown' + suffix, 'availability'); icon.set('src', M.util.image_url('i/hide', 'core')); icon.set('alt', shownStr); this.span.set('title', shownStr + ' \u2022 ' + M.util.get_string('hide_verb', 'availability')); }; if (shown) { setShown.call(this); } else { setHidden.call(this); } // Update when button is clicked. var click = function(e) { e.preventDefault(); if (this.isHidden()) { setShown.call(this); } else { setHidden.call(this); } M.core_availability.form.update(); }; this.span.on('click', click, this); this.span.on('key', click, 'up:32', this); this.span.on('key', function(e) { e.preventDefault(); }, 'down:32', this); this.disabledSpan = Y.Node.create('<span class="availability-eye-disabled col-form-label" href="#">'); var disabledIcon = Y.Node.create('<img />'); var disabledStr = M.util.get_string('hidden' + suffix, 'availability'); disabledIcon.set('src', M.util.image_url('i/show', 'core')); disabledIcon.set('alt', disabledStr); this.disabledSpan.set('title', disabledStr + ' \u2022 ' + M.util.get_string('disabled_verb', 'availability')); this.disabledSpan.appendChild(disabledIcon); this.disabledSpan.hide(); }; /** * True if this eye icon is an individual one (see above). * * @property individual * @type Boolean */ M.core_availability.EyeIcon.prototype.individual = false; /** * YUI node for the span that contains this icon. * * @property span * @type Y.Node */ M.core_availability.EyeIcon.prototype.span = null; /** * YUI node for the span that contains the "disabled" state of the icon. * * @property span * @type Y.Node */ M.core_availability.EyeIcon.prototype.disabledSpan = null; /** * Checks the current state of the icon. * * @method isHidden * @return {Boolean} True if this icon is set to 'hidden' */ M.core_availability.EyeIcon.prototype.isHidden = function() { var suffix = this.individual ? '_individual' : '_all', compare = M.util.get_string('hidden' + suffix, 'availability'); return this.span.one('img').get('alt') === compare; }; /** * Checks whether the eye icon is disabled, and a dummy "hidden" icon displayed instead. * * @method isDisabled * @return {Boolean} True if this icon is disabled */ M.core_availability.EyeIcon.prototype.isDisabled = function() { return this.span.hasAttribute('hidden'); }; /** * Locks the state of the icon. * * @method setLocked */ M.core_availability.EyeIcon.prototype.setDisabled = function() { if (!this.isDisabled()) { this.span.hide(); this.disabledSpan.show(); } }; /** * Unlocks the icon so it can be changed. * * @method setUnlocked */ M.core_availability.EyeIcon.prototype.setEnabled = function() { if (this.isDisabled()) { this.span.show(); this.disabledSpan.hide(); } }; /** * Delete icon (to delete an Item or List). * * @class M.core_availability.DeleteIcon * @constructor * @param {M.core_availability.Item|M.core_availability.List} toDelete Thing to delete */ M.core_availability.DeleteIcon = function(toDelete) { this.span = Y.Node.create('<a class="d-inline-block col-form-label availability-delete px-3" href="#" title="' + M.util.get_string('delete', 'moodle') + '" role="button">'); var img = Y.Node.create('<img src="' + M.util.image_url('t/delete', 'core') + '" alt="' + M.util.get_string('delete', 'moodle') + '" />'); this.span.appendChild(img); var click = function(e) { e.preventDefault(); M.core_availability.form.rootList.deleteDescendant(toDelete); M.core_availability.form.rootList.renumber(); }; this.span.on('click', click, this); this.span.on('key', click, 'up:32', this); this.span.on('key', function(e) { e.preventDefault(); }, 'down:32', this); }; /** * YUI node for the span that contains this icon. * * @property span * @type Y.Node */ M.core_availability.DeleteIcon.prototype.span = null; }, '@VERSION@', { "requires": [ "base", "node", "event", "event-delegate", "panel", "moodle-core-notification-dialogue", "json" ] }); yui/build/moodle-core_availability-form/moodle-core_availability-form-min.js 0000644 00000046251 15215712063 0023402 0 ustar 00 YUI.add("moodle-core_availability-form",function(d,i){M.core_availability=M.core_availability||{},M.core_availability.form={plugins:{},field:null,mainDiv:null,rootList:null,idCounter:0,restrictByGroup:null,init:function(i){var t,e,a,l,n;for(t in i)e=i[t],(a=M[e[0]].form).init.apply(a,e);if(this.field=d.one("#id_availabilityconditionsjson"),this.field.setAttribute("aria-hidden","true"),this.mainDiv=d.Node.create('<div class="availability-field fcontainer"></div>'),this.field.insert(this.mainDiv,"after"),n=null,""!==(l=this.field.get("value")))try{n=d.JSON.parse(l)}catch(o){this.field.set("value","")}this.rootList=new M.core_availability.List(n,!0),this.mainDiv.appendChild(this.rootList.node),this.update(),this.rootList.renumber(),this.mainDiv.setAttribute("aria-live","polite"),this.field.ancestor("form").on("submit",function(){this.mainDiv.all("input,textarea,select").set("disabled",!0)},this),this.restrictByGroup=d.one("#restrictbygroup"),this.restrictByGroup&&(this.restrictByGroup.on("click",this.addRestrictByGroup,this),l=d.one("#id_groupmode"),n=d.one("#id_groupingid"),l&&l.on("change",this.updateRestrictByGroup,this),n&&n.on("change",this.updateRestrictByGroup,this),this.updateRestrictByGroup()),this.parent=d.one("#fitem_id_availabilityconditionsjson"),this.parent.removeClass("d-none"),document.getElementById("availabilityconditions-loading").remove()},update:function(){var i=this.rootList.getValue(),t=[];this.rootList.fillErrors(t),0!==t.length&&(i.errors=t),this.field.set("value",d.JSON.stringify(i)),this.updateRestrictByGroup()},updateRestrictByGroup:function(){var i,t,e,a;this.restrictByGroup&&("&"!==this.rootList.getValue().op||(this.rootList.hasItemOfType("group")||this.rootList.hasItemOfType("grouping"))?this.restrictByGroup.set("disabled",!0):(i=d.one("#id_groupmode"),t=d.one("#id_groupingid"),e=1===Number(this.restrictByGroup.getData("groupavailability")),a=1===Number(this.restrictByGroup.getData("groupingavailability")),i&&0!==Number(i.get("value"))&&e||t&&0!==Number(t.get("value"))&&a?this.restrictByGroup.set("disabled",!1):this.restrictByGroup.set("disabled",!0)))},addRestrictByGroup:function(i){var t,e,a,l;i.preventDefault(),i=d.one("#id_groupmode"),t=d.one("#id_groupingid"),e=1===Number(this.restrictByGroup.getData("groupavailability")),a=1===Number(this.restrictByGroup.getData("groupingavailability")),t&&0!==Number(t.get("value"))&&a?l=new M.core_availability.Item({type:"grouping",id:Number(t.get("value"))},!0):i&&e&&(l=new M.core_availability.Item({type:"group"},!0)),null!==l&&(this.rootList.addChild(l),this.update(),this.rootList.renumber(),this.rootList.updateHtml())}},M.core_availability.plugin={allowAdd:!1,init:function(i,t,e,a){i=i.replace(/^availability_/,"");this.allowAdd=t,(M.core_availability.form.plugins[i]=this).initInner.apply(this,e),this.displayMode=a},initInner:function(){},getNode:function(){throw"getNode not implemented"},fillValue:function(){throw"fillValue not implemented"},fillErrors:function(){},focusAfterAdd:function(i){i.one("input:not([disabled]),select:not([disabled])").focus()}},M.core_availability.List=function(i,t,e){var a,l,n;if(this.children=[],t!==undefined&&(this.root=t),this.node=d.Node.create('<div class="availability-list"><h3 class="accesshide"></h3><div class="availability-inner"><div class="availability-header mb-1"><span>'+M.util.get_string("listheader_sign_before","availability")+'</span> <label><span class="accesshide">'+M.util.get_string("label_sign","availability")+' </span><select class="availability-neg custom-select mx-1" title="'+M.util.get_string("label_sign","availability")+'"><option value="">'+M.util.get_string("listheader_sign_pos","availability")+'</option><option value="!">'+M.util.get_string("listheader_sign_neg","availability")+'</option></select></label> <span class="availability-single">'+M.util.get_string("listheader_single","availability")+'</span><span class="availability-multi">'+M.util.get_string("listheader_multi_before","availability")+' <label><span class="accesshide">'+M.util.get_string("label_multi","availability")+' </span><select class="availability-op custom-select mx-1" title="'+M.util.get_string("label_multi","availability")+'"><option value="&">'+M.util.get_string("listheader_multi_and","availability")+'</option><option value="|">'+M.util.get_string("listheader_multi_or","availability")+"</option></select></label> "+M.util.get_string("listheader_multi_after","availability")+'</span></div><div class="availability-children"></div><div class="availability-none"><span class="px-3">'+M.util.get_string("none","moodle")+'</span></div><div class="clearfix mt-1"></div><div class="availability-button"></div></div><div class="clearfix"></div></div>'),t||this.node.addClass("availability-childlist d-sm-flex align-items-center"),this.inner=this.node.one("> .availability-inner"),a=!0,t?(i&&i.show!==undefined&&(a=i.show),this.eyeIcon=new M.core_availability.EyeIcon(!1,a),this.node.one(".availability-header").get("firstChild").insert(this.eyeIcon.span,"before"),this.node.one(".availability-header").get("firstChild").insert(this.eyeIcon.disabledSpan,"before"),this.on("availability:privateRuleSet",function(i){i.target.getDOMNode().dataset["private"]=!0,this.updatePrivateStatus()}),this.on("availability:privateRuleUnset",function(i){delete i.target.getDOMNode().dataset["private"],this.updatePrivateStatus()})):e&&(i&&i.showc!==undefined&&(a=i.showc),this.eyeIcon=new M.core_availability.EyeIcon(!1,a),this.inner.insert(this.eyeIcon.span,"before"),this.inner.insert(this.eyeIcon.disabledSpan,"before")),t||(e=new M.core_availability.DeleteIcon(this),(a=this.node.one(".availability-none")).appendChild(document.createTextNode(" ")),a.appendChild(e.span),a.appendChild(d.Node.create('<span class="mt-1 badge bg-warning text-dark">'+M.util.get_string("invalid","availability")+"</span>"))),(t=d.Node.create('<button type="button" class="btn btn-secondary mt-1">'+M.util.get_string("addrestriction","availability")+"</button>") ).on("click",function(){this.clickAdd()},this),this.node.one("div.availability-button").appendChild(t),i){switch(i.op){case"&":case"|":this.node.one(".availability-neg").set("value","");break;case"!&":case"!|":this.node.one(".availability-neg").set("value","!")}switch(i.op){case"&":case"!&":this.node.one(".availability-op").set("value","&");break;case"|":case"!|":this.node.one(".availability-op").set("value","|")}for(l=0;l<i.c.length;l++)n=i.c[l],this.root&&i&&i.showc!==undefined&&(n.showc=i.showc[l]),n=n.type!==undefined?new M.core_availability.Item(n,this.root):new M.core_availability.List(n,!1,this.root),this.addChild(n)}this.node.one(".availability-neg").on("change",function(){M.util.js_pending("availability-neg-change"),M.core_availability.form.update(),this.updateHtml(),M.util.js_complete("availability-neg-change")},this),this.node.one(".availability-op").on("change",function(){M.util.js_pending("availability-op-change"),M.core_availability.form.update(),this.updateHtml(),M.util.js_complete("availability-op-change")},this),this.updateHtml()},d.augment(M.core_availability.List,d.EventTarget,!0,null,{emitFacade:!0}),M.core_availability.List.prototype.addChild=function(i){0<this.children.length&&this.inner.one(".availability-children").appendChild(d.Node.create('<div class="availability-connector"><span class="label"></span></div>')),this.children.push(i),i.addTarget(this),this.inner.one(".availability-children").appendChild(i.node)},M.core_availability.List.prototype.focusAfterAdd=function(){this.inner.one("button").focus()},M.core_availability.List.prototype.isIndividualShowIcons=function(){var i,t;if(!this.root)throw"Can only call this on root list";return i="!"===this.node.one(".availability-neg").get("value"),t="|"===this.node.one(".availability-op").get("value"),!i&&!t||i&&t},M.core_availability.List.prototype.renumber=function(i){var t,e={count:this.children.length},a=i===undefined?e.number="":(e.number=i+":",i+"."),e=M.util.get_string("setheading","availability",e);for(this.node.one("> h3").set("innerHTML",e),this.node.one("> h3").getDOMNode().dataset.restrictionOrder=i||"root",t=0;t<this.children.length;t++)this.children[t].renumber(a+(t+1))},M.core_availability.List.prototype.updateHtml=function(){var i,t,e,a;if(0<this.children.length?(this.inner.one("> .availability-children").removeAttribute("aria-hidden"),this.inner.one("> .availability-none").setAttribute("aria-hidden","true"),this.inner.one("> .availability-header").removeAttribute("aria-hidden"),1<this.children.length?(this.inner.one(".availability-single").setAttribute("aria-hidden","true"),this.inner.one(".availability-multi").removeAttribute("aria-hidden")):(this.inner.one(".availability-single").removeAttribute("aria-hidden"),this.inner.one(".availability-multi").setAttribute("aria-hidden","true"))):(this.inner.one("> .availability-children").setAttribute("aria-hidden","true"),this.inner.one("> .availability-none").removeAttribute("aria-hidden"),this.inner.one("> .availability-header").setAttribute("aria-hidden","true")),this.root){for(i=this.isIndividualShowIcons(),t=0;t<this.children.length;t++)e=this.children[t],i?(e.eyeIcon.span.removeAttribute("aria-hidden"),e.eyeIcon.disabledSpan.removeAttribute("aria-hidden")):(e.eyeIcon.span.setAttribute("aria-hidden","true"),e.eyeIcon.disabledSpan.setAttribute("aria-hidden","true"));i?(this.eyeIcon.span.setAttribute("aria-hidden","true"),this.eyeIcon.disabledSpan.setAttribute("aria-hidden","true")):(this.eyeIcon.span.removeAttribute("aria-hidden"),this.eyeIcon.disabledSpan.removeAttribute("aria-hidden")),this.updatePrivateStatus()}a="&"===this.inner.one(".availability-op").get("value")?M.util.get_string("and","availability"):M.util.get_string("or","availability"),this.inner.all("> .availability-children > .availability-connector span.label").each(function(i){i.set("innerHTML",a)})},M.core_availability.List.prototype.deleteDescendant=function(i){for(var t,e,a=0;a<this.children.length;a++){if((t=this.children[a])===i)return this.children.splice(a,1),e=t.node,0<this.children.length&&(e.previous(".availability-connector")?e.previous(".availability-connector"):e.next(".availability-connector")).remove(),this.inner.one("> .availability-children").removeChild(e),M.core_availability.form.update(),this.updateHtml(),this.inner.one("> .availability-button").one("button").focus(),!0;if(t instanceof M.core_availability.List&&t.deleteDescendant(i))return!0}return!1},M.core_availability.List.prototype.clickAdd=function(){var i,t,e,a,l,n=d.Node.create('<div><ul class="list-unstyled container-fluid"></ul><div class="availability-buttons mdl-align"><button type="button" class="btn btn-secondary">'+M.util.get_string("cancel","moodle")+"</button></div></div>"),o=n.one("button"),s={dialog:null},r=n.one("ul");for(l in M.core_availability.form.plugins)M.core_availability.form.plugins[l].allowAdd&&(i=d.Node.create('<li class="clearfix row"></li>'),(e=d.Node.create('<div class="col-6"><button type="button" class="btn btn-secondary w-100"id="'+(t="availability_addrestriction_"+l)+'">'+M.util.get_string("title","availability_"+l)+"</button></div>")).on("click",this.getAddHandler(l,s),this),i.appendChild(e),a=d.Node.create('<div class="col-6"><label for="'+t+'">'+M.util.get_string("description","availability_"+l)+"</label></div>"),i.appendChild(a),r.appendChild(i));i=d.Node.create('<li class="clearfix row"></li>'),(e=d.Node.create('<div class="col-6"><button type="button" class="btn btn-secondary w-100"id="'+(t="availability_addrestriction_list_")+'">'+M.util.get_string("condition_group","availability")+"</button></div>")).on("click",this.getAddHandler(null,s),this),i.appendChild(e),a=d.Node.create('<div class="col-6"><label for="'+t+'">'+M.util.get_string("condition_group_info","availability")+"</label></div>"),i.appendChild(a),r.appendChild(i),n={headerContent:M.util.get_string("addrestriction","availability"),bodyContent:n,additionalBaseClass:"availability-dialogue",draggable:!0,modal:!0, closeButton:!1,width:"450px"},s.dialog=new M.core.dialogue(n),s.dialog.show(),o.on("click",function(){s.dialog.hide(),this.inner.one("> .availability-button").one("button").focus()},this)},M.core_availability.List.prototype.getAddHandler=function(t,e){return function(){var i=!0;t&&M.core_availability.form.plugins[t].displayMode&&(i=!1),i=t?new M.core_availability.Item({type:t,creating:!0,showc:i},this.root):new M.core_availability.List({c:[],showc:i},!1,this.root),this.addChild(i),M.core_availability.form.update(),M.core_availability.form.rootList.renumber(),this.updateHtml(),e.dialog.hide(),i.focusAfterAdd()}},M.core_availability.List.prototype.getValue=function(){var i,t,e={};for(e.op=this.node.one(".availability-neg").get("value")+this.node.one(".availability-op").get("value"),e.c=[],i=0;i<this.children.length;i++)e.c.push(this.children[i].getValue());if(this.root)if(this.isIndividualShowIcons())for(e.showc=[],i=0;i<this.children.length;i++)t=this.children[i].eyeIcon,e.showc.push(!t.isHidden()&&!t.isDisabled());else e.show=!this.eyeIcon.isHidden()&&!this.eyeIcon.isDisabled();return e},M.core_availability.List.prototype.fillErrors=function(i){0!==this.children.length||this.root||i.push("availability:error_list_nochildren");for(var t=0;t<this.children.length;t++)this.children[t].fillErrors(i)},M.core_availability.List.prototype.hasItemOfType=function(i){for(var t,e=0;e<this.children.length;e++)if((t=this.children[e])instanceof M.core_availability.List){if(t.hasItemOfType(i))return!0}else if(t.pluginType===i)return!0;return!1},M.core_availability.List.prototype.getEyeIcons=function(){var i,t,e=[];for(e.push(this.eyeIcon),i=0;i<this.children.length;i++)null!==(t=this.children[i]).eyeIcon&&e.push(t.eyeIcon),t instanceof M.core_availability.List&&e.concat(t.getEyeIcons());return e},M.core_availability.List.prototype.updatePrivateStatus=function(){var i,t,e,a;if(!this.root)throw new Error("Can only call this on root list");for(i=!this.node.all("[data-private]").isEmpty(),e=0,a=(t=this.getEyeIcons()).length;e<a;e++)i?t[e].setDisabled():t[e].setEnabled()},M.core_availability.List.prototype.eyeIcon=null,M.core_availability.List.prototype.root=!1,M.core_availability.List.prototype.children=null,M.core_availability.List.prototype.node=null,M.core_availability.List.prototype.inner=null,M.core_availability.Item=function(i,t){this.pluginType=i.type,M.core_availability.form.plugins[i.type]===undefined?(this.plugin=null,this.pluginNode=d.Node.create('<div class="availability-warning">'+M.util.get_string("missingplugin","availability")+"</div>")):(this.plugin=M.core_availability.form.plugins[i.type],this.pluginNode=this.plugin.getNode(i),this.pluginNode.addClass("availability_"+i.type)),d.augment(this.pluginNode,d.EventTarget,!0,null,{emitFacade:!0}),this.pluginNode.addTarget(this),this.node=d.Node.create('<div class="availability-item d-sm-flex align-items-center"><h3 class="accesshide"></h3></div>'),t&&(t=!0,i.showc!==undefined&&(t=i.showc),this.eyeIcon=new M.core_availability.EyeIcon(!0,t),this.node.appendChild(this.eyeIcon.span),this.node.appendChild(this.eyeIcon.disabledSpan)),this.pluginNode.addClass("availability-plugincontrols"),this.node.appendChild(this.pluginNode),i=new M.core_availability.DeleteIcon(this),this.node.appendChild(i.span),this.node.appendChild(document.createTextNode(" ")),this.node.appendChild(d.Node.create('<span class="badge bg-warning text-dark"/>'))},d.augment(M.core_availability.Item,d.EventTarget,!0,null,{emitFacade:!0}),M.core_availability.Item.prototype.getValue=function(){var i={type:this.pluginType};return this.plugin&&this.plugin.fillValue(i,this.pluginNode),i},M.core_availability.Item.prototype.fillErrors=function(i){var t,e,a,l=i.length;this.plugin?this.plugin.fillErrors(i,this.pluginNode):i.push("core_availability:item_unknowntype"),t=this.node.one("> .bg-warning"),i.length===l||t.get("firstChild")?i.length===l&&t.get("firstChild")&&t.get("firstChild").remove():(l="",e=(i=i[i.length-1].split(":"))[0],a="[["+(i=i[1])+","+e+"]]",(l=M.util.get_string(i,e))===a&&(l=M.util.get_string("invalid","availability")),t.appendChild(document.createTextNode(l)))},M.core_availability.Item.prototype.renumber=function(i){var t={number:i};this.plugin?t.type=M.util.get_string("title","availability_"+this.pluginType):t.type="["+this.pluginType+"]",t.number=i+":",t=M.util.get_string("itemheading","availability",t),this.node.one("> h3").set("innerHTML",t),this.node.one("> h3").getDOMNode().dataset.restrictionOrder=i||"root"},M.core_availability.Item.prototype.focusAfterAdd=function(){this.plugin.focusAfterAdd(this.pluginNode)},M.core_availability.Item.prototype.pluginType=null,M.core_availability.Item.prototype.plugin=null,M.core_availability.Item.prototype.eyeIcon=null,M.core_availability.Item.prototype.node=null,M.core_availability.Item.prototype.pluginNode=null,M.core_availability.EyeIcon=function(i,t){var e,a,l,n;this.individual=i,this.span=d.Node.create('<a class="availability-eye col-form-label" href="#" role="button">'),e=d.Node.create("<img />"),this.span.appendChild(e),a=i?"_individual":"_all",l=function(){var i=M.util.get_string("hidden"+a,"availability");e.set("src",M.util.image_url("i/show","core")),e.set("alt",i),this.span.set("title",i+" • "+M.util.get_string("show_verb","availability"))},n=function(){var i=M.util.get_string("shown"+a,"availability");e.set("src",M.util.image_url("i/hide","core")),e.set("alt",i),this.span.set("title",i+" • "+M.util.get_string("hide_verb","availability"))},(t?n:l).call(this),this.span.on("click",i=function(i){i.preventDefault(),(this.isHidden()?n:l).call(this),M.core_availability.form.update()},this),this.span.on("key",i,"up:32",this),this.span.on("key",function(i){i.preventDefault()},"down:32",this),this.disabledSpan=d.Node.create('<span class="availability-eye-disabled col-form-label" href="#">'),t=d.Node.create("<img />"),i=M.util.get_string("hidden"+a,"availability"),t.set("src",M.util.image_url("i/show","core")),t.set("alt",i), this.disabledSpan.set("title",i+" • "+M.util.get_string("disabled_verb","availability")),this.disabledSpan.appendChild(t),this.disabledSpan.hide()},M.core_availability.EyeIcon.prototype.individual=!1,M.core_availability.EyeIcon.prototype.span=null,M.core_availability.EyeIcon.prototype.disabledSpan=null,M.core_availability.EyeIcon.prototype.isHidden=function(){var i=this.individual?"_individual":"_all",i=M.util.get_string("hidden"+i,"availability");return this.span.one("img").get("alt")===i},M.core_availability.EyeIcon.prototype.isDisabled=function(){return this.span.hasAttribute("hidden")},M.core_availability.EyeIcon.prototype.setDisabled=function(){this.isDisabled()||(this.span.hide(),this.disabledSpan.show())},M.core_availability.EyeIcon.prototype.setEnabled=function(){this.isDisabled()&&(this.span.show(),this.disabledSpan.hide())},M.core_availability.DeleteIcon=function(t){var i;this.span=d.Node.create('<a class="d-inline-block col-form-label availability-delete px-3" href="#" title="'+M.util.get_string("delete","moodle")+'" role="button">'),i=d.Node.create('<img src="'+M.util.image_url("t/delete","core")+'" alt="'+M.util.get_string("delete","moodle")+'" />'),this.span.appendChild(i),this.span.on("click",i=function(i){i.preventDefault(),M.core_availability.form.rootList.deleteDescendant(t),M.core_availability.form.rootList.renumber()},this),this.span.on("key",i,"up:32",this),this.span.on("key",function(i){i.preventDefault()},"down:32",this)},M.core_availability.DeleteIcon.prototype.span=null},"@VERSION@",{requires:["base","node","event","event-delegate","panel","moodle-core-notification-dialogue","json"]}); upgrade.txt 0000644 00000005404 15215712063 0006741 0 ustar 00 === 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This files describes API changes in /availability/*. The information here is intended only for developers. === 4.3 === * The `availability_info templatable` no longer exports 'showmore' information, and any component rendering avaialable_info should handle showmore/showless behaviour by itself. That means that 'showmorelink', 'hidden' and 'abbreviate' won't be exported for template in the root element neither its child items, and `core_availability/availability_more` module has beed deprecated === 4.2 === * The form for editing availability conditions now supports "private" conditions, that should not be shown to users. If a "private" condition is selected (as determined by the condition plugin), the whole ruleset will be forcibly set to "Hidden entirely if student doesn't meet the conditions". * The group condition plugin will now treat any rule containing a group with a visibility setting other than `visibility:ALL` as private. === 4.0 === * Method render_core_availability_multiple_messages() is deprecated. Please use core_availability\\output\\multiple_messages The new rendereable will produce output with a 'more' link when there is lots of availability information. * There were existing restrictions on what condition plugins can do in the get_description method (for example they mustn't call format_string), which were not well documented. New functions description_cm_name(), description_format_string(), description_callback() can be used so that condition plugins to behave correctly in all situations. === 3.2 === * Condition plugins must replace the CSS selector "#fitem_id_availabilityconditionsjson" with ".availability-field". This selector is often used in your plugin's yui/src/form/js/form.js file. === 2.9 === * Condition plugins can now implement a new include_after_restore function to indicate that they should be removed during the restore process. (This is implemented so that group and grouping conditions are removed if groups are not restored.) === 2.8 === * There is a new API function in the info_module/info_section objects (and related functions in internal API): get_user_list_sql. This returns SQL code that does roughly the same as filter_user_list to return a list of users who should be shown as having access to the module or section. * Any third-party availability plugins which return true to is_applied_to_user_lists (and therefore previously implemented filter_user_list) should now also implement get_user_list_sql. If not implemented, a debugging warning will occur when anybody calls get_user_list_sql if the affected plugin is in use, and that user list will not be filtered by the plugin.
| ver. 1.4 |
Github
|
.
| PHP 8.3.30 | Generation time: 0.34 |
proxy
|
phpinfo
|
Settings