MDL-74149 Usertours: Filters to exclude categories and courses
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
define("tool_usertours/tour_filters",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;_exports.init=()=>{initConfigurationCategoryFilter()};const initConfigurationCategoryFilter=()=>{const categorySelect=document.querySelector("[name='filter_category[]']"),excludeSelect=document.querySelector("[name='filter_exclude_category[]']"),excludeCategoriesContainer=document.getElementById("fitem_id_filter_exclude_category");categorySelect&&excludeSelect&&(categorySelect.addEventListener("change",(()=>{updateExcludeCategories(categorySelect,excludeSelect,excludeCategoriesContainer)})),updateExcludeCategories(categorySelect,excludeSelect,excludeCategoriesContainer))},updateExcludeCategories=(categorySelect,excludeSelect,excludeCategoriesContainer)=>{const selectedCategories=new Set(Array.from(categorySelect.selectedOptions).map((option=>option.value))),excludeSelected=new Set(Array.from(excludeSelect.selectedOptions).map((option=>option.value))),excludeOptions=new Map,anySelected=selectedCategories.has("__ANYVALUE__");var select;Array.from(categorySelect.options).forEach((option=>{const isNotAny="__ANYVALUE__"!==option.value;if(anySelected&&isNotAny)excludeOptions.set(option.value,option.text);else if(isNotAny)for(const selected of selectedCategories){const selectedOption=categorySelect.querySelector('option[value="'.concat(selected,'"]'));if(option.text.startsWith("".concat(selectedOption.text," / "))){excludeOptions.set(option.value,option.text);break}}})),excludeOptions.size?(excludeSelect.innerHTML="",Array.from(excludeOptions).sort(((_ref,_ref2)=>{let[,a]=_ref,[,b]=_ref2;return a.localeCompare(b)})).forEach((_ref3=>{let[key,value]=_ref3;const option=document.createElement("option");option.value=key,option.text=value,excludeSelected.has(key)&&(option.selected=!0),excludeSelect.appendChild(option)})),(select=excludeSelect).size=Math.min(select.options.length||1,10),excludeCategoriesContainer.style.display="flex"):(excludeCategoriesContainer.style.display="none",excludeSelect.innerHTML="")}}));
|
||||
|
||||
//# sourceMappingURL=tour_filters.min.js.map
|
||||
File diff suppressed because one or more lines are too long
Executable
+117
@@ -0,0 +1,117 @@
|
||||
// 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 tours filters.
|
||||
*
|
||||
* @module tool_usertours/tour_filters
|
||||
* @copyright 2025 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
const ANY_VALUE = "__ANYVALUE__";
|
||||
|
||||
export const init = () => {
|
||||
// Initialize the category filter
|
||||
initConfigurationCategoryFilter();
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize the category filter for the configuration page.
|
||||
*/
|
||||
const initConfigurationCategoryFilter = () => {
|
||||
const categorySelect = document.querySelector("[name='filter_category[]']");
|
||||
const excludeSelect = document.querySelector("[name='filter_exclude_category[]']");
|
||||
const excludeCategoriesContainer = document.getElementById('fitem_id_filter_exclude_category');
|
||||
|
||||
if (categorySelect && excludeSelect) {
|
||||
// Add event listeners to update the exclude categories when the include categories change.
|
||||
categorySelect.addEventListener("change", () => {
|
||||
updateExcludeCategories(categorySelect, excludeSelect, excludeCategoriesContainer);
|
||||
});
|
||||
|
||||
// Initialize the exclude categories based on the selected include categories.
|
||||
updateExcludeCategories(categorySelect, excludeSelect, excludeCategoriesContainer);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Adjust the height of a select element based on the number of options.
|
||||
*
|
||||
* @param {HTMLSelectElement} select
|
||||
*/
|
||||
const adjustHeight = (select) => {
|
||||
select.size = Math.min(select.options.length || 1, 10);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the exclude categories based on the selected include categories.
|
||||
*
|
||||
* @param {HTMLSelectElement} categorySelect
|
||||
* @param {HTMLSelectElement} excludeSelect
|
||||
* @param {HTMLElement} excludeCategoriesContainer
|
||||
*/
|
||||
const updateExcludeCategories = (categorySelect, excludeSelect, excludeCategoriesContainer) => {
|
||||
// Get the selected categories and update the 'Any' option.
|
||||
const selectedCategories = new Set(Array.from(categorySelect.selectedOptions).map(option => option.value));
|
||||
|
||||
// Get the selected exclude categories and create a map of options.
|
||||
const excludeSelected = new Set(Array.from(excludeSelect.selectedOptions).map(option => option.value));
|
||||
const excludeOptions = new Map();
|
||||
|
||||
// Flag to check if 'Any' value is selected.
|
||||
const anySelected = selectedCategories.has(ANY_VALUE);
|
||||
Array.from(categorySelect.options).forEach(option => {
|
||||
const isNotAny = option.value !== ANY_VALUE;
|
||||
|
||||
// If 'Any' is selected, include all options in excludeOptions.
|
||||
if (anySelected && isNotAny) {
|
||||
excludeOptions.set(option.value, option.text);
|
||||
} else if (isNotAny) {
|
||||
// Otherwise, check if the option is a child of any selected category.
|
||||
for (const selected of selectedCategories) {
|
||||
const selectedOption = categorySelect.querySelector(`option[value="${selected}"]`);
|
||||
if (option.text.startsWith(`${selectedOption.text} / `)) {
|
||||
excludeOptions.set(option.value, option.text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (excludeOptions.size) {
|
||||
// Update the exclude categories select element.
|
||||
excludeSelect.innerHTML = '';
|
||||
Array.from(excludeOptions)
|
||||
.sort(([, a], [, b]) => a.localeCompare(b))
|
||||
.forEach(([key, value]) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = key;
|
||||
option.text = value;
|
||||
if (excludeSelected.has(key)) {
|
||||
option.selected = true;
|
||||
}
|
||||
excludeSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Adjust the height of the select elements.
|
||||
adjustHeight(excludeSelect);
|
||||
excludeCategoriesContainer.style.display = 'flex';
|
||||
} else {
|
||||
// Hide the exclude categories container when no child categories exist.
|
||||
excludeCategoriesContainer.style.display = 'none';
|
||||
// Clear selections to prevent submitting excluded categories when container is hidden.
|
||||
excludeSelect.innerHTML = '';
|
||||
}
|
||||
|
||||
};
|
||||
@@ -123,4 +123,17 @@ abstract class base {
|
||||
|
||||
$tour->set_filter_values($filtername, $newvalue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default validation for filter forms.
|
||||
* Returns an empty array by default if not overridden.
|
||||
*
|
||||
* @param array $data The submitted form data.
|
||||
* @param array $files The files submitted with the form.
|
||||
* @return array The errors array.
|
||||
*/
|
||||
public static function validate_form(array $data, array $files): array {
|
||||
// Default implementation, returns no errors.
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Regular → Executable
+49
-22
@@ -28,42 +28,48 @@ use context;
|
||||
*/
|
||||
class category extends base {
|
||||
/**
|
||||
* The name of the filter.
|
||||
*
|
||||
* @return string
|
||||
* The exclude key constant.
|
||||
*/
|
||||
public const EXCLUDE_KEY = 'exclude_category';
|
||||
|
||||
#[\Override]
|
||||
public static function get_filter_name() {
|
||||
return 'category';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the list of available filter options.
|
||||
*
|
||||
* @return array An array whose keys are the valid options
|
||||
* And whose values are the values to display
|
||||
*/
|
||||
#[\Override]
|
||||
public static function get_filter_options() {
|
||||
$options = \core_course_category::make_categories_list();
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the filter matches the specified tour and/or context.
|
||||
*
|
||||
* @param tour $tour The tour to check
|
||||
* @param context $context The context to check
|
||||
* @return boolean
|
||||
*/
|
||||
#[\Override]
|
||||
public static function add_filter_to_form(\MoodleQuickForm &$mform) {
|
||||
parent::add_filter_to_form($mform);
|
||||
$excludekey = 'filter_' . self::EXCLUDE_KEY;
|
||||
$mform->addElement(
|
||||
'select',
|
||||
$excludekey,
|
||||
get_string($excludekey, 'tool_usertours'),
|
||||
static::get_filter_options(),
|
||||
['multiple' => true]
|
||||
);
|
||||
$mform->addHelpButton($excludekey, $excludekey, 'tool_usertours');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public static function filter_matches(tour $tour, context $context) {
|
||||
$values = $tour->get_filter_values(self::get_filter_name());
|
||||
if (empty($values) || empty($values[0])) {
|
||||
// There are no values configured, meaning all.
|
||||
return true;
|
||||
$includevalues = $tour->get_filter_values(static::get_filter_name());
|
||||
$excludevalues = $tour->get_filter_values(self::EXCLUDE_KEY);
|
||||
|
||||
if (empty($includevalues) || empty($includevalues[0])) {
|
||||
return !static::check_contexts($context, $excludevalues);
|
||||
}
|
||||
|
||||
if ($context->contextlevel < CONTEXT_COURSECAT) {
|
||||
return false;
|
||||
}
|
||||
return self::check_contexts($context, $values);
|
||||
return self::check_contexts($context, $includevalues) && !self::check_contexts($context, $excludevalues);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,7 +79,11 @@ class category extends base {
|
||||
* @param array $values
|
||||
* @return boolean
|
||||
*/
|
||||
private static function check_contexts(context $context, $values) {
|
||||
private static function check_contexts(context $context, array $values): bool {
|
||||
if (empty($values)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($context->contextlevel > CONTEXT_COURSECAT) {
|
||||
return self::check_contexts($context->get_parent_context(), $values);
|
||||
} else if ($context->contextlevel == CONTEXT_COURSECAT) {
|
||||
@@ -86,4 +96,21 @@ class category extends base {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public static function prepare_filter_values_for_form(tour $tour, \stdClass $data) {
|
||||
parent::prepare_filter_values_for_form($tour, $data);
|
||||
$excludekey = 'filter_' . self::EXCLUDE_KEY;
|
||||
$data->$excludekey = $tour->get_filter_values(self::EXCLUDE_KEY);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public static function save_filter_values_from_form(tour $tour, \stdClass $data) {
|
||||
parent::save_filter_values_from_form($tour, $data);
|
||||
$excludekey = 'filter_' . self::EXCLUDE_KEY;
|
||||
$excludevalues = $data->$excludekey;
|
||||
$tour->set_filter_values(self::EXCLUDE_KEY, $excludevalues);
|
||||
}
|
||||
}
|
||||
|
||||
Regular → Executable
+90
-20
@@ -27,12 +27,23 @@ use context;
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class course extends base {
|
||||
/** @var string Option to select all courses. */
|
||||
public const OPERATOR_ALL = 'all';
|
||||
/** @var string Option to select specific courses. */
|
||||
public const OPERATOR_SELECT = 'select';
|
||||
/** @var string Option to select all courses except specific courses. */
|
||||
public const OPERATOR_EXCEPT = 'except';
|
||||
/** @var string The filter operator key constant. */
|
||||
public const OPERATOR_KEY = 'course_operator';
|
||||
/** @var string The filter key constant. */
|
||||
public const FILTER_KEY = 'filter_course';
|
||||
|
||||
/**
|
||||
* The name of the filter.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_filter_name() {
|
||||
public static function get_filter_name(): string {
|
||||
return 'course';
|
||||
}
|
||||
|
||||
@@ -42,14 +53,37 @@ class course extends base {
|
||||
* @param \MoodleQuickForm $mform
|
||||
*/
|
||||
public static function add_filter_to_form(\MoodleQuickForm &$mform) {
|
||||
// Add the operator selector.
|
||||
$operatorkey = 'filter_' . self::OPERATOR_KEY;
|
||||
$mform->addElement('select', $operatorkey, get_string($operatorkey, 'tool_usertours'), static::get_operator_options());
|
||||
$mform->setDefault($operatorkey, static::OPERATOR_ALL);
|
||||
$mform->addHelpButton($operatorkey, $operatorkey, 'tool_usertours');
|
||||
|
||||
// Add the course selector.
|
||||
$key = self::FILTER_KEY;
|
||||
$options = ['multiple' => true];
|
||||
|
||||
$filtername = self::get_filter_name();
|
||||
$key = "filter_{$filtername}";
|
||||
|
||||
$mform->addElement('course', $key, get_string($key, 'tool_usertours'), $options);
|
||||
$mform->addElement("course", $key, get_string($key, 'tool_usertours'), $options);
|
||||
$mform->setDefault($key, '0');
|
||||
$mform->addHelpButton($key, $key, 'tool_usertours');
|
||||
$mform->hideIf($key, $operatorkey, 'eq', self::OPERATOR_ALL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate form data specific to the course filter.
|
||||
*
|
||||
* @param array $data The current form data.
|
||||
* @param array $files The current form files.
|
||||
* @return array Any validation errors for this filter.
|
||||
*/
|
||||
public static function validate_form(array $data, array $files): array {
|
||||
$errors = [];
|
||||
$key = static::FILTER_KEY;
|
||||
$operatorkey = 'filter_' . self::OPERATOR_KEY;
|
||||
if ($data[$operatorkey] !== static::OPERATOR_ALL && empty($data[$key])) {
|
||||
$errors[$key] = get_string('filter_course_error_course_selection', 'tool_usertours');
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,17 +93,24 @@ class course extends base {
|
||||
* @param context $context The context to check
|
||||
* @return boolean
|
||||
*/
|
||||
public static function filter_matches(tour $tour, context $context) {
|
||||
public static function filter_matches(tour $tour, context $context): bool {
|
||||
global $COURSE;
|
||||
$values = $tour->get_filter_values(self::get_filter_name());
|
||||
$values = $tour->get_filter_values(static::get_filter_name());
|
||||
$operator = $tour->get_filter_values(static::OPERATOR_KEY)[0] ?? static::OPERATOR_ALL;
|
||||
|
||||
if (empty($values) || empty($values[0])) {
|
||||
// There are no values configured, meaning all.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (empty($COURSE->id)) {
|
||||
return false;
|
||||
}
|
||||
return in_array($COURSE->id, $values);
|
||||
|
||||
return match ($operator) {
|
||||
static::OPERATOR_SELECT => in_array($COURSE->id, $values),
|
||||
static::OPERATOR_EXCEPT => !in_array($COURSE->id, $values),
|
||||
default => true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,13 +121,18 @@ class course extends base {
|
||||
* @return stdClass
|
||||
*/
|
||||
public static function prepare_filter_values_for_form(tour $tour, \stdClass $data) {
|
||||
// Prepare the operator value.
|
||||
$operatorfiltername = static::OPERATOR_KEY;
|
||||
$operatorkey = 'filter_' . $operatorfiltername;
|
||||
$operator = $tour->get_filter_values($operatorfiltername)[0] ?? static::OPERATOR_ALL;
|
||||
$data->$operatorkey = $operator;
|
||||
|
||||
// Prepare the course value.
|
||||
$filtername = static::get_filter_name();
|
||||
$key = "filter_{$filtername}";
|
||||
$values = $tour->get_filter_values($filtername);
|
||||
if (empty($values)) {
|
||||
$values = 0;
|
||||
}
|
||||
$data->$key = $values;
|
||||
$key = 'filter_' . $filtername;
|
||||
$values = $tour->get_filter_values($filtername) ?: 0;
|
||||
$data->$key = $data->$operatorkey === static::OPERATOR_ALL ? 0 : $values;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
@@ -96,13 +142,37 @@ class course extends base {
|
||||
* @param tour $tour The tour to save values to
|
||||
* @param stdClass $data The data submitted in the form
|
||||
*/
|
||||
public static function save_filter_values_from_form(tour $tour, \stdClass $data) {
|
||||
public static function save_filter_values_from_form(
|
||||
tour $tour,
|
||||
\stdClass $data,
|
||||
) {
|
||||
$operatorfiltername = static::OPERATOR_KEY;
|
||||
$operatorkey = 'filter_' . $operatorfiltername;
|
||||
$tour->set_filter_values($operatorfiltername, [$data->$operatorkey]);
|
||||
$filtername = static::get_filter_name();
|
||||
$key = "filter_{$filtername}";
|
||||
$newvalue = $data->$key;
|
||||
if (empty($data->$key)) {
|
||||
if ($data->$operatorkey === static::OPERATOR_ALL) {
|
||||
$newvalue = [];
|
||||
} else {
|
||||
$key = 'filter_' . $filtername;
|
||||
$newvalue = $data->$key;
|
||||
if (empty($data->$key)) {
|
||||
$newvalue = [];
|
||||
}
|
||||
}
|
||||
$tour->set_filter_values($filtername, $newvalue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the available operator options.
|
||||
*
|
||||
* @return string[] The available operator options.
|
||||
*/
|
||||
public static function get_operator_options(): array {
|
||||
$operatorkey = 'filter_' . self::OPERATOR_KEY;
|
||||
return [
|
||||
static::OPERATOR_ALL => get_string($operatorkey . '_' . static::OPERATOR_ALL, 'tool_usertours'),
|
||||
static::OPERATOR_SELECT => get_string($operatorkey . '_' . static::OPERATOR_SELECT, 'tool_usertours'),
|
||||
static::OPERATOR_EXCEPT => get_string($operatorkey . '_' . static::OPERATOR_EXCEPT, 'tool_usertours'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Regular → Executable
+12
@@ -113,4 +113,16 @@ class edittour extends \moodleform {
|
||||
|
||||
$this->add_action_buttons();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function validation($data, $files): array {
|
||||
$errors = parent::validation($data, $files);
|
||||
|
||||
// Loop through each filter class and merge any validation errors.
|
||||
foreach (helper::get_all_filters() as $filterclass) {
|
||||
$errors = array_merge($errors, $filterclass::validate_form($data, $files));
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
|
||||
Regular → Executable
+1
@@ -398,6 +398,7 @@ class manager {
|
||||
}
|
||||
|
||||
$form->display();
|
||||
$PAGE->requires->js_call_amd('tool_usertours/tour_filters', 'init');
|
||||
$this->footer();
|
||||
}
|
||||
}
|
||||
|
||||
Regular → Executable
+23
-3
@@ -73,13 +73,33 @@ $string['filter_accessdate'] = 'Access date';
|
||||
$string['filter_accessdate_enabled'] = 'Enable access date filter';
|
||||
$string['filter_accessdate_enabled_help'] = 'Only show the tour to new users or users who have accessed the site recently.';
|
||||
$string['filter_category'] = 'Category';
|
||||
$string['filter_category_help'] = 'Show the tour on a page that is associated with a course in the selected category.';
|
||||
$string['filter_course'] = 'Courses';
|
||||
$string['filter_course_help'] = 'Show the tour on a page that is associated with the selected course.';
|
||||
$string['filter_category_help'] = "Show this tour on pages associated with courses in the selected categories.";
|
||||
$string['filter_course'] = 'Selected courses';
|
||||
$string['filter_course_error_course_selection'] = 'You must select at least one course';
|
||||
$string['filter_course_help'] = 'Depending on the value of the Courses filter:
|
||||
|
||||
* **Selected courses only**: Show this tour on pages associated with the selected courses.
|
||||
* **All courses except selected**: Do not show this tour on pages associated with the selected courses.';
|
||||
$string['filter_course_operator'] = 'Courses';
|
||||
$string['filter_course_operator_all'] = 'All courses';
|
||||
$string['filter_course_operator_except'] = 'All courses except selected';
|
||||
$string['filter_course_operator_help'] = "
|
||||
The tour can be configured to appear only on certain courses.
|
||||
|
||||
* **All courses**: Show this tour regardless of course.
|
||||
* **Selected courses only**: Show this tour only on pages associated with the course(s) selected below.
|
||||
* **All courses except selected**: Do not show this tour on pages associated with the course(s) selected below.";
|
||||
$string['filter_course_operator_select'] = 'Selected courses only';
|
||||
$string['filter_courseformat'] = 'Course format';
|
||||
$string['filter_courseformat_help'] = 'Show the tour on a page that is associated with a course using the selected course format.';
|
||||
$string['filter_cssselector'] = 'CSS selector';
|
||||
$string['filter_cssselector_help'] = 'Only show the tour when the specified CSS selector is found on the page.';
|
||||
$string['filter_exclude_category'] = 'Exclude categories';
|
||||
$string['filter_exclude_category_help'] = 'Do not show this tour on pages associated with the selected course categories.
|
||||
|
||||
If you select a parent category, the related sub-categories will automatically be selected and cannot be selected individually.
|
||||
|
||||
Unselect the parent category to select sub-categories again.';
|
||||
$string['filter_header'] = 'Tour filters';
|
||||
$string['filter_help'] = 'Select the conditions under which the tour will be shown. All of the filters must match for a tour to be shown to a user.';
|
||||
$string['filter_date_account_creation'] = 'User account creation date within';
|
||||
|
||||
Regular → Executable
+130
-3
@@ -74,11 +74,60 @@ Feature: Apply tour filters to a tour
|
||||
And I log in as "student1"
|
||||
When I am on "Course 1" course homepage
|
||||
And I wait until the page is ready
|
||||
Then I should see "Welcome to your course tour."
|
||||
And I should see "Welcome to your course tour."
|
||||
When I am on "Course 2" course homepage
|
||||
And I wait until the page is ready
|
||||
Then I should see "Welcome to your course tour."
|
||||
|
||||
@javascript
|
||||
Scenario: Add tour for multiple categories and exclude category
|
||||
Given the following "categories" exist:
|
||||
| name | category | idnumber |
|
||||
| MainCat | 0 | CAT1 |
|
||||
| SubCat | CAT1 | CAT2 |
|
||||
| SubCat2 | CAT1 | CAT3 |
|
||||
| MainCat2| 0 | CAT4 |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | CAT1 |
|
||||
| Course 2 | C2 | CAT2 |
|
||||
| Course 3 | C3 | CAT3 |
|
||||
| Course 4 | C4 | CAT4 |
|
||||
And the following "users" exist:
|
||||
| username |
|
||||
| student1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| student1 | C1 | student |
|
||||
| student1 | C2 | student |
|
||||
| student1 | C3 | student |
|
||||
| student1 | C4 | student |
|
||||
And I log in as "admin"
|
||||
And I add a new user tour with:
|
||||
| Name | First tour |
|
||||
| Description | My first tour |
|
||||
| Apply to URL match | /course/view.php% |
|
||||
| Tour is enabled | 1 |
|
||||
| Category | MainCat,MainCat2 |
|
||||
| Exclude categories | MainCat / SubCat2 |
|
||||
And I add steps to the "First tour" tour:
|
||||
| targettype | Title | id_content | Content type |
|
||||
| Display in middle of page | Welcome | Welcome to your course tour. | Manual |
|
||||
And I log out
|
||||
And I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
When I wait until the page is ready
|
||||
Then I should see "Welcome to your course tour."
|
||||
And I am on "Course 2" course homepage
|
||||
And I wait until the page is ready
|
||||
And I should see "Welcome to your course tour."
|
||||
And I am on "Course 3" course homepage
|
||||
And I wait until the page is ready
|
||||
And I should not see "Welcome to your course tour."
|
||||
And I am on "Course 4" course homepage
|
||||
And I wait until the page is ready
|
||||
And I should see "Welcome to your course tour."
|
||||
|
||||
@javascript
|
||||
Scenario: Add tour for a specific courseformat
|
||||
Given the following "courses" exist:
|
||||
@@ -111,6 +160,38 @@ Feature: Apply tour filters to a tour
|
||||
And I wait until the page is ready
|
||||
Then I should see "Welcome to your course tour."
|
||||
|
||||
@javascript
|
||||
Scenario: Add tour for a specific course with all courses filter
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | format |
|
||||
| Course 1 | C1 | topics |
|
||||
| Course 2 | C2 | weeks |
|
||||
And the following "users" exist:
|
||||
| username |
|
||||
| student1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| student1 | C1 | student |
|
||||
| student1 | C2 | student |
|
||||
And I log in as "admin"
|
||||
And I add a new user tour with:
|
||||
| Name | First tour |
|
||||
| Description | My first tour |
|
||||
| Apply to URL match | /course/view.php% |
|
||||
| Tour is enabled | 1 |
|
||||
| Courses | All courses |
|
||||
And I add steps to the "First tour" tour:
|
||||
| targettype | Title | id_content | Content type |
|
||||
| Display in middle of page | Welcome | Welcome to your course tour. | Manual |
|
||||
And I log out
|
||||
And I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I wait until the page is ready
|
||||
And I should see "Welcome to your course tour."
|
||||
When I am on "Course 2" course homepage
|
||||
And I wait until the page is ready
|
||||
Then I should see "Welcome to your course tour."
|
||||
|
||||
@javascript
|
||||
Scenario: Add tour for a specific course
|
||||
Given the following "courses" exist:
|
||||
@@ -130,10 +211,11 @@ Feature: Apply tour filters to a tour
|
||||
| Description | My first tour |
|
||||
| Apply to URL match | /course/view.php% |
|
||||
| Tour is enabled | 1 |
|
||||
| Courses | C1 |
|
||||
| Courses | Selected courses |
|
||||
| Selected courses | C1 |
|
||||
And I add steps to the "First tour" tour:
|
||||
| targettype | Title | id_content | Content type |
|
||||
| Display in middle of page | Welcome | Welcome to your course tour. | Manual |
|
||||
| Display in middle of page | Welcome | Welcome to your course tour. | Manual |
|
||||
And I log out
|
||||
And I log in as "student1"
|
||||
When I am on "Course 1" course homepage
|
||||
@@ -143,6 +225,39 @@ Feature: Apply tour filters to a tour
|
||||
And I wait until the page is ready
|
||||
Then I should not see "Welcome to your course tour."
|
||||
|
||||
@javascript
|
||||
Scenario: Add tour for a excluded course
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | format |
|
||||
| Course 1 | C1 | topics |
|
||||
| Course 2 | C2 | weeks |
|
||||
And the following "users" exist:
|
||||
| username |
|
||||
| student1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| student1 | C1 | student |
|
||||
| student1 | C2 | student |
|
||||
And I log in as "admin"
|
||||
And I add a new user tour with:
|
||||
| Name | First tour |
|
||||
| Description | My first tour |
|
||||
| Apply to URL match | /course/view.php% |
|
||||
| Tour is enabled | 1 |
|
||||
| Courses | All courses except selected |
|
||||
| Selected courses | C1 |
|
||||
And I add steps to the "First tour" tour:
|
||||
| targettype | Title | id_content | Content type |
|
||||
| Display in middle of page | Welcome | Welcome to your course tour. | Manual |
|
||||
And I log out
|
||||
And I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I wait until the page is ready
|
||||
And I should not see "Welcome to your course tour."
|
||||
When I am on "Course 2" course homepage
|
||||
And I wait until the page is ready
|
||||
Then I should see "Welcome to your course tour."
|
||||
|
||||
@javascript
|
||||
Scenario: Add tours with CSS selectors
|
||||
Given the following "users" exist:
|
||||
@@ -220,3 +335,15 @@ Feature: Apply tour filters to a tour
|
||||
And I click on "Move tour down" "link" in the "The first tour" "table_row"
|
||||
And I am on homepage
|
||||
Then I should see "Welcome to the Third tour"
|
||||
|
||||
@javascript
|
||||
Scenario: Show or hide the Exclude Categories option if the selected categories have no child categories
|
||||
Given I log in as "admin"
|
||||
And I open the User tour settings page
|
||||
When I click on "Create a new tour" "link"
|
||||
Then "Exclude categories" "select" should be visible
|
||||
And I should see "Category 1" in the "Exclude categories" "select"
|
||||
And I select "Category 1" from the "Category" singleselect
|
||||
And "Exclude categories" "select" should not be visible
|
||||
And I select "All" from the "Category" singleselect
|
||||
And I should see "Category 1" in the "Exclude categories" "select"
|
||||
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
<?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 tool_usertours;
|
||||
|
||||
use context_course;
|
||||
use context_coursecat;
|
||||
use context_system;
|
||||
use tool_usertours\local\filter\category;
|
||||
use context;
|
||||
|
||||
/**
|
||||
* Tests for category filter.
|
||||
*
|
||||
* @package tool_usertours
|
||||
* @copyright 2025 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \tool_usertours\local\filter\category
|
||||
*/
|
||||
final class category_filter_test extends \advanced_testcase {
|
||||
/** @var \core_course_category */
|
||||
private \core_course_category $category1;
|
||||
/** @var \core_course_category */
|
||||
private \core_course_category $category2;
|
||||
/** @var \core_course_category */
|
||||
private \core_course_category $childcategory1;
|
||||
/** @var \core_course_category */
|
||||
private \core_course_category $childcategory2;
|
||||
/** @var \stdClass */
|
||||
private \stdClass $course1;
|
||||
/** @var \stdClass */
|
||||
private \stdClass $course2;
|
||||
/** @var \stdClass */
|
||||
private \stdClass $coursechild1;
|
||||
|
||||
public function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create parent categories.
|
||||
$this->category1 = $this->getDataGenerator()->create_category();
|
||||
$this->category2 = $this->getDataGenerator()->create_category();
|
||||
|
||||
// Create child categories.
|
||||
$this->childcategory1 = $this->getDataGenerator()->create_category(['parent' => $this->category1->id]);
|
||||
$this->childcategory2 = $this->getDataGenerator()->create_category(['parent' => $this->category2->id]);
|
||||
|
||||
// Create courses.
|
||||
$this->course1 = $this->getDataGenerator()->create_course(['category' => $this->category1->id]);
|
||||
$this->course2 = $this->getDataGenerator()->create_course(['category' => $this->category2->id]);
|
||||
$this->coursechild1 = $this->getDataGenerator()->create_course(['category' => $this->childcategory1->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_filter_matches.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function filter_matches_provider(): array {
|
||||
return [
|
||||
'Parent category excluded, child category context' => [
|
||||
['exclude' => ['{{CATEGORY1_ID}}']],
|
||||
'category:{{CHILD_CATEGORY1_ID}}',
|
||||
false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the filter_matches method.
|
||||
*
|
||||
* @dataProvider filter_matches_provider
|
||||
* @param array $tourconfig Tour configuration
|
||||
* @param string $contextinfo Context information
|
||||
* @param bool $expected Expected result
|
||||
*/
|
||||
public function test_filter_matches(array $tourconfig, string $contextinfo, bool $expected): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Replace placeholder IDs with actual IDs.
|
||||
$tourconfig = $this->replace_ids($tourconfig);
|
||||
$contextinfo = $this->replace_ids($contextinfo);
|
||||
|
||||
$context = $this->create_context_from_string($contextinfo);
|
||||
|
||||
$tour = new tour();
|
||||
$tour->set_filter_values('category', $tourconfig['include'] ?? []);
|
||||
$tour->set_filter_values('exclude_category', $tourconfig['exclude'] ?? []);
|
||||
|
||||
$result = category::filter_matches($tour, $context);
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the get_filter_name method.
|
||||
*/
|
||||
public function test_get_filter_name(): void {
|
||||
$this->assertEquals('category', category::get_filter_name());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the get_filter_options method.
|
||||
*/
|
||||
public function test_get_filter_options(): void {
|
||||
$options = category::get_filter_options();
|
||||
|
||||
$this->assertIsArray($options);
|
||||
$this->assertArrayHasKey($this->category1->id, $options);
|
||||
$this->assertArrayHasKey($this->category2->id, $options);
|
||||
$this->assertArrayHasKey($this->childcategory1->id, $options);
|
||||
$this->assertArrayHasKey($this->childcategory2->id, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a context object from a string.
|
||||
*
|
||||
* @param string $contextinfo The context information.
|
||||
* @return context The context object.
|
||||
*/
|
||||
private function create_context_from_string(string $contextinfo): context {
|
||||
$parts = explode(':', $contextinfo);
|
||||
$contextlevel = $parts[0];
|
||||
$instanceid = isset($parts[1]) && is_numeric($parts[1]) ? (int)$parts[1] : 0;
|
||||
|
||||
return match ($contextlevel) {
|
||||
'system' => context_system::instance(),
|
||||
'category' => context_coursecat::instance($instanceid),
|
||||
'course' => context_course::instance($instanceid)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace placeholder IDs with actual IDs.
|
||||
*
|
||||
* @param mixed $data The data to process.
|
||||
* @return mixed The processed data.
|
||||
*/
|
||||
private function replace_ids($data) {
|
||||
if (is_array($data)) {
|
||||
return array_map([$this, 'replace_ids'], $data);
|
||||
} else if (is_string($data)) {
|
||||
$replacements = [
|
||||
'{{CATEGORY1_ID}}' => $this->category1->id,
|
||||
'{{CATEGORY2_ID}}' => $this->category2->id,
|
||||
'{{CHILD_CATEGORY1_ID}}' => $this->childcategory1->id,
|
||||
'{{CHILD_CATEGORY2_ID}}' => $this->childcategory2->id,
|
||||
'{{COURSE1_ID}}' => $this->course1->id,
|
||||
'{{COURSE2_ID}}' => $this->course2->id,
|
||||
'{{COURSE_CHILD1_ID}}' => $this->coursechild1->id,
|
||||
];
|
||||
return strtr($data, $replacements);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
<?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 tool_usertours;
|
||||
|
||||
use tool_usertours\local\filter\course;
|
||||
|
||||
/**
|
||||
* Tests for course filter.
|
||||
*
|
||||
* @package tool_usertours
|
||||
* @copyright 2025 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \tool_usertours\local\filter\course
|
||||
*/
|
||||
final class course_filter_test extends \advanced_testcase {
|
||||
public function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->resetAfterTest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Data Provider for filter_matches method.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function filter_matches_provider(): array {
|
||||
return [
|
||||
'No filter set; Matches' => [
|
||||
'all',
|
||||
true,
|
||||
],
|
||||
'Select specific courses; Match' => [
|
||||
'select',
|
||||
true,
|
||||
],
|
||||
'Select specific courses; No match' => [
|
||||
'select',
|
||||
false,
|
||||
],
|
||||
'Except specific courses; Match' => [
|
||||
'except',
|
||||
true,
|
||||
],
|
||||
'Except specific courses; No match' => [
|
||||
'except',
|
||||
false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test filter matches.
|
||||
*
|
||||
* @dataProvider filter_matches_provider
|
||||
*
|
||||
* @param string $operator the filter operator.
|
||||
* @param bool $expected result expected.
|
||||
*/
|
||||
public function test_filter_matches(string $operator, bool $expected): void {
|
||||
global $COURSE;
|
||||
|
||||
// Create courses for testing.
|
||||
$course1 = $this->getDataGenerator()->create_course();
|
||||
$course2 = $this->getDataGenerator()->create_course();
|
||||
$course3 = $this->getDataGenerator()->create_course();
|
||||
|
||||
// Set global $COURSE variable to the first course created.
|
||||
$COURSE = $course1;
|
||||
|
||||
$tour = new tour();
|
||||
if ($operator === course::OPERATOR_SELECT) {
|
||||
// Test case for selecting specific courses.
|
||||
$tour->set_filter_values('course', $expected ? [$course1->id, $course2->id] : [$course2->id, $course3->id]);
|
||||
} else if ($operator === course::OPERATOR_EXCEPT) {
|
||||
// Test case for excluding specific courses.
|
||||
$tour->set_filter_values('course', $expected ? [$course2->id, $course3->id] : [$course1->id, $course2->id]);
|
||||
}
|
||||
|
||||
$tour->set_filter_values('course_operator', [$operator]);
|
||||
|
||||
$context = \context_course::instance($COURSE->id);
|
||||
|
||||
$this->assertEquals($expected, course::filter_matches($tour, $context));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test validating course selection.
|
||||
*/
|
||||
public function test_validate_form(): void {
|
||||
$fields = [
|
||||
'filter_course_operator' => course::OPERATOR_SELECT,
|
||||
'filter_course' => [],
|
||||
];
|
||||
$errors = [];
|
||||
|
||||
$errors = course::validate_form($fields, $errors);
|
||||
|
||||
$this->assertArrayHasKey('filter_course', $errors);
|
||||
$this->assertEquals(
|
||||
get_string('filter_course_error_course_selection', 'tool_usertours'),
|
||||
$errors['filter_course']
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user