MDL-74149 Usertours: Filters to exclude categories and courses

This commit is contained in:
vietlx426
2025-03-21 15:57:53 +07:00
parent c64e967ce6
commit 1aca8a1f00
12 changed files with 725 additions and 48 deletions
+3
View File
@@ -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
+117
View File
@@ -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 [];
}
}
+49 -22
View File
@@ -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);
}
}
+90 -20
View File
@@ -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'),
];
}
}
+12
View File
@@ -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;
}
}
+1
View File
@@ -398,6 +398,7 @@ class manager {
}
$form->display();
$PAGE->requires->js_call_amd('tool_usertours/tour_filters', 'init');
$this->footer();
}
}
+23 -3
View File
@@ -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';
+130 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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']
);
}
}