MDL-44070 Conditional availability enhancements (2): subsystem, API

This commit defines the new /availability root folder, with
/availability/classes, /availability/tests, and
/availability/condition where the condition plugins will live.
Condition plugin prefix is availability_, e.g. availability_date.

Rationale for this organisation:

1. I was originally going to put this in /lib/availability but
   it has been pointed out that putting even more junk in lib
   is probably bad.
2. 'availability' and 'condition' are the two names used in code
   to refer to this system ($CFG->enableavailability).
3. The prefix has to be short enough to allow database tables
   (although in practice I assume that condition plugins will not
   normally contain database tables).

The new API includes a Boolean tree structure that controls the
availability of an item.

AMOS BEGIN
 CPY [availabilityconditions,core_condition],[restrictaccess,core_availability]
 CPY [enableavailability,core_condition],[enableavailability,core_availability]
 CPY [configenableavailability,core_condition],[enableavailability_desc,core_availability]
AMOS END
This commit is contained in:
sam marshall 2014-03-26 12:02:30 +00:00
parent 8e97006ad0
commit d3db4b037c
31 changed files with 5479 additions and 1 deletions

View File

@ -0,0 +1,126 @@
<?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/>.
/**
* Provides an overview of installed availability conditions.
*
* You can also enable/disable them from this screen.
*
* @package tool_availabilityconditions
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../../config.php');
require_once($CFG->libdir . '/adminlib.php');
require_once($CFG->libdir . '/tablelib.php');
admin_externalpage_setup('manageavailability');
// Get sorted list of all availability condition plugins.
$plugins = array();
foreach (core_component::get_plugin_list('availability') as $plugin => $plugindir) {
if (get_string_manager()->string_exists('pluginname', 'availability_' . $plugin)) {
$strpluginname = get_string('pluginname', 'availability_' . $plugin);
} else {
$strpluginname = $plugin;
}
$plugins[$plugin] = $strpluginname;
}
core_collator::asort($plugins);
// Do plugin actions.
$pageurl = new moodle_url('/' . $CFG->admin . '/availabilityconditions.php');
if (($plugin = optional_param('plugin', '', PARAM_PLUGIN))) {
require_sesskey();
if (!array_key_exists($plugin, $plugins)) {
print_error('invalidcomponent', 'error', $pageurl);
}
$action = required_param('action', PARAM_ALPHA);
switch ($action) {
case 'hide' :
set_config('disabled', 1, 'availability_' . $plugin);
break;
case 'show' :
unset_config('disabled', 'availability_' . $plugin);
break;
}
core_plugin_manager::reset_caches();
// Always redirect back after an action.
redirect($pageurl);
}
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('manageplugins', 'availability'));
// Show a table of installed availability conditions.
$table = new flexible_table('availabilityconditions_administration_table');
$table->define_columns(array('name', 'version', 'enable'));
$table->define_headers(array(get_string('plugin'),
get_string('version'), get_string('hide') . '/' . get_string('show')));
$table->define_baseurl($PAGE->url);
$table->set_attribute('id', 'availabilityconditions');
$table->set_attribute('class', 'admintable generaltable');
$table->setup();
$enabledlist = core\plugininfo\availability::get_enabled_plugins();
foreach ($plugins as $plugin => $name) {
// Get version or ? if unknown.
$version = get_config('availability_' . $plugin);
if (!empty($version->version)) {
$version = $version->version;
} else {
$version = '?';
}
// Get enabled status and use to grey out name if necessary.
$enabled = in_array($plugin, $enabledlist);
if ($enabled) {
$enabledaction = 'hide';
$enabledstr = get_string('hide');
$class = '';
} else {
$enabledaction = 'show';
$enabledstr = get_string('show');
$class = 'dimmed_text';
}
$namespan = html_writer::span($name, $class);
// Make enable control. This is a POST request (using a form control rather
// than just a link) because it makes a database change.
$targeturl = new moodle_url('availabilityconditions.php', array(
'plugin' => $plugin, 'action' => $enabledaction, 'sesskey' => sesskey()));
$enablecontrol = html_writer::tag('form', html_writer::div(
html_writer::empty_tag('input', array('type' => 'hidden',
'name' => 'sesskey', 'value' => sesskey())) .
html_writer::empty_tag('input', array('type' => 'hidden',
'name' => 'plugin', 'value' => $plugin)) .
html_writer::empty_tag('input', array('type' => 'hidden',
'name' => 'action', 'value' => $enabledaction)) .
html_writer::empty_tag('input', array('type' => 'image',
'src' => $OUTPUT->pix_url('t/' . $enabledaction), 'alt' => $enabledstr,
'title' => $enabledstr))
), array(
'method' => 'post', 'action' => 'availabilityconditions.php'));
$table->add_data(array($namespan, $version, $enablecontrol));
}
$table->print_html();
echo $OUTPUT->footer();

View File

@ -0,0 +1,26 @@
<?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/>.
/**
* Strings for availability conditions options.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['manageplugins'] = 'Manage restrictions';
$string['pluginname'] = 'Availability condition management';

View File

@ -0,0 +1,37 @@
<?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/>.
/**
* Adds settings links to admin tree.
*
* @package tool_availabilityconditions
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
if ($hassiteconfig) {
$ADMIN->add('modules', new admin_category('availabilitysettings',
new lang_string('type_availability_plural', 'plugin')));
$ADMIN->add('availabilitysettings', new admin_externalpage('manageavailability',
new lang_string('manageplugins', 'tool_availabilityconditions'),
$CFG->wwwroot . '/' . $CFG->admin . '/tool/availabilityconditions/'));
foreach (core_plugin_manager::instance()->get_plugins_of_type('availability') as $plugin) {
/** @var \core\plugininfo\format $plugin */
$plugin->load_settings($ADMIN, 'availabilitysettings', $hassiteconfig);
}
}

View File

@ -0,0 +1,29 @@
<?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
*
* @package tool_availabilityconditions
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2014012900;
$plugin->requires = 2014040401;
$plugin->component = 'tool_availabilityconditions';

View File

@ -0,0 +1,77 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Used while evaluating conditions in bulk.
*
* This object caches get_users_by_capability results in case they are needed
* by multiple conditions.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_availability;
defined('MOODLE_INTERNAL') || die();
/**
* Used while evaluating conditions in bulk.
*
* This object caches get_users_by_capability results in case they are needed
* by multiple conditions.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class capability_checker {
/** @var \context Course or module context */
protected $context;
/** @var array Associative array of capability => result */
protected $cache = array();
/**
* Constructs for given context.
*
* @param \context $context Context
*/
public function __construct(\context $context) {
$this->context = $context;
}
/**
* Gets users on course who have the specified capability. Returns an array
* of user objects which only contain the 'id' field. If the same capability
* has already been checked (e.g. by another condition) then a cached
* result will be used.
*
* More fields are not necessary because this code is only used to filter
* users from an existing list.
*
* @param string $capability Required capability
* @return array Associative array of user id => objects containing only id
*/
public function get_users_by_capability($capability) {
if (!array_key_exists($capability, $this->cache)) {
$this->cache[$capability] = get_users_by_capability(
$this->context, $capability, 'u.id');
}
return $this->cache[$capability];
}
}

View File

@ -0,0 +1,190 @@
<?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
*/
public abstract 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));
}
/**
* 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.
*
* The special string <AVAILABILITY_CMNAME_123/> can be returned, where
* 123 is any number. It will be replaced with the correctly-formatted
* name for that activity.
*
* @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 abstract 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.
*
* The special string <AVAILABILITY_CMNAME_123/> can be returned, where
* 123 is any number. It will be replaced with the correctly-formatted
* name for that activity.
*
* @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
*/
protected abstract 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;
}
}

View File

@ -0,0 +1,209 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Class with front-end (editing form) functionality.
*
* This is a base class of a class implemented by each component, and also has
* static methods.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_availability;
defined('MOODLE_INTERNAL') || die();
/**
* Class with front-end (editing form) functionality.
*
* This is a base class of a class implemented by each component, and also has
* static methods.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class frontend {
/**
* Decides whether this plugin should be available in a given course. The
* plugin can do this depending on course or system settings.
*
* Default returns true.
*
* @param stdClass $course Course object
* @param \cm_info $cm Course-module currently being edited (null if none)
* @param \section_info $section Section currently being edited (null if none)
*/
protected function allow_add($course, \cm_info $cm = null,
\section_info $section = null) {
return true;
}
/**
* Gets a list of string identifiers (in the plugin's language file) that
* are required in JavaScript for this plugin. The default returns nothing.
*
* You do not need to include the 'title' string (which is used by core) as
* this is automatically added.
*
* @return array Array of required string identifiers
*/
protected function get_javascript_strings() {
return array();
}
/**
* Gets additional parameters for the plugin's initInner function.
*
* Default returns no parameters.
*
* @param stdClass $course Course object
* @param \cm_info $cm Course-module currently being edited (null if none)
* @param \section_info $section Section currently being edited (null if none)
* @return array Array of parameters for the JavaScript function
*/
protected function get_javascript_init_params($course, \cm_info $cm = null,
\section_info $section = null) {
return array();
}
/**
* Gets the Frankenstyle component name for this plugin.
*
* @return string The component name for this plugin
*/
protected function get_component() {
return preg_replace('~^(availability_.*?)\\\\frontend$~', '$1', get_class($this));
}
/**
* Includes JavaScript for the main system and all plugins.
*
* @param stdClass $course Course object
* @param \cm_info $cm Course-module currently being edited (null if none)
* @param \section_info $section Section currently being edited (null if none)
*/
public static function include_all_javascript($course, \cm_info $cm = null,
\section_info $section = null) {
global $PAGE;
// Prepare array of required YUI modules. It is bad for performance to
// make multiple yui_module calls, so we group all the plugin modules
// into a single call (the main init function will call init for each
// plugin).
$modules = array('moodle-core_availability-form', 'base', 'node',
'panel', 'moodle-core-notification-dialogue', 'json');
// Work out JS to include for all components.
$pluginmanager = \core_plugin_manager::instance();
$enabled = $pluginmanager->get_enabled_plugins('availability');
$componentparams = new \stdClass();
foreach ($enabled as $plugin => $info) {
// Create plugin front-end object.
$class = '\availability_' . $plugin . '\frontend';
$frontend = new $class();
// Add to array of required YUI modules.
$component = $frontend->get_component();
$modules[] = 'moodle-' . $component . '-form';
// Get parameters for this plugin.
$componentparams->{$plugin} = array($component,
$frontend->allow_add($course, $cm, $section),
$frontend->get_javascript_init_params($course, $cm, $section));
// Include strings for this plugin.
$identifiers = $frontend->get_javascript_strings();
$identifiers[] = 'title';
$identifiers[] = 'description';
$PAGE->requires->strings_for_js($identifiers, $component);
}
// Include all JS (in one call). The init function runs on DOM ready.
$PAGE->requires->yui_module($modules,
'M.core_availability.form.init', array($componentparams), null, true);
// Include main strings.
$PAGE->requires->strings_for_js(array('none', 'cancel', 'delete', 'choosedots'),
'moodle');
$PAGE->requires->strings_for_js(array('addrestriction', 'invalid',
'listheader_sign_before', 'listheader_sign_pos',
'listheader_sign_neg', 'listheader_single',
'listheader_multi_after', 'listheader_multi_before',
'listheader_multi_or', 'listheader_multi_and',
'unknowncondition', 'hide_verb', 'hidden_individual',
'show_verb', 'shown_individual', 'hidden_all', 'shown_all',
'condition_group', 'condition_group_info', 'and', 'or',
'label_multi', 'label_sign', 'setheading', 'itemheading',
'missingplugin'),
'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;
}
}

View File

@ -0,0 +1,628 @@
<?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;
/**
* 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
* @throws coding_exception If data is not valid JSON format
*/
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
*/
public abstract 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) {
// We catch the message because it causes fatal problems in most of
// the GUI if this exception gets thrown (you can't edit the
// activity to fix it). Obviously it should never happen anyway, but
// just in case.
debugging('Error processing availability data for &lsquo;' .
$this->get_thing_name() . '&rsquo;: ' . s($e->a), DEBUG_DEVELOPER);
$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() {
if (is_null($this->availability)) {
return true;
} else {
return $this->get_availability_tree()->is_available_for_all();
}
}
/**
* 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) {
// Again we catch the message to avoid problems in GUI.
debugging('Error processing availability data for &lsquo;' .
$this->get_thing_name() . '&rsquo;: ' . s($e->a), DEBUG_DEVELOPER);
return false;
}
}
/**
* 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
*/
public function update_after_restore($restoreid, $courseid, \base_logger $logger) {
$tree = $this->get_availability_tree();
$changed = $tree->update_after_restore($restoreid, $courseid, $logger,
$this->get_thing_name());
if ($changed) {
// Save modified data.
$structure = $tree->save();
$this->set_in_database(json_encode($structure));
}
}
/**
* 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
*/
protected abstract function get_thing_name();
/**
* Stores an updated availability tree JSON structure into the relevant
* database table.
*
* @param string $availabilty New JSON value
*/
protected abstract 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).
*
* If you enable $modgroupmembersonly, then it also supports the
* groupmembersonly field for modules. This is off by default because
* we are not yet moving the groupmembersonly option into this new API.
*
* @param stdClass $rec Object possibly containing legacy fields
* @param bool $section True if this is a section
* @param bool $modgroupmembersonly True if groupmembersonly is converted for mods
* @return string|null New availability value or null if none
*/
public static function convert_legacy_fields($rec, $section, $modgroupmembersonly = false) {
// Do nothing if the fields are not set.
if (empty($rec->availablefrom) && empty($rec->availableuntil) &&
(!$modgroupmembersonly || 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 (($modgroupmembersonly && !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());
$this->modinfo = get_fast_modinfo($this->get_course());
$result = $tree->filter_user_list($users, false, $this, $checker);
$this->modinfo = null;
return $result;
}
/**
* 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 string $info Info string
* @param int|stdClass $courseorid
* @return Correctly formatted info string
*/
public static function format_info($info, $courseorid) {
// 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]);
return format_string($cm->name, true, array('context' => $context));
}, $info);
return $info;
}
/**
* Used in course/lib.php because we need to disable the completion tickbox
* JS (using the non-JS version instead, which causes a page reload) if a
* completion tickbox value may affect a conditional activity.
*
* @param stdClass $course Moodle course object
* @param int $cmid Course-module id
* @return bool True if this is used in a condition, false otherwise
*/
public static function completion_value_used($course, $cmid) {
// Access all plugins. Normally only the completion plugin is going
// to affect this value, but it's potentially possible that some other
// plugin could also rely on the completion plugin.
$pluginmanager = \core_plugin_manager::instance();
$enabled = $pluginmanager->get_enabled_plugins('availability');
$componentparams = new \stdClass();
foreach ($enabled as $plugin => $info) {
// Use the static method.
$class = '\availability_' . $plugin . '\condition';
if ($class::completion_value_used($course, $cmid)) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,192 @@
<?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 '<AVAILABILITY_CMNAME_' . $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);
}
/**
* Checks if an activity is visible to the given user.
*
* Unlike other checks in the availability system, this check includes the
* $cm->visible flag and also (if enabled) the groupmembersonly feature.
* 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), '*', MUST_EXIST);
}
// Check the groupmembersonly feature.
if (!groups_course_module_visible($cm, $userid)) {
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);
return $modinfo->get_cm($cm->id)->uservisible;
}
}

View File

@ -0,0 +1,74 @@
<?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(\get_course($section->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 set_in_database($availability) {
global $DB;
$DB->set_field('course_sections', 'availability', $availability,
array('id' => $this->section->id));
}
/**
* Gets the section object. Intended for use by conditions.
*
* @return section_info Section
*/
public function get_section() {
return $this->section;
}
}

View File

@ -0,0 +1,95 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Class represents the result of an availability check for the user.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_availability;
defined('MOODLE_INTERNAL') || die();
/**
* Class represents the result of an availability check for the user.
*
* You can pass an object of this class to tree::get_result_information to
* display suitable student information about the result.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class result {
/** @var bool True if the item is available */
protected $available;
/** @var tree_node[] Array of nodes to display in failure information (node=>node). */
protected $shownodes = array();
/**
* Constructs result.
*
* @param bool $available True if available
* @param tree_node $node Node if failed & should be displayed
* @param result[] $failedchildren Array of children who failed too
*/
public function __construct($available, tree_node $node = null,
array $failedchildren = array()) {
$this->available = $available;
if (!$available) {
if ($node) {
$this->shownodes[spl_object_hash($node)] = $node;
}
foreach ($failedchildren as $child) {
foreach ($child->shownodes as $key => $node) {
$this->shownodes[$key] = $node;
}
}
}
}
/**
* Checks if the result was a yes.
*
* @return bool True if the activity is available
*/
public function is_available() {
return $this->available;
}
/**
* Filters the provided array so that it only includes nodes which are
* supposed to be displayed in the result output. (I.e. those for which
* the user failed the test, and which are not set to totally hide
* output.)
*
* @param tree_node[] $array Input array of nodes
* @return array Output array containing only those nodes set for display
*/
public function filter_nodes(array $array) {
$out = array();
foreach ($array as $key => $node) {
if (array_key_exists(spl_object_hash($node), $this->shownodes)) {
$out[$key] = $node;
}
}
return $out;
}
}

View File

@ -0,0 +1,624 @@
<?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 (defined('PHPUNIT_TEST')) {
$enabled['mock'] = true;
}
// Get children.
if (!isset($structure->c)) {
throw new \coding_exception('Invalid availability structure (missing ->c)');
}
if (!is_array($structure->c)) {
throw new \coding_exception('Invalid availability structure (->c not array)');
}
if (is_array($this->showchildren) && count($structure->showc) != count($structure->c)) {
throw new \coding_exception('Invalid availability structure (->c, ->showc mismatch)');
}
$this->children = array();
foreach ($structure->c as $index => $child) {
if (!is_object($child)) {
throw new \coding_exception('Invalid availability structure (child not object)');
}
// First see if it's a condition. These have a defined type.
if (isset($child->type)) {
// Look for a plugin of this type.
$classname = '\availability_' . $child->type . '\condition';
if (!array_key_exists($child->type, $enabled)) {
if ($lax) {
// On load of existing settings, ignore if class
// doesn't exist.
continue;
} else {
throw new \coding_exception('Unknown condition type: ' . $child->type);
}
}
$this->children[] = new $classname($child);
} else {
// Not a condition. Must be a subtree.
$this->children[] = new tree($child, $lax, false);
}
if (!is_null($this->showchildren)) {
$this->showchildren[] = $structure->showc[$index];
}
}
}
public function check_available($not, info $info, $grabthelot, $userid) {
// If there are no children in this group, we just treat it as available.
$information = '';
if (!$this->children) {
return new result(true);
}
// Get logic flags from operator.
list($innernot, $andoperator) = $this->get_logic_flags($not);
if ($andoperator) {
$allow = true;
} else {
$allow = false;
}
$failedchildren = array();
$totallyhide = !$this->show;
foreach ($this->children as $index => $child) {
// Check available and get info.
$childresult = $child->check_available(
$innernot, $info, $grabthelot, $userid);
$childyes = $childresult->is_available();
if (!$childyes) {
$failedchildren[] = $childresult;
if (!is_null($this->showchildren) && !$this->showchildren[$index]) {
$totallyhide = true;
}
}
if ($andoperator && !$childyes) {
$allow = false;
// Do not exit loop at this point, as we will still include other info.
} else if (!$andoperator && $childyes) {
// Exit loop since we are going to allow access (from this tree at least).
$allow = true;
break;
}
}
if ($allow) {
return new result(true);
} else if ($totallyhide) {
return new result(false);
} else {
return new result(false, $this, $failedchildren);
}
}
public function is_applied_to_user_lists() {
return true;
}
/**
* Tests against a user list. Users who cannot access the activity due to
* availability restrictions will be removed from the list.
*
* This test ONLY includes conditions which are marked as being applied to
* user lists. For example, group conditions are included but date
* conditions are not included.
*
* The function operates reasonably efficiently i.e. should not do per-user
* database queries. It is however likely to be fairly slow.
*
* @param array $users Array of userid => object
* @param bool $not If tree's parent indicates it's being checked negatively
* @param info $info Info about current context
* @param capability_checker $checker Capability checker
* @return array Filtered version of input array
*/
public function filter_user_list(array $users, $not, info $info,
capability_checker $checker) {
// Get logic flags from operator.
list($innernot, $andoperator) = $this->get_logic_flags($not);
if ($andoperator) {
// For AND, start with the whole result and whittle it down.
$result = $users;
} else {
// For OR, start with nothing.
$result = array();
$anyconditions = false;
}
// Loop through all valid children.
foreach ($this->children as $index => $child) {
if (!$child->is_applied_to_user_lists()) {
continue;
}
$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 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 $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)
*/
protected function get_full_information_recursive(
$not, info $info, result $result = null, $root, $hidden = false) {
global $PAGE;
// 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.
$renderer = $PAGE->get_renderer('core', 'availability');
return $renderer->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;
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;
}
/**
* 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 $child) {
$thischanged = $child->update_after_restore($restoreid, $courseid,
$logger, $name);
$changed = $changed || $thischanged;
}
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;
}
}

View File

@ -0,0 +1,162 @@
<?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 {
/**
* 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
*/
public abstract 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
*/
public abstract 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)
*/
public abstract function save();
/**
* 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.
*
* @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
*/
public abstract 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.
*
* 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.
*
* @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)');
}
}

63
availability/renderer.php Normal file
View File

@ -0,0 +1,63 @@
<?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/>.
/**
* 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
*/
/**
* 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 {
/**
* Renders HTML for the result of two or more availability restriction
* messages being combined in a list.
*
* The supplied messages should already take account of the 'not' option,
* e.g. an example message could be 'User profile field Department must
* not be set to Maths'.
*
* This function will not be called unless there are at least two messages.
*
* @param bool $root True if this is a root-level list for an activity
* @param bool $andoperator True if the messages are being combined as AND
* @param bool $roothidden True if the root level should use 'hidden' message
* @param array $messages Messages to render
* @return string Combined HTML
*/
public function multiple_messages($root, $andoperator, $roothidden, array $messages) {
// Get initial message.
$out = get_string('list_' . ($root ? 'root_' : '') .
($andoperator ? 'and' : 'or') . ($roothidden ? '_hidden' : ''),
'availability');
// Make the list.
$out .= html_writer::start_tag('ul');
foreach ($messages as $message) {
$out .= html_writer::tag('li', $message);
}
$out .= html_writer::end_tag('ul');
return $out;
}
}

View File

@ -0,0 +1,169 @@
@core @core_availability
Feature: display_availability
In order to know which activities are available
As a user
I need to see appropriate availability restrictions for activities and sections
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 I log in as "admin"
And I set the following administration settings values:
| Enable conditional access | 1 |
And I log out
@javascript
Scenario: Activity availability display
# Set up.
Given I log in as "teacher1"
And I follow "Course 1"
And I turn editing mode on
# Add a Page with 1 restriction.
When I add a "Page" to section "1"
And I set the following fields to these values:
| Name | Page 1 |
| Description | Test |
| Page content | Test |
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 "January"
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 add a "Page" to section "2"
And I set the following fields to these values:
| Name | Page 2 |
| Description | Test |
| Page content | Test |
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 "January"
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.org"
And I press "Save and return to course"
# Add another Page with no restrictions.
And I add a "Page" to section "3" and I fill the form with:
| Name | Page 3 |
| Description | Test |
| Page content | Test |
# Page 1 should show in single-line format, showing the date
Then I should see "Available until" in the "#section-1 .availabilityinfo" "css_element"
And I should see "2013" in the "#section-1 .availabilityinfo" "css_element"
And I should see "2013" in the "#section-1 .availabilityinfo" "css_element"
And "li" "css_element" should not exist in the "#section-1 .availabilityinfo" "css_element"
# Page 2 should show in list format.
And "li" "css_element" should exist in the "#section-2 .availabilityinfo" "css_element"
And I should see "Not available unless:" in the "#section-2 .availabilityinfo" "css_element"
And I should see "It is before" in the "#section-2 .availabilityinfo" "css_element"
And I should see "hidden otherwise" in the "#section-2 .availabilityinfo" "css_element"
And I should see "Email address" in the "#section-2 .availabilityinfo" "css_element"
# Page 3 should not have available info.
And "#section-3 .availabilityinfo" "css_element" should not exist
# Change to student view.
Given I log out
And I log in as "student1"
And I follow "Course 1"
# Page 1 display still there but should be dimmed and not a link.
Then I should see "Page 1" in the "#section-1 .dimmed_text" "css_element"
And ".activityinstance a" "css_element" should not exist in the "#section-1" "css_element"
# Date display should be present.
And I should see "Available until" in the "#section-1" "css_element"
# 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 ".activityinstance a" "css_element" should exist in the "#section-3" "css_element"
@javascript
Scenario: Section availability display
# Set up.
Given I log in as "teacher1"
And I follow "Course 1"
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 "Save changes"
# Section 2 is the same but hidden from students
When 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 homepage
And I follow "Course 1"
# Add Pages to each section.
And I add a "Page" to section "1" and I fill the form with:
| Name | Page 1 |
| Description | Test |
| Page content | Test |
And I add a "Page" to section "2" and I fill the form with:
| Name | Page 2 |
| Description | Test |
| Page content | Test |
And I add a "Page" to section "3" and I fill the form with:
| Name | Page 3 |
| Description | Test |
| Page content | Test |
# Check display
Then I should see "Available until" in the "#section-1 .availabilityinfo" "css_element"
And I should see "Available until" in the "#section-2 .availabilityinfo" "css_element"
And I should see "hidden otherwise" in the "#section-2 .availabilityinfo" "css_element"
# Change to student view.
Given I log out
And I log in as "student1"
And I follow "Course 1"
# 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 "Topic 1" in the "region-main" "region"
And I should see "Available until" in the "#section-1 .availabilityinfo" "css_element"
# Section 2 should not be available at all
And I should not see "Topic 2" in the "region-main" "region"

View File

@ -0,0 +1,155 @@
@core @core_availability @wip
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
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 |
# This scenario does not need JavaScript, but I have added the tag because
# after today's composer/package update, I get this error:
# Notice: Undefined index: host in
# [...]\vendor\symfony\browser-kit\Symfony\Component\BrowserKit\CookieJar.php
# line 217
# Adding @javascript here causes it not to use whatever they just broke, and
# it then works again.
@javascript
Scenario: Confirm the 'enable availability' option is working
When I log in as "teacher1"
And I follow "Course 1"
And I turn editing mode on
And I add a "Page" to section "1"
Then "Restrict access" "fieldset" should not exist
Given I follow "C1"
When I edit the section "1"
Then "Restrict access" "fieldset" should not exist
When I log out
And I log in as "admin"
And I set the following administration settings values:
| Enable conditional access | 1 |
When I log out
And I log in as "teacher1"
And I follow "Course 1"
And I turn editing mode on
And I add a "Page" to section "1"
Then "Restrict access" "fieldset" should exist
Given I follow "C1"
When I edit the section "1"
Then "Restrict access" "fieldset" should exist
@javascript
Scenario: Edit availability using settings in activity form
# Set up.
Given I log in as "admin"
And I set the following administration settings values:
| Enable conditional access | 1 |
And I log out
And I log in as "teacher1"
And I follow "Course 1"
# Add a Page and check it has None in so far.
And I turn editing mode on
And I add a "Page" to 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"
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 greyed-out"
# 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 greyed-out"
# 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"
@javascript
Scenario: Edit availability using settings in section form
# Set up.
Given I log in as "admin"
And I set the following administration settings values:
| Enable conditional access | 1 |
And I log out
And I log in as "teacher1"
And I follow "Course 1"
And I turn 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"

View File

@ -0,0 +1,70 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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
*/
use core_availability\capability_checker;
defined('MOODLE_INTERNAL') || die();
/**
* 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
*/
class core_availability_capability_checker_testcase extends advanced_testcase {
/**
* Tests loading a class from /availability/classes.
*/
public function test_capability_checker() {
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());
}
}

View File

@ -0,0 +1,55 @@
<?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/>.
/**
* 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
*/
defined('MOODLE_INTERNAL') || die();
/**
* 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
*/
class core_availability_component_testcase extends advanced_testcase {
/**
* Tests loading a class from /availability/classes.
*/
public function test_load_class() {
$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() {
// 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->assertEquals(array('completion', 'date', 'grade', 'group', 'grouping', 'profile'),
array_keys($list));
}
}

View File

@ -0,0 +1,125 @@
<?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;
}
}

View File

@ -0,0 +1,74 @@
<?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 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);
}
}

View File

@ -0,0 +1,481 @@
<?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/>.
/**
* 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
*/
defined('MOODLE_INTERNAL') || die();
use core_availability\info;
use core_availability\info_module;
use core_availability\info_section;
/**
* 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
*/
class info_testcase extends \advanced_testcase {
public function setUp() {
// Load the mock condition so that it can be used.
require_once(__DIR__ . '/fixtures/mock_condition.php');
}
/**
* Tests the info_module class (is_available, get_full_information).
*/
public function test_info_module() {
global $DB;
// Create a course and pages.
$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 = phpunit_util::get_debugging_messages();
phpunit_util::reset_debugging();
$this->assertEquals(1, count($debugging));
$this->assertContains('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() {
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 = phpunit_util::get_debugging_messages();
phpunit_util::reset_debugging();
$this->assertEquals(1, count($debugging));
$this->assertContains('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() {
global $CFG, $DB;
require_once($CFG->dirroot . '/course/lib.php');
$this->resetAfterTest();
// 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.
// 4. With groupmembersonly flag.
$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);
$pages[4] = $pagegen->create_instance($rec, array('groupmembersonly' => 1));
$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));
// Groupmembersonly uses a different flag.
$CFG->enableavailability = false;
$this->assertTrue(info_module::is_user_visible($pages[4]->cmid, $student->id, false));
$CFG->enablegroupmembersonly = true;
$this->assertFalse(info_module::is_user_visible($pages[4]->cmid, $student->id, false));
// There is no way to clear a static cache used in grouplib, so test the
// positive case on an identical but different user.
$group = $generator->create_group(array('courseid' => $course->id));
groups_add_member($group, $student2);
$this->assertTrue(info_module::is_user_visible($pages[4]->cmid, $student2->id, false));
}
/**
* Tests the convert_legacy_fields function used in restore.
*/
public function test_convert_legacy_fields() {
// 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 - only if flag enabled.
$rec->groupmembersonly = 1;
$this->assertEquals(
'{"op":"&","showc":[false],"c":[{"type":"grouping","id":7}]}',
info::convert_legacy_fields($rec, false, true));
$this->assertNull(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, true));
// 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":[true,false],"c":[' .
'{"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() {
// 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() {
// 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_users() function.
*/
public function test_filter_users() {
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();
$allusers = array($u1->id => $u1, $u2->id => $u2, $u3->id => $u3);
$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)));
// 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));
$this->assertEquals(array($u3->id),
array_keys($info->filter_user_list($allusers)));
$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)));
// 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)));
// Check the other section...
$CFG->enableavailability = true;
$info = new info_section($modinfo->get_section_info(2));
$this->assertEquals(array($u1->id, $u2->id),
array_keys($info->filter_user_list($allusers)));
// And the module in that section - which has combined the section and
// module restrictions.
$info = new info_module($modinfo->get_cm($page2->cmid));
$this->assertEquals(array($u2->id),
array_keys($info->filter_user_list($allusers)));
}
}

View File

@ -0,0 +1,642 @@
<?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/>.
/**
* 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
*/
use core_availability\capability_checker;
defined('MOODLE_INTERNAL') || die();
/**
* 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
*/
class tree_testcase extends \advanced_testcase {
public function setUp() {
// Load the mock classes so they can be used.
require_once(__DIR__ . '/fixtures/mock_condition.php');
require_once(__DIR__ . '/fixtures/mock_info.php');
}
/**
* Tests constructing a tree with errors.
*/
public function test_construct_errors() {
try {
new \core_availability\tree('frog');
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('not object', $e->getMessage());
}
try {
new \core_availability\tree((object)array());
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('missing ->op', $e->getMessage());
}
try {
new \core_availability\tree((object)array('op' => '*'));
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('unknown ->op', $e->getMessage());
}
try {
new \core_availability\tree((object)array('op' => '|'));
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('missing ->show', $e->getMessage());
}
try {
new \core_availability\tree((object)array('op' => '|', 'show' => 0));
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('->show not bool', $e->getMessage());
}
try {
new \core_availability\tree((object)array('op' => '&'));
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('missing ->showc', $e->getMessage());
}
try {
new \core_availability\tree((object)array('op' => '&', 'showc' => 0));
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('->showc not array', $e->getMessage());
}
try {
new \core_availability\tree((object)array('op' => '&', 'showc' => array(0)));
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('->showc value not bool', $e->getMessage());
}
try {
new \core_availability\tree((object)array('op' => '|', 'show' => true));
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('missing ->c', $e->getMessage());
}
try {
new \core_availability\tree((object)array('op' => '|', 'show' => true,
'c' => 'side'));
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('->c not array', $e->getMessage());
}
try {
new \core_availability\tree((object)array('op' => '|', 'show' => true,
'c' => array(3)));
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('child not object', $e->getMessage());
}
try {
new \core_availability\tree((object)array('op' => '|', 'show' => true,
'c' => array((object)array('type' => 'doesnotexist'))));
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('Unknown condition type: doesnotexist', $e->getMessage());
}
try {
new \core_availability\tree((object)array('op' => '|', 'show' => true,
'c' => array((object)array())));
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('missing ->op', $e->getMessage());
}
try {
new \core_availability\tree((object)array('op' => '&',
'c' => array((object)array('op' => '&', 'c' => array())),
'showc' => array(true, true)
));
$this->fail();
} catch (coding_exception $e) {
$this->assertContains('->c, ->showc mismatch', $e->getMessage());
}
}
/**
* Tests constructing a tree with plugin that does not exist (ignored).
*/
public function test_construct_ignore_missing_plugin() {
// Construct a tree with & combination of one condition that doesn't exist.
$tree = new \core_availability\tree(self::tree(array(
(object)array('type' => 'doesnotexist'))), 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() {
$structure = self::tree(array(
self::tree(array()),
self::tree(array(
self::tree(array(), '!|')), '!&')),
'&', null, array(true, true));
$tree = new \core_availability\tree($structure);
$this->assertEquals('&(+|(),+!&(!|()))', (string)$tree);
}
/**
* Tests constructing tree using the mock plugin.
*/
public function test_construct_with_mock_plugin() {
$structure = self::tree(array(
self::mock(array('a' => true, 'm' => ''))));
$tree = new \core_availability\tree($structure);
$this->assertEquals('+|({mock:y,})', (string)$tree);
}
/**
* Tests the check_available and get_result_information functions.
*/
public function test_check_available() {
global $USER;
// Setup.
$this->resetAfterTest();
$info = new \core_availability\mock_info();
$this->setAdminUser();
$information = '';
// No conditions.
$structure = self::tree(array());
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->assertRegExp('~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->assertRegExp('~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->assertRegExp('~!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->assertRegExp('~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(
self::tree(array(
self::mock(array('a' => true, 'm' => 'no'))), '!&'));
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(
self::tree(array(
self::mock(array('a' => false, 'm' => '1')),
self::mock(array('a' => false, 'm' => '2'))
), '&', null, array(true, true)),
self::mock(array('a' => false, 'm' => 3)));
list ($available, $information) = $this->get_available_results(
$structure, $info, $USER->id);
$this->assertFalse($available);
$this->assertRegExp('~<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) {
$tree = new \core_availability\tree($structure);
$result = $tree->check_available(false, $info, true, $userid);
return array($result->is_available(), $tree->get_result_information($info, $result));
}
/**
* Tests the is_available_for_all() function.
*/
public function test_is_available_for_all() {
// Empty tree is always available.
$structure = self::tree(array());
$tree = new \core_availability\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 \core_availability\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 \core_availability\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 \core_availability\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 \core_availability\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 \core_availability\tree($structure);
$this->assertTrue($tree->is_available_for_all());
}
/**
* Tests the get_full_information() function.
*/
public function test_get_full_information() {
// Setup.
$info = new \core_availability\mock_info();
// No conditions.
$structure = self::tree(array());
$tree = new \core_availability\tree($structure);
$this->assertEquals('', $tree->get_full_information($info));
// Condition (normal and NOT).
$structure->c = array(
self::mock(array('m' => 'thing')));
$tree = new \core_availability\tree($structure);
$this->assertEquals('SA: [FULL]thing',
$tree->get_full_information($info));
$structure->op = '!&';
$tree = new \core_availability\tree($structure);
$this->assertEquals('SA: ![FULL]thing',
$tree->get_full_information($info));
// Complex structure.
$structure->op = '|';
$structure->c = array(
self::tree(array(
self::mock(array('m' => '1')),
self::mock(array('m' => '2'))), '&'),
self::mock(array('m' => 3)));
$tree = new \core_availability\tree($structure);
$this->assertRegExp('~<ul.*<ul.*<li.*1.*<li.*2.*</ul>.*<li.*3~',
$tree->get_full_information($info));
// Test intro messages before list. First, OR message.
$structure->c = array(
self::mock(array('m' => '1')),
self::mock(array('m' => '2'))
);
$tree = new \core_availability\tree($structure);
$this->assertRegExp('~Not available unless any of:.*<ul>~',
$tree->get_full_information($info));
// Now, OR message when not shown.
$structure->show = false;
$tree = new \core_availability\tree($structure);
$this->assertRegExp('~hidden.*<ul>~',
$tree->get_full_information($info));
// AND message.
$structure->op = '&';
unset($structure->show);
$structure->showc = array(false, false);
$tree = new \core_availability\tree($structure);
$this->assertRegExp('~Not available unless:.*<ul>~',
$tree->get_full_information($info));
// Hidden markers on items.
$this->assertRegExp('~1.*hidden.*2.*hidden~',
$tree->get_full_information($info));
// Hidden markers on child tree and items.
$structure->c[1] = self::tree(array(
self::mock(array('m' => '2')),
self::mock(array('m' => '3'))), '&');
$tree = new \core_availability\tree($structure);
$this->assertRegExp('~1.*hidden.*All of \(hidden.*2.*3~',
$tree->get_full_information($info));
$structure->c[1]->op = '|';
$tree = new \core_availability\tree($structure);
$this->assertRegExp('~1.*hidden.*Any of \(hidden.*2.*3~',
$tree->get_full_information($info));
// Hidden markers on single-item display, AND and OR.
$structure->showc = array(false);
$structure->c = array(
self::mock(array('m' => '1'))
);
$tree = new \core_availability\tree($structure);
$this->assertRegExp('~1.*hidden~',
$tree->get_full_information($info));
unset($structure->showc);
$structure->show = false;
$structure->op = '|';
$tree = new \core_availability\tree($structure);
$this->assertRegExp('~1.*hidden~',
$tree->get_full_information($info));
// Hidden marker if single item is tree.
$structure->c[0] = self::tree(array(
self::mock(array('m' => '1')),
self::mock(array('m' => '2'))), '&', null);
$tree = new \core_availability\tree($structure);
$this->assertRegExp('~Not available \(hidden.*1.*2~',
$tree->get_full_information($info));
// Single item tree containing single item.
unset($structure->c[0]->c[1]);
$tree = new \core_availability\tree($structure);
$this->assertRegExp('~SA.*1.*hidden~',
$tree->get_full_information($info));
}
/**
* Tests the get_all_children() function.
*/
public function test_get_all_children() {
// Create a tree with nothing in.
$structure = self::tree(array());
$tree1 = new \core_availability\tree($structure);
// Create second tree with complex structure.
$structure->c = array(
self::tree(array(
self::mock(array('m' => '1')),
self::mock(array('m' => '2'))
)),
self::mock(array('m' => 3)));
$tree2 = new \core_availability\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() {
// Create tree with structure of 3 mocks.
$structure = self::tree(array(
self::tree(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 \core_availability\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() {
$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 = self::tree(array(self::mock(array())));
$tree = new \core_availability\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 = self::tree(array(self::mock(array('filter' => array(2, 3)))));
$tree = new \core_availability\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 = self::tree(array(
self::mock(array('filter' => array(3))),
self::mock(array('filter' => array(1)))));
$tree = new \core_availability\tree($structure);
$result = $tree->filter_user_list($users, false, $info, $checker);
ksort($result);
$this->assertEquals(array(1, 3), array_keys($result));
// Tree with two condition that both filter (&).
$structure = self::tree(array(
self::mock(array('filter' => array(2, 3))),
self::mock(array('filter' => array(1, 2)))), '&', false,
array(true, true));
$tree = new \core_availability\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 = self::tree(array(
self::tree(
self::mock(array('filter' => array(1))), '!&', null)));
$tree = new \core_availability\tree($structure);
$result = $tree->filter_user_list($users, false, $info, $checker);
ksort($result);
$this->assertEquals(array(2, 3), array_keys($result));
}
/**
* Utility function to build the PHP structure representing a tree.
*
* @param array|tree_node $children Children or single child
* @param string $op Operator
* @param bool|null $show True/false show param or null if none
* @param array|null $showc showc param or null if none
* @return \stdClass Structure object
*/
protected static function tree($children, $op = '|', $show = true, $showc = null) {
$structure = new stdClass();
$structure->op = $op;
if (!is_array($children)) {
$children = array($children);
}
$structure->c = $children;
if ($showc !== null) {
$structure->showc = $showc;
}
if ($show !== null) {
$structure->show = $show;
}
return $structure;
}
/**
* 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;
}
}

View File

@ -0,0 +1,10 @@
{
"name": "moodle-core_availability-form",
"builds": {
"moodle-core_availability-form": {
"jsfiles": [
"form.js"
]
}
}
}

1020
availability/yui/src/form/js/form.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,12 @@
{
"moodle-core_availability-form": {
"requires": [
"base",
"node",
"event",
"panel",
"moodle-core-notification-dialogue",
"json"
]
}
}

65
lang/en/availability.php Normal file
View File

@ -0,0 +1,65 @@
<?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/>.
/**
* Strings for availability system.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['accessrestrictions'] = 'Access restrictions';
$string['addrestriction'] = 'Add restriction...';
$string['and'] = 'and';
$string['condition_group'] = 'Restriction set';
$string['condition_group_info'] = 'Add a set of nested restrictions to apply complex logic.';
$string['enableavailability'] = 'Enable conditional access';
$string['enableavailability_desc'] = 'When enabled, this lets you set conditions (based on date, grade, or completion) that control whether an activity or resource can be accessed.';
$string['error_list_nochildren'] = 'Restriction sets should contain at least one condition.';
$string['hidden_marker'] = '(hidden otherwise)';
$string['hidden_individual'] = 'Hidden entirely if user does not meet this condition';
$string['invalid'] = 'Please set';
$string['itemheading'] = '{$a->number} {$a->type} restriction';
$string['shown_individual'] = 'Displayed greyed-out if user does not meet this condition';
$string['hide_verb'] = 'Click to hide';
$string['show_verb'] = 'Click to show';
$string['hidden_all'] = 'Hidden entirely if user does not meet conditions';
$string['shown_all'] = 'Displayed greyed-out if user does not meet conditions';
$string['label_multi'] = 'Required restrictions';
$string['label_sign'] = 'Restriction type';
$string['list_and'] = 'All of:';
$string['list_and_hidden'] = 'All of (hidden otherwise):';
$string['list_or'] = 'Any of:';
$string['list_or_hidden'] = 'Any of (hidden otherwise):';
$string['list_root_and'] = 'Not available unless:';
$string['list_root_and_hidden'] = 'Not available (hidden) unless:';
$string['list_root_or'] = 'Not available unless any of:';
$string['list_root_or_hidden'] = 'Not available (hidden) unless any of:';
$string['listheader_sign_before'] = 'Student';
$string['listheader_sign_pos'] = 'must';
$string['listheader_sign_neg'] = 'must not';
$string['listheader_single'] = 'match the following';
$string['listheader_multi_after'] = 'of the following';
$string['listheader_multi_and'] = 'all';
$string['listheader_multi_before'] = 'match';
$string['listheader_multi_or'] = 'any';
$string['manageplugins'] = 'Manage restrictions';
$string['missingplugin'] = 'Missing restriction plugin';
$string['or'] = 'or';
$string['restrictaccess'] = 'Restrict access';
$string['setheading'] = '{$a->number} Set of {$a->count} restriction(s)';
$string['unknowncondition'] = 'Unknown condition (deleted condition plugin)';

View File

@ -86,6 +86,8 @@ $string['status_uptodate'] = 'Installed';
$string['systemname'] = 'Identifier';
$string['type_auth'] = 'Authentication method';
$string['type_auth_plural'] = 'Authentication methods';
$string['type_availability'] = 'Availability restriction';
$string['type_availability_plural'] = 'Availability restrictions';
$string['type_block'] = 'Block';
$string['type_block_plural'] = 'Blocks';
$string['type_cachelock'] = 'Cache lock handler';

View File

@ -312,6 +312,7 @@ $cache = '.var_export($cache, true).';
'access' => null,
'admin' => $CFG->dirroot.'/'.$CFG->admin,
'auth' => $CFG->dirroot.'/auth',
'availability' => $CFG->dirroot . '/availability',
'backup' => $CFG->dirroot.'/backup/util/ui',
'badges' => $CFG->dirroot.'/badges',
'block' => $CFG->dirroot.'/blocks',
@ -386,6 +387,7 @@ $cache = '.var_export($cache, true).';
global $CFG;
$types = array(
'availability' => $CFG->dirroot . '/availability/condition',
'qtype' => $CFG->dirroot.'/question/type',
'mod' => $CFG->dirroot.'/mod',
'auth' => $CFG->dirroot.'/auth',

View File

@ -957,6 +957,10 @@ class core_plugin_manager {
'shibboleth', 'webservice'
),
'availability' => array(
'completion', 'date', 'grade', 'group', 'grouping', 'profile'
),
'block' => array(
'activity_modules', 'admin_bookmarks', 'badges', 'blog_menu',
'blog_recent', 'blog_tags', 'calendar_month',
@ -1126,7 +1130,7 @@ class core_plugin_manager {
),
'tool' => array(
'assignmentupgrade', 'behat', 'capability', 'customlang',
'assignmentupgrade', 'availabilityconditions', 'behat', 'capability', 'customlang',
'dbtransfer', 'generator', 'health', 'innodb', 'installaddon',
'langimport', 'log', 'multilangupgrade', 'phpunit', 'profiling',
'qeupgradehelper', 'replace', 'spamcleaner', 'task', 'timezoneimport',

View File

@ -0,0 +1,56 @@
<?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 for availability plugins.
*
* @package core
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\plugininfo;
defined('MOODLE_INTERNAL') || die();
/**
* Class for availability plugins.
*
* @package core
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class availability extends base {
public static function get_enabled_plugins() {
global $DB;
// Get all available plugins.
$plugins = \core_plugin_manager::instance()->get_installed_plugins('availability');
if (!$plugins) {
return array();
}
// Check they are enabled using get_config (which is cached and hopefully fast).
$enabled = array();
foreach ($plugins as $plugin => $version) {
$disabled = get_config('availability_' . $plugin, 'disabled');
if (empty($disabled)) {
$enabled[$plugin] = $plugin;
}
}
return $enabled;
}
}

View File

@ -64,9 +64,13 @@
<directory suffix="_test.php">grade/tests</directory>
<directory suffix="_test.php">grade/grading/tests</directory>
</testsuite>
<testsuite name="core_availability">
<directory suffix="_test.php">availability/tests</directory>
</testsuite>
<testsuite name="core_backup">
<directory suffix="_test.php">backup/controller/tests</directory>
<directory suffix="_test.php">backup/converter/moodle1/tests</directory>
<directory suffix="_test.php">backup/moodle2/tests</directory>
<directory suffix="_test.php">backup/util</directory>
</testsuite>
<testsuite name="core_badges">