first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,654 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* User profile field condition.
*
* @package availability_profile
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace availability_profile;
defined('MOODLE_INTERNAL') || die();
/**
* User profile field condition.
*
* @package availability_profile
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class condition extends \core_availability\condition {
/** @var string Operator: field contains value */
const OP_CONTAINS = 'contains';
/** @var string Operator: field does not contain value */
const OP_DOES_NOT_CONTAIN = 'doesnotcontain';
/** @var string Operator: field equals value */
const OP_IS_EQUAL_TO = 'isequalto';
/** @var string Operator: field starts with value */
const OP_STARTS_WITH = 'startswith';
/** @var string Operator: field ends with value */
const OP_ENDS_WITH = 'endswith';
/** @var string Operator: field is empty */
const OP_IS_EMPTY = 'isempty';
/** @var string Operator: field is not empty */
const OP_IS_NOT_EMPTY = 'isnotempty';
/** @var array|null Array of custom profile fields (static cache within request) */
protected static $customprofilefields = null;
/** @var string Field name (for standard fields) or '' if custom field */
protected $standardfield = '';
/** @var int Field name (for custom fields) or '' if standard field */
protected $customfield = '';
/** @var string Operator type (OP_xx constant) */
protected $operator;
/** @var string Expected value for field */
protected $value = '';
/**
* Constructor.
*
* @param \stdClass $structure Data structure from JSON decode
* @throws \coding_exception If invalid data structure.
*/
public function __construct($structure) {
// Get operator.
if (isset($structure->op) && in_array($structure->op, array(self::OP_CONTAINS,
self::OP_DOES_NOT_CONTAIN, self::OP_IS_EQUAL_TO, self::OP_STARTS_WITH,
self::OP_ENDS_WITH, self::OP_IS_EMPTY, self::OP_IS_NOT_EMPTY), true)) {
$this->operator = $structure->op;
} else {
throw new \coding_exception('Missing or invalid ->op for profile condition');
}
// For operators other than the empty/not empty ones, require value.
switch($this->operator) {
case self::OP_IS_EMPTY:
case self::OP_IS_NOT_EMPTY:
if (isset($structure->v)) {
throw new \coding_exception('Unexpected ->v for non-value operator');
}
break;
default:
if (isset($structure->v) && is_string($structure->v)) {
$this->value = $structure->v;
} else {
throw new \coding_exception('Missing or invalid ->v for profile condition');
}
break;
}
// Get field type.
if (property_exists($structure, 'sf')) {
if (property_exists($structure, 'cf')) {
throw new \coding_exception('Both ->sf and ->cf for profile condition');
}
if (is_string($structure->sf)) {
$this->standardfield = $structure->sf;
} else {
throw new \coding_exception('Invalid ->sf for profile condition');
}
} else if (property_exists($structure, 'cf')) {
if (is_string($structure->cf)) {
$this->customfield = $structure->cf;
} else {
throw new \coding_exception('Invalid ->cf for profile condition');
}
} else {
throw new \coding_exception('Missing ->sf or ->cf for profile condition');
}
}
public function save() {
$result = (object)array('type' => 'profile', 'op' => $this->operator);
if ($this->customfield) {
$result->cf = $this->customfield;
} else {
$result->sf = $this->standardfield;
}
switch($this->operator) {
case self::OP_IS_EMPTY:
case self::OP_IS_NOT_EMPTY:
break;
default:
$result->v = $this->value;
break;
}
return $result;
}
/**
* Returns a JSON object which corresponds to a condition of this type.
*
* Intended for unit testing, as normally the JSON values are constructed
* by JavaScript code.
*
* @param bool $customfield True if this is a custom field
* @param string $fieldname Field name
* @param string $operator Operator name (OP_xx constant)
* @param string|null $value Value (not required for some operator types)
* @return stdClass Object representing condition
*/
public static function get_json($customfield, $fieldname, $operator, $value = null) {
$result = (object)array('type' => 'profile', 'op' => $operator);
if ($customfield) {
$result->cf = $fieldname;
} else {
$result->sf = $fieldname;
}
switch ($operator) {
case self::OP_IS_EMPTY:
case self::OP_IS_NOT_EMPTY:
break;
default:
if (is_null($value)) {
throw new \coding_exception('Operator requires value');
}
$result->v = $value;
break;
}
return $result;
}
public function is_available($not, \core_availability\info $info, $grabthelot, $userid) {
$uservalue = $this->get_cached_user_profile_field($userid);
$allow = self::is_field_condition_met($this->operator, $uservalue, $this->value);
if ($not) {
$allow = !$allow;
}
return $allow;
}
public function get_description($full, $not, \core_availability\info $info) {
$course = $info->get_course();
// Display the fieldname into current lang.
if ($this->customfield) {
// Is a custom profile field (will use multilang).
$customfields = self::get_custom_profile_fields();
if (array_key_exists($this->customfield, $customfields)) {
$translatedfieldname = $customfields[$this->customfield]->name;
} else {
$translatedfieldname = get_string('missing', 'availability_profile',
$this->customfield);
}
} else {
$standardfields = self::get_standard_profile_fields();
if (array_key_exists($this->standardfield, $standardfields)) {
$translatedfieldname = $standardfields[$this->standardfield];
} else {
$translatedfieldname = get_string('missing', 'availability_profile', $this->standardfield);
}
}
$a = new \stdClass();
// Not safe to call format_string here; use the special function to call it later.
$a->field = self::description_format_string($translatedfieldname);
$a->value = s($this->value);
if ($not) {
// When doing NOT strings, we replace the operator with its inverse.
// Some of them don't have inverses, so for those we use a new
// identifier which is only used for this lang string.
switch($this->operator) {
case self::OP_CONTAINS:
$opname = self::OP_DOES_NOT_CONTAIN;
break;
case self::OP_DOES_NOT_CONTAIN:
$opname = self::OP_CONTAINS;
break;
case self::OP_ENDS_WITH:
$opname = 'notendswith';
break;
case self::OP_IS_EMPTY:
$opname = self::OP_IS_NOT_EMPTY;
break;
case self::OP_IS_EQUAL_TO:
$opname = 'notisequalto';
break;
case self::OP_IS_NOT_EMPTY:
$opname = self::OP_IS_EMPTY;
break;
case self::OP_STARTS_WITH:
$opname = 'notstartswith';
break;
default:
throw new \coding_exception('Unexpected operator: ' . $this->operator);
}
} else {
$opname = $this->operator;
}
return get_string('requires_' . $opname, 'availability_profile', $a);
}
protected function get_debug_string() {
if ($this->customfield) {
$out = '*' . $this->customfield;
} else {
$out = $this->standardfield;
}
$out .= ' ' . $this->operator;
switch($this->operator) {
case self::OP_IS_EMPTY:
case self::OP_IS_NOT_EMPTY:
break;
default:
$out .= ' ' . $this->value;
break;
}
return $out;
}
/**
* Returns true if a field meets the required conditions, false otherwise.
*
* @param string $operator the requirement/condition
* @param string $uservalue the user's value
* @param string $value the value required
* @return boolean True if conditions are met
*/
protected static function is_field_condition_met($operator, $uservalue, $value) {
if ($uservalue === false) {
// If the user value is false this is an instant fail.
// All user values come from the database as either data or the default.
// They will always be a string.
return false;
}
$fieldconditionmet = true;
// Just to be doubly sure it is a string.
$uservalue = (string)$uservalue;
switch($operator) {
case self::OP_CONTAINS:
$pos = strpos($uservalue, $value);
if ($pos === false) {
$fieldconditionmet = false;
}
break;
case self::OP_DOES_NOT_CONTAIN:
if (!empty($value)) {
$pos = strpos($uservalue, $value);
if ($pos !== false) {
$fieldconditionmet = false;
}
}
break;
case self::OP_IS_EQUAL_TO:
if ($value !== $uservalue) {
$fieldconditionmet = false;
}
break;
case self::OP_STARTS_WITH:
$length = strlen($value);
if ((substr($uservalue, 0, $length) !== $value)) {
$fieldconditionmet = false;
}
break;
case self::OP_ENDS_WITH:
$length = strlen($value);
$start = $length * -1;
if (substr($uservalue, $start) !== $value) {
$fieldconditionmet = false;
}
break;
case self::OP_IS_EMPTY:
if (!empty($uservalue)) {
$fieldconditionmet = false;
}
break;
case self::OP_IS_NOT_EMPTY:
if (empty($uservalue)) {
$fieldconditionmet = false;
}
break;
}
return $fieldconditionmet;
}
/**
* Return list of standard user profile fields used by the condition
*
* @return string[]
*/
public static function get_standard_profile_fields(): array {
return [
'firstname' => \core_user\fields::get_display_name('firstname'),
'lastname' => \core_user\fields::get_display_name('lastname'),
'email' => \core_user\fields::get_display_name('email'),
'city' => \core_user\fields::get_display_name('city'),
'country' => \core_user\fields::get_display_name('country'),
'idnumber' => \core_user\fields::get_display_name('idnumber'),
'institution' => \core_user\fields::get_display_name('institution'),
'department' => \core_user\fields::get_display_name('department'),
'phone1' => \core_user\fields::get_display_name('phone1'),
'phone2' => \core_user\fields::get_display_name('phone2'),
'address' => \core_user\fields::get_display_name('address'),
];
}
/**
* Gets data about custom profile fields. Cached statically in current
* request.
*
* This only includes fields which can be tested by the system (those whose
* data is cached in $USER object) - basically doesn't include textarea type
* fields.
*
* @return array Array of records indexed by shortname
*/
public static function get_custom_profile_fields() {
global $DB, $CFG;
if (self::$customprofilefields === null) {
// Get fields and store them indexed by shortname.
require_once($CFG->dirroot . '/user/profile/lib.php');
$fields = profile_get_custom_fields(true);
self::$customprofilefields = array();
foreach ($fields as $field) {
self::$customprofilefields[$field->shortname] = $field;
}
}
return self::$customprofilefields;
}
/**
* Wipes the static cache (for use in unit tests).
*/
public static function wipe_static_cache() {
self::$customprofilefields = null;
}
/**
* Return the value for a user's profile field
*
* @param int $userid User ID
* @return string|bool Value, or false if user does not have a value for this field
*/
protected function get_cached_user_profile_field($userid) {
global $USER, $DB, $CFG;
$iscurrentuser = $USER->id == $userid;
if (isguestuser($userid) || ($iscurrentuser && !isloggedin())) {
// Must be logged in and can't be the guest.
return false;
}
// Custom profile fields will be numeric, there are no numeric standard profile fields so this is not a problem.
$iscustomprofilefield = $this->customfield ? true : false;
if ($iscustomprofilefield) {
// As its a custom profile field we need to map the id back to the actual field.
// We'll also preload all of the other custom profile fields just in case and ensure we have the
// default value available as well.
if (!array_key_exists($this->customfield, self::get_custom_profile_fields())) {
// No such field exists.
// This shouldn't normally happen but occur if things go wrong when deleting a custom profile field
// or when restoring a backup of a course with user profile field conditions.
return false;
}
$field = $this->customfield;
} else {
$field = $this->standardfield;
}
// If its the current user than most likely we will be able to get this information from $USER.
// If its a regular profile field then it should already be available, if not then we have a mega problem.
// If its a custom profile field then it should be available but may not be. If it is then we use the value
// available, otherwise we load all custom profile fields into a temp object and refer to that.
// Noting its not going be great for performance if we have to use the temp object as it involves loading the
// custom profile field API and classes.
if ($iscurrentuser) {
if (!$iscustomprofilefield) {
if (property_exists($USER, $field)) {
return $USER->{$field};
} else {
// Unknown user field. This should not happen.
throw new \coding_exception('Requested user profile field does not exist');
}
}
// Checking if the custom profile fields are already available.
if (!isset($USER->profile)) {
// Drat! they're not. We need to use a temp object and load them.
// We don't use $USER as the profile fields are loaded into the object.
$user = new \stdClass;
$user->id = $USER->id;
// This should ALWAYS be set, but just in case we check.
require_once($CFG->dirroot . '/user/profile/lib.php');
profile_load_custom_fields($user);
if (array_key_exists($field, $user->profile)) {
return $user->profile[$field];
}
} else if (array_key_exists($field, $USER->profile)) {
// Hurrah they're available, this is easy.
return $USER->profile[$field];
}
// The profile field doesn't exist.
return false;
} else {
// Loading for another user.
if ($iscustomprofilefield) {
// Fetch the data for the field. Noting we keep this query simple so that Database caching takes care of performance
// for us (this will likely be hit again).
// We are able to do this because we've already pre-loaded the custom fields.
$data = $DB->get_field('user_info_data', 'data', array('userid' => $userid,
'fieldid' => self::$customprofilefields[$field]->id), IGNORE_MISSING);
// If we have data return that, otherwise return the default.
if ($data !== false) {
return $data;
} else {
return self::$customprofilefields[$field]->defaultdata;
}
} else {
// Its a standard field, retrieve it from the user.
return $DB->get_field('user', $field, array('id' => $userid), MUST_EXIST);
}
}
return false;
}
public function is_applied_to_user_lists() {
// Profile conditions are assumed to be 'permanent', so they affect the
// display of user lists for activities.
return true;
}
public function filter_user_list(array $users, $not, \core_availability\info $info,
\core_availability\capability_checker $checker) {
global $CFG, $DB;
// If the array is empty already, just return it.
if (!$users) {
return $users;
}
// Get all users from the list who match the condition.
list ($sql, $params) = $DB->get_in_or_equal(array_keys($users));
if ($this->customfield) {
$customfields = self::get_custom_profile_fields();
if (!array_key_exists($this->customfield, $customfields)) {
// If the field isn't found, nobody matches.
return array();
}
$customfield = $customfields[$this->customfield];
// Fetch custom field value for all users.
$values = $DB->get_records_select('user_info_data', 'fieldid = ? AND userid ' . $sql,
array_merge(array($customfield->id), $params),
'', 'userid, data');
$valuefield = 'data';
$default = $customfield->defaultdata;
} else {
$standardfields = self::get_standard_profile_fields();
if (!array_key_exists($this->standardfield, $standardfields)) {
// If the field isn't found, nobody matches.
return [];
}
$values = $DB->get_records_select('user', 'id ' . $sql, $params,
'', 'id, '. $this->standardfield);
$valuefield = $this->standardfield;
$default = '';
}
// Filter the user list.
$result = array();
foreach ($users as $id => $user) {
// Get value for user.
if (array_key_exists($id, $values)) {
$value = $values[$id]->{$valuefield};
} else {
$value = $default;
}
// Check value.
$allow = $this->is_field_condition_met($this->operator, $value, $this->value);
if ($not) {
$allow = !$allow;
}
if ($allow) {
$result[$id] = $user;
}
}
return $result;
}
/**
* Gets SQL to match a field against this condition. The second copy of the
* field is in case you're using variables for the field so that it needs
* to be two different ones.
*
* @param string $field Field name
* @param string $field2 Second copy of field name (default same).
* @param boolean $istext Any of the fields correspond to a TEXT column in database (true) or not (false).
* @return array Array of SQL and parameters
*/
private function get_condition_sql($field, $field2 = null, $istext = false) {
global $DB;
if (is_null($field2)) {
$field2 = $field;
}
$params = array();
switch($this->operator) {
case self::OP_CONTAINS:
$sql = $DB->sql_like($field, self::unique_sql_parameter(
$params, '%' . $this->value . '%'));
break;
case self::OP_DOES_NOT_CONTAIN:
if (empty($this->value)) {
// The 'does not contain nothing' expression matches everyone.
return null;
}
$sql = $DB->sql_like($field, self::unique_sql_parameter(
$params, '%' . $this->value . '%'), true, true, true);
break;
case self::OP_IS_EQUAL_TO:
if ($istext) {
$sql = $DB->sql_compare_text($field) . ' = ' . $DB->sql_compare_text(
self::unique_sql_parameter($params, $this->value));
} else {
$sql = $field . ' = ' . self::unique_sql_parameter(
$params, $this->value);
}
break;
case self::OP_STARTS_WITH:
$sql = $DB->sql_like($field, self::unique_sql_parameter(
$params, $this->value . '%'));
break;
case self::OP_ENDS_WITH:
$sql = $DB->sql_like($field, self::unique_sql_parameter(
$params, '%' . $this->value));
break;
case self::OP_IS_EMPTY:
// Mimic PHP empty() behaviour for strings, '0' or ''.
$emptystring = self::unique_sql_parameter($params, '');
if ($istext) {
$sql = '(' . $DB->sql_compare_text($field) . " IN ('0', $emptystring) OR $field2 IS NULL)";
} else {
$sql = '(' . $field . " IN ('0', $emptystring) OR $field2 IS NULL)";
}
break;
case self::OP_IS_NOT_EMPTY:
$emptystring = self::unique_sql_parameter($params, '');
if ($istext) {
$sql = '(' . $DB->sql_compare_text($field) . " NOT IN ('0', $emptystring) AND $field2 IS NOT NULL)";
} else {
$sql = '(' . $field . " NOT IN ('0', $emptystring) AND $field2 IS NOT NULL)";
}
break;
}
return array($sql, $params);
}
public function get_user_list_sql($not, \core_availability\info $info, $onlyactive) {
global $DB;
// Build suitable SQL depending on custom or standard field.
if ($this->customfield) {
$customfields = self::get_custom_profile_fields();
if (!array_key_exists($this->customfield, $customfields)) {
// If the field isn't found, nobody matches.
return array('SELECT id FROM {user} WHERE 0 = 1', array());
}
$customfield = $customfields[$this->customfield];
$mainparams = array();
$tablesql = "LEFT JOIN {user_info_data} ud ON ud.fieldid = " .
self::unique_sql_parameter($mainparams, $customfield->id) .
" AND ud.userid = userids.id";
list ($condition, $conditionparams) = $this->get_condition_sql('ud.data', null, true);
$mainparams = array_merge($mainparams, $conditionparams);
// If default is true, then allow that too.
if ($this->is_field_condition_met(
$this->operator, $customfield->defaultdata, $this->value)) {
$where = "((ud.data IS NOT NULL AND $condition) OR (ud.data IS NULL))";
} else {
$where = "(ud.data IS NOT NULL AND $condition)";
}
} else {
$standardfields = self::get_standard_profile_fields();
if (!array_key_exists($this->standardfield, $standardfields)) {
// If the field isn't found, nobody matches.
return ['SELECT id FROM {user} WHERE 0 = 1', []];
}
$tablesql = "JOIN {user} u ON u.id = userids.id";
list ($where, $mainparams) = $this->get_condition_sql(
'u.' . $this->standardfield);
}
// Handle NOT.
if ($not) {
$where = 'NOT (' . $where . ')';
}
// Get enrolled user SQL and combine with this query.
list ($enrolsql, $enrolparams) =
get_enrolled_sql($info->get_context(), '', 0, $onlyactive);
$sql = "SELECT userids.id
FROM ($enrolsql) userids
$tablesql
WHERE $where";
$params = array_merge($enrolparams, $mainparams);
return array($sql, $params);
}
}
@@ -0,0 +1,62 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Front-end class.
*
* @package availability_profile
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace availability_profile;
defined('MOODLE_INTERNAL') || die();
/**
* Front-end class.
*
* @package availability_profile
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class frontend extends \core_availability\frontend {
protected function get_javascript_strings() {
return array('op_contains', 'op_doesnotcontain', 'op_endswith', 'op_isempty',
'op_isequalto', 'op_isnotempty', 'op_startswith', 'conditiontitle',
'label_operator', 'label_value');
}
protected function get_javascript_init_params($course, \cm_info $cm = null,
\section_info $section = null) {
// Standard user fields.
$standardfields = condition::get_standard_profile_fields();
\core_collator::asort($standardfields);
// Custom fields.
$customfields = array();
$options = array('context' => \context_course::instance($course->id));
foreach (condition::get_custom_profile_fields() as $field) {
$customfields[$field->shortname] = format_string($field->name, true, $options);
}
\core_collator::asort($customfields);
// Make arrays into JavaScript format (non-associative, ordered) and return.
return array(self::convert_associative_array_for_js($standardfields, 'field', 'display'),
self::convert_associative_array_for_js($customfields, 'field', 'display'));
}
}
@@ -0,0 +1,46 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy Subsystem implementation for availability_profile.
*
* @package availability_profile
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace availability_profile\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for availability_profile implementing null_provider.
*
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,51 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Language strings.
*
* @package availability_profile
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['conditiontitle'] = 'User profile field';
$string['description'] = 'Control access based on fields within the student\'s profile.';
$string['error_selectfield'] = 'You must select a profile field.';
$string['error_setvalue'] = 'You must type a value.';
$string['label_operator'] = 'Method of comparison';
$string['label_value'] = 'Value to compare against';
$string['pluginname'] = 'Restriction by profile';
$string['requires_contains'] = 'Your <strong>{$a->field}</strong> contains <strong>{$a->value}</strong>';
$string['requires_doesnotcontain'] = 'Your <strong>{$a->field}</strong> does not contain <strong>{$a->value}</strong>';
$string['requires_endswith'] = 'Your <strong>{$a->field}</strong> ends with <strong>{$a->value}</strong>';
$string['requires_isempty'] = 'Your <strong>{$a->field}</strong> is empty';
$string['requires_isequalto'] = 'Your <strong>{$a->field}</strong> is <strong>{$a->value}</strong>';
$string['requires_isnotempty'] = 'Your <strong>{$a->field}</strong> is not empty';
$string['requires_notendswith'] = 'Your <strong>{$a->field}</strong> does not end with <strong>{$a->value}</strong>';
$string['requires_notisequalto'] = 'Your <strong>{$a->field}</strong> is not <strong>{$a->value}</strong>';
$string['requires_notstartswith'] = 'Your <strong>{$a->field}</strong> does not start with <strong>{$a->value}</strong>';
$string['requires_startswith'] = 'Your <strong>{$a->field}</strong> starts with <strong>{$a->value}</strong>';
$string['missing'] = '(Missing field: {$a})';
$string['title'] = 'User profile';
$string['op_contains'] = 'contains';
$string['op_doesnotcontain'] = 'doesn\'t contain';
$string['op_endswith'] = 'ends with';
$string['op_isempty'] = 'is empty';
$string['op_isequalto'] = 'is equal to';
$string['op_isnotempty'] = 'is not empty';
$string['op_startswith'] = 'starts with';
$string['privacy:metadata'] = 'The Restriction by profile plugin does not store any personal data.';
@@ -0,0 +1,109 @@
@availability @availability_profile
Feature: availability_profile
In order to control student access to activities
As a teacher
I need to set profile conditions which prevent student access
Background:
Given the following "courses" exist:
| fullname | shortname | format | enablecompletion |
| Course 1 | C1 | topics | 1 |
And the following "users" exist:
| username | email |
| teacher1 | t@example.com |
| student1 | s@example.com |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "activities" exist:
| activity | course | name |
| page | C1 | P1 |
| page | C1 | P2 |
@javascript
Scenario: Test condition
# Basic setup.
Given I am on the "P1" "page activity editing" page logged in as "teacher1"
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "User profile" "button"
And I set the field "User profile field" to "Email address"
And I set the field "Value to compare against" to "s@example.com"
And I click on ".availability-item .availability-eye img" "css_element"
And I click on "Save and return to course" "button"
# Add
And I am on the "P2" "page activity editing" page
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "User profile" "button"
And I set the field "User profile field" to "Email address"
And I set the field "Value to compare against" to "q@example.com"
And I click on ".availability-item .availability-eye img" "css_element"
And I click on "Save and return to course" "button"
# Log back in as student.
When I am on the "Course 1" "course" page logged in as "student1"
# I see P1 but not P2.
Then I should see "P1" in the "region-main" "region"
And I should not see "P2" in the "region-main" "region"
@javascript
Scenario: Test with custom user profile field
Given the following "custom profile fields" exist:
| datatype | shortname | name |
| text | superfield | Super field |
# Set field value for user.
And I am on the "s@example.com" "user > editing" page logged in as "admin"
And I expand all fieldsets
And I set the field "Super field" to "Bananaman"
And I click on "Update profile" "button"
# Set Page activity which has requirement on this field.
And I am on the "P1" "page activity editing" page
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "User profile" "button"
And I set the following fields to these values:
| User profile field | Super field |
| Value to compare against | Bananaman |
And I click on ".availability-item .availability-eye img" "css_element"
And I click on "Save and return to course" "button"
# Edit it again and check the setting still works.
When I am on the P1 "page activity editing" page
And I expand all fieldsets
Then the field "User profile field" matches value "Super field"
And the field "Value to compare against" matches value "Bananaman"
# Log out and back in as student. Should be able to see activity.
And I am on the "Course 1" "course" page logged in as "student1"
Then I should see "P1" in the "region-main" "region"
@javascript
Scenario: Condition display with filters
# Teacher sets up a restriction on group G1, using multilang filter.
Given the following "custom profile fields" exist:
| datatype | shortname | name | param2 |
| text | frog | <span lang="en" class="multilang">F-One</span><span lang="fr" class="multilang">F-Un</span> | 100 |
And the "multilang" filter is "on"
And the "multilang" filter applies to "content and headings"
# The activity names filter is enabled because it triggered a bug in older versions.
And the "activitynames" filter is "on"
And the "activitynames" filter applies to "content and headings"
And I am on the "P1" "page activity editing" page logged in as "teacher1"
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "User profile" "button" in the "Add restriction..." "dialogue"
And I set the following fields to these values:
| User profile field | F-One |
| Value to compare against | 111 |
And I click on "Save and return to course" "button"
And I log out
# Student sees information about no access to group, with group name in correct language.
When I am on the "C1" "Course" page logged in as "student1"
Then I should see "Not available unless: Your F-One is 111"
And I should not see "F-Un"
@@ -0,0 +1,536 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace availability_profile;
/**
* Unit tests for the user profile condition.
*
* @package availability_profile
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class condition_test extends \advanced_testcase {
/** @var profile_define_text Profile field for testing */
protected $profilefield;
/** @var array Array of user IDs for whome we already set the profile field */
protected $setusers = array();
/** @var condition Current condition */
private $cond;
/** @var \core_availability\info Current info */
private $info;
public function setUp(): void {
global $DB, $CFG;
$this->resetAfterTest();
// Add a custom profile field type.
$this->profilefield = $this->getDataGenerator()->create_custom_profile_field(array(
'shortname' => 'frogtype', 'name' => 'Type of frog',
'datatype' => 'text'));
// Clear static cache.
\availability_profile\condition::wipe_static_cache();
// Load the mock info class so that it can be used.
require_once($CFG->dirroot . '/availability/tests/fixtures/mock_info.php');
}
/**
* Tests constructing and using date condition as part of tree.
*/
public function test_in_tree(): void {
global $USER;
$this->setAdminUser();
$info = new \core_availability\mock_info();
$structure = (object)array('op' => '|', 'show' => true, 'c' => array(
(object)array('type' => 'profile',
'op' => condition::OP_IS_EQUAL_TO,
'cf' => 'frogtype', 'v' => 'tree')));
$tree = new \core_availability\tree($structure);
// Initial check (user does not have custom field).
$result = $tree->check_available(false, $info, true, $USER->id);
$this->assertFalse($result->is_available());
// Set field.
$this->set_field($USER->id, 'tree');
// Now it's true!
$result = $tree->check_available(false, $info, true, $USER->id);
$this->assertTrue($result->is_available());
}
/**
* Tests the constructor including error conditions. Also tests the
* string conversion feature (intended for debugging only).
*/
public function test_constructor(): void {
// No parameters.
$structure = new \stdClass();
try {
$cond = new condition($structure);
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('Missing or invalid ->op', $e->getMessage());
}
// Invalid op.
$structure->op = 'isklingonfor';
try {
$cond = new condition($structure);
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('Missing or invalid ->op', $e->getMessage());
}
// Missing value.
$structure->op = condition::OP_IS_EQUAL_TO;
try {
$cond = new condition($structure);
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('Missing or invalid ->v', $e->getMessage());
}
// Invalid value (not string).
$structure->v = false;
try {
$cond = new condition($structure);
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('Missing or invalid ->v', $e->getMessage());
}
// Unexpected value.
$structure->op = condition::OP_IS_EMPTY;
try {
$cond = new condition($structure);
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('Unexpected ->v', $e->getMessage());
}
// Missing field.
$structure->op = condition::OP_IS_EQUAL_TO;
$structure->v = 'flying';
try {
$cond = new condition($structure);
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('Missing ->sf or ->cf', $e->getMessage());
}
// Invalid field (not string).
$structure->sf = 42;
try {
$cond = new condition($structure);
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('Invalid ->sf', $e->getMessage());
}
// Both fields.
$structure->sf = 'department';
$structure->cf = 'frogtype';
try {
$cond = new condition($structure);
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('Both ->sf and ->cf', $e->getMessage());
}
// Invalid ->cf field (not string).
unset($structure->sf);
$structure->cf = false;
try {
$cond = new condition($structure);
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('Invalid ->cf', $e->getMessage());
}
// Valid examples (checks values are correctly included).
$structure->cf = 'frogtype';
$cond = new condition($structure);
$this->assertEquals('{profile:*frogtype isequalto flying}', (string)$cond);
unset($structure->v);
$structure->op = condition::OP_IS_EMPTY;
$cond = new condition($structure);
$this->assertEquals('{profile:*frogtype isempty}', (string)$cond);
unset($structure->cf);
$structure->sf = 'department';
$cond = new condition($structure);
$this->assertEquals('{profile:department isempty}', (string)$cond);
}
/**
* Tests the save() function.
*/
public function test_save(): void {
$structure = (object)array('cf' => 'frogtype', 'op' => condition::OP_IS_EMPTY);
$cond = new condition($structure);
$structure->type = 'profile';
$this->assertEquals($structure, $cond->save());
$structure = (object)array('cf' => 'frogtype', 'op' => condition::OP_ENDS_WITH,
'v' => 'bouncy');
$cond = new condition($structure);
$structure->type = 'profile';
$this->assertEquals($structure, $cond->save());
}
/**
* Tests the is_available function. There is no separate test for
* get_full_information because that function is called from is_available
* and we test its values here.
*/
public function test_is_available(): void {
global $USER, $SITE, $DB;
$this->setAdminUser();
$info = new \core_availability\mock_info();
// Prepare to test with all operators against custom field using all
// combinations of NOT and true/false states..
$information = 'x';
$structure = (object)array('cf' => 'frogtype');
$structure->op = condition::OP_IS_NOT_EMPTY;
$cond = new condition($structure);
$this->assert_is_available_result(false, '~Type of frog.*is not empty~',
$cond, $info, $USER->id);
$this->set_field($USER->id, 'poison dart');
$this->assert_is_available_result(true, '~Type of frog.*is empty~',
$cond, $info, $USER->id);
$structure->op = condition::OP_IS_EMPTY;
$cond = new condition($structure);
$this->assert_is_available_result(false, '~.*Type of frog.*is empty~',
$cond, $info, $USER->id);
$this->set_field($USER->id, null);
$this->assert_is_available_result(true, '~.*Type of frog.*is not empty~',
$cond, $info, $USER->id);
$this->set_field($USER->id, '');
$this->assert_is_available_result(true, '~.*Type of frog.*is not empty~',
$cond, $info, $USER->id);
$structure->op = condition::OP_CONTAINS;
$structure->v = 'llf';
$cond = new condition($structure);
$this->assert_is_available_result(false, '~Type of frog.*contains.*llf~',
$cond, $info, $USER->id);
$this->set_field($USER->id, 'bullfrog');
$this->assert_is_available_result(true, '~Type of frog.*does not contain.*llf~',
$cond, $info, $USER->id);
$structure->op = condition::OP_DOES_NOT_CONTAIN;
$cond = new condition($structure);
$this->assert_is_available_result(false, '~Type of frog.*does not contain.*llf~',
$cond, $info, $USER->id);
$this->set_field($USER->id, 'goliath');
$this->assert_is_available_result(true, '~Type of frog.*contains.*llf~',
$cond, $info, $USER->id);
$structure->op = condition::OP_IS_EQUAL_TO;
$structure->v = 'Kermit';
$cond = new condition($structure);
$this->assert_is_available_result(false, '~Type of frog.*is <.*Kermit~',
$cond, $info, $USER->id);
$this->set_field($USER->id, 'Kermit');
$this->assert_is_available_result(true, '~Type of frog.*is not.*Kermit~',
$cond, $info, $USER->id);
$structure->op = condition::OP_STARTS_WITH;
$structure->v = 'Kerm';
$cond = new condition($structure);
$this->assert_is_available_result(true, '~Type of frog.*does not start.*Kerm~',
$cond, $info, $USER->id);
$this->set_field($USER->id, 'Keroppi');
$this->assert_is_available_result(false, '~Type of frog.*starts.*Kerm~',
$cond, $info, $USER->id);
$structure->op = condition::OP_ENDS_WITH;
$structure->v = 'ppi';
$cond = new condition($structure);
$this->assert_is_available_result(true, '~Type of frog.*does not end.*ppi~',
$cond, $info, $USER->id);
$this->set_field($USER->id, 'Kermit');
$this->assert_is_available_result(false, '~Type of frog.*ends.*ppi~',
$cond, $info, $USER->id);
// Also test is_available for a different (not current) user.
$generator = $this->getDataGenerator();
$user = $generator->create_user();
$structure->op = condition::OP_CONTAINS;
$structure->v = 'rne';
$cond = new condition($structure);
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
$this->set_field($user->id, 'horned');
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
// Now check with a standard field (department).
$structure = (object)array('op' => condition::OP_IS_EQUAL_TO,
'sf' => 'department', 'v' => 'Cheese Studies');
$cond = new condition($structure);
$this->assertFalse($cond->is_available(false, $info, true, $USER->id));
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
// Check the message (should be using lang string with capital, which
// is evidence that it called the right function to get the name).
$information = $cond->get_description(false, false, $info);
$information = \core_availability\info::format_info($information, $info->get_course());
$this->assertMatchesRegularExpression('~Department~', $information);
// Set the field to true for both users and retry.
$DB->set_field('user', 'department', 'Cheese Studies', array('id' => $user->id));
$USER->department = 'Cheese Studies';
$this->assertTrue($cond->is_available(false, $info, true, $USER->id));
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
}
/**
* Tests what happens with custom fields that are text areas. These should
* not be offered in the menu because their data is not included in user
* object
*/
public function test_custom_textarea_field(): void {
global $USER, $SITE, $DB;
$this->setAdminUser();
$info = new \core_availability\mock_info();
// Add custom textarea type.
$customfield = $this->getDataGenerator()->create_custom_profile_field(array(
'shortname' => 'longtext', 'name' => 'Long text',
'datatype' => 'textarea'));
// The list of fields should include the text field added in setUp(),
// but should not include the textarea field added just now.
$fields = condition::get_custom_profile_fields();
$this->assertArrayHasKey('frogtype', $fields);
$this->assertArrayNotHasKey('longtext', $fields);
}
/**
* Sets the custom profile field used for testing.
*
* @param int $userid User id
* @param string|null $value Field value or null to clear
* @param int $fieldid Field id or 0 to use default one
*/
protected function set_field($userid, $value, $fieldid = 0) {
global $DB, $USER;
if (!$fieldid) {
$fieldid = $this->profilefield->id;
}
$alreadyset = array_key_exists($userid, $this->setusers);
if (is_null($value)) {
$DB->delete_records('user_info_data',
array('userid' => $userid, 'fieldid' => $fieldid));
unset($this->setusers[$userid]);
} else if ($alreadyset) {
$DB->set_field('user_info_data', 'data', $value,
array('userid' => $userid, 'fieldid' => $fieldid));
} else {
$DB->insert_record('user_info_data', array('userid' => $userid,
'fieldid' => $fieldid, 'data' => $value));
$this->setusers[$userid] = true;
}
}
/**
* Checks the result of is_available. This function is to save duplicated
* code; it does two checks (the normal is_available with $not set to true
* and set to false). Whichever result is expected to be true, it checks
* $information ends up as empty string for that one, and as a regex match
* for another one.
*
* @param bool $yes If the positive test is expected to return true
* @param string $failpattern Regex pattern to match text when it returns false
* @param condition $cond Condition
* @param \core_availability\info $info Information about current context
* @param int $userid User id
*/
protected function assert_is_available_result($yes, $failpattern, condition $cond,
\core_availability\info $info, $userid) {
// Positive (normal) test.
$this->assertEquals($yes, $cond->is_available(false, $info, true, $userid),
'Failed checking normal (positive) result');
if (!$yes) {
$information = $cond->get_description(false, false, $info);
$information = \core_availability\info::format_info($information, $info->get_course());
$this->assertMatchesRegularExpression($failpattern, $information);
}
// Negative (NOT) test.
$this->assertEquals(!$yes, $cond->is_available(true, $info, true, $userid),
'Failed checking NOT (negative) result');
if ($yes) {
$information = $cond->get_description(false, true, $info);
$information = \core_availability\info::format_info($information, $info->get_course());
$this->assertMatchesRegularExpression($failpattern, $information);
}
}
/**
* Tests the filter_users (bulk checking) function.
*/
public function test_filter_users(): void {
global $DB, $CFG;
$this->resetAfterTest();
$CFG->enableavailability = true;
// Erase static cache before test.
condition::wipe_static_cache();
// Make a test course and some users.
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$student1 = $generator->create_user(array('institution' => 'Unseen University'));
$student2 = $generator->create_user(array('institution' => 'Hogwarts'));
$student3 = $generator->create_user(array('institution' => 'Unseen University'));
$allusers = array();
foreach (array($student1, $student2, $student3) as $student) {
$generator->enrol_user($student->id, $course->id);
$allusers[$student->id] = $student;
}
$this->set_field($student1->id, 'poison dart');
$this->set_field($student2->id, 'poison dart');
$info = new \core_availability\mock_info($course);
$checker = new \core_availability\capability_checker($info->get_context());
// Test standard field condition (positive and negative).
$cond = new condition((object)array('sf' => 'institution', 'op' => 'contains', 'v' => 'Unseen'));
$result = array_keys($cond->filter_user_list($allusers, false, $info, $checker));
ksort($result);
$this->assertEquals(array($student1->id, $student3->id), $result);
$result = array_keys($cond->filter_user_list($allusers, true, $info, $checker));
ksort($result);
$this->assertEquals(array($student2->id), $result);
// Test custom field condition.
$cond = new condition((object)array('cf' => 'frogtype', 'op' => 'contains', 'v' => 'poison'));
$result = array_keys($cond->filter_user_list($allusers, false, $info, $checker));
ksort($result);
$this->assertEquals(array($student1->id, $student2->id), $result);
$result = array_keys($cond->filter_user_list($allusers, true, $info, $checker));
ksort($result);
$this->assertEquals(array($student3->id), $result);
}
/**
* Tests getting user list SQL. This is a different test from the above because
* there is some additional code in this function so more variants need testing.
*/
public function test_get_user_list_sql(): void {
global $DB, $CFG;
$this->resetAfterTest();
$CFG->enableavailability = true;
// Erase static cache before test.
condition::wipe_static_cache();
// For testing, make another info field with default value.
$otherprofilefield = $this->getDataGenerator()->create_custom_profile_field(array(
'shortname' => 'tonguestyle', 'name' => 'Tongue style',
'datatype' => 'text', 'defaultdata' => 'Slimy'));
// Make a test course and some users.
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$student1 = $generator->create_user(array('institution' => 'Unseen University'));
$student2 = $generator->create_user(array('institution' => 'Hogwarts'));
$student3 = $generator->create_user(array('institution' => 'Unseen University'));
$student4 = $generator->create_user(array('institution' => '0'));
$allusers = array();
foreach (array($student1, $student2, $student3, $student4) as $student) {
$generator->enrol_user($student->id, $course->id);
$allusers[$student->id] = $student;
}
$this->set_field($student1->id, 'poison dart');
$this->set_field($student2->id, 'poison dart');
$this->set_field($student3->id, 'Rough', $otherprofilefield->id);
$this->info = new \core_availability\mock_info($course);
// Test standard field condition (positive).
$this->cond = new condition((object)array('sf' => 'institution',
'op' => condition::OP_CONTAINS, 'v' => 'Univ'));
$this->assert_user_list_sql_results(array($student1->id, $student3->id));
// Now try it negative.
$this->assert_user_list_sql_results(array($student2->id, $student4->id), true);
// Try all the other condition types.
$this->cond = new condition((object)array('sf' => 'institution',
'op' => condition::OP_DOES_NOT_CONTAIN, 'v' => 's'));
$this->assert_user_list_sql_results(array($student4->id));
$this->cond = new condition((object)array('sf' => 'institution',
'op' => condition::OP_IS_EQUAL_TO, 'v' => 'Hogwarts'));
$this->assert_user_list_sql_results(array($student2->id));
$this->cond = new condition((object)array('sf' => 'institution',
'op' => condition::OP_STARTS_WITH, 'v' => 'U'));
$this->assert_user_list_sql_results(array($student1->id, $student3->id));
$this->cond = new condition((object)array('sf' => 'institution',
'op' => condition::OP_ENDS_WITH, 'v' => 'rts'));
$this->assert_user_list_sql_results(array($student2->id));
$this->cond = new condition((object)array('sf' => 'institution',
'op' => condition::OP_IS_EMPTY));
$this->assert_user_list_sql_results(array($student4->id));
$this->cond = new condition((object)array('sf' => 'institution',
'op' => condition::OP_IS_NOT_EMPTY));
$this->assert_user_list_sql_results(array($student1->id, $student2->id, $student3->id));
// Try with a custom field condition that doesn't have a default.
$this->cond = new condition((object)array('cf' => 'frogtype',
'op' => condition::OP_CONTAINS, 'v' => 'poison'));
$this->assert_user_list_sql_results(array($student1->id, $student2->id));
$this->cond = new condition((object)array('cf' => 'frogtype',
'op' => condition::OP_IS_EMPTY));
$this->assert_user_list_sql_results(array($student3->id, $student4->id));
// Try with one that does have a default.
$this->cond = new condition((object)array('cf' => 'tonguestyle',
'op' => condition::OP_STARTS_WITH, 'v' => 'Sli'));
$this->assert_user_list_sql_results(array($student1->id, $student2->id,
$student4->id));
$this->cond = new condition((object)array('cf' => 'tonguestyle',
'op' => condition::OP_IS_EMPTY));
$this->assert_user_list_sql_results(array());
}
/**
* Convenience function. Gets the user list SQL and runs it, then checks
* results.
*
* @param array $expected Array of expected user ids
* @param bool $not True if using NOT condition
*/
private function assert_user_list_sql_results(array $expected, $not = false) {
global $DB;
list ($sql, $params) = $this->cond->get_user_list_sql($not, $this->info, true);
$result = $DB->get_fieldset_sql($sql, $params);
sort($result);
$this->assertEquals($expected, $result);
}
}
@@ -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 info.
*
* @package availability_profile
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->component = 'availability_profile';
@@ -0,0 +1,139 @@
YUI.add('moodle-availability_profile-form', function (Y, NAME) {
/**
* JavaScript for form editing profile conditions.
*
* @module moodle-availability_profile-form
*/
M.availability_profile = M.availability_profile || {};
/**
* @class M.availability_profile.form
* @extends M.core_availability.plugin
*/
M.availability_profile.form = Y.Object(M.core_availability.plugin);
/**
* Groupings available for selection (alphabetical order).
*
* @property profiles
* @type Array
*/
M.availability_profile.form.profiles = null;
/**
* Initialises this plugin.
*
* @method initInner
* @param {Array} standardFields Array of objects with .field, .display
* @param {Array} customFields Array of objects with .field, .display
*/
M.availability_profile.form.initInner = function(standardFields, customFields) {
this.standardFields = standardFields;
this.customFields = customFields;
};
M.availability_profile.form.getNode = function(json) {
// Create HTML structure.
var html = '<span class="availability-group"><label><span class="pr-3">' +
M.util.get_string('conditiontitle', 'availability_profile') + '</span> ' +
'<select name="field" class="custom-select">' +
'<option value="choose">' + M.util.get_string('choosedots', 'moodle') + '</option>';
var fieldInfo;
for (var i = 0; i < this.standardFields.length; i++) {
fieldInfo = this.standardFields[i];
// String has already been escaped using format_string.
html += '<option value="sf_' + fieldInfo.field + '">' + fieldInfo.display + '</option>';
}
for (i = 0; i < this.customFields.length; i++) {
fieldInfo = this.customFields[i];
// String has already been escaped using format_string.
html += '<option value="cf_' + fieldInfo.field + '">' + fieldInfo.display + '</option>';
}
html += '</select></label> <label><span class="accesshide">' + M.util.get_string('label_operator', 'availability_profile') +
' </span><select name="op" title="' + M.util.get_string('label_operator', 'availability_profile') + '"' +
' class="custom-select">';
var operators = ['isequalto', 'contains', 'doesnotcontain', 'startswith', 'endswith',
'isempty', 'isnotempty'];
for (i = 0; i < operators.length; i++) {
html += '<option value="' + operators[i] + '">' +
M.util.get_string('op_' + operators[i], 'availability_profile') + '</option>';
}
html += '</select></label> <label><span class="accesshide">' + M.util.get_string('label_value', 'availability_profile') +
'</span><input name="value" type="text" class="form-control" style="width: 10em" title="' +
M.util.get_string('label_value', 'availability_profile') + '"/></label></span>';
var node = Y.Node.create('<span class="d-flex flex-wrap align-items-center">' + html + '</span>');
// Set initial values if specified.
if (json.sf !== undefined &&
node.one('select[name=field] > option[value=sf_' + json.sf + ']')) {
node.one('select[name=field]').set('value', 'sf_' + json.sf);
} else if (json.cf !== undefined &&
node.one('select[name=field] > option[value=cf_' + json.cf + ']')) {
node.one('select[name=field]').set('value', 'cf_' + json.cf);
}
if (json.op !== undefined &&
node.one('select[name=op] > option[value=' + json.op + ']')) {
node.one('select[name=op]').set('value', json.op);
if (json.op === 'isempty' || json.op === 'isnotempty') {
node.one('input[name=value]').set('disabled', true);
}
}
if (json.v !== undefined) {
node.one('input').set('value', json.v);
}
// Add event handlers (first time only).
if (!M.availability_profile.form.addedEvents) {
M.availability_profile.form.addedEvents = true;
var updateForm = function(input) {
var ancestorNode = input.ancestor('span.availability_profile');
var op = ancestorNode.one('select[name=op]');
var novalue = (op.get('value') === 'isempty' || op.get('value') === 'isnotempty');
ancestorNode.one('input[name=value]').set('disabled', novalue);
M.core_availability.form.update();
};
var root = Y.one('.availability-field');
root.delegate('change', function() {
updateForm(this);
}, '.availability_profile select');
root.delegate('change', function() {
updateForm(this);
}, '.availability_profile input[name=value]');
}
return node;
};
M.availability_profile.form.fillValue = function(value, node) {
// Set field.
var field = node.one('select[name=field]').get('value');
if (field.substr(0, 3) === 'sf_') {
value.sf = field.substr(3);
} else if (field.substr(0, 3) === 'cf_') {
value.cf = field.substr(3);
}
// Operator and value
value.op = node.one('select[name=op]').get('value');
var valueNode = node.one('input[name=value]');
if (!valueNode.get('disabled')) {
value.v = valueNode.get('value');
}
};
M.availability_profile.form.fillErrors = function(errors, node) {
var value = {};
this.fillValue(value, node);
// Check profile item id.
if (value.sf === undefined && value.cf === undefined) {
errors.push('availability_profile:error_selectfield');
}
if (value.v !== undefined && /^\s*$/.test(value.v)) {
errors.push('availability_profile:error_setvalue');
}
};
}, '@VERSION@', {"requires": ["base", "node", "event", "moodle-core_availability-form"]});
@@ -0,0 +1 @@
YUI.add("moodle-availability_profile-form",function(n,e){M.availability_profile=M.availability_profile||{},M.availability_profile.form=n.Object(M.core_availability.plugin),M.availability_profile.form.profiles=null,M.availability_profile.form.initInner=function(e,i){this.standardFields=e,this.customFields=i},M.availability_profile.form.getNode=function(e){for(var i,l,a,t,o='<span class="availability-group"><label><span class="pr-3">'+M.util.get_string("conditiontitle","availability_profile")+'</span> <select name="field" class="custom-select"><option value="choose">'+M.util.get_string("choosedots","moodle")+"</option>",s=0;s<this.standardFields.length;s++)o+='<option value="sf_'+(i=this.standardFields[s]).field+'">'+i.display+"</option>";for(s=0;s<this.customFields.length;s++)o+='<option value="cf_'+(i=this.customFields[s]).field+'">'+i.display+"</option>";for(o+='</select></label> <label><span class="accesshide">'+M.util.get_string("label_operator","availability_profile")+' </span><select name="op" title="'+M.util.get_string("label_operator","availability_profile")+'" class="custom-select">',l=["isequalto","contains","doesnotcontain","startswith","endswith","isempty","isnotempty"],s=0;s<l.length;s++)o+='<option value="'+l[s]+'">'+M.util.get_string("op_"+l[s],"availability_profile")+"</option>";return o+='</select></label> <label><span class="accesshide">'+M.util.get_string("label_value","availability_profile")+'</span><input name="value" type="text" class="form-control" style="width: 10em" title="'+M.util.get_string("label_value","availability_profile")+'"/></label></span>',a=n.Node.create('<span class="d-flex flex-wrap align-items-center">'+o+"</span>"),e.sf!==undefined&&a.one("select[name=field] > option[value=sf_"+e.sf+"]")?a.one("select[name=field]").set("value","sf_"+e.sf):e.cf!==undefined&&a.one("select[name=field] > option[value=cf_"+e.cf+"]")&&a.one("select[name=field]").set("value","cf_"+e.cf),e.op!==undefined&&a.one("select[name=op] > option[value="+e.op+"]")&&(a.one("select[name=op]").set("value",e.op),"isempty"!==e.op&&"isnotempty"!==e.op||a.one("input[name=value]").set("disabled",!0)),e.v!==undefined&&a.one("input").set("value",e.v),M.availability_profile.form.addedEvents||(M.availability_profile.form.addedEvents=!0,t=function(e){var e=e.ancestor("span.availability_profile"),i=e.one("select[name=op]"),i="isempty"===i.get("value")||"isnotempty"===i.get("value");e.one("input[name=value]").set("disabled",i),M.core_availability.form.update()},(e=n.one(".availability-field")).delegate("change",function(){t(this)},".availability_profile select"),e.delegate("change",function(){t(this)},".availability_profile input[name=value]")),a},M.availability_profile.form.fillValue=function(e,i){var l=i.one("select[name=field]").get("value");"sf_"===l.substr(0,3)?e.sf=l.substr(3):"cf_"===l.substr(0,3)&&(e.cf=l.substr(3)),e.op=i.one("select[name=op]").get("value"),(l=i.one("input[name=value]")).get("disabled")||(e.v=l.get("value"))},M.availability_profile.form.fillErrors=function(e,i){var l={};this.fillValue(l,i),l.sf===undefined&&l.cf===undefined&&e.push("availability_profile:error_selectfield"),l.v!==undefined&&/^\s*$/.test(l.v)&&e.push("availability_profile:error_setvalue")}},"@VERSION@",{requires:["base","node","event","moodle-core_availability-form"]});
@@ -0,0 +1,139 @@
YUI.add('moodle-availability_profile-form', function (Y, NAME) {
/**
* JavaScript for form editing profile conditions.
*
* @module moodle-availability_profile-form
*/
M.availability_profile = M.availability_profile || {};
/**
* @class M.availability_profile.form
* @extends M.core_availability.plugin
*/
M.availability_profile.form = Y.Object(M.core_availability.plugin);
/**
* Groupings available for selection (alphabetical order).
*
* @property profiles
* @type Array
*/
M.availability_profile.form.profiles = null;
/**
* Initialises this plugin.
*
* @method initInner
* @param {Array} standardFields Array of objects with .field, .display
* @param {Array} customFields Array of objects with .field, .display
*/
M.availability_profile.form.initInner = function(standardFields, customFields) {
this.standardFields = standardFields;
this.customFields = customFields;
};
M.availability_profile.form.getNode = function(json) {
// Create HTML structure.
var html = '<span class="availability-group"><label><span class="pr-3">' +
M.util.get_string('conditiontitle', 'availability_profile') + '</span> ' +
'<select name="field" class="custom-select">' +
'<option value="choose">' + M.util.get_string('choosedots', 'moodle') + '</option>';
var fieldInfo;
for (var i = 0; i < this.standardFields.length; i++) {
fieldInfo = this.standardFields[i];
// String has already been escaped using format_string.
html += '<option value="sf_' + fieldInfo.field + '">' + fieldInfo.display + '</option>';
}
for (i = 0; i < this.customFields.length; i++) {
fieldInfo = this.customFields[i];
// String has already been escaped using format_string.
html += '<option value="cf_' + fieldInfo.field + '">' + fieldInfo.display + '</option>';
}
html += '</select></label> <label><span class="accesshide">' + M.util.get_string('label_operator', 'availability_profile') +
' </span><select name="op" title="' + M.util.get_string('label_operator', 'availability_profile') + '"' +
' class="custom-select">';
var operators = ['isequalto', 'contains', 'doesnotcontain', 'startswith', 'endswith',
'isempty', 'isnotempty'];
for (i = 0; i < operators.length; i++) {
html += '<option value="' + operators[i] + '">' +
M.util.get_string('op_' + operators[i], 'availability_profile') + '</option>';
}
html += '</select></label> <label><span class="accesshide">' + M.util.get_string('label_value', 'availability_profile') +
'</span><input name="value" type="text" class="form-control" style="width: 10em" title="' +
M.util.get_string('label_value', 'availability_profile') + '"/></label></span>';
var node = Y.Node.create('<span class="d-flex flex-wrap align-items-center">' + html + '</span>');
// Set initial values if specified.
if (json.sf !== undefined &&
node.one('select[name=field] > option[value=sf_' + json.sf + ']')) {
node.one('select[name=field]').set('value', 'sf_' + json.sf);
} else if (json.cf !== undefined &&
node.one('select[name=field] > option[value=cf_' + json.cf + ']')) {
node.one('select[name=field]').set('value', 'cf_' + json.cf);
}
if (json.op !== undefined &&
node.one('select[name=op] > option[value=' + json.op + ']')) {
node.one('select[name=op]').set('value', json.op);
if (json.op === 'isempty' || json.op === 'isnotempty') {
node.one('input[name=value]').set('disabled', true);
}
}
if (json.v !== undefined) {
node.one('input').set('value', json.v);
}
// Add event handlers (first time only).
if (!M.availability_profile.form.addedEvents) {
M.availability_profile.form.addedEvents = true;
var updateForm = function(input) {
var ancestorNode = input.ancestor('span.availability_profile');
var op = ancestorNode.one('select[name=op]');
var novalue = (op.get('value') === 'isempty' || op.get('value') === 'isnotempty');
ancestorNode.one('input[name=value]').set('disabled', novalue);
M.core_availability.form.update();
};
var root = Y.one('.availability-field');
root.delegate('change', function() {
updateForm(this);
}, '.availability_profile select');
root.delegate('change', function() {
updateForm(this);
}, '.availability_profile input[name=value]');
}
return node;
};
M.availability_profile.form.fillValue = function(value, node) {
// Set field.
var field = node.one('select[name=field]').get('value');
if (field.substr(0, 3) === 'sf_') {
value.sf = field.substr(3);
} else if (field.substr(0, 3) === 'cf_') {
value.cf = field.substr(3);
}
// Operator and value
value.op = node.one('select[name=op]').get('value');
var valueNode = node.one('input[name=value]');
if (!valueNode.get('disabled')) {
value.v = valueNode.get('value');
}
};
M.availability_profile.form.fillErrors = function(errors, node) {
var value = {};
this.fillValue(value, node);
// Check profile item id.
if (value.sf === undefined && value.cf === undefined) {
errors.push('availability_profile:error_selectfield');
}
if (value.v !== undefined && /^\s*$/.test(value.v)) {
errors.push('availability_profile:error_setvalue');
}
};
}, '@VERSION@', {"requires": ["base", "node", "event", "moodle-core_availability-form"]});
@@ -0,0 +1,10 @@
{
"name": "moodle-availability_profile-form",
"builds": {
"moodle-availability_profile-form": {
"jsfiles": [
"form.js"
]
}
}
}
+134
View File
@@ -0,0 +1,134 @@
/**
* JavaScript for form editing profile conditions.
*
* @module moodle-availability_profile-form
*/
M.availability_profile = M.availability_profile || {};
/**
* @class M.availability_profile.form
* @extends M.core_availability.plugin
*/
M.availability_profile.form = Y.Object(M.core_availability.plugin);
/**
* Groupings available for selection (alphabetical order).
*
* @property profiles
* @type Array
*/
M.availability_profile.form.profiles = null;
/**
* Initialises this plugin.
*
* @method initInner
* @param {Array} standardFields Array of objects with .field, .display
* @param {Array} customFields Array of objects with .field, .display
*/
M.availability_profile.form.initInner = function(standardFields, customFields) {
this.standardFields = standardFields;
this.customFields = customFields;
};
M.availability_profile.form.getNode = function(json) {
// Create HTML structure.
var html = '<span class="availability-group"><label><span class="pr-3">' +
M.util.get_string('conditiontitle', 'availability_profile') + '</span> ' +
'<select name="field" class="custom-select">' +
'<option value="choose">' + M.util.get_string('choosedots', 'moodle') + '</option>';
var fieldInfo;
for (var i = 0; i < this.standardFields.length; i++) {
fieldInfo = this.standardFields[i];
// String has already been escaped using format_string.
html += '<option value="sf_' + fieldInfo.field + '">' + fieldInfo.display + '</option>';
}
for (i = 0; i < this.customFields.length; i++) {
fieldInfo = this.customFields[i];
// String has already been escaped using format_string.
html += '<option value="cf_' + fieldInfo.field + '">' + fieldInfo.display + '</option>';
}
html += '</select></label> <label><span class="accesshide">' + M.util.get_string('label_operator', 'availability_profile') +
' </span><select name="op" title="' + M.util.get_string('label_operator', 'availability_profile') + '"' +
' class="custom-select">';
var operators = ['isequalto', 'contains', 'doesnotcontain', 'startswith', 'endswith',
'isempty', 'isnotempty'];
for (i = 0; i < operators.length; i++) {
html += '<option value="' + operators[i] + '">' +
M.util.get_string('op_' + operators[i], 'availability_profile') + '</option>';
}
html += '</select></label> <label><span class="accesshide">' + M.util.get_string('label_value', 'availability_profile') +
'</span><input name="value" type="text" class="form-control" style="width: 10em" title="' +
M.util.get_string('label_value', 'availability_profile') + '"/></label></span>';
var node = Y.Node.create('<span class="d-flex flex-wrap align-items-center">' + html + '</span>');
// Set initial values if specified.
if (json.sf !== undefined &&
node.one('select[name=field] > option[value=sf_' + json.sf + ']')) {
node.one('select[name=field]').set('value', 'sf_' + json.sf);
} else if (json.cf !== undefined &&
node.one('select[name=field] > option[value=cf_' + json.cf + ']')) {
node.one('select[name=field]').set('value', 'cf_' + json.cf);
}
if (json.op !== undefined &&
node.one('select[name=op] > option[value=' + json.op + ']')) {
node.one('select[name=op]').set('value', json.op);
if (json.op === 'isempty' || json.op === 'isnotempty') {
node.one('input[name=value]').set('disabled', true);
}
}
if (json.v !== undefined) {
node.one('input').set('value', json.v);
}
// Add event handlers (first time only).
if (!M.availability_profile.form.addedEvents) {
M.availability_profile.form.addedEvents = true;
var updateForm = function(input) {
var ancestorNode = input.ancestor('span.availability_profile');
var op = ancestorNode.one('select[name=op]');
var novalue = (op.get('value') === 'isempty' || op.get('value') === 'isnotempty');
ancestorNode.one('input[name=value]').set('disabled', novalue);
M.core_availability.form.update();
};
var root = Y.one('.availability-field');
root.delegate('change', function() {
updateForm(this);
}, '.availability_profile select');
root.delegate('change', function() {
updateForm(this);
}, '.availability_profile input[name=value]');
}
return node;
};
M.availability_profile.form.fillValue = function(value, node) {
// Set field.
var field = node.one('select[name=field]').get('value');
if (field.substr(0, 3) === 'sf_') {
value.sf = field.substr(3);
} else if (field.substr(0, 3) === 'cf_') {
value.cf = field.substr(3);
}
// Operator and value
value.op = node.one('select[name=op]').get('value');
var valueNode = node.one('input[name=value]');
if (!valueNode.get('disabled')) {
value.v = valueNode.get('value');
}
};
M.availability_profile.form.fillErrors = function(errors, node) {
var value = {};
this.fillValue(value, node);
// Check profile item id.
if (value.sf === undefined && value.cf === undefined) {
errors.push('availability_profile:error_selectfield');
}
if (value.v !== undefined && /^\s*$/.test(value.v)) {
errors.push('availability_profile:error_setvalue');
}
};
@@ -0,0 +1,10 @@
{
"moodle-availability_profile-form": {
"requires": [
"base",
"node",
"event",
"moodle-core_availability-form"
]
}
}