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,67 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
/**
* Availability related behat steps and selectors definitions.
*
* @package core_availability
* @category test
* @copyright 2023 Amaia Anabitarte <amaia@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_availability extends behat_base {
/**
* Return the list of partial named selectors.
*
* @return array
*/
public static function get_partial_named_selectors(): array {
return [
new behat_component_named_selector(
'Activity availability', [
".//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')]"
. "[descendant::*[contains(normalize-space(.), %locator%)]]//div[@data-region='availabilityinfo']",
]
),
new behat_component_named_selector(
'Section availability', [".//li[@id = %locator%]//div[@data-region='availabilityinfo']"],
),
new behat_component_named_selector(
'Set Of Restrictions', ["//div[h3[@data-restriction-order=%locator% and contains(text(), 'Set of')]]"],
),
];
}
/**
* Return the list of exact named selectors
*
* @return array
*/
public static function get_exact_named_selectors(): array {
return [
new behat_component_named_selector(
'Availability Button Area',
[
"//h3[@data-restriction-order=%locator%]/following-sibling::div[contains(@class,'availability-inner')]/"
. "div[contains(@class,'availability-button')]",
],
),
];
}
}
@@ -0,0 +1,166 @@
@core @core_availability
Feature: Display availability for activities and sections
In order to know which activities are available
As a user
I need to see appropriate availability restrictions for activities and sections
# PURPOSE OF THIS TEST FEATURE:
#
# This test is to do a basic check of the user interface relating to display
# of availability conditions - i.e. if there's a condition, does it show up;
# are we doing the HTML correctly; does it correctly hide an activity where
# the options are set to not show it at all.
#
# Things this test is not:
# - It is not a test of the date condition specifically. The date condition is
# only used as an example in order to get the availability information to
# display. (The date condition has its own Behat test in
# /availability/condition/date/tests/behat.)
# - It is not a complete test of the logic. This is supposed to be a shallow
# check of the user interface parts and doesn't cover all logical
# possibilities. The logic is tested in PHPUnit tests instead, which are
# much more efficient. (Again there are unit tests for the overall system
# and for each condition type.)
Background:
Given the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
| format | topics |
| initsections | 1 |
And the following "users" exist:
| username |
| teacher1 |
| student1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "activities" exist:
| activity | course | section | name |
| page | C1 | 1 | Page 1 |
| page | C1 | 2 | Page 2 |
| page | C1 | 3 | Page 3 |
@javascript
Scenario: Activity availability display
# Set up.
Given I am on the "Page 1" "page activity editing" page logged in as "teacher1"
And I expand all fieldsets
And I press "Add restriction..."
And I click on "Date" "button" in the "Add restriction..." "dialogue"
And I set the field "direction" to "until"
And I set the field "x[year]" to "2013"
And I set the field "x[month]" to "March"
And I press "Save and return to course"
# Add a Page with 2 restrictions - one is set to hide from students if failed.
And I am on the "Page 2" "page activity editing" page
And I expand all fieldsets
And I press "Add restriction..."
And I click on "Date" "button" in the "Add restriction..." "dialogue"
And I set the field "direction" to "until"
And I set the field "x[year]" to "2013"
And I set the field "x[month]" to "March"
And I click on ".availability-item .availability-eye img" "css_element"
And I press "Add restriction..."
And I click on "User profile" "button" in the "Add restriction..." "dialogue"
And I set the field "User profile field" to "Email address"
And I set the field "Value to compare against" to "email@example.com"
And I set the field "Method of comparison" to "is equal to"
And I press "Save and return to course"
# Page 1 should show in single-line format, showing the date
Then I should see "Available until" in the "Page 1" "core_availability > Activity availability"
And I should see "2013" in the "Page 1" "core_availability > Activity availability"
And I should see "2013" in the "Page 1" "core_availability > Activity availability"
And "li" "css_element" should not exist in the "Page 1" "core_availability > Activity availability"
And "Show more" "button" should not exist in the "Page 1" "core_availability > Activity availability"
# Page 2 should show in list format.
And "li" "css_element" should exist in the "Page 2" "core_availability > Activity availability"
And I should see "Not available unless:" in the "Page 2" "core_availability > Activity availability"
And I should see "It is before" in the "Page 2" "core_availability > Activity availability"
And I should see "hidden otherwise" in the "Page 2" "core_availability > Activity availability"
And I click on "Show more" "button" in the "Page 2" "activity"
And I should see "Email address" in the "Page 2" "core_availability > Activity availability"
And I click on "Show less" "button" in the "Page 2" "core_availability > Activity availability"
And I should not see "Email address" in the "Page 2" "core_availability > Activity availability"
# Page 3 should not have available info.
And "Page 3" "core_availability > Activity availability" should not exist
# Change to student view.
Given I am on the "C1" "Course" page logged in as "student1"
# Page 1 display still there but should not be a link.
Then I should see "Page 1" in the "#section-1" "css_element"
And ".activity-instance a" "css_element" should not exist in the "Section 1" "section"
# Date display should be present.
And I should see "Available until" in the "Section 1" "section"
# Page 2 display not there at all
And I should not see "Page 2" in the "region-main" "region"
# Page 3 display and link
And I should see "Page 3" in the "region-main" "region"
And ".activity-instance a" "css_element" should exist in the "Section 3" "section"
@javascript
Scenario: Section availability display
# Set up.
Given I am on the "C1" "Course" page logged in as "teacher1"
And I turn editing mode on
# Add a restriction to section 1 (visible to students).
When I edit the section "1"
And I expand all fieldsets
And I press "Add restriction..."
And I click on "Date" "button" in the "Add restriction..." "dialogue"
And I set the field "direction" to "until"
And I set the field "x[year]" to "2013"
And I press "Add restriction..."
And I click on "User profile" "button" in the "Add restriction..." "dialogue"
And I set the field "User profile field" to "Email address"
And I set the field "Value to compare against" to "email@example.com"
And I set the field "Method of comparison" to "is equal to"
And I press "Save changes"
# Section 2 is the same but hidden from students
And I am on "Course 1" course homepage
And I edit the section "2"
And I expand all fieldsets
And I press "Add restriction..."
And I click on "Date" "button" in the "Add restriction..." "dialogue"
And I set the field "direction" to "until"
And I set the field "x[year]" to "2013"
And I click on ".availability-item .availability-eye img" "css_element"
And I press "Save changes"
# This is necessary because otherwise it fails in Chrome, see MDL-44959
And I am on "Course 1" course homepage
# Check display
Then I should see "Not available unless" in the "section-1" "core_availability > Section availability"
And I should see "Available until" in the "section-2" "core_availability > Section availability"
And I should see "hidden otherwise" in the "section-2" "core_availability > Section availability"
# Change to student view.
Given I am on the "Course 1" "Course" page logged in as "student1"
# The contents of both sections should be hidden.
Then I should not see "Page 1" in the "region-main" "region"
And I should not see "Page 2" in the "region-main" "region"
And I should see "Page 3" in the "region-main" "region"
# Section 1 should be visible and show info.
And I should see "Section 1" in the "region-main" "region"
And I should see "Not available unless" in the "section-1" "core_availability > Section availability"
And I click on "Show more" "button" in the "section-1" "core_availability > Section availability"
And I should see "Email address" in the "section-1" "core_availability > Section availability"
And I click on "Show less" "button" in the "section-1" "core_availability > Section availability"
And I should not see "Email address" in the "section-1" "core_availability > Section availability"
# Section 2 should not be available at all
And I should not see "Section 2" in the "region-main" "region"
@@ -0,0 +1,279 @@
@core @core_availability
Feature: edit_availability
In order to control which students can see activities
As a teacher
I need to set up availability options for activities and sections
# PURPOSE OF THIS TEST FEATURE:
#
# This test covers the user interface around editing availability conditions,
# especially the JavaScript code which is not tested elsewhere (e.g. does the
# 'Add restriction' dialog work). It tests both forms and also the admin
# setting interface.
#
# This test does not check the detailed behaviour of the availability system,
# which is mainly covered in PHPUnit (and, from the user interface
# perspective, in the other Behat tests for each type of condition).
Background:
Given the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "users" exist:
| username |
| teacher1 |
| student1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "activity" exists:
| activity | forum |
| course | C1 |
| name | MyForum |
Scenario: Confirm the 'enable availability' option is working
Given the following config values are set as admin:
| enableavailability | 0 |
When I log in as "teacher1"
And the following "activity" exists:
| activity | page |
| course | C1 |
| idnumber | 0001 |
| section | 1 |
| name | Page1 |
| intro | pageintro |
And I am on "Course 1" course homepage with editing mode on
And I follow "Page1"
And I navigate to "Settings" in current page administration
Then "Restrict access" "fieldset" should not exist
Given I am on "Course 1" course homepage
When I edit the section "1"
Then "Restrict access" "fieldset" should not exist
And the following config values are set as admin:
| enableavailability | 1 |
And the following "activity" exists:
| activity | page |
| course | C1 |
| idnumber | 0002 |
| name | Page2 |
And I am on the "Page2" "page activity editing" page
Then "Restrict access" "fieldset" should exist
Given I am on "Course 1" course homepage
When I edit the section "1"
Then "Restrict access" "fieldset" should exist
@javascript
Scenario: Edit availability using settings in activity form
# Set up.
Given the following "activity" exists:
| activity | page |
| course | C1 |
| section | 1 |
| name | P1 |
And I am on the "P1" "page activity editing" page logged in as "teacher1"
And I expand all fieldsets
Then I should see "None" in the "Restrict access" "fieldset"
# Add a Date restriction and check it appears.
When I click on "Add restriction..." "button"
Then "Add restriction..." "dialogue" should be visible
When I click on "Date" "button" in the "Add restriction..." "dialogue"
Then "Add restriction..." "dialogue" should not exist
And I should not see "None" in the "Restrict access" "fieldset"
And "Restriction type" "select" should be visible
And I should see "Date" in the "Restrict access" "fieldset"
And ".availability-item .availability-eye img" "css_element" should be visible
And ".availability-item .availability-delete img" "css_element" should be visible
And the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Displayed if student"
# Toggle the eye icon.
When I click on ".availability-item .availability-eye img" "css_element"
Then the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Hidden entirely"
When I click on ".availability-item .availability-eye img" "css_element"
Then the "alt" attribute of ".availability-item .availability-eye img" "css_element" should contain "Displayed if student"
# Click the delete button.
When I click on ".availability-item .availability-delete img" "css_element"
Then I should not see "Date" in the "Restrict access" "fieldset"
# Add a nested restriction set and check it appears.
When I click on "Add restriction..." "button"
And I click on "Restriction set" "button" in the "Add restriction..." "dialogue"
Then ".availability-children .availability-list" "css_element" should be visible
And I should see "None" in the ".availability-children .availability-list" "css_element"
And I should see "Please set" in the ".availability-children .availability-list" "css_element"
And I should see "Add restriction" in the ".availability-children .availability-list" "css_element"
# Click on the button to add a restriction inside the nested set.
When I click on "Add restriction..." "button" in the ".availability-children .availability-list" "css_element"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
Then I should not see "None" in the ".availability-children .availability-list" "css_element"
And I should not see "Please set" in the ".availability-children .availability-list" "css_element"
And I should see "Date" in the ".availability-children .availability-list" "css_element"
# OK, let's delete the date inside the nested set...
When I click on ".availability-item .availability-delete img" "css_element" in the ".availability-item" "css_element"
Then I should not see "Date" in the ".availability-children .availability-list" "css_element"
And I should see "None" in the ".availability-children .availability-list" "css_element"
# ...and the nested set itself.
When I click on ".availability-none .availability-delete img" "css_element"
Then ".availability-children .availability-list" "css_element" should not exist
# Add two dates so we can check the connectors.
When I click on "Add restriction..." "button"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
And I click on "Add restriction..." "button"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
Then I should see "and" in the "Restrict access" "fieldset"
And "Required restrictions" "select" should be visible
# Try changing the connector type.
When I set the field "Required restrictions" to "any"
Then I should not see "and" in the "Restrict access" "fieldset"
And I should see "or" in the "Restrict access" "fieldset"
# Now delete one of the dates and check the connector goes away.
When I click on ".availability-item .availability-delete img" "css_element"
Then I should not see "or" in the "Restrict access" "fieldset"
# Add a nested restriction set with two dates so there will be inner connector.
When I click on "Add restriction..." "button"
And I click on "Restriction set" "button" in the "Add restriction..." "dialogue"
And I click on "Add restriction..." "button" in the ".availability-children .availability-list" "css_element"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
And I click on "Add restriction..." "button" in the ".availability-children .availability-list" "css_element"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
Then I should see "and" in the ".availability-children .availability-list .availability-connector" "css_element"
# Check changing the outer one does not affect the inner one.
When I set the field "Required restrictions" to "all"
Then I should not see "or" in the "Restrict access" "fieldset"
When I set the field "Required restrictions" to "any"
Then I should see "or" in the "Restrict access" "fieldset"
And I should not see "or" in the ".availability-children .availability-list .availability-connector" "css_element"
@javascript
Scenario: Edit availability using settings in section form
# Set up.
Given I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
# Edit a section
When I edit the section "1"
And I expand all fieldsets
Then I should see "None" in the "Restrict access" "fieldset"
# Add a Date restriction and check it appears.
When I click on "Add restriction..." "button"
When I click on "Date" "button" in the "Add restriction..." "dialogue"
And I should not see "None" in the "Restrict access" "fieldset"
And "Restriction type" "select" should be visible
And I should see "Date" in the "Restrict access" "fieldset"
@javascript
Scenario: 'Add group/grouping access restriction' button unavailable
# Button does not exist when conditional access restrictions are turned off.
Given the following config values are set as admin:
| enableavailability | 0 |
And I am on the "MyForum" "forum activity editing" page logged in as admin
When I expand all fieldsets
Then "Add group/grouping access restriction" "button" should not exist
@javascript
Scenario: Use the 'Add group/grouping access restriction' button
# Button should initially be disabled.
Given the following "groupings" exist:
| name | course | idnumber |
| GX1 | C1 | GXI1 |
And I am on the "MyForum" "forum activity editing" page logged in as admin
When I expand all fieldsets
Then the "Add group/grouping access restriction" "button" should be disabled
# Turn on separate groups.
And I set the field "Group mode" to "Separate groups"
And the "Add group/grouping access restriction" "button" should be enabled
# Press the button and check it adds a restriction and disables itself.
And I should see "None" in the "Restrict access" "fieldset"
And I press "Add group/grouping access restriction"
And I should see "Group" in the "Restrict access" "fieldset"
And the "Add group/grouping access restriction" "button" should be disabled
# Delete the restriction and check it is enabled again.
And I click on "Delete" "link" in the "Restrict access" "fieldset"
And the "Add group/grouping access restriction" "button" should be enabled
# Try a grouping instead.
And I set the field "Grouping" to "GX1"
And I press "Add group/grouping access restriction"
And I should see "Grouping" in the "Restrict access" "fieldset"
# Check the button still works after saving and editing.
And I press "Save and display"
And I navigate to "Settings" in current page administration
And I expand all fieldsets
And the "Add group/grouping access restriction" "button" should be disabled
And I should see "Grouping" in the "Restrict access" "fieldset"
# And check it's still active if I delete the condition.
And I click on "Delete" "link" in the "Restrict access" "fieldset"
And the "Add group/grouping access restriction" "button" should be enabled
@javascript
Scenario: Edit section availability using course page link
# Setting a restriction up
Given I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I edit the section "1"
And I expand all fieldsets
And I press "Add restriction..."
And I click on "Date" "button" in the "Add restriction..." "dialogue"
And I press "Save changes"
# Testing edit restrictions link
And "Edit restrictions" "link" should exist in the "section-1" "core_availability > Section availability"
When I click on "Edit restrictions" "link" in the "section-1" "core_availability > Section availability"
Then I should see "Restrict access"
And I should not see "Summary of General"
And I should see "Collapse all"
And I should not see "Expand all"
And I click on "Cancel" "button"
And I am on "Course 1" course homepage with editing mode off
And I should not see "Edit restrictions"
@javascript
Scenario: Edit activity availability using course page link
# Setting a restriction up
Given I am on the "MyForum" "forum activity editing" page logged in as teacher1
And I expand all fieldsets
And I press "Add restriction..."
And I click on "Date" "button" in the "Add restriction..." "dialogue"
When I press "Save and return to course"
# Edit restrictions link not displayed when editing mode is off.
Then "Edit restrictions" "link" should not exist in the "MyForum" "core_availability > Activity availability"
# Testing edit restrictions link
But I am on "Course 1" course homepage with editing mode on
And "Edit restrictions" "link" should exist in the "MyForum" "core_availability > Activity availability"
And I click on "Edit restrictions" "link" in the "MyForum" "core_availability > Activity availability"
And I should see "Restrict access"
And I should not see "Content"
And I should see "Collapse all"
And I should not see "Expand all"
@javascript
Scenario: Edit activity availability button is shown after duplicating an activity
# Setting a restriction up
Given I am on the "MyForum" "forum activity editing" page logged in as teacher1
And I expand all fieldsets
And I press "Add restriction..."
And I click on "Date" "button" in the "Add restriction..." "dialogue"
When I press "Save and return to course"
And I turn editing mode on
And I duplicate "MyForum" activity
# Testing edit restrictions link
Then "Edit restrictions" "link" should exist in the "MyForum (copy)" "core_availability > Activity availability"
@@ -0,0 +1,149 @@
@core @core_availability @javascript
Feature: Private rule sets
In order to prevent private data being leaked in restriction sets
As a teacher
I want to have restrictions hidden when a private condition is selected
Background:
Given the following "courses" exist:
| fullname | shortname | format | enablecompletion | numsections |
| Course 1 | C1 | topics | 1 | 3 |
And the following "users" exist:
| username |
| teacher1 |
| student1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "groups" exist:
| name | course | idnumber | visibility |
| Group A | C1 | GA | 0 |
| Group B | C1 | GB | 1 |
And I log in as "teacher1"
And I add a page activity to course "Course 1" section "1"
And I expand all fieldsets
Scenario: Add restriction with visible condition (must match), display option should be active
When I click on "Add restriction..." "button"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
Then ".availability-children .availability-eye" "css_element" should be visible
And ".availability-children .availability-eye-disabled" "css_element" should not be visible
And the "title" attribute of ".availability-eye" "css_element" should contain "Click to hide"
Scenario: Add restriction with private condition (must match), display option should be disabled
When I click on "Add restriction..." "button"
And I click on "Group" "button" in the "Add restriction..." "dialogue"
And I set the field "Group" to "Group B"
Then ".availability-children .availability-eye" "css_element" should not be visible
And ".availability-children .availability-eye-disabled" "css_element" should be visible
And the "title" attribute of ".availability-eye-disabled" "css_element" should contain "Cannot be changed as ruleset includes a rule containing private data."
Scenario: Add restrictions with a visible and a private condition (must match all), display option should be disabled
When I click on "Add restriction..." "button"
And I click on "Group" "button" in the "Add restriction..." "dialogue"
And I set the field "Group" to "Group B"
When I click on "Add restriction..." "button"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
Then ".availability-children .availability-eye" "css_element" should not be visible
And ".availability-children .availability-eye-disabled" "css_element" should be visible
Scenario: Remove private condition (must match), display option should be active
When I click on "Add restriction..." "button"
And I click on "Group" "button" in the "Add restriction..." "dialogue"
And I set the field "Group" to "Group B"
And I click on "Add restriction..." "button"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
Then ".availability-children .availability-eye" "css_element" should not be visible
And ".availability-children .availability-eye-disabled" "css_element" should be visible
# Should pick the first one (Group B)
When I click on ".availability-item .availability-delete img" "css_element"
Then ".availability-children .availability-eye" "css_element" should be visible
And ".availability-children .availability-eye-disabled" "css_element" should not be visible
Scenario: Set a private condition to a visible value (must match), display option should be active
When I click on "Add restriction..." "button"
And I click on "Group" "button" in the "Add restriction..." "dialogue"
And I set the field "Group" to "Group B"
Then ".availability-children .availability-eye" "css_element" should not be visible
And ".availability-children .availability-eye-disabled" "css_element" should be visible
# Should pick the first one (Group B)
When I set the field "Group" to "Group A"
Then ".availability-children .availability-eye" "css_element" should be visible
And ".availability-children .availability-eye-disabled" "css_element" should not be visible
Scenario: Add restrictions with a visible and a private condition (must match any), display option should be disabled
When I click on "Add restriction..." "button"
And I click on "Group" "button" in the "Add restriction..." "dialogue"
And I set the field "Group" to "Group B"
And I click on "Add restriction..." "button"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
And I set the field "Required restrictions" to "any"
# "Hidden" icon should be shown in header.
And ".availability-children .availability-eye" "css_element" should not be visible
And ".availability-children .availability-eye-disabled" "css_element" should not be visible
And ".availability-header .availability-eye" "css_element" should not be visible
And ".availability-header .availability-eye-disabled" "css_element" should be visible
Scenario: Add restriction with private condition (must not match), display option should be disabled
When I click on "Add restriction..." "button"
And I click on "Group" "button" in the "Add restriction..." "dialogue"
And I set the field "Group" to "Group B"
And I set the field "Restriction type" to "must not"
# "Hidden" icon should be shown in header.
And ".availability-children .availability-eye" "css_element" should not be visible
And ".availability-children .availability-eye-disabled" "css_element" should not be visible
And ".availability-header .availability-eye" "css_element" should not be visible
And ".availability-header .availability-eye-disabled" "css_element" should be visible
Scenario: Add restrictions with a visible and a private condition (must not match all), display option should be disabled
When I click on "Add restriction..." "button"
And I click on "Group" "button" in the "Add restriction..." "dialogue"
And I set the field "Group" to "Group B"
And I click on "Add restriction..." "button"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
And I set the field "Restriction type" to "must not"
# "Hidden" icon should be shown in header.
And ".availability-children .availability-eye" "css_element" should not be visible
And ".availability-children .availability-eye-disabled" "css_element" should not be visible
And ".availability-header .availability-eye" "css_element" should not be visible
And ".availability-header .availability-eye-disabled" "css_element" should be visible
Scenario: Add restrictions with a visible and a private condition (must not match any), display option should be disabled
When I click on "Add restriction..." "button"
And I click on "Group" "button" in the "Add restriction..." "dialogue"
And I set the field "Group" to "Group B"
And I click on "Add restriction..." "button"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
And I set the field "Restriction type" to "must not"
And I set the field "Required restrictions" to "any"
# "Hidden" icon should be shown in conditions, but not in the header.
And ".availability-header .availability-eye" "css_element" should not be visible
And ".availability-header .availability-eye-disabled" "css_element" should not be visible
And ".availability-children .availability-eye" "css_element" should not be visible
And ".availability-children .availability-eye-disabled" "css_element" should be visible
Scenario: Private conditions should not show to unprivileged users
Given I set the field "Name" to "Test page"
And I set the field "Page content" to "test"
And I click on "Add restriction..." "button"
And I click on "Group" "button" in the "Add restriction..." "dialogue"
And I set the field "Group" to "Group B"
And I press "Save and return to course"
And I log out
And I log in as "student1"
When I am on "Course 1" course homepage
Then I should not see "Test page"
And I should not see "Not available unless: You belong to Group B"
Scenario: Loading a rule set containing private conditions should disable display option
Given I set the field "Name" to "Test page"
And I set the field "Page content" to "test"
And I click on "Add restriction..." "button"
And I click on "Group" "button" in the "Add restriction..." "dialogue"
And I set the field "Group" to "Group B"
And I press "Save and display"
When I follow "Settings"
And I expand all fieldsets
Then ".availability-children .availability-eye" "css_element" should not be visible
And ".availability-children .availability-eye-disabled" "css_element" should be visible
@@ -0,0 +1,60 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_availability;
/**
* Unit tests for the capability checker class.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class capability_checker_test extends \advanced_testcase {
/**
* Tests loading a class from /availability/classes.
*/
public function test_capability_checker(): void {
global $CFG, $DB;
$this->resetAfterTest();
// Create a course with teacher and student.
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$roleids = $DB->get_records_menu('role', null, '', 'shortname, id');
$teacher = $generator->create_user();
$student = $generator->create_user();
$generator->enrol_user($teacher->id, $course->id, $roleids['teacher']);
$generator->enrol_user($student->id, $course->id, $roleids['student']);
// Check a capability which they both have.
$context = \context_course::instance($course->id);
$checker = new capability_checker($context);
$result = array_keys($checker->get_users_by_capability('mod/forum:replypost'));
sort($result);
$this->assertEquals(array($teacher->id, $student->id), $result);
// And one that only teachers have.
$result = array_keys($checker->get_users_by_capability('mod/forum:deleteanypost'));
$this->assertEquals(array($teacher->id), $result);
// Check the caching is working.
$before = $DB->perf_get_queries();
$result = array_keys($checker->get_users_by_capability('mod/forum:deleteanypost'));
$this->assertEquals(array($teacher->id), $result);
$this->assertEquals($before, $DB->perf_get_queries());
}
}
+51
View File
@@ -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/>.
namespace core_availability;
/**
* Unit tests for the component and plugin definitions for availability system.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class component_test extends \advanced_testcase {
/**
* Tests loading a class from /availability/classes.
*/
public function test_load_class(): void {
$result = get_class_methods('\core_availability\info');
$this->assertTrue(is_array($result));
}
/**
* Tests the plugininfo class is present and working.
*/
public function test_plugin_info(): void {
// This code will throw debugging information if the plugininfo class
// is missing. Unfortunately it doesn't actually cause the test to
// fail, but it's obvious when running test at least.
$pluginmanager = \core_plugin_manager::instance();
$list = $pluginmanager->get_enabled_plugins('availability');
$this->assertArrayHasKey('completion', $list);
$this->assertArrayHasKey('date', $list);
$this->assertArrayHasKey('grade', $list);
$this->assertArrayHasKey('group', $list);
$this->assertArrayHasKey('grouping', $list);
$this->assertArrayHasKey('profile', $list);
}
}
+139
View File
@@ -0,0 +1,139 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Mock condition.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace availability_mock;
defined('MOODLE_INTERNAL') || die();
/**
* Mock condition.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class condition extends \core_availability\condition {
/** @var bool True if available */
protected $available;
/** @var string Message if not available */
protected $message;
/** @var bool True if available for all (normal state) */
protected $forall;
/** @var bool True if available for all (NOT state) */
protected $forallnot;
/** @var string Dependency table (empty if none) */
protected $dependtable;
/** @var id Dependency id (0 if none) */
protected $dependid;
/** @var array Array of user ids for filter results, empty if no filter support */
protected $filter;
/**
* Constructs a mock condition with given structure.
*
* @param \stdClass $structure Structure object
*/
public function __construct($structure) {
$this->available = isset($structure->a) ? $structure->a : false;
$this->message = isset($structure->m) ? $structure->m : '';
$this->forall = isset($structure->all) ? $structure->all : false;
$this->forallnot = isset($structure->allnot) ? $structure->allnot : false;
$this->dependtable = isset($structure->table) ? $structure->table : '';
$this->dependid = isset($structure->id) ? $structure->id : 0;
$this->filter = isset($structure->filter) ? $structure->filter : array();
}
public function save() {
return (object)array('a' => $this->available, 'm' => $this->message,
'all' => $this->forall, 'allnot' => $this->forallnot,
'table' => $this->dependtable, 'id' => $this->dependid,
'filter' => $this->filter);
}
public function is_available($not, \core_availability\info $info, $grabthelot, $userid) {
return $not ? !$this->available : $this->available;
}
public function is_available_for_all($not = false) {
return $not ? $this->forallnot : $this->forall;
}
public function get_description($full, $not, \core_availability\info $info) {
$fulltext = $full ? '[FULL]' : '';
$nottext = $not ? '!' : '';
return $nottext . $fulltext . $this->message;
}
public function get_standalone_description(
$full, $not, \core_availability\info $info) {
// Override so that we can spot that this function is used.
return 'SA: ' . $this->get_description($full, $not, $info);
}
public function update_dependency_id($table, $oldid, $newid) {
if ($table === $this->dependtable && (int)$oldid === (int)$this->dependid) {
$this->dependid = $newid;
return true;
} else {
return false;
}
}
protected function get_debug_string() {
return ($this->available ? 'y' : 'n') . ',' . $this->message;
}
public function is_applied_to_user_lists() {
return $this->filter;
}
public function filter_user_list(array $users, $not, \core_availability\info $info,
\core_availability\capability_checker $checker) {
$result = array();
foreach ($users as $id => $user) {
$match = in_array($id, $this->filter);
if ($not) {
$match = !$match;
}
if ($match) {
$result[$id] = $user;
}
}
return $result;
}
public function get_user_list_sql($not, \core_availability\info $info, $onlyactive) {
global $DB;
// The data for this condition is not really stored in the database,
// so we return SQL that contains the hard-coded user list.
list ($enrolsql, $enrolparams) =
get_enrolled_sql($info->get_context(), '', 0, $onlyactive);
$condition = $not ? 'NOT' : '';
list ($matchsql, $matchparams) = $DB->get_in_or_equal($this->filter, SQL_PARAMS_NAMED);
$sql = "SELECT userids.id
FROM ($enrolsql) userids
WHERE $condition (userids.id $matchsql)";
return array($sql, array_merge($enrolparams, $matchparams));
}
}
+78
View File
@@ -0,0 +1,78 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* For use in unit tests that require an info object which isn't really used.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_availability;
defined('MOODLE_INTERNAL') || die();
/**
* For use in unit tests that require an info object which isn't really used.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mock_info extends info {
/** @var int User id for modinfo */
protected $userid;
/**
* Constructs with item details.
*
* @param \stdClass $course Optional course param (otherwise uses $SITE)
* @param int $userid Userid for modinfo (if used)
*/
public function __construct($course = null, $userid = 0) {
global $SITE;
if (!$course) {
$course = $SITE;
}
parent::__construct($course, true, null);
$this->userid = $userid;
}
protected function get_thing_name() {
return 'Mock';
}
public function get_context() {
return \context_course::instance($this->get_course()->id);
}
protected function get_view_hidden_capability() {
return 'moodle/course:ignoreavailabilityrestrictions';
}
protected function set_in_database($availability) {
}
public function get_modinfo() {
// Allow modinfo usage outside is_available etc., so we can use this
// to directly call into condition is_available.
if (!$this->userid) {
throw new \coding_exception('Need to set mock_info userid');
}
return get_fast_modinfo($this->course, $this->userid);
}
}
+115
View File
@@ -0,0 +1,115 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* For use in unit tests that require an info module which isn't really used.
*
* @package core_availability
* @copyright 2019 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_availability;
defined('MOODLE_INTERNAL') || die();
/**
* For use in unit tests that require an info module which isn't really used.
*
* @package core_availability
* @copyright 2019 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mock_info_module extends info_module {
/** @var int User id for modinfo */
protected $userid;
/** @var \cm_info Activity. */
protected $cm;
/**
* Constructs with item details.
*
* @param int $userid Userid for modinfo (if used)
* @param \cm_info $cm Course-module object
*/
public function __construct($userid = 0, \cm_info $cm = null) {
parent::__construct($cm);
$this->userid = $userid;
$this->cm = $cm;
}
/**
* Just returns a mock name.
*
* @return string Name of item
*/
protected function get_thing_name() {
return 'Mock Module';
}
/**
* Returns the current context.
*
* @return \context Context for this item
*/
public function get_context() {
return \context_course::instance($this->get_course()->id);
}
/**
* Returns the cappability used to ignore access restrictions.
*
* @return string Name of capability used to view hidden items of this type
*/
protected function get_view_hidden_capability() {
return 'moodle/course:ignoreavailabilityrestrictions';
}
/**
* Mocks don't need to save anything into DB.
*
* @param string $availability New JSON value
*/
protected function set_in_database($availability) {
}
/**
* Obtains the modinfo associated with this availability information.
*
* Note: This field is available ONLY for use by conditions when calculating
* availability or information.
*
* @return \course_modinfo Modinfo
* @throws \coding_exception If called at incorrect times
*/
public function get_modinfo() {
// Allow modinfo usage outside is_available etc., so we can use this
// to directly call into condition is_available.
if (!$this->userid) {
throw new \coding_exception('Need to set mock_info userid');
}
return get_fast_modinfo($this->course, $this->userid);
}
/**
* Override course-module info.
* @param \cm_info $cm
*/
public function set_cm(\cm_info $cm) {
$this->cm = $cm;
}
}
+116
View File
@@ -0,0 +1,116 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* For use in unit tests that require an info section which isn't really used.
*
* @package core_availability
* @copyright 2019 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_availability;
defined('MOODLE_INTERNAL') || die();
/**
* For use in unit tests that require an info section which isn't really used.
*
* @package core_availability
* @copyright 2019 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mock_info_section extends info_section {
/** @var int User id for modinfo */
protected $userid;
/** @var \section_info Section. */
protected $section;
/**
* Constructs with item details.
*
* @param int $userid Userid for modinfo (if used)
* @param \section_info $section Section object
*/
public function __construct($userid = 0, \section_info $section = null) {
parent::__construct($section);
$this->userid = $userid;
$this->section = $section;
}
/**
* Just returns a mock name.
*
* @return string Name of item
*/
protected function get_thing_name() {
return 'Mock Section';
}
/**
* Returns the current context.
*
* @return \context Context for this item
*/
public function get_context() {
return \context_course::instance($this->get_course()->id);
}
/**
* Returns the cappability used to ignore access restrictions.
*
* @return string Name of capability used to view hidden items of this type
*/
protected function get_view_hidden_capability() {
return 'moodle/course:ignoreavailabilityrestrictions';
}
/**
* Mocks don't need to save anything into DB.
*
* @param string $availability New JSON value
*/
protected function set_in_database($availability) {
}
/**
* Obtains the modinfo associated with this availability information.
*
* Note: This field is available ONLY for use by conditions when calculating
* availability or information.
*
* @return \course_modinfo Modinfo
* @throws \coding_exception If called at incorrect times
*/
public function get_modinfo() {
// Allow modinfo usage outside is_available etc., so we can use this
// to directly call into condition is_available.
if (!$this->userid) {
throw new \coding_exception('Need to set mock_info userid');
}
return get_fast_modinfo($this->course, $this->userid);
}
/**
* Override section info.
*
* @param \section_info $section
*/
public function set_section(\section_info $section) {
$this->section = $section;
}
}
+561
View File
@@ -0,0 +1,561 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_availability;
/**
* Unit tests for info and subclasses.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class info_test extends \advanced_testcase {
public function setUp(): void {
// Load the mock condition so that it can be used.
require_once(__DIR__ . '/fixtures/mock_condition.php');
}
/**
* Tests the info_module class (is_available, get_full_information).
*/
public function test_info_module(): void {
global $DB, $CFG;
// Create a course and pages.
$CFG->enableavailability = 0;
$this->setAdminUser();
$this->resetAfterTest();
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$rec = array('course' => $course);
$page1 = $generator->get_plugin_generator('mod_page')->create_instance($rec);
$page2 = $generator->get_plugin_generator('mod_page')->create_instance($rec);
$page3 = $generator->get_plugin_generator('mod_page')->create_instance($rec);
$page4 = $generator->get_plugin_generator('mod_page')->create_instance($rec);
// Set up the availability option for the pages to mock options.
$DB->set_field('course_modules', 'availability', '{"op":"|","show":true,"c":[' .
'{"type":"mock","a":false,"m":"grandmaster flash"}]}', array('id' => $page1->cmid));
$DB->set_field('course_modules', 'availability', '{"op":"|","show":true,"c":[' .
'{"type":"mock","a":true,"m":"the furious five"}]}', array('id' => $page2->cmid));
// Third page is invalid. (Fourth has no availability settings.)
$DB->set_field('course_modules', 'availability', '{{{', array('id' => $page3->cmid));
$modinfo = get_fast_modinfo($course);
$cm1 = $modinfo->get_cm($page1->cmid);
$cm2 = $modinfo->get_cm($page2->cmid);
$cm3 = $modinfo->get_cm($page3->cmid);
$cm4 = $modinfo->get_cm($page4->cmid);
// Do availability and full information checks.
$info = new info_module($cm1);
$information = '';
$this->assertFalse($info->is_available($information));
$this->assertEquals('SA: grandmaster flash', $information);
$this->assertEquals('SA: [FULL]grandmaster flash', $info->get_full_information());
$info = new info_module($cm2);
$this->assertTrue($info->is_available($information));
$this->assertEquals('', $information);
$this->assertEquals('SA: [FULL]the furious five', $info->get_full_information());
// Check invalid one.
$info = new info_module($cm3);
$this->assertFalse($info->is_available($information));
$debugging = $this->getDebuggingMessages();
$this->resetDebugging();
$this->assertEquals(1, count($debugging));
$this->assertStringContainsString('Invalid availability', $debugging[0]->message);
// Check empty one.
$info = new info_module($cm4);
$this->assertTrue($info->is_available($information));
$this->assertEquals('', $information);
$this->assertEquals('', $info->get_full_information());
}
/**
* Tests the info_section class (is_available, get_full_information).
*/
public function test_info_section(): void {
global $DB;
// Create a course.
$this->setAdminUser();
$this->resetAfterTest();
$generator = $this->getDataGenerator();
$course = $generator->create_course(
array('numsections' => 4), array('createsections' => true));
// Set up the availability option for the sections to mock options.
$DB->set_field('course_sections', 'availability', '{"op":"|","show":true,"c":[' .
'{"type":"mock","a":false,"m":"public"}]}',
array('course' => $course->id, 'section' => 1));
$DB->set_field('course_sections', 'availability', '{"op":"|","show":true,"c":[' .
'{"type":"mock","a":true,"m":"enemy"}]}',
array('course' => $course->id, 'section' => 2));
// Third section is invalid. (Fourth has no availability setting.)
$DB->set_field('course_sections', 'availability', '{{{',
array('course' => $course->id, 'section' => 3));
$modinfo = get_fast_modinfo($course);
$sections = $modinfo->get_section_info_all();
// Do availability and full information checks.
$info = new info_section($sections[1]);
$information = '';
$this->assertFalse($info->is_available($information));
$this->assertEquals('SA: public', $information);
$this->assertEquals('SA: [FULL]public', $info->get_full_information());
$info = new info_section($sections[2]);
$this->assertTrue($info->is_available($information));
$this->assertEquals('', $information);
$this->assertEquals('SA: [FULL]enemy', $info->get_full_information());
// Check invalid one.
$info = new info_section($sections[3]);
$this->assertFalse($info->is_available($information));
$debugging = $this->getDebuggingMessages();
$this->resetDebugging();
$this->assertEquals(1, count($debugging));
$this->assertStringContainsString('Invalid availability', $debugging[0]->message);
// Check empty one.
$info = new info_section($sections[4]);
$this->assertTrue($info->is_available($information));
$this->assertEquals('', $information);
$this->assertEquals('', $info->get_full_information());
}
/**
* Tests the is_user_visible() static function in info_module.
*/
public function test_is_user_visible(): void {
global $CFG, $DB;
require_once($CFG->dirroot . '/course/lib.php');
$this->resetAfterTest();
$CFG->enableavailability = 0;
// Create a course and some pages:
// 0. Invisible due to visible=0.
// 1. Availability restriction (mock, set to fail).
// 2. Availability restriction on section (mock, set to fail).
// 3. Actually visible.
$generator = $this->getDataGenerator();
$course = $generator->create_course(
array('numsections' => 1), array('createsections' => true));
$rec = array('course' => $course, );
$pages = array();
$pagegen = $generator->get_plugin_generator('mod_page');
$pages[0] = $pagegen->create_instance($rec, array('visible' => 0));
$pages[1] = $pagegen->create_instance($rec);
$pages[2] = $pagegen->create_instance($rec);
$pages[3] = $pagegen->create_instance($rec);
$modinfo = get_fast_modinfo($course);
$section = $modinfo->get_section_info(1);
$cm = $modinfo->get_cm($pages[2]->cmid);
moveto_module($cm, $section);
// Set the availability restrictions in database. The enableavailability
// setting is off so these do not take effect yet.
$notavailable = '{"op":"|","show":true,"c":[{"type":"mock","a":false}]}';
$DB->set_field('course_sections', 'availability',
$notavailable, array('id' => $section->id));
$DB->set_field('course_modules', 'availability',
$notavailable, array('id' => $pages[1]->cmid));
get_fast_modinfo($course, 0, true);
// Set up 4 users - a teacher and student plus somebody who isn't even
// on the course. Also going to use admin user and a spare student to
// avoid cache problems.
$roleids = $DB->get_records_menu('role', null, '', 'shortname, id');
$teacher = $generator->create_user();
$student = $generator->create_user();
$student2 = $generator->create_user();
$other = $generator->create_user();
$admin = $DB->get_record('user', array('username' => 'admin'));
$generator->enrol_user($teacher->id, $course->id, $roleids['teacher']);
$generator->enrol_user($student->id, $course->id, $roleids['student']);
$generator->enrol_user($student2->id, $course->id, $roleids['student']);
// Basic case when availability disabled, for visible item.
$this->assertTrue(info_module::is_user_visible($pages[3]->cmid, $student->id, false));
// Specifying as an object should not make any queries.
$cm = $DB->get_record('course_modules', array('id' => $pages[3]->cmid));
$beforequeries = $DB->perf_get_queries();
$this->assertTrue(info_module::is_user_visible($cm, $student->id, false));
$this->assertEquals($beforequeries, $DB->perf_get_queries());
// Specifying as cm_info for correct user should not make any more queries
// if we have already obtained dynamic data.
$modinfo = get_fast_modinfo($course, $student->id);
$cminfo = $modinfo->get_cm($cm->id);
// This will obtain dynamic data.
$name = $cminfo->name;
$beforequeries = $DB->perf_get_queries();
$this->assertTrue(info_module::is_user_visible($cminfo, $student->id, false));
$this->assertEquals($beforequeries, $DB->perf_get_queries());
// Function does not care if you are in the course (unless $checkcourse).
$this->assertTrue(info_module::is_user_visible($cm, $other->id, false));
// With $checkcourse, check for enrolled, not enrolled, and admin user.
$this->assertTrue(info_module::is_user_visible($cm, $student->id, true));
$this->assertFalse(info_module::is_user_visible($cm, $other->id, true));
$this->assertTrue(info_module::is_user_visible($cm, $admin->id, true));
// With availability off, the student can access all except the
// visible=0 one.
$this->assertFalse(info_module::is_user_visible($pages[0]->cmid, $student->id, false));
$this->assertTrue(info_module::is_user_visible($pages[1]->cmid, $student->id, false));
$this->assertTrue(info_module::is_user_visible($pages[2]->cmid, $student->id, false));
// Teacher and admin can even access the visible=0 one.
$this->assertTrue(info_module::is_user_visible($pages[0]->cmid, $teacher->id, false));
$this->assertTrue(info_module::is_user_visible($pages[0]->cmid, $admin->id, false));
// Now enable availability (and clear cache).
$CFG->enableavailability = true;
get_fast_modinfo($course, 0, true);
// Student cannot access the activity restricted by its own or by the
// section's availability.
$this->assertFalse(info_module::is_user_visible($pages[1]->cmid, $student->id, false));
$this->assertFalse(info_module::is_user_visible($pages[2]->cmid, $student->id, false));
}
/**
* Tests the convert_legacy_fields function used in restore.
*/
public function test_convert_legacy_fields(): void {
// Check with no availability conditions first.
$rec = (object)array('availablefrom' => 0, 'availableuntil' => 0,
'groupingid' => 7, 'showavailability' => 1);
$this->assertNull(info::convert_legacy_fields($rec, false));
// Check same list for a section.
$this->assertEquals(
'{"op":"&","showc":[false],"c":[{"type":"grouping","id":7}]}',
info::convert_legacy_fields($rec, true));
// Check groupmembersonly with grouping.
$rec->groupmembersonly = 1;
$this->assertEquals(
'{"op":"&","showc":[false],"c":[{"type":"grouping","id":7}]}',
info::convert_legacy_fields($rec, false));
// Check groupmembersonly without grouping.
$rec->groupingid = 0;
$this->assertEquals(
'{"op":"&","showc":[false],"c":[{"type":"group"}]}',
info::convert_legacy_fields($rec, false));
// Check start date.
$rec->groupmembersonly = 0;
$rec->availablefrom = 123;
$this->assertEquals(
'{"op":"&","showc":[true],"c":[{"type":"date","d":">=","t":123}]}',
info::convert_legacy_fields($rec, false));
// Start date with show = false.
$rec->showavailability = 0;
$this->assertEquals(
'{"op":"&","showc":[false],"c":[{"type":"date","d":">=","t":123}]}',
info::convert_legacy_fields($rec, false));
// End date.
$rec->showavailability = 1;
$rec->availablefrom = 0;
$rec->availableuntil = 456;
$this->assertEquals(
'{"op":"&","showc":[false],"c":[{"type":"date","d":"<","t":456}]}',
info::convert_legacy_fields($rec, false));
// All together now.
$rec->groupingid = 7;
$rec->groupmembersonly = 1;
$rec->availablefrom = 123;
$this->assertEquals(
'{"op":"&","showc":[false,true,false],"c":[' .
'{"type":"grouping","id":7},' .
'{"type":"date","d":">=","t":123},' .
'{"type":"date","d":"<","t":456}' .
']}',
info::convert_legacy_fields($rec, false));
$this->assertEquals(
'{"op":"&","showc":[false,true,false],"c":[' .
'{"type":"grouping","id":7},' .
'{"type":"date","d":">=","t":123},' .
'{"type":"date","d":"<","t":456}' .
']}',
info::convert_legacy_fields($rec, false, true));
}
/**
* Tests the add_legacy_availability_condition function used in restore.
*/
public function test_add_legacy_availability_condition(): void {
// Completion condition tests.
$rec = (object)array('sourcecmid' => 7, 'requiredcompletion' => 1);
// No previous availability, show = true.
$this->assertEquals(
'{"op":"&","showc":[true],"c":[{"type":"completion","cm":7,"e":1}]}',
info::add_legacy_availability_condition(null, $rec, true));
// No previous availability, show = false.
$this->assertEquals(
'{"op":"&","showc":[false],"c":[{"type":"completion","cm":7,"e":1}]}',
info::add_legacy_availability_condition(null, $rec, false));
// Existing availability.
$before = '{"op":"&","showc":[true],"c":[{"type":"date","d":">=","t":70}]}';
$this->assertEquals(
'{"op":"&","showc":[true,true],"c":['.
'{"type":"date","d":">=","t":70},' .
'{"type":"completion","cm":7,"e":1}' .
']}',
info::add_legacy_availability_condition($before, $rec, true));
// Grade condition tests.
$rec = (object)array('gradeitemid' => 3, 'grademin' => 7, 'grademax' => null);
$this->assertEquals(
'{"op":"&","showc":[true],"c":[{"type":"grade","id":3,"min":7.00000}]}',
info::add_legacy_availability_condition(null, $rec, true));
$rec->grademax = 8;
$this->assertEquals(
'{"op":"&","showc":[true],"c":[{"type":"grade","id":3,"min":7.00000,"max":8.00000}]}',
info::add_legacy_availability_condition(null, $rec, true));
unset($rec->grademax);
unset($rec->grademin);
$this->assertEquals(
'{"op":"&","showc":[true],"c":[{"type":"grade","id":3}]}',
info::add_legacy_availability_condition(null, $rec, true));
// Note: There is no need to test the grade condition with show
// true/false and existing availability, because this uses the same
// function.
}
/**
* Tests the add_legacy_availability_field_condition function used in restore.
*/
public function test_add_legacy_availability_field_condition(): void {
// User field, normal operator.
$rec = (object)array('userfield' => 'email', 'shortname' => null,
'operator' => 'contains', 'value' => '@');
$this->assertEquals(
'{"op":"&","showc":[true],"c":[' .
'{"type":"profile","op":"contains","sf":"email","v":"@"}]}',
info::add_legacy_availability_field_condition(null, $rec, true));
// User field, non-value operator.
$rec = (object)array('userfield' => 'email', 'shortname' => null,
'operator' => 'isempty', 'value' => '');
$this->assertEquals(
'{"op":"&","showc":[true],"c":[' .
'{"type":"profile","op":"isempty","sf":"email"}]}',
info::add_legacy_availability_field_condition(null, $rec, true));
// Custom field.
$rec = (object)array('userfield' => null, 'shortname' => 'frogtype',
'operator' => 'isempty', 'value' => '');
$this->assertEquals(
'{"op":"&","showc":[true],"c":[' .
'{"type":"profile","op":"isempty","cf":"frogtype"}]}',
info::add_legacy_availability_field_condition(null, $rec, true));
}
/**
* Tests the filter_user_list() and get_user_list_sql() functions.
*/
public function test_filter_user_list(): void {
global $CFG, $DB;
require_once($CFG->dirroot . '/course/lib.php');
$this->resetAfterTest();
$CFG->enableavailability = true;
// Create a course with 2 sections and 2 pages and 3 users.
// Availability is set up initially on the 'page/section 2' items.
$generator = $this->getDataGenerator();
$course = $generator->create_course(
array('numsections' => 2), array('createsections' => true));
$u1 = $generator->create_user();
$u2 = $generator->create_user();
$u3 = $generator->create_user();
$studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'), MUST_EXIST);
$allusers = array($u1->id => $u1, $u2->id => $u2, $u3->id => $u3);
$generator->enrol_user($u1->id, $course->id, $studentroleid);
$generator->enrol_user($u2->id, $course->id, $studentroleid);
$generator->enrol_user($u3->id, $course->id, $studentroleid);
// Page 2 allows access to users 2 and 3, while section 2 allows access
// to users 1 and 2.
$pagegen = $generator->get_plugin_generator('mod_page');
$page = $pagegen->create_instance(array('course' => $course));
$page2 = $pagegen->create_instance(array('course' => $course,
'availability' => '{"op":"|","show":true,"c":[{"type":"mock","filter":[' .
$u2->id . ',' . $u3->id . ']}]}'));
$modinfo = get_fast_modinfo($course);
$section = $modinfo->get_section_info(1);
$section2 = $modinfo->get_section_info(2);
$DB->set_field('course_sections', 'availability',
'{"op":"|","show":true,"c":[{"type":"mock","filter":[' . $u1->id . ',' . $u2->id .']}]}',
array('id' => $section2->id));
moveto_module($modinfo->get_cm($page2->cmid), $section2);
// With no restrictions, returns full list.
$info = new info_module($modinfo->get_cm($page->cmid));
$this->assertEquals(array($u1->id, $u2->id, $u3->id),
array_keys($info->filter_user_list($allusers)));
$this->assertEquals(array('', array()), $info->get_user_list_sql(true));
// Set an availability restriction in database for section 1.
// For the section we set it so it doesn't support filters; for the
// module we have a filter.
$DB->set_field('course_sections', 'availability',
'{"op":"|","show":true,"c":[{"type":"mock","a":false}]}',
array('id' => $section->id));
$DB->set_field('course_modules', 'availability',
'{"op":"|","show":true,"c":[{"type":"mock","filter":[' . $u3->id .']}]}',
array('id' => $page->cmid));
rebuild_course_cache($course->id, true);
$modinfo = get_fast_modinfo($course);
// Now it should work (for the module).
$info = new info_module($modinfo->get_cm($page->cmid));
$expected = array($u3->id);
$this->assertEquals($expected,
array_keys($info->filter_user_list($allusers)));
list ($sql, $params) = $info->get_user_list_sql();
$result = $DB->get_fieldset_sql($sql, $params);
sort($result);
$this->assertEquals($expected, $result);
$info = new info_section($modinfo->get_section_info(1));
$this->assertEquals(array($u1->id, $u2->id, $u3->id),
array_keys($info->filter_user_list($allusers)));
$this->assertEquals(array('', array()), $info->get_user_list_sql(true));
// With availability disabled, module returns full list too.
$CFG->enableavailability = false;
$info = new info_module($modinfo->get_cm($page->cmid));
$this->assertEquals(array($u1->id, $u2->id, $u3->id),
array_keys($info->filter_user_list($allusers)));
$this->assertEquals(array('', array()), $info->get_user_list_sql(true));
// Check the other section...
$CFG->enableavailability = true;
$info = new info_section($modinfo->get_section_info(2));
$expected = array($u1->id, $u2->id);
$this->assertEquals($expected, array_keys($info->filter_user_list($allusers)));
list ($sql, $params) = $info->get_user_list_sql(true);
$result = $DB->get_fieldset_sql($sql, $params);
sort($result);
$this->assertEquals($expected, $result);
// And the module in that section - which has combined the section and
// module restrictions.
$info = new info_module($modinfo->get_cm($page2->cmid));
$expected = array($u2->id);
$this->assertEquals($expected, array_keys($info->filter_user_list($allusers)));
list ($sql, $params) = $info->get_user_list_sql(true);
$result = $DB->get_fieldset_sql($sql, $params);
sort($result);
$this->assertEquals($expected, $result);
// If the students have viewhiddenactivities, they get past the module
// restriction.
role_change_permission($studentroleid, \context_module::instance($page2->cmid),
'moodle/course:ignoreavailabilityrestrictions', CAP_ALLOW);
$expected = array($u1->id, $u2->id);
$this->assertEquals($expected, array_keys($info->filter_user_list($allusers)));
list ($sql, $params) = $info->get_user_list_sql(true);
$result = $DB->get_fieldset_sql($sql, $params);
sort($result);
$this->assertEquals($expected, $result);
// If they have viewhiddensections, they also get past the section
// restriction.
role_change_permission($studentroleid, \context_course::instance($course->id),
'moodle/course:ignoreavailabilityrestrictions', CAP_ALLOW);
$expected = array($u1->id, $u2->id, $u3->id);
$this->assertEquals($expected, array_keys($info->filter_user_list($allusers)));
list ($sql, $params) = $info->get_user_list_sql(true);
$result = $DB->get_fieldset_sql($sql, $params);
sort($result);
$this->assertEquals($expected, $result);
}
/**
* Tests the info_module class when involved in a recursive call to $cm->name.
*/
public function test_info_recursive_name_call(): void {
global $DB;
$this->resetAfterTest();
// Create a course and page.
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$page1 = $generator->create_module('page', ['course' => $course->id, 'name' => 'Page1']);
// Set invalid availability.
$DB->set_field('course_modules', 'availability', 'not valid', ['id' => $page1->cmid]);
// Get the cm_info object.
$this->setAdminUser();
$modinfo = get_fast_modinfo($course);
$cm1 = $modinfo->get_cm($page1->cmid);
// At this point we will generate dynamic data for $cm1, which will cause the debugging
// call below.
$this->assertEquals('Page1', $cm1->name);
$this->assertDebuggingCalled('Error processing availability data for ' .
'&lsquo;Page1&rsquo;: Invalid availability text');
}
/**
* Test for the is_available_for_all() method of the info base class.
* @covers \core_availability\info_module::is_available_for_all
*/
public function test_is_available_for_all(): void {
global $CFG, $DB;
$this->resetAfterTest();
$CFG->enableavailability = 0;
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$page = $generator->get_plugin_generator('mod_page')->create_instance(['course' => $course]);
// Set an availability restriction and reset the modinfo cache.
// The enableavailability setting is disabled so this does not take effect yet.
$notavailable = '{"op":"|","show":true,"c":[{"type":"mock","a":false}]}';
$DB->set_field('course_modules', 'availability', $notavailable, ['id' => $page->cmid]);
get_fast_modinfo($course, 0, true);
// Availability is disabled, so we expect this module to be available for everyone.
$modinfo = get_fast_modinfo($course);
$info = new info_module($modinfo->get_cm($page->cmid));
$this->assertTrue($info->is_available_for_all());
// Now, enable availability restrictions, and check again.
// This time, we expect it to return false, because of the access restriction.
$CFG->enableavailability = 1;
$this->assertFalse($info->is_available_for_all());
}
}
+814
View File
@@ -0,0 +1,814 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_availability;
/**
* Unit tests for the condition tree class and related logic.
*
* @package core_availability
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tree_test extends \advanced_testcase {
public function setUp(): void {
// Load the mock classes so they can be used.
require_once(__DIR__ . '/fixtures/mock_condition.php');
require_once(__DIR__ . '/fixtures/mock_info.php');
}
/**
* Tests constructing a tree with errors.
*/
public function test_construct_errors(): void {
try {
new tree('frog');
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('not object', $e->getMessage());
}
try {
new tree((object)array());
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('missing ->op', $e->getMessage());
}
try {
new tree((object)array('op' => '*'));
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('unknown ->op', $e->getMessage());
}
try {
new tree((object)array('op' => '|'));
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('missing ->show', $e->getMessage());
}
try {
new tree((object)array('op' => '|', 'show' => 0));
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('->show not bool', $e->getMessage());
}
try {
new tree((object)array('op' => '&'));
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('missing ->showc', $e->getMessage());
}
try {
new tree((object)array('op' => '&', 'showc' => 0));
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('->showc not array', $e->getMessage());
}
try {
new tree((object)array('op' => '&', 'showc' => array(0)));
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('->showc value not bool', $e->getMessage());
}
try {
new tree((object)array('op' => '|', 'show' => true));
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('missing ->c', $e->getMessage());
}
try {
new tree((object)array('op' => '|', 'show' => true,
'c' => 'side'));
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('->c not array', $e->getMessage());
}
try {
new tree((object)array('op' => '|', 'show' => true,
'c' => array(3)));
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('child not object', $e->getMessage());
}
try {
new tree((object)array('op' => '|', 'show' => true,
'c' => array((object)array('type' => 'doesnotexist'))));
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('Unknown condition type: doesnotexist', $e->getMessage());
}
try {
new tree((object)array('op' => '|', 'show' => true,
'c' => array((object)array())));
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('missing ->op', $e->getMessage());
}
try {
new tree((object)array('op' => '&',
'c' => array((object)array('op' => '&', 'c' => array())),
'showc' => array(true, true)
));
$this->fail();
} catch (\coding_exception $e) {
$this->assertStringContainsString('->c, ->showc mismatch', $e->getMessage());
}
}
/**
* Tests constructing a tree with plugin that does not exist (ignored).
*/
public function test_construct_ignore_missing_plugin(): void {
// Construct a tree with & combination of one condition that doesn't exist.
$tree = new tree(tree::get_root_json(array(
(object)array('type' => 'doesnotexist')), tree::OP_OR), true);
// Expected result is an empty tree with | condition, shown.
$this->assertEquals('+|()', (string)$tree);
}
/**
* Tests constructing a tree with subtrees using all available operators.
*/
public function test_construct_just_trees(): void {
$structure = tree::get_root_json(array(
tree::get_nested_json(array(), tree::OP_OR),
tree::get_nested_json(array(
tree::get_nested_json(array(), tree::OP_NOT_OR)), tree::OP_NOT_AND)),
tree::OP_AND, array(true, true));
$tree = new tree($structure);
$this->assertEquals('&(+|(),+!&(!|()))', (string)$tree);
}
/**
* Tests constructing tree using the mock plugin.
*/
public function test_construct_with_mock_plugin(): void {
$structure = tree::get_root_json(array(
self::mock(array('a' => true, 'm' => ''))), tree::OP_OR);
$tree = new tree($structure);
$this->assertEquals('+|({mock:y,})', (string)$tree);
}
/**
* Tests the check_available and get_result_information functions.
*/
public function test_check_available(): void {
global $USER;
// Setup.
$this->resetAfterTest();
$info = new \core_availability\mock_info();
$this->setAdminUser();
$information = '';
// No conditions.
$structure = tree::get_root_json(array(), tree::OP_OR);
list ($available, $information) = $this->get_available_results(
$structure, $info, $USER->id);
$this->assertTrue($available);
// One condition set to yes.
$structure->c = array(
self::mock(array('a' => true)));
list ($available, $information) = $this->get_available_results(
$structure, $info, $USER->id);
$this->assertTrue($available);
// One condition set to no.
$structure->c = array(
self::mock(array('a' => false, 'm' => 'no')));
list ($available, $information) = $this->get_available_results(
$structure, $info, $USER->id);
$this->assertFalse($available);
$this->assertEquals('SA: no', $information);
// Two conditions, OR, resolving as true.
$structure->c = array(
self::mock(array('a' => false, 'm' => 'no')),
self::mock(array('a' => true)));
list ($available, $information) = $this->get_available_results(
$structure, $info, $USER->id);
$this->assertTrue($available);
$this->assertEquals('', $information);
// Two conditions, OR, resolving as false.
$structure->c = array(
self::mock(array('a' => false, 'm' => 'no')),
self::mock(array('a' => false, 'm' => 'way')));
list ($available, $information) = $this->get_available_results(
$structure, $info, $USER->id);
$this->assertFalse($available);
$this->assertMatchesRegularExpression('~any of.*no.*way~', $information);
// Two conditions, OR, resolving as false, no display.
$structure->show = false;
list ($available, $information) = $this->get_available_results(
$structure, $info, $USER->id);
$this->assertFalse($available);
$this->assertEquals('', $information);
// Two conditions, AND, resolving as true.
$structure->op = '&';
unset($structure->show);
$structure->showc = array(true, true);
$structure->c = array(
self::mock(array('a' => true)),
self::mock(array('a' => true)));
list ($available, $information) = $this->get_available_results(
$structure, $info, $USER->id);
$this->assertTrue($available);
// Two conditions, AND, one false.
$structure->c = array(
self::mock(array('a' => false, 'm' => 'wom')),
self::mock(array('a' => true, 'm' => '')));
list ($available, $information) = $this->get_available_results(
$structure, $info, $USER->id);
$this->assertFalse($available);
$this->assertEquals('SA: wom', $information);
// Two conditions, AND, both false.
$structure->c = array(
self::mock(array('a' => false, 'm' => 'wom')),
self::mock(array('a' => false, 'm' => 'bat')));
list ($available, $information) = $this->get_available_results(
$structure, $info, $USER->id);
$this->assertFalse($available);
$this->assertMatchesRegularExpression('~wom.*bat~', $information);
// Two conditions, AND, both false, show turned off for one. When
// show is turned off, that means if you don't have that condition
// you don't get to see anything at all.
$structure->showc[0] = false;
list ($available, $information) = $this->get_available_results(
$structure, $info, $USER->id);
$this->assertFalse($available);
$this->assertEquals('', $information);
$structure->showc[0] = true;
// Two conditions, NOT OR, both false.
$structure->op = '!|';
list ($available, $information) = $this->get_available_results(
$structure, $info, $USER->id);
$this->assertTrue($available);
// Two conditions, NOT OR, one true.
$structure->c[0]->a = true;
list ($available, $information) = $this->get_available_results(
$structure, $info, $USER->id);
$this->assertFalse($available);
$this->assertEquals('SA: !wom', $information);
// Two conditions, NOT OR, both true.
$structure->c[1]->a = true;
list ($available, $information) = $this->get_available_results(
$structure, $info, $USER->id);
$this->assertFalse($available);
$this->assertMatchesRegularExpression('~!wom.*!bat~', $information);
// Two conditions, NOT AND, both true.
$structure->op = '!&';
unset($structure->showc);
$structure->show = true;
list ($available, $information) = $this->get_available_results(
$structure, $info, $USER->id);
$this->assertFalse($available);
$this->assertMatchesRegularExpression('~any of.*!wom.*!bat~', $information);
// Two conditions, NOT AND, one true.
$structure->c[1]->a = false;
list ($available, $information) = $this->get_available_results(
$structure, $info, $USER->id);
$this->assertTrue($available);
// Nested NOT conditions; true.
$structure->c = array(
tree::get_nested_json(array(
self::mock(array('a' => true, 'm' => 'no'))), tree::OP_NOT_AND));
list ($available, $information) = $this->get_available_results(
$structure, $info, $USER->id);
$this->assertTrue($available);
// Nested NOT conditions; false (note no ! in message).
$structure->c[0]->c[0]->a = false;
list ($available, $information) = $this->get_available_results(
$structure, $info, $USER->id);
$this->assertFalse($available);
$this->assertEquals('SA: no', $information);
// Nested condition groups, message test.
$structure->op = '|';
$structure->c = array(
tree::get_nested_json(array(
self::mock(array('a' => false, 'm' => '1')),
self::mock(array('a' => false, 'm' => '2'))
), tree::OP_AND),
self::mock(array('a' => false, 'm' => 3)));
list ($available, $information) = $this->get_available_results(
$structure, $info, $USER->id);
$this->assertFalse($available);
$this->assertMatchesRegularExpression('~<ul.*<ul.*<li.*1.*<li.*2.*</ul>.*<li.*3~', $information);
}
/**
* Shortcut function to check availability and also get information.
*
* @param \stdClass $structure Tree structure
* @param \core_availability\info $info Location info
* @param int $userid User id
*/
protected function get_available_results($structure, \core_availability\info $info, $userid) {
global $PAGE, $OUTPUT;
$tree = new tree($structure);
$result = $tree->check_available(false, $info, true, $userid);
$information = $tree->get_result_information($info, $result);
if (!is_string($information)) {
$renderable = new \core_availability\output\availability_info($information);
$information = str_replace(array("\r", "\n"), '', $OUTPUT->render($renderable));
}
return array($result->is_available(), $information);
}
/**
* Shortcut function to render the full availability information.
*
* @param \stdClass $structure Tree structure
* @param \core_availability\info $info Location info
*/
protected function render_full_information($structure, \core_availability\info $info) {
global $OUTPUT;
$tree = new tree($structure);
$information = $tree->get_full_information($info);
$renderable = new \core_availability\output\availability_info($information);
$html = $OUTPUT->render($renderable);
return str_replace(array("\r", "\n"), '', $html);
}
/**
* Tests the is_available_for_all() function.
*/
public function test_is_available_for_all(): void {
// Empty tree is always available.
$structure = tree::get_root_json(array(), tree::OP_OR);
$tree = new tree($structure);
$this->assertTrue($tree->is_available_for_all());
// Tree with normal item in it, not always available.
$structure->c[0] = (object)array('type' => 'mock');
$tree = new tree($structure);
$this->assertFalse($tree->is_available_for_all());
// OR tree with one always-available item.
$structure->c[1] = self::mock(array('all' => true));
$tree = new tree($structure);
$this->assertTrue($tree->is_available_for_all());
// AND tree with one always-available and one not.
$structure->op = '&';
$structure->showc = array(true, true);
unset($structure->show);
$tree = new tree($structure);
$this->assertFalse($tree->is_available_for_all());
// Test NOT conditions (items not always-available).
$structure->op = '!&';
$structure->show = true;
unset($structure->showc);
$tree = new tree($structure);
$this->assertFalse($tree->is_available_for_all());
// Test again with one item always-available for NOT mode.
$structure->c[1]->allnot = true;
$tree = new tree($structure);
$this->assertTrue($tree->is_available_for_all());
}
/**
* Tests the get_full_information() function.
*/
public function test_get_full_information(): void {
global $PAGE;
// Setup.
$info = new \core_availability\mock_info();
// No conditions.
$structure = tree::get_root_json(array(), tree::OP_OR);
$tree = new tree($structure);
$this->assertEquals('', $tree->get_full_information($info));
// Condition (normal and NOT).
$structure->c = array(
self::mock(array('m' => 'thing')));
$tree = new tree($structure);
$this->assertEquals('SA: [FULL]thing',
$tree->get_full_information($info));
$structure->op = '!&';
$tree = new tree($structure);
$this->assertEquals('SA: ![FULL]thing',
$tree->get_full_information($info));
// Complex structure.
$structure->op = '|';
$structure->c = array(
tree::get_nested_json(array(
self::mock(array('m' => '1')),
self::mock(array('m' => '2'))), tree::OP_AND),
self::mock(array('m' => 3)));
$this->assertMatchesRegularExpression('~<ul.*<ul.*<li.*1.*<li.*2.*</ul>.*<li.*3~',
$this->render_full_information($structure, $info));
// Test intro messages before list. First, OR message.
$structure->c = array(
self::mock(array('m' => '1')),
self::mock(array('m' => '2'))
);
$this->assertMatchesRegularExpression('~Not available unless any of:.*<ul~',
$this->render_full_information($structure, $info));
// Now, OR message when not shown.
$structure->show = false;
$this->assertMatchesRegularExpression('~hidden.*<ul~',
$this->render_full_information($structure, $info));
// AND message.
$structure->op = '&';
unset($structure->show);
$structure->showc = array(false, false);
$this->assertMatchesRegularExpression('~Not available unless:.*<ul~',
$this->render_full_information($structure, $info));
// Hidden markers on items.
$this->assertMatchesRegularExpression('~1.*hidden.*2.*hidden~',
$this->render_full_information($structure, $info));
// Hidden markers on child tree and items.
$structure->c[1] = tree::get_nested_json(array(
self::mock(array('m' => '2')),
self::mock(array('m' => '3'))), tree::OP_AND);
$this->assertMatchesRegularExpression('~1.*hidden.*All of \(hidden.*2.*3~',
$this->render_full_information($structure, $info));
$structure->c[1]->op = '|';
$this->assertMatchesRegularExpression('~1.*hidden.*Any of \(hidden.*2.*3~',
$this->render_full_information($structure, $info));
// Hidden markers on single-item display, AND and OR.
$structure->showc = array(false);
$structure->c = array(
self::mock(array('m' => '1'))
);
$tree = new tree($structure);
$this->assertMatchesRegularExpression('~1.*hidden~',
$tree->get_full_information($info));
unset($structure->showc);
$structure->show = false;
$structure->op = '|';
$tree = new tree($structure);
$this->assertMatchesRegularExpression('~1.*hidden~',
$tree->get_full_information($info));
// Hidden marker if single item is tree.
$structure->c[0] = tree::get_nested_json(array(
self::mock(array('m' => '1')),
self::mock(array('m' => '2'))), tree::OP_AND);
$this->assertMatchesRegularExpression('~Not available \(hidden.*1.*2~',
$this->render_full_information($structure, $info));
// Single item tree containing single item.
unset($structure->c[0]->c[1]);
$tree = new tree($structure);
$this->assertMatchesRegularExpression('~SA.*1.*hidden~',
$tree->get_full_information($info));
}
/**
* Tests the is_empty() function.
*/
public function test_is_empty(): void {
// Tree with nothing in should be empty.
$structure = tree::get_root_json(array(), tree::OP_OR);
$tree = new tree($structure);
$this->assertTrue($tree->is_empty());
// Tree with something in is not empty.
$structure = tree::get_root_json(array(self::mock(array('m' => '1'))), tree::OP_OR);
$tree = new tree($structure);
$this->assertFalse($tree->is_empty());
}
/**
* Tests the get_all_children() function.
*/
public function test_get_all_children(): void {
// Create a tree with nothing in.
$structure = tree::get_root_json(array(), tree::OP_OR);
$tree1 = new tree($structure);
// Create second tree with complex structure.
$structure->c = array(
tree::get_nested_json(array(
self::mock(array('m' => '1')),
self::mock(array('m' => '2'))
), tree::OP_OR),
self::mock(array('m' => 3)));
$tree2 = new tree($structure);
// Check list of conditions from both trees.
$this->assertEquals(array(), $tree1->get_all_children('core_availability\condition'));
$result = $tree2->get_all_children('core_availability\condition');
$this->assertEquals(3, count($result));
$this->assertEquals('{mock:n,1}', (string)$result[0]);
$this->assertEquals('{mock:n,2}', (string)$result[1]);
$this->assertEquals('{mock:n,3}', (string)$result[2]);
// Check specific type, should give same results.
$result2 = $tree2->get_all_children('availability_mock\condition');
$this->assertEquals($result, $result2);
}
/**
* Tests the update_dependency_id() function.
*/
public function test_update_dependency_id(): void {
// Create tree with structure of 3 mocks.
$structure = tree::get_root_json(array(
tree::get_nested_json(array(
self::mock(array('table' => 'frogs', 'id' => 9)),
self::mock(array('table' => 'zombies', 'id' => 9))
)),
self::mock(array('table' => 'frogs', 'id' => 9))));
// Get 'before' value.
$tree = new tree($structure);
$before = $tree->save();
// Try replacing a table or id that isn't used.
$this->assertFalse($tree->update_dependency_id('toads', 9, 13));
$this->assertFalse($tree->update_dependency_id('frogs', 7, 8));
$this->assertEquals($before, $tree->save());
// Replace the zombies one.
$this->assertTrue($tree->update_dependency_id('zombies', 9, 666));
$after = $tree->save();
$this->assertEquals(666, $after->c[0]->c[1]->id);
// And the frogs one.
$this->assertTrue($tree->update_dependency_id('frogs', 9, 3));
$after = $tree->save();
$this->assertEquals(3, $after->c[0]->c[0]->id);
$this->assertEquals(3, $after->c[1]->id);
}
/**
* Tests the filter_users function.
*/
public function test_filter_users(): void {
$info = new \core_availability\mock_info();
$checker = new capability_checker($info->get_context());
// Don't need to create real users in database, just use these ids.
$users = array(1 => null, 2 => null, 3 => null);
// Test basic tree with one condition that doesn't filter.
$structure = tree::get_root_json(array(self::mock(array())));
$tree = new tree($structure);
$result = $tree->filter_user_list($users, false, $info, $checker);
ksort($result);
$this->assertEquals(array(1, 2, 3), array_keys($result));
// Now a tree with one condition that filters.
$structure = tree::get_root_json(array(self::mock(array('filter' => array(2, 3)))));
$tree = new tree($structure);
$result = $tree->filter_user_list($users, false, $info, $checker);
ksort($result);
$this->assertEquals(array(2, 3), array_keys($result));
// Tree with two conditions that both filter (|).
$structure = tree::get_root_json(array(
self::mock(array('filter' => array(3))),
self::mock(array('filter' => array(1)))), tree::OP_OR);
$tree = new tree($structure);
$result = $tree->filter_user_list($users, false, $info, $checker);
ksort($result);
$this->assertEquals(array(1, 3), array_keys($result));
// Tree with OR condition one of which doesn't filter.
$structure = tree::get_root_json(array(
self::mock(array('filter' => array(3))),
self::mock(array())), tree::OP_OR);
$tree = new tree($structure);
$result = $tree->filter_user_list($users, false, $info, $checker);
ksort($result);
$this->assertEquals(array(1, 2, 3), array_keys($result));
// Tree with two condition that both filter (&).
$structure = tree::get_root_json(array(
self::mock(array('filter' => array(2, 3))),
self::mock(array('filter' => array(1, 2)))));
$tree = new tree($structure);
$result = $tree->filter_user_list($users, false, $info, $checker);
ksort($result);
$this->assertEquals(array(2), array_keys($result));
// Tree with child tree with NOT condition.
$structure = tree::get_root_json(array(
tree::get_nested_json(array(
self::mock(array('filter' => array(1)))), tree::OP_NOT_AND)));
$tree = new tree($structure);
$result = $tree->filter_user_list($users, false, $info, $checker);
ksort($result);
$this->assertEquals(array(2, 3), array_keys($result));
}
/**
* Tests the get_json methods in tree (which are mainly for use in testing
* but might be used elsewhere).
*/
public function test_get_json(): void {
// Create a simple child object (fake).
$child = (object)array('type' => 'fake');
$childstr = json_encode($child);
// Minimal case.
$this->assertEquals(
(object)array('op' => '&', 'c' => array()),
tree::get_nested_json(array()));
// Children and different operator.
$this->assertEquals(
(object)array('op' => '|', 'c' => array($child, $child)),
tree::get_nested_json(array($child, $child), tree::OP_OR));
// Root empty.
$this->assertEquals('{"op":"&","c":[],"showc":[]}',
json_encode(tree::get_root_json(array(), tree::OP_AND)));
// Root with children (multi-show operator).
$this->assertEquals('{"op":"&","c":[' . $childstr . ',' . $childstr .
'],"showc":[true,true]}',
json_encode(tree::get_root_json(array($child, $child), tree::OP_AND)));
// Root with children (single-show operator).
$this->assertEquals('{"op":"|","c":[' . $childstr . ',' . $childstr .
'],"show":true}',
json_encode(tree::get_root_json(array($child, $child), tree::OP_OR)));
// Root with children (specified show boolean).
$this->assertEquals('{"op":"&","c":[' . $childstr . ',' . $childstr .
'],"showc":[false,false]}',
json_encode(tree::get_root_json(array($child, $child), tree::OP_AND, false)));
// Root with children (specified show array).
$this->assertEquals('{"op":"&","c":[' . $childstr . ',' . $childstr .
'],"showc":[true,false]}',
json_encode(tree::get_root_json(array($child, $child), tree::OP_AND, array(true, false))));
}
/**
* Tests the behaviour of the counter in unique_sql_parameter().
*
* There was a problem with static counters used to implement a sequence of
* parameter placeholders (MDL-53481). As always with static variables, it
* is a bit tricky to unit test the behaviour reliably as it depends on the
* actual tests executed and also their order.
*
* To minimise risk of false expected behaviour, this test method should be
* first one where {@link core_availability\tree::get_user_list_sql()} is
* used. We also use higher number of condition instances to increase the
* risk of the counter collision, should there remain a problem.
*/
public function test_unique_sql_parameter_behaviour(): void {
global $DB;
$this->resetAfterTest();
$generator = $this->getDataGenerator();
// Create a test course with multiple groupings and groups and a student in each of them.
$course = $generator->create_course();
$user = $generator->create_user();
$studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
$generator->enrol_user($user->id, $course->id, $studentroleid);
// The total number of groupings and groups must not be greater than 61.
// There is a limit in MySQL on the max number of joined tables.
$groups = [];
for ($i = 0; $i < 25; $i++) {
$group = $generator->create_group(array('courseid' => $course->id));
groups_add_member($group, $user);
$groups[] = $group;
}
$groupings = [];
for ($i = 0; $i < 25; $i++) {
$groupings[] = $generator->create_grouping(array('courseid' => $course->id));
}
foreach ($groupings as $grouping) {
foreach ($groups as $group) {
groups_assign_grouping($grouping->id, $group->id);
}
}
$info = new \core_availability\mock_info($course);
// Make a huge tree with 'AND' of all groups and groupings conditions.
$conditions = [];
foreach ($groups as $group) {
$conditions[] = \availability_group\condition::get_json($group->id);
}
foreach ($groupings as $groupingid) {
$conditions[] = \availability_grouping\condition::get_json($grouping->id);
}
shuffle($conditions);
$tree = new tree(tree::get_root_json($conditions));
list($sql, $params) = $tree->get_user_list_sql(false, $info, false);
// This must not throw exception.
$DB->fix_sql_params($sql, $params);
}
/**
* Tests get_user_list_sql.
*/
public function test_get_user_list_sql(): void {
global $DB;
$this->resetAfterTest();
$generator = $this->getDataGenerator();
// Create a test course with 2 groups and users in each combination of them.
$course = $generator->create_course();
$group1 = $generator->create_group(array('courseid' => $course->id));
$group2 = $generator->create_group(array('courseid' => $course->id));
$userin1 = $generator->create_user();
$userin2 = $generator->create_user();
$userinboth = $generator->create_user();
$userinneither = $generator->create_user();
$studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
foreach (array($userin1, $userin2, $userinboth, $userinneither) as $user) {
$generator->enrol_user($user->id, $course->id, $studentroleid);
}
groups_add_member($group1, $userin1);
groups_add_member($group2, $userin2);
groups_add_member($group1, $userinboth);
groups_add_member($group2, $userinboth);
$info = new \core_availability\mock_info($course);
// Tree with single group condition.
$tree = new tree(tree::get_root_json(array(
\availability_group\condition::get_json($group1->id)
)));
list($sql, $params) = $tree->get_user_list_sql(false, $info, false);
$result = $DB->get_fieldset_sql($sql, $params);
sort($result);
$this->assertEquals(array($userin1->id, $userinboth->id), $result);
// Tree with 'AND' of both group conditions.
$tree = new tree(tree::get_root_json(array(
\availability_group\condition::get_json($group1->id),
\availability_group\condition::get_json($group2->id)
)));
list($sql, $params) = $tree->get_user_list_sql(false, $info, false);
$result = $DB->get_fieldset_sql($sql, $params);
sort($result);
$this->assertEquals(array($userinboth->id), $result);
// Tree with 'AND' of both group conditions.
$tree = new tree(tree::get_root_json(array(
\availability_group\condition::get_json($group1->id),
\availability_group\condition::get_json($group2->id)
), tree::OP_OR));
list($sql, $params) = $tree->get_user_list_sql(false, $info, false);
$result = $DB->get_fieldset_sql($sql, $params);
sort($result);
$this->assertEquals(array($userin1->id, $userin2->id, $userinboth->id), $result);
// Check with flipped logic (NOT above level of tree).
list($sql, $params) = $tree->get_user_list_sql(true, $info, false);
$result = $DB->get_fieldset_sql($sql, $params);
sort($result);
$this->assertEquals(array($userinneither->id), $result);
// Tree with 'OR' of group conditions and a non-filtering condition.
// The non-filtering condition should mean that ALL users are included.
$tree = new tree(tree::get_root_json(array(
\availability_group\condition::get_json($group1->id),
\availability_date\condition::get_json(\availability_date\condition::DIRECTION_UNTIL, 3)
), tree::OP_OR));
list($sql, $params) = $tree->get_user_list_sql(false, $info, false);
$this->assertEquals('', $sql);
$this->assertEquals(array(), $params);
}
/**
* Utility function to build the PHP structure representing a mock condition.
*
* @param array $params Mock parameters
* @return \stdClass Structure object
*/
protected static function mock(array $params) {
$params['type'] = 'mock';
return (object)$params;
}
}