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
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,112 @@
@core @core_courseformat
Feature: Verify activity group mode interface.
In order to edit the course activity group mode
As a teacher
I need to be able edit the group mode form the page course
Background:
Given the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
| category | 0 |
| numsections | 3 |
And the following "groups" exist:
| name | course | idnumber |
| G1 | C1 | GI1 |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
@javascript
Scenario: Teacher can see the group mode badges in both edit and no edit mode
Given the following "activities" exist:
| activity | name | intro | course | idnumber | section | groupmode |
| forum | Activity sample 1 | Test forum description | C1 | sample1 | 1 | 0 |
| forum | Activity sample 2 | Test forum description | C1 | sample2 | 1 | 1 |
| forum | Activity sample 3 | Test forum description | C1 | sample3 | 1 | 2 |
And I am on the "C1" "Course" page logged in as "teacher1"
When I turn editing mode on
Then "Visible groups" "icon" should not exist in the "Activity sample 1" "activity"
And "Separate groups" "icon" should not exist in the "Activity sample 1" "activity"
And "Visible groups" "icon" should not exist in the "Activity sample 2" "activity"
And "Separate groups" "icon" should exist in the "Activity sample 2" "activity"
And "Visible groups" "icon" should exist in the "Activity sample 3" "activity"
And "Separate groups" "icon" should not exist in the "Activity sample 3" "activity"
And I turn editing mode off
And "Visible groups" "icon" should not exist in the "Activity sample 1" "activity"
And "Separate groups" "icon" should not exist in the "Activity sample 1" "activity"
And "Visible groups" "icon" should not exist in the "Activity sample 2" "activity"
And "Separate groups" "icon" should exist in the "Activity sample 2" "activity"
And "Visible groups" "icon" should exist in the "Activity sample 3" "activity"
And "Separate groups" "icon" should not exist in the "Activity sample 3" "activity"
@javascript
Scenario: Teacher can edit the group mode using the activity group mode badge
Given the following "activities" exist:
| activity | name | intro | course | idnumber | section | groupmode |
| forum | Activity sample | Test forum description | C1 | sample | 1 | 1 |
And I am on the "C1" "Course" page logged in as "teacher1"
And I turn editing mode on
And I click on "Separate groups" "icon" in the "Activity sample" "core_courseformat > Activity groupmode"
And I click on "Visible groups" "link" in the "Activity sample" "core_courseformat > Activity groupmode"
And "Separate groups" "icon" should not exist in the "Activity sample" "core_courseformat > Activity groupmode"
And "Visible groups" "icon" should exist in the "Activity sample" "core_courseformat > Activity groupmode"
When I click on "Visible groups" "icon" in the "Activity sample" "core_courseformat > Activity groupmode"
And I click on "Separate groups" "link" in the "Activity sample" "core_courseformat > Activity groupmode"
And "Visible groups" "icon" should not exist in the "Activity sample" "core_courseformat > Activity groupmode"
And "Separate groups" "icon" should exist in the "Activity sample" "core_courseformat > Activity groupmode"
Then I click on "Separate groups" "icon" in the "Activity sample" "core_courseformat > Activity groupmode"
And I click on "No groups" "link" in the "Activity sample" "core_courseformat > Activity groupmode"
And "Separate groups" "icon" should not exist in the "Activity sample" "core_courseformat > Activity groupmode"
And "Visible groups" "icon" should not exist in the "Activity sample" "core_courseformat > Activity groupmode"
And I open "Activity sample" actions menu
And I click on "No groups" "icon" in the "Activity sample" "core_courseformat > Activity groupmode"
And I click on "Separate groups" "link" in the "Activity sample" "core_courseformat > Activity groupmode"
And "Separate groups" "icon" should exist in the "Activity sample" "core_courseformat > Activity groupmode"
@javascript
Scenario: Teacher can edit the group mode using the activity action menu
Given the following "activities" exist:
| activity | name | intro | course | idnumber | section | groupmode |
| forum | Activity sample | Test forum description | C1 | sample | 1 | 1 |
And I am on the "C1" "Course" page logged in as "teacher1"
And I turn editing mode on
And I open "Activity sample" actions menu
And I choose "Group mode > Visible groups" in the open action menu
And "Separate groups" "icon" should not exist in the "Activity sample" "core_courseformat > Activity groupmode"
And "Visible groups" "icon" should exist in the "Activity sample" "core_courseformat > Activity groupmode"
When I open "Activity sample" actions menu
And I choose "Group mode > Separate groups" in the open action menu
And "Visible groups" "icon" should not exist in the "Activity sample" "core_courseformat > Activity groupmode"
And "Separate groups" "icon" should exist in the "Activity sample" "core_courseformat > Activity groupmode"
Then I open "Activity sample" actions menu
And I choose "Group mode > No groups" in the open action menu
And "Separate groups" "icon" should not exist in the "Activity sample" "core_courseformat > Activity groupmode"
And "Visible groups" "icon" should not exist in the "Activity sample" "core_courseformat > Activity groupmode"
And I open "Activity sample" actions menu
And I choose "Group mode > Separate groups" in the open action menu
And "Separate groups" "icon" should exist in the "Activity sample" "core_courseformat > Activity groupmode"
@javascript
Scenario: Teacher cannot see Group mode submenu for an activity when group mode has been forced
Given the following "course" exists:
| fullname | Course 2 |
| shortname | C2 |
| category | 0 |
| numsections | 3 |
| groupmode | 1 |
| groupmodeforce | 1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C2 | editingteacher |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section |
| forum | Activity sample | Test forum description | C2 | sample | 1 |
And I am on the "C2" "Course" page logged in as "teacher1"
And I turn editing mode on
When I open "Activity sample" actions menu
Then "Group mode" "link" should not exist in the "Activity sample" "activity"
And "Separate groups" "button" should not exist in the "Activity sample" "core_courseformat > Activity groupmode"
@@ -0,0 +1,49 @@
@core @core_courseformat @javascript
Feature: Activity type tooltip.
In order to see the activity type
As a Teacher
I need to be able to see a tooltip with the plugin name in editing mode.
Background:
Given the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
| category | 0 |
| numsections | 1 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section |
| assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 |
| page | Activity sample 2 | Test page description | C1 | sample2 | 1 |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
Scenario: Teacher can see the activity type tooltip only while editing.
Given I am on the "C1" "Course" page logged in as "teacher1"
And the "title" attribute of "Activity sample 1" "core_courseformat > Activity icon" should not be set
And the "title" attribute of "Activity sample 2" "core_courseformat > Activity icon" should not be set
And I turn editing mode on
Then the "title" attribute of "Activity sample 1" "core_courseformat > Activity icon" should contain "Assignment"
And the "title" attribute of "Activity sample 2" "core_courseformat > Activity icon" should contain "Page"
Scenario: Student cannot see the activity type tooltip.
Given I am on the "C1" "Course" page logged in as "student1"
Then the "title" attribute of "Activity sample 1" "core_courseformat > Activity icon" should not be set
Scenario: Student cannot see the activity icon link if does not have access.
Given I am on the "Activity sample 2" "page activity editing" page logged in as "admin"
When 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"
And I log out
And I am on the "C1" "Course" page logged in as "student1"
Then "Page icon" "link" should not exist in the "Activity sample 2" "activity"
@@ -0,0 +1,76 @@
@core @core_courseformat
Feature: Verify activity visibility interface.
In order to edit the course activity visibility
As a teacher
I need to be able to see the updateds visibility information
Background:
Given the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
| category | 0 |
| numsections | 3 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section | visible |
| assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 | 1 |
| assign | Activity sample 2 | Test assignment description | C1 | sample2 | 1 | 0 |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
Given I am on the "C1" "Course" page logged in as "teacher1"
And I turn editing mode on
@javascript
Scenario: Teacher can hide an activity using the actions menu.
Given I should not see "Hidden from students" in the "Activity sample 1" "activity"
When I open "Activity sample 1" actions menu
And I choose "Hide" in the open action menu
Then I should see "Hidden from students" in the "Activity sample 1" "core_courseformat > Activity visibility"
@javascript
Scenario: Teacher can show an activity using the actions menu.
Given I should see "Hidden from students" in the "Activity sample 2" "core_courseformat > Activity visibility"
When I open "Activity sample 2" actions menu
And I choose "Show" in the open action menu
Then I should not see "Hidden from students" in the "Activity sample 2" "activity"
@javascript
Scenario: Teacher can make available but not shown an activity using the actions menu.
Given the following config values are set as admin:
| allowstealth | 1 |
And I reload the page
And I should see "Hidden from students" in the "Activity sample 2" "core_courseformat > Activity visibility"
When I open "Activity sample 2" actions menu
And I choose "Availability > Make available but don't show on course page" in the open action menu
Then I should not see "Hidden from students" in the "Activity sample 2" "activity"
And I should see "Available but not shown on course page" in the "Activity sample 2" "core_courseformat > Activity visibility"
@javascript
Scenario: Teacher can show an activity using the visibility badge.
Given I should see "Hidden from students" in the "Activity sample 2" "core_courseformat > Activity visibility"
When I click on "Hidden from students" "button" in the "Activity sample 2" "core_courseformat > Activity visibility"
And I click on "Show on course page" "link" in the "Activity sample 2" "core_courseformat > Activity visibility"
Then I should not see "Hidden from students" in the "Activity sample 2" "activity"
@javascript
Scenario: Teacher can make available but not shown an activity using the visibility badge.
Given the following config values are set as admin:
| allowstealth | 1 |
And I reload the page
When I click on "Hidden from students" "button" in the "Activity sample 2" "core_courseformat > Activity visibility"
And I click on "Make available but don't show on course page" "link" in the "Activity sample 2" "core_courseformat > Activity visibility"
Then I should not see "Hidden from students" in the "Activity sample 2" "activity"
And I should see "Available but not shown on course page" in the "Activity sample 2" "core_courseformat > Activity visibility"
@javascript
Scenario: Make available but not shown is available only when stealth activities are enabled.
Given I click on "Hidden from students" "button" in the "Activity sample 2" "core_courseformat > Activity visibility"
And I should not see "Make available but don't show on course page" in the "Activity sample 2" "activity"
When the following config values are set as admin:
| allowstealth | 1 |
And I reload the page
And I click on "Hidden from students" "button" in the "Activity sample 2" "core_courseformat > Activity visibility"
Then I should see "Make available but don't show on course page" in the "Activity sample 2" "core_courseformat > Activity visibility"
@@ -0,0 +1,52 @@
<?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/>.
declare(strict_types=1);
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/../../../../lib/behat/behat_base.php');
/**
* Behat step definitions for Course format
*
* @package core_courseformat
* @copyright 2023 Mikel Martín <mikel@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_courseformat extends behat_base {
/**
* Return the list of partial named selectors
*
* @return behat_component_named_selector[]
*/
public static function get_partial_named_selectors(): array {
return [
new behat_component_named_selector('Activity completion', [
".//*[@data-activityname=%locator%]//*[@data-region='completionrequirements']",
]),
new behat_component_named_selector('Activity groupmode', [
".//*[@data-activityname=%locator%]//*[@data-region='groupmode']",
]),
new behat_component_named_selector('Activity visibility', [
".//*[@data-activityname=%locator%]//*[@data-region='visibility']",
]),
new behat_component_named_selector('Activity icon', [
".//*[@data-activityname=%locator%]//*[@data-region='activity-icon']",
]),
];
}
}
@@ -0,0 +1,173 @@
@core @core_courseformat @core_course @show_editor @javascript
Feature: Bulk course activity actions.
In order to edit the course activities
As a teacher
I need to be able to edit activities in bulk.
Background:
Given the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
| category | 0 |
| numsections | 4 |
| initsections | 1 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section |
| assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 |
| assign | Activity sample 2 | Test assignment description | C1 | sample2 | 1 |
| assign | Activity sample 3 | Test assignment description | C1 | sample3 | 2 |
| assign | Activity sample 4 | Test assignment description | C1 | sample4 | 2 |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following config values are set as admin:
| allowstealth | 1 |
And I am on the "C1" "Course" page logged in as "teacher1"
And I turn editing mode on
And I click on "Bulk actions" "button"
And I should see "0 selected" in the "sticky-footer" "region"
Scenario: Bulk hiding activities
Given I should not see "Hidden from students" in the "Activity sample 1" "activity"
And I should not see "Hidden from students" in the "Activity sample 2" "activity"
And I should not see "Hidden from students" in the "Activity sample 3" "activity"
And I should not see "Hidden from students" in the "Activity sample 4" "activity"
And I click on "Select activity Activity sample 1" "checkbox"
And I click on "Select activity Activity sample 3" "checkbox"
And I should see "2 selected" in the "sticky-footer" "region"
When I click on "Activity availability" "button" in the "sticky-footer" "region"
And I click on "Hide on course page" "radio" in the "Availability" "dialogue"
And I click on "Apply" "button" in the "Availability" "dialogue"
Then I should see "Hidden from students" in the "Activity sample 1" "activity"
And I should not see "Hidden from students" in the "Activity sample 2" "activity"
And I should see "Hidden from students" in the "Activity sample 3" "activity"
And I should not see "Hidden from students" in the "Activity sample 4" "activity"
And I should see "0 selected" in the "sticky-footer" "region"
Scenario: Bulk showing activities
Given the following "activities" exist:
| activity | name | intro | course | idnumber | section | visible |
| assign | Activity sample 5 | Test assignment description | C1 | sample5 | 1 | 0 |
| assign | Activity sample 6 | Test assignment description | C1 | sample6 | 2 | 0 |
And I reload the page
And I click on "Bulk actions" "button"
And I should not see "Hidden from students" in the "Activity sample 4" "activity"
And I should see "Hidden from students" in the "Activity sample 5" "activity"
And I should see "Hidden from students" in the "Activity sample 6" "activity"
And I click on "Select activity Activity sample 4" "checkbox"
And I click on "Select activity Activity sample 5" "checkbox"
And I click on "Select activity Activity sample 6" "checkbox"
And I should see "3 selected" in the "sticky-footer" "region"
When I click on "Activity availability" "button" in the "sticky-footer" "region"
And I click on "Show on course page" "radio" in the "Availability" "dialogue"
And I click on "Apply" "button" in the "Availability" "dialogue"
Then I should not see "Hidden from students" in the "Activity sample 4" "activity"
And I should not see "Hidden from students" in the "Activity sample 5" "activity"
And I should not see "Hidden from students" in the "Activity sample 6" "activity"
And I should see "0 selected" in the "sticky-footer" "region"
Scenario: Bulk stealth is only available if the site has stealth enabled
Given I click on "Select activity Activity sample 1" "checkbox"
And I should see "1 selected" in the "sticky-footer" "region"
And I click on "Activity availability" "button" in the "sticky-footer" "region"
And I should see "Make available" in the "Availability" "dialogue"
When the following config values are set as admin:
| allowstealth | 0 |
And I reload the page
And I click on "Bulk actions" "button"
Then I click on "Select activity Activity sample 1" "checkbox"
And I should see "1 selected" in the "sticky-footer" "region"
And I click on "Activity availability" "button" in the "sticky-footer" "region"
And I should not see "Make available" in the "Availability" "dialogue"
Scenario: Bulk stealth activities
Given I click on "Select activity Activity sample 1" "checkbox"
And I click on "Activity availability" "button" in the "sticky-footer" "region"
And I click on "Hide on course page" "radio" in the "Availability" "dialogue"
And I click on "Apply" "button" in the "Availability" "dialogue"
And I should see "Hidden from students" in the "Activity sample 1" "activity"
And I should not see "Available but not shown on course page" in the "Activity sample 3" "activity"
When I click on "Select activity Activity sample 1" "checkbox"
And I click on "Select activity Activity sample 3" "checkbox"
And I should see "2 selected" in the "sticky-footer" "region"
And I click on "Activity availability" "button" in the "sticky-footer" "region"
And I click on "Make available but don't show on course page" "radio" in the "Availability" "dialogue"
And I click on "Apply" "button" in the "Availability" "dialogue"
Then I should see "Available but not shown on course page" in the "Activity sample 1" "activity"
And I should see "Available but not shown on course page" in the "Activity sample 3" "activity"
Scenario: Bulk duplicate activities
Given I click on "Select activity Activity sample 1" "checkbox"
And I click on "Select activity Activity sample 3" "checkbox"
And I should see "2 selected" in the "sticky-footer" "region"
When I click on "Duplicate activities" "button" in the "sticky-footer" "region"
Then I should see "Activity sample 1" in the "Section 1" "section"
And I should see "Activity sample 1 (copy)" in the "Section 1" "section"
And "Activity sample 1 (copy)" "activity" should appear after "Activity sample 1" "activity"
And I should see "Activity sample 3" in the "Section 2" "section"
And I should see "Activity sample 3 (copy)" in the "Section 2" "section"
And "Activity sample 3 (copy)" "activity" should appear after "Activity sample 3" "activity"
Scenario: Bulk delete activities
Given I should see "Activity sample 1" in the "Section 1" "section"
And I should see "Activity sample 2" in the "Section 1" "section"
And I should see "Activity sample 3" in the "Section 2" "section"
And I should see "Activity sample 4" in the "Section 2" "section"
And I click on "Select activity Activity sample 1" "checkbox"
And I click on "Select activity Activity sample 3" "checkbox"
And I should see "2 selected" in the "sticky-footer" "region"
When I click on "Delete activities" "button" in the "sticky-footer" "region"
And I click on "Delete" "button" in the "Delete selected activities?" "dialogue"
Then I should not see "Activity sample 1" in the "Section 1" "section"
And I should see "Activity sample 2" in the "Section 1" "section"
And I should not see "Activity sample 3" in the "Section 2" "section"
And I should see "Activity sample 4" in the "Section 2" "section"
And I should see "0 selected" in the "sticky-footer" "region"
Scenario: Bulk move activities after a specific activity
Given I should see "Activity sample 1" in the "Section 1" "section"
And I should see "Activity sample 2" in the "Section 1" "section"
And I should see "Activity sample 3" in the "Section 2" "section"
And I should see "Activity sample 4" in the "Section 2" "section"
And I click on "Select activity Activity sample 1" "checkbox"
And I click on "Select activity Activity sample 3" "checkbox"
And I should see "2 selected" in the "sticky-footer" "region"
When I click on "Move activities" "button" in the "sticky-footer" "region"
And I click on "Activity sample 2" "link" in the "Move selected activities" "dialogue"
And I should see "0 selected" in the "sticky-footer" "region"
# Check activities are moved to the right sections.
Then I should see "Activity sample 1" in the "Section 1" "section"
And I should see "Activity sample 2" in the "Section 1" "section"
And I should see "Activity sample 3" in the "Section 1" "section"
And I should not see "Activity sample 3" in the "Section 2" "section"
And I should see "Activity sample 4" in the "Section 2" "section"
# Check new activities order.
And "Activity sample 1" "activity" should appear after "Activity sample 2" "activity"
And "Activity sample 3" "activity" should appear after "Activity sample 1" "activity"
And "Activity sample 4" "activity" should appear after "Activity sample 3" "activity"
Scenario: Bulk move activities after a specific section header
Given I should see "Activity sample 1" in the "Section 1" "section"
And I should see "Activity sample 2" in the "Section 1" "section"
And I should see "Activity sample 3" in the "Section 2" "section"
And I should see "Activity sample 4" in the "Section 2" "section"
And I click on "Select activity Activity sample 1" "checkbox"
And I click on "Select activity Activity sample 3" "checkbox"
And I should see "2 selected" in the "sticky-footer" "region"
When I click on "Move activities" "button" in the "sticky-footer" "region"
And I click on "Section 3" "link" in the "Move selected activities" "dialogue"
And I should see "0 selected" in the "sticky-footer" "region"
# Check activities are moved to the right sections.
Then I should see "Activity sample 1" in the "Section 3" "section"
Then I should not see "Activity sample 1" in the "Section 1" "section"
And I should see "Activity sample 2" in the "Section 1" "section"
And I should see "Activity sample 3" in the "Section 3" "section"
And I should not see "Activity sample 3" in the "Section 2" "section"
And I should see "Activity sample 4" in the "Section 2" "section"
# Check new activities order.
And "Activity sample 4" "activity" should appear after "Activity sample 2" "activity"
And "Activity sample 1" "activity" should appear after "Activity sample 4" "activity"
And "Activity sample 3" "activity" should appear after "Activity sample 1" "activity"
@@ -0,0 +1,213 @@
@core @core_courseformat @core_course @show_editor @javascript
Feature: Bulk course section actions.
In order to edit the course section
As a teacher
I need to be able to edit sections in bulk.
Background:
Given the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
| category | 0 |
| numsections | 4 |
| initsections | 1 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section |
| assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 |
| assign | Activity sample 2 | Test assignment description | C1 | sample2 | 1 |
| assign | Activity sample 3 | Test assignment description | C1 | sample3 | 2 |
| assign | Activity sample 4 | Test assignment description | C1 | sample4 | 2 |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following config values are set as admin:
| allowstealth | 1 |
And I am on the "C1" "Course" page logged in as "teacher1"
And I turn editing mode on
And I click on "Bulk actions" "button"
And I should see "0 selected" in the "sticky-footer" "region"
Scenario: Bulk hide sections
Given I should not see "Hidden from students" in the "Activity sample 1" "activity"
And I should not see "Hidden from students" in the "Activity sample 2" "activity"
And I should not see "Hidden from students" in the "Activity sample 3" "activity"
And I should not see "Hidden from students" in the "Activity sample 4" "activity"
And I should not see "Hidden from students" in the "Section 1" "section"
And I should not see "Hidden from students" in the "Section 2" "section"
And I should not see "Hidden from students" in the "Section 3" "section"
And I should not see "Hidden from students" in the "Section 4" "section"
When I click on "Select section Section 1" "checkbox"
And I click on "Select section Section 2" "checkbox"
And I should see "2 selected" in the "sticky-footer" "region"
And I click on "Sections availability" "button" in the "sticky-footer" "region"
And I click on "Hide on course page" "radio" in the "Sections availability" "dialogue"
And I click on "Apply" "button" in the "Sections availability" "dialogue"
Then I should see "Hidden from students" in the "Activity sample 1" "activity"
And I should see "Hidden from students" in the "Activity sample 2" "activity"
And I should see "Hidden from students" in the "Activity sample 3" "activity"
And I should see "Hidden from students" in the "Activity sample 4" "activity"
And I should see "Hidden from students" in the "Section 1" "section"
And I should see "Hidden from students" in the "Section 2" "section"
And I should not see "Hidden from students" in the "Section 3" "section"
And I should not see "Hidden from students" in the "Section 4" "section"
And I should see "0 selected" in the "sticky-footer" "region"
Scenario: Bulk show sections
Given I click on "Select section Section 1" "checkbox"
And I click on "Select section Section 3" "checkbox"
And I click on "Sections availability" "button" in the "sticky-footer" "region"
And I click on "Hide on course page" "radio" in the "Sections availability" "dialogue"
And I click on "Apply" "button" in the "Sections availability" "dialogue"
And I should see "Hidden from students" in the "Activity sample 1" "activity"
And I should see "Hidden from students" in the "Activity sample 2" "activity"
And I should not see "Hidden from students" in the "Activity sample 3" "activity"
And I should not see "Hidden from students" in the "Activity sample 4" "activity"
And I should see "Hidden from students" in the "Section 1" "section"
And I should not see "Hidden from students" in the "Section 2" "section"
And I should see "Hidden from students" in the "Section 3" "section"
And I should not see "Hidden from students" in the "Section 4" "section"
When I click on "Select section Section 1" "checkbox"
And I click on "Select section Section 2" "checkbox"
And I should see "2 selected" in the "sticky-footer" "region"
And I click on "Sections availability" "button" in the "sticky-footer" "region"
And I click on "Show on course page" "radio" in the "Sections availability" "dialogue"
And I click on "Apply" "button" in the "Sections availability" "dialogue"
Then I should not see "Hidden from students" in the "Activity sample 1" "activity"
And I should not see "Hidden from students" in the "Activity sample 2" "activity"
And I should not see "Hidden from students" in the "Activity sample 3" "activity"
And I should not see "Hidden from students" in the "Activity sample 4" "activity"
And I should not see "Hidden from students" in the "Section 1" "section"
And I should not see "Hidden from students" in the "Section 2" "section"
And I should see "Hidden from students" in the "Section 3" "section"
And I should not see "Hidden from students" in the "Section 4" "section"
Scenario: Bulk delete sections with content ask for confirmation
Given I should see "Section 1" in the "region-main" "region"
And I should see "Section 2" in the "region-main" "region"
And I should see "Section 3" in the "region-main" "region"
And I should see "Section 4" in the "region-main" "region"
And I should see "Activity sample 1" in the "Section 1" "section"
And I should see "Activity sample 2" in the "Section 1" "section"
And I should see "Activity sample 3" in the "Section 2" "section"
And I should see "Activity sample 4" in the "Section 2" "section"
And I click on "Select section Section 1" "checkbox"
And I click on "Select section Section 2" "checkbox"
And I should see "2 selected" in the "sticky-footer" "region"
When I click on "Delete sections" "button" in the "sticky-footer" "region"
And I click on "Delete" "button" in the "Delete selected sections?" "dialogue"
Then I should see "Section 3" in the "region-main" "region"
And I should see "Section 4" in the "region-main" "region"
And I should not see "Section 1" in the "region-main" "region"
And I should not see "Section 2" in the "region-main" "region"
And I should not see "Activity sample 1" in the "Section 3" "section"
And I should not see "Activity sample 2" in the "Section 3" "section"
And I should not see "Activity sample 3" in the "Section 4" "section"
And I should not see "Activity sample 4" in the "Section 4" "section"
And I should see "0 selected" in the "sticky-footer" "region"
Scenario: Bulk delete sections without content does not ask for confirmation
Given I should see "Section 1" in the "region-main" "region"
And I should see "Section 2" in the "region-main" "region"
And I should see "Section 3" in the "region-main" "region"
And I should see "Section 4" in the "region-main" "region"
And I should see "Activity sample 1" in the "Section 1" "section"
And I should see "Activity sample 2" in the "Section 1" "section"
And I should see "Activity sample 3" in the "Section 2" "section"
And I should see "Activity sample 4" in the "Section 2" "section"
And I click on "Select section Section 3" "checkbox"
And I click on "Select section Section 4" "checkbox"
And I should see "2 selected" in the "sticky-footer" "region"
When I click on "Delete sections" "button" in the "sticky-footer" "region"
And I click on "Delete" "button" in the "Delete selected sections?" "dialogue"
Then I should see "Section 1" in the "region-main" "region"
And I should see "Section 2" in the "region-main" "region"
And I should not see "Section 3" in the "region-main" "region"
And I should not see "Section 4" in the "region-main" "region"
And I should see "Activity sample 1" in the "Section 1" "section"
And I should see "Activity sample 2" in the "Section 1" "section"
And I should see "Activity sample 3" in the "Section 2" "section"
And I should see "Activity sample 4" in the "Section 2" "section"
And I should see "0 selected" in the "sticky-footer" "region"
Scenario: Bulk delete both section with content and empty section ask for confirmation
Given I should see "Section 1" in the "region-main" "region"
And I should see "Section 2" in the "region-main" "region"
And I should see "Section 3" in the "region-main" "region"
And I should see "Section 4" in the "region-main" "region"
And I should see "Activity sample 1" in the "Section 1" "section"
And I should see "Activity sample 2" in the "Section 1" "section"
And I should see "Activity sample 3" in the "Section 2" "section"
And I should see "Activity sample 4" in the "Section 2" "section"
And I click on "Select section Section 2" "checkbox"
And I click on "Select section Section 3" "checkbox"
And I should see "2 selected" in the "sticky-footer" "region"
When I click on "Delete sections" "button" in the "sticky-footer" "region"
And I click on "Delete" "button" in the "Delete selected sections?" "dialogue"
Then I should see "Section 1" in the "region-main" "region"
And I should see "Section 4" in the "region-main" "region"
And I should not see "Section 2" in the "region-main" "region"
And I should not see "Section 3" in the "region-main" "region"
And I should see "Activity sample 1" in the "Section 1" "section"
And I should see "Activity sample 1" in the "Section 1" "section"
And I should see "Activity sample 2" in the "Section 1" "section"
And I should not see "Activity sample 3" in the "Section 4" "section"
And I should not see "Activity sample 4" in the "Section 4" "section"
And I should see "0 selected" in the "sticky-footer" "region"
Scenario: Bulk move sections after general section
Given I set the field "Edit section name" in the "Section 2" "section" to "Move one"
And I set the field "Edit section name" in the "Section 3" "section" to "Move two"
And I click on "Select section Move one" "checkbox"
And I click on "Select section Move two" "checkbox"
And I should see "2 selected" in the "sticky-footer" "region"
When I click on "Move sections" "button" in the "sticky-footer" "region"
And I click on "General" "link" in the "Move selected sections" "dialogue"
# Check activities are moved with the sections.
Then I should see "Activity sample 1" in the "Section 1" "section"
And I should see "Activity sample 2" in the "Section 1" "section"
And I should see "Activity sample 3" in the "Move one" "section"
And I should see "Activity sample 4" in the "Move one" "section"
# Check new section order.
And "Move one" "section" should appear after "General" "section"
And "Move two" "section" should appear after "Move one" "section"
And "Section 1" "section" should appear after "Move two" "section"
And "Section 4" "section" should appear after "Section 1" "section"
Scenario: Bulk move sections at the end of the course
Given I set the field "Edit section name" in the "Section 3" "section" to "Move me"
And I click on "Select section Section 2" "checkbox"
And I click on "Select section Move me" "checkbox"
And I should see "2 selected" in the "sticky-footer" "region"
When I click on "Move sections" "button" in the "sticky-footer" "region"
And I click on "Section 4" "link" in the "Move selected sections" "dialogue"
# Check activities are moved with the sections.
Then I should see "Activity sample 1" in the "Section 1" "section"
And I should see "Activity sample 2" in the "Section 1" "section"
And I should see "Activity sample 3" in the "Section 2" "section"
And I should see "Activity sample 4" in the "Section 2" "section"
# Check new section order.
And "Section 1" "section" should appear after "General" "section"
And "Section 4" "section" should appear after "Section 1" "section"
And "Section 2" "section" should appear after "Section 4" "section"
And "Move me" "section" should appear after "Section 2" "section"
Scenario: Bulk move sections in the middle of the course
Given I set the field "Edit section name" in the "Section 4" "section" to "Move me"
And I click on "Select section Section 1" "checkbox"
And I click on "Select section Move me" "checkbox"
And I should see "2 selected" in the "sticky-footer" "region"
When I click on "Move sections" "button" in the "sticky-footer" "region"
And I click on "Section 2" "link" in the "Move selected sections" "dialogue"
# Check activities are moved with the sections.
Then I should see "Activity sample 1" in the "Section 1" "section"
And I should see "Activity sample 2" in the "Section 1" "section"
And I should see "Activity sample 3" in the "Section 2" "section"
And I should see "Activity sample 4" in the "Section 2" "section"
# Check new section order.
And "Section 2" "section" should appear after "General" "section"
And "Section 1" "section" should appear after "Section 2" "section"
And "Move me" "section" should appear after "Section 1" "section"
And "Section 3" "section" should appear after "Move me" "section"
@@ -0,0 +1,243 @@
@core @core_courseformat @show_editor @javascript
Feature: Bulk activity and section selection.
In order to edit the course activities
As a teacher with capability 'moodle/course:manageactivities'
I need to be able to bulk select activities or sections.
Background:
Given the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
| category | 0 |
| numsections | 4 |
| initsections | 1 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section |
| assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 |
| assign | Activity sample 2 | Test assignment description | C1 | sample2 | 1 |
| assign | Activity sample 3 | Test assignment description | C1 | sample3 | 2 |
| assign | Activity sample 4 | Test assignment description | C1 | sample4 | 2 |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And I am on the "C1" "Course" page logged in as "teacher1"
And I turn editing mode on
Scenario: Enable and disable bulk editing
When I click on "Bulk actions" "button"
Then I should see "0 selected" in the "sticky-footer" "region"
And the focused element is "Select section Section 1" "checkbox"
And I click on "Close bulk actions" "button" in the "sticky-footer" "region"
And "sticky-footer" "region" should not be visible
And the focused element is "Bulk actions" "button"
Scenario: Selecting activities disable section selection
Given I click on "Bulk actions" "button"
And I should see "0 selected" in the "sticky-footer" "region"
When I click on "Select activity Activity sample 1" "checkbox"
And I should see "1 selected" in the "sticky-footer" "region"
Then the "Select section Section 1" "checkbox" should be disabled
Scenario: Selecting sections disable activity selection
Given I click on "Bulk actions" "button"
And I should see "0 selected" in the "sticky-footer" "region"
When I click on "Select section Section 1" "checkbox"
And I should see "1 selected" in the "sticky-footer" "region"
Then the "Select activity Activity sample 1" "checkbox" should be disabled
Scenario: Disable bulk resets the selection
Given I click on "Bulk actions" "button"
And I click on "Select activity Activity sample 1" "checkbox"
And I click on "Select activity Activity sample 2" "checkbox"
And I should see "2 selected" in the "sticky-footer" "region"
And the field "Activity sample 1" matches value "1"
And the field "Activity sample 2" matches value "1"
And the field "Activity sample 3" matches value ""
And the field "Activity sample 4" matches value ""
When I click on "Close bulk actions" "button" in the "sticky-footer" "region"
And I click on "Bulk actions" "button"
Then I should see "0 selected" in the "sticky-footer" "region"
And the field "Activity sample 1" matches value ""
And the field "Activity sample 2" matches value ""
And the field "Activity sample 3" matches value ""
And the field "Activity sample 4" matches value ""
Scenario: Select all is disabled until an activity is selected
Given I click on "Bulk actions" "button"
And the "Select all" "checkbox" should be disabled
When I click on "Select activity Activity sample 1" "checkbox"
And I should see "1 selected" in the "sticky-footer" "region"
Then the "Select all" "checkbox" should be enabled
Scenario: Select all is disabled until a section is selected
Given I click on "Bulk actions" "button"
And the "Select all" "checkbox" should be disabled
When I click on "Select section Section 1" "checkbox"
And I should see "1 selected" in the "sticky-footer" "region"
Then the "Select all" "checkbox" should be enabled
Scenario: Select all when an activity is selected will select all activities
Given I click on "Bulk actions" "button"
And I click on "Select activity Activity sample 1" "checkbox"
And I should see "1 selected" in the "sticky-footer" "region"
And the field "Activity sample 1" matches value "1"
And the field "Activity sample 2" matches value ""
And the field "Activity sample 3" matches value ""
And the field "Activity sample 4" matches value ""
And the "Select all" "checkbox" should be enabled
When I click on "Select all" "checkbox" in the "sticky-footer" "region"
Then I should see "4 selected" in the "sticky-footer" "region"
And the field "Activity sample 1" matches value "1"
And the field "Activity sample 2" matches value "1"
And the field "Activity sample 3" matches value "1"
And the field "Activity sample 4" matches value "1"
Scenario: Select all when a section is selected will select all sections
Given I click on "Bulk actions" "button"
And I click on "Select section Section 1" "checkbox"
And I should see "1 selected" in the "sticky-footer" "region"
And the field "Select section Section 1" matches value "1"
And the field "Select section Section 2" matches value ""
And the field "Select section Section 3" matches value ""
And the field "Select section Section 4" matches value ""
And the "Select all" "checkbox" should be enabled
When I click on "Select all" "checkbox" in the "sticky-footer" "region"
Then I should see "4 selected" in the "sticky-footer" "region"
And the field "Select section Section 1" matches value "1"
And the field "Select section Section 2" matches value "1"
And the field "Select section Section 3" matches value "1"
And the field "Select section Section 4" matches value "1"
Scenario: Click on a select all with all sections selected unselects all sections
Given I click on "Bulk actions" "button"
And I click on "Select section Section 1" "checkbox"
And I click on "Select section Section 2" "checkbox"
And I click on "Select section Section 3" "checkbox"
And I click on "Select section Section 4" "checkbox"
And I should see "4 selected" in the "sticky-footer" "region"
And the "Select all" "checkbox" should be enabled
When I click on "Select all" "checkbox" in the "sticky-footer" "region"
Then I should see "0 selected" in the "sticky-footer" "region"
And the focused element is "Select section Section 1" "checkbox"
And the field "Select section Section 1" matches value ""
And the field "Select section Section 2" matches value ""
And the field "Select section Section 3" matches value ""
And the field "Select section Section 4" matches value ""
Scenario: Click on a select all with all activity selected unselects all activities
Given I click on "Bulk actions" "button"
And I click on "Select activity Activity sample 1" "checkbox"
And I click on "Select activity Activity sample 2" "checkbox"
And I click on "Select activity Activity sample 3" "checkbox"
And I click on "Select activity Activity sample 4" "checkbox"
And I should see "4 selected" in the "sticky-footer" "region"
And the "Select all" "checkbox" should be enabled
When I click on "Select all" "checkbox" in the "sticky-footer" "region"
Then I should see "0 selected" in the "sticky-footer" "region"
And the focused element is "Select section Section 1" "checkbox"
And the field "Activity sample 1" matches value ""
And the field "Activity sample 2" matches value ""
And the field "Activity sample 3" matches value ""
And the field "Activity sample 4" matches value ""
Scenario: Click an activity name in bulk mode select and unselects the activity
Given I click on "Bulk actions" "button"
And I should see "0 selected" in the "sticky-footer" "region"
When I click on "Activity sample 1" "link" in the "Section 1" "section"
And the field "Activity sample 1" matches value "1"
And the field "Activity sample 2" matches value ""
And the field "Activity sample 3" matches value ""
And the field "Activity sample 4" matches value ""
And I click on "Activity sample 2" "link" in the "Section 1" "section"
And the field "Activity sample 1" matches value "1"
And the field "Activity sample 2" matches value "1"
And the field "Activity sample 3" matches value ""
And the field "Activity sample 4" matches value ""
And I should see "2 selected" in the "sticky-footer" "region"
Then I click on "Activity sample 1" "link" in the "Section 1" "section"
And I should see "1 selected" in the "sticky-footer" "region"
And the field "Activity sample 1" matches value ""
And the field "Activity sample 2" matches value "1"
And the field "Activity sample 3" matches value ""
And the field "Activity sample 4" matches value ""
Scenario: Select a range of activities using shift
Given I click on "Bulk actions" "button"
And I should see "0 selected" in the "sticky-footer" "region"
When I click on "Activity sample 1" "link" in the "Section 1" "section"
And the field "Activity sample 1" matches value "1"
And the field "Activity sample 2" matches value ""
And the field "Activity sample 3" matches value ""
And the field "Activity sample 4" matches value ""
And I shift click on "Activity sample 3" "link" in the "Section 2" "section"
Then I should see "3 selected" in the "sticky-footer" "region"
And the field "Activity sample 1" matches value "1"
And the field "Activity sample 2" matches value "1"
And the field "Activity sample 3" matches value "1"
And the field "Activity sample 4" matches value ""
Scenario: Select all activities in a section using alt
Given I click on "Bulk actions" "button"
And I should see "0 selected" in the "sticky-footer" "region"
When I alt click on "Activity sample 3" "link" in the "Section 2" "section"
Then I should see "2 selected" in the "sticky-footer" "region"
And the field "Activity sample 1" matches value ""
And the field "Activity sample 2" matches value ""
And the field "Activity sample 3" matches value "1"
And the field "Activity sample 4" matches value "1"
Scenario: Select a range of sections using shift
Given I click on "Bulk actions" "button"
And I should see "0 selected" in the "sticky-footer" "region"
When I click on "Select section Section 1" "checkbox"
And the field "Select section Section 1" matches value "1"
And the field "Select section Section 2" matches value ""
And the field "Select section Section 3" matches value ""
And the field "Select section Section 4" matches value ""
And I shift click on "Select section Section 3" "checkbox" in the "page" "region"
Then I should see "3 selected" in the "sticky-footer" "region"
And the field "Select section Section 1" matches value "1"
And the field "Select section Section 2" matches value "1"
And the field "Select section Section 3" matches value "1"
And the field "Select section Section 4" matches value ""
Scenario: Select all section with alt click
Given I click on "Bulk actions" "button"
And I should see "0 selected" in the "sticky-footer" "region"
And the field "Select section Section 1" matches value ""
And the field "Select section Section 2" matches value ""
And the field "Select section Section 3" matches value ""
And the field "Select section Section 4" matches value ""
When I alt click on "Select section Section 3" "checkbox" in the "page" "region"
And I should see "4 selected" in the "sticky-footer" "region"
And the field "Select section Section 1" matches value "1"
And the field "Select section Section 2" matches value "1"
And the field "Select section Section 3" matches value "1"
And the field "Select section Section 4" matches value "1"
Scenario: Bulk section selection is available also in one section per page
Given I navigate to "Settings" in current page administration
And I expand all fieldsets
And I set the field "Course layout" to "Show one section per page"
And I click on "Save and display" "button"
And I click on "Bulk actions" "button"
And I should see "0 selected" in the "sticky-footer" "region"
When I click on "Select section Section 1" "checkbox"
And I click on "Select section Section 2" "checkbox"
Then I should see "2 selected" in the "sticky-footer" "region"
Scenario: Bulk selection in small devices
Given I change the viewport size to "mobile"
And I close block drawer if open
When I click on "Bulk actions" "button"
Then I should not see "0 selected" in the "sticky-footer" "region"
And I should not see "Select all" in the "sticky-footer" "region"
And I should not see "Availability" in the "sticky-footer" "region"
And I should not see "Duplicate" in the "sticky-footer" "region"
And I should not see "Move" in the "sticky-footer" "region"
And I should not see "Delete" in the "sticky-footer" "region"
And I click on "Close bulk actions" "button" in the "sticky-footer" "region"
And "sticky-footer" "region" should not be visible
@@ -0,0 +1,99 @@
@core @core_courseformat @core_course @show_editor @javascript
Feature: Bulk course section actions one section per page
In order to edit the course section in one section per page setting
As a teacher
I need to be able to edit sections in bulk in both display modes.
Background:
Given the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
| category | 0 |
| numsections | 4 |
| coursedisplay | 1 |
| initsections | 1 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section |
| assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 |
| assign | Activity sample 2 | Test assignment description | C1 | sample2 | 1 |
| assign | Activity sample 3 | Test assignment description | C1 | sample3 | 2 |
| assign | Activity sample 4 | Test assignment description | C1 | sample4 | 2 |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following config values are set as admin:
| allowstealth | 1 |
And I am on the "C1" "Course" page logged in as "teacher1"
And I turn editing mode on
And I click on "Bulk actions" "button"
And I should see "0 selected" in the "sticky-footer" "region"
Scenario: Bulk section edit is only available when multiple sections are displayed
Given I click on "Select section Section 1" "checkbox"
And I should see "1 selected" in the "sticky-footer" "region"
And I click on "Close bulk actions" "button" in the "sticky-footer" "region"
# Move to single section page.
When I am on the "C1 > Section 1" "course > section" page
And I click on "Bulk actions" "button"
Then "Select section Section 1" "checkbox" should not exist
Scenario: Bulk availability sections in one section per page
Given I should not see "Hidden from students" in the "Activity sample 1" "activity"
And I should not see "Hidden from students" in the "Activity sample 2" "activity"
And I should not see "Hidden from students" in the "Section 1" "section"
And I should not see "Hidden from students" in the "Section 2" "section"
And I click on "Select section Section 1" "checkbox"
And I should see "1 selected" in the "sticky-footer" "region"
And I click on "Sections availability" "button" in the "sticky-footer" "region"
And I click on "Hide on course page" "radio" in the "Section availability" "dialogue"
When I click on "Apply" "button" in the "Section availability" "dialogue"
And I should see "Hidden from students" in the "Activity sample 1" "activity"
And I should see "Hidden from students" in the "Activity sample 2" "activity"
And I should see "Hidden from students" in the "Section 1" "section"
And I should not see "Hidden from students" in the "Section 2" "section"
And I should see "0 selected" in the "sticky-footer" "region"
Scenario: Bulk delete sections in one section per page
Given I should see "Section 1" in the "region-main" "region"
And I should see "Section 2" in the "region-main" "region"
And I should see "Section 3" in the "region-main" "region"
And I should see "Section 4" in the "region-main" "region"
And I should see "Activity sample 1" in the "Section 1" "section"
And I should see "Activity sample 2" in the "Section 1" "section"
And I should see "Activity sample 3" in the "Section 2" "section"
And I should see "Activity sample 4" in the "Section 2" "section"
And I click on "Select section Section 1" "checkbox"
And I should see "1 selected" in the "sticky-footer" "region"
When I click on "Delete sections" "button" in the "sticky-footer" "region"
And I click on "Delete" "button" in the "Delete section?" "dialogue"
Then I should see "Section 2" in the "region-main" "region"
And I should see "Section 3" in the "region-main" "region"
And I should see "Section 4" in the "region-main" "region"
And I should not see "Section 1" in the "region-main" "region"
And I should not see "Activity sample 1"
And I should not see "Activity sample 2"
And I should see "Activity sample 3" in the "Section 2" "section"
And I should see "Activity sample 4" in the "Section 2" "section"
And I should see "0 selected" in the "sticky-footer" "region"
Scenario: Bulk move sections in one section per page
Given I set the field "Edit section name" in the "Section 2" "section" to "Move one"
And I set the field "Edit section name" in the "Section 3" "section" to "Move two"
And I click on "Select section Move one" "checkbox"
And I click on "Select section Move two" "checkbox"
And I should see "2 selected" in the "sticky-footer" "region"
When I click on "Move sections" "button" in the "sticky-footer" "region"
And I click on "General" "link" in the "Move selected sections" "dialogue"
# Check activities are moved with the sections.
Then I should see "Activity sample 1" in the "Section 1" "section"
And I should see "Activity sample 2" in the "Section 1" "section"
And I should see "Activity sample 3" in the "Move one" "section"
And I should see "Activity sample 4" in the "Move one" "section"
# Check new section order.
And "Move one" "section" should appear after "General" "section"
And "Move two" "section" should appear after "Move one" "section"
And "Section 1" "section" should appear after "Move two" "section"
And "Section 4" "section" should appear after "Section 1" "section"
@@ -0,0 +1,77 @@
@core @core_course @core_courseformat
Feature: Course content collapsed user preferences
In order to quickly access the course content
As a user
I need to keep the course sections collapsed when I return to the course.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
| category | 0 |
| enablecompletion | 1 |
| numsections | 5 |
| initsections | 1 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section |
| assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 |
| book | Activity sample 2 | | C1 | sample2 | 2 |
| choice | Activity sample 3 | Test choice description | C1 | sample3 | 3 |
| assign | Activity sample 4 | Test assignment description | C1 | sample1 | 4 |
| assign | Activity sample 5 | Test assignment description | C1 | sample1 | 5 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| teacher1 | C1 | editingteacher |
@javascript
Scenario: Course content preferences
Given I am on the "C1" "Course" page logged in as "teacher1"
And I should see "Section 1" in the "region-main" "region"
And I should see "Activity sample 1" in the "region-main" "region"
And I should see "Section 2" in the "region-main" "region"
And I should see "Activity sample 2" in the "region-main" "region"
And I should see "Section 3" in the "region-main" "region"
And I should see "Activity sample 3" in the "region-main" "region"
And I click on "#collapssesection1" "css_element"
When I reload the page
Then I should see "Section 1" in the "region-main" "region"
And I should not see "Activity sample 1" in the "region-main" "region"
And I should see "Section 2" in the "region-main" "region"
And I should see "Activity sample 2" in the "region-main" "region"
And I should see "Section 3" in the "region-main" "region"
And I should see "Activity sample 3" in the "region-main" "region"
And I click on "#collapssesection2" "css_element"
And I reload the page
And I should see "Section 1" in the "region-main" "region"
And I should not see "Activity sample 1" in the "region-main" "region"
And I should see "Section 2" in the "region-main" "region"
And I should not see "Activity sample 2" in the "region-main" "region"
And I should see "Section 3" in the "region-main" "region"
And I should see "Activity sample 3" in the "region-main" "region"
And I click on "#collapssesection3" "css_element"
And I reload the page
And I should see "Section 1" in the "region-main" "region"
And I should not see "Activity sample 1" in the "region-main" "region"
And I should see "Section 2" in the "region-main" "region"
And I should not see "Activity sample 2" in the "region-main" "region"
And I should see "Section 3" in the "region-main" "region"
And I should not see "Activity sample 3" in the "region-main" "region"
And I click on "#collapssesection2" "css_element"
And I click on "#collapssesection3" "css_element"
And I reload the page
And I should see "Section 1" in the "region-main" "region"
And I should not see "Activity sample 1" in the "region-main" "region"
And I click on "#collapssesection4" "css_element"
And I turn editing mode on
And I delete section "1"
And I click on "Delete" "button" in the ".modal" "css_element"
And I should not see "Activity sample 1" in the "region-main" "region"
And I should see "Activity sample 2" in the "region-main" "region"
And I should see "Activity sample 3" in the "region-main" "region"
And I should not see "Activity sample 4" in the "region-main" "region"
And I should see "Activity sample 5" in the "region-main" "region"
@@ -0,0 +1,383 @@
@core @core_course @core_courseformat
Feature: Course index depending on role
In order to quickly access the course structure
As a user
I need to see the current course structure in the course index.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
| category | 0 |
| enablecompletion | 1 |
| numsections | 4 |
| initsections | 1 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section |
| assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 |
| book | Activity sample 2 | | C1 | sample2 | 2 |
| choice | Activity sample 3 | Test choice description | C1 | sample3 | 3 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| teacher1 | C1 | editingteacher |
# The course index is hidden by default in small devices.
And I change window size to "large"
@javascript
Scenario: Course index is present on course pages.
Given I am on the "C1" "Course" page logged in as "teacher1"
And the "multilang" filter is "on"
And the "multilang" filter applies to "content and headings"
# Course index is visible on Course main page
When I am on the "C1" "Course" page logged in as "teacher1"
And "courseindex-content" "region" should be visible
# Course index is visible on Settings page
And I am on the "C1" "course editing" page
And "courseindex-content" "region" should be visible
# Course index is visible on Participants page
And I am on the "C1" "enrolled users" page
And "courseindex-content" "region" should be visible
# Course index is visible on Enrolment methods page
And I am on the "C1" "enrolment methods" page
And "courseindex-content" "region" should be visible
# Course index is visible on Groups page
And I am on the "C1" "groups" page
And "courseindex-content" "region" should be visible
# Course index is visible on Permissions page
And I am on the "C1" "permissions" page
And "courseindex-content" "region" should be visible
# Course index is visible on Activity edition page
And I am on the "Activity sample 1" "assign activity editing" page
And "courseindex-content" "region" should be visible
And I set the field "Assignment name" in the "General" "fieldset" to "<span lang=\"en\" class=\"multilang\">Activity</span><span lang=\"de\" class=\"multilang\">Aktivität</span> sample 1"
And I press "Save and display"
# Course index is visible on Activity page
And "courseindex-content" "region" should be visible
And I should see "Activity sample 1" in the "courseindex-content" "region"
@javascript
Scenario: Course index as a teacher
Given I log in as "teacher1"
When I am on "Course 1" course homepage
Then I should see "Section 1" in the "courseindex-content" "region"
And I should see "Section 2" in the "courseindex-content" "region"
And I should see "Section 3" in the "courseindex-content" "region"
And I should see "Activity sample 1" in the "courseindex-content" "region"
And I should see "Activity sample 2" in the "courseindex-content" "region"
And I should see "Activity sample 3" in the "courseindex-content" "region"
@javascript
Scenario: Teacher can see hiden activities and sections
Given I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
And I hide section "2"
And I open "Activity sample 3" actions menu
And I choose "Hide" in the open action menu
And I log out
And I log in as "teacher1"
When I am on "Course 1" course homepage
Then I should see "Section 1" in the "courseindex-content" "region"
And I should see "Section 2" in the "courseindex-content" "region"
And I should see "Section 3" in the "courseindex-content" "region"
And I should see "Activity sample 1" in the "courseindex-content" "region"
And I should see "Activity sample 2" in the "courseindex-content" "region"
And I should see "Activity sample 3" in the "courseindex-content" "region"
@javascript
Scenario: Students can only see visible activies and sections
Given I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
And I hide section "2"
And I open "Activity sample 3" actions menu
And I choose "Hide" in the open action menu
And I log out
And I log in as "student1"
When I am on "Course 1" course homepage
Then I should see "Section 1" in the "courseindex-content" "region"
And I should not see "Section 2" in the "courseindex-content" "region"
And I should see "Section 3" in the "courseindex-content" "region"
And I should see "Activity sample 1" in the "courseindex-content" "region"
And I should not see "Activity sample 2" in the "courseindex-content" "region"
And I should not see "Activity sample 3" in the "courseindex-content" "region"
@javascript
Scenario: Delete an activity as a teacher
Given I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
When I delete "Activity sample 2" activity
Then I should not see "Activity sample 2" in the "courseindex-content" "region"
@javascript
Scenario: Highlight sections are represented in the course index.
Given I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I turn section "2" highlighting on
# Current section is only marked visually in the course index.
And the "class" attribute of "#courseindex-content [data-for='section'][data-number='2']" "css_element" should contain "current"
And I should not see "Highlighted" in the "#courseindex-content [data-for='section'][data-number='1']" "css_element"
And I should see "Highlighted" in the "#courseindex-content [data-for='section'][data-number='2']" "css_element"
When I turn section "1" highlighting on
# Current section is only marked visually in the course index.
Then the "class" attribute of "#courseindex-content [data-for='section'][data-number='1']" "css_element" should contain "current"
And I should see "Highlighted" in the "#courseindex-content [data-for='section'][data-number='1']" "css_element"
And I should not see "Highlighted" in the "#courseindex-content [data-for='section'][data-number='2']" "css_element"
@javascript
Scenario: Course index toggling
Given the following "activities" exist:
| activity | name | course | idnumber | section |
| book | Second activity in section 1 | C1 | sample4 | 1 |
When I am on the "Course 1" course page logged in as teacher1
# Sections should be opened by default.
Then I should see "Section 1" in the "courseindex-content" "region"
And I should see "Activity sample 1" in the "courseindex-content" "region"
And I should see "Second activity in section 1" in the "courseindex-content" "region"
And I should see "Section 2" in the "courseindex-content" "region"
And I should see "Activity sample 2" in the "courseindex-content" "region"
And I should see "Section 3" in the "courseindex-content" "region"
And I should see "Activity sample 3" in the "courseindex-content" "region"
# Collapse a section 1 via chevron.
And I click on "Collapse" "link" in the ".courseindex-section[data-number='1']" "css_element"
And I should see "Section 1" in the "courseindex-content" "region"
And I should not see "Activity sample 1" in the "courseindex-content" "region"
And I should not see "Second activity in section 1" in the "courseindex-content" "region"
And I should see "Section 2" in the "courseindex-content" "region"
And I should see "Activity sample 2" in the "courseindex-content" "region"
And I should see "Section 3" in the "courseindex-content" "region"
And I should see "Activity sample 3" in the "courseindex-content" "region"
# Expand section 1 via section name.
And I click on "Section 1" "link" in the "courseindex-content" "region"
And I should see "Section 1" in the "courseindex-content" "region"
And I should see "Activity sample 1" in the "courseindex-content" "region"
And I should see "Second activity in section 1" in the "courseindex-content" "region"
And I should see "Section 2" in the "courseindex-content" "region"
And I should see "Activity sample 2" in the "courseindex-content" "region"
And I should see "Section 3" in the "courseindex-content" "region"
And I should see "Activity sample 3" in the "courseindex-content" "region"
# Collapse a section 2 via chevron.
And I click on "Collapse" "link" in the ".courseindex-section[data-number='2']" "css_element"
And I should see "Section 1" in the "courseindex-content" "region"
And I should see "Activity sample 1" in the "courseindex-content" "region"
And I should see "Second activity in section 1" in the "courseindex-content" "region"
And I should see "Section 2" in the "courseindex-content" "region"
And I should not see "Activity sample 2" in the "courseindex-content" "region"
And I should see "Section 3" in the "courseindex-content" "region"
And I should see "Activity sample 3" in the "courseindex-content" "region"
# Expand section 2 via chevron.
And I click on "Expand" "link" in the ".courseindex-section[data-number='2']" "css_element"
And I should see "Section 1" in the "courseindex-content" "region"
And I should see "Activity sample 1" in the "courseindex-content" "region"
And I should see "Second activity in section 1" in the "courseindex-content" "region"
And I should see "Section 2" in the "courseindex-content" "region"
And I should see "Activity sample 2" in the "courseindex-content" "region"
And I should see "Section 3" in the "courseindex-content" "region"
And I should see "Activity sample 3" in the "courseindex-content" "region"
# Click a section name does not collapse the section.
And I click on "Section 2" "link" in the "courseindex-content" "region"
And I should see "Section 1" in the "courseindex-content" "region"
And I should see "Activity sample 1" in the "courseindex-content" "region"
And I should see "Second activity in section 1" in the "courseindex-content" "region"
And I should see "Section 2" in the "courseindex-content" "region"
And I should see "Activity sample 2" in the "courseindex-content" "region"
And I should see "Section 3" in the "courseindex-content" "region"
And I should see "Activity sample 3" in the "courseindex-content" "region"
@javascript
Scenario: Course index toggling all sections
When I am on the "Course 1" course page logged in as teacher1
# Sections should be opened by default.
Then I should see "Section 1" in the "courseindex-content" "region"
And I should see "Activity sample 1" in the "courseindex-content" "region"
And I should see "Section 2" in the "courseindex-content" "region"
And I should see "Activity sample 2" in the "courseindex-content" "region"
And I should see "Section 3" in the "courseindex-content" "region"
And I should see "Activity sample 3" in the "courseindex-content" "region"
# Collapse all sections
And I click on "Course index options" "button" in the "#courseindexdrawercontrols" "css_element"
And I click on "Collapse all" "link" in the "#courseindexdrawercontrols" "css_element"
And I should see "Section 1" in the "courseindex-content" "region"
And I should not see "Activity sample 1" in the "courseindex-content" "region"
And I should see "Section 2" in the "courseindex-content" "region"
And I should not see "Activity sample 2" in the "courseindex-content" "region"
And I should see "Section 3" in the "courseindex-content" "region"
And I should not see "Activity sample 3" in the "courseindex-content" "region"
# Expand all sections
And I click on "Course index options" "button" in the "#courseindexdrawercontrols" "css_element"
And I click on "Expand all" "link" in the "#courseindexdrawercontrols" "css_element"
And I should see "Section 1" in the "courseindex-content" "region"
And I should see "Activity sample 1" in the "courseindex-content" "region"
And I should see "Section 2" in the "courseindex-content" "region"
And I should see "Activity sample 2" in the "courseindex-content" "region"
And I should see "Section 3" in the "courseindex-content" "region"
And I should see "Activity sample 3" in the "courseindex-content" "region"
@javascript
Scenario: Course index section preferences
When I am on the "C1" "Course" page logged in as "teacher1"
Then I should see "Section 1" in the "courseindex-content" "region"
And I should see "Activity sample 1" in the "courseindex-content" "region"
And I should see "Section 2" in the "courseindex-content" "region"
And I should see "Activity sample 2" in the "courseindex-content" "region"
And I should see "Section 3" in the "courseindex-content" "region"
And I should see "Activity sample 3" in the "courseindex-content" "region"
# Collapse section 1.
And I click on "Collapse" "link" in the ".courseindex-section[data-number='1']" "css_element"
And I reload the page
And I should see "Section 1" in the "courseindex-content" "region"
And I should not see "Activity sample 1" in the "courseindex-content" "region"
And I should see "Section 2" in the "courseindex-content" "region"
And I should see "Activity sample 2" in the "courseindex-content" "region"
And I should see "Section 3" in the "courseindex-content" "region"
And I should see "Activity sample 3" in the "courseindex-content" "region"
# Collapse section 3.
And I click on "Collapse" "link" in the ".courseindex-section[data-number='3']" "css_element"
And I reload the page
And I should see "Section 1" in the "courseindex-content" "region"
And I should not see "Activity sample 1" in the "courseindex-content" "region"
And I should see "Section 2" in the "courseindex-content" "region"
And I should see "Activity sample 2" in the "courseindex-content" "region"
And I should see "Section 3" in the "courseindex-content" "region"
And I should not see "Activity sample 3" in the "courseindex-content" "region"
# Delete section 1
And I turn editing mode on
And I delete section "1"
And I click on "Delete" "button" in the ".modal" "css_element"
And I reload the page
And I should not see "Activity sample 1" in the "courseindex-content" "region"
And I should see "Section 2" in the "courseindex-content" "region"
And I should see "Activity sample 2" in the "courseindex-content" "region"
And I should see "Section 3" in the "courseindex-content" "region"
And I should not see "Activity sample 3" in the "courseindex-content" "region"
@javascript
Scenario: Adding section should alter the course index
Given I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
When I click on "Add section" "link" in the "course-addsection" "region"
Then I should see "New section" in the "courseindex-content" "region"
@javascript
Scenario: Remove a section should alter the course index
Given I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
When I delete section "4"
And I click on "Delete" "button" in the ".modal" "css_element"
Then I should not see "Section 4" in the "courseindex-content" "region"
@javascript
Scenario: Delete a previous section should alter the course index unnamed sections
Given I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
When I delete section "1"
And I click on "Delete" "button" in the ".modal" "css_element"
Then I should not see "Section 1" in the "courseindex-content" "region"
And I should not see "Activity sample 1" in the "courseindex-content" "region"
@javascript
Scenario: Course index locked activity link
Given the following config values are set as admin:
| enableavailability | 1 |
And I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I navigate to "Settings" in current page administration
And I expand all fieldsets
And I set the field "Course layout" to "Show one section per page"
And I click on "Save and display" "button"
# Add access restriction to Activity sample 3.
And I open "Activity sample 3" actions menu
And I click on "Edit settings" "link" in the "Activity sample 3" activity
And I expand all fieldsets
And I click on "Add restriction..." "button"
And I click on "Date" "button" in the "Add restriction..." "dialogue"
And I set the following fields to these values:
| x[day] | 31 |
| x[month] | 12 |
| x[year] | ## +1 year ## %Y ## |
And I press "Save and return to course"
And I log out
# Check course index link goes to the specific section.
When I log in as "student1"
And I am on "Course 1" course homepage
And I click on "Section 1" "link" in the "region-main" "region"
And I should not see "Activity sample 3" in the "region-main" "region"
And I click on "Activity sample 3" "link" in the "courseindex-content" "region"
Then I should see "Activity sample 3" in the "region-main" "region"
@javascript
Scenario Outline: Course index is displayed by default depending on the screen size.
When I change window size to "<device>"
And I am on the "C1" "Course" page logged in as "student1"
Then "courseindex-content" "region" should <bydefault> visible
And I reload the page
And "courseindex-content" "region" should <bydefault> visible
# Check whenever preferences are saved.
And I click on "<action1> course index" "button"
And I reload the page
And "courseindex-content" "region" should <visible1> visible
And I click on "<action2> course index" "button"
And I reload the page
And "courseindex-content" "region" should <visible2> visible
Examples:
| device | bydefault | action1 | visible1 | action2 | visible2 |
| large | be | Close | not be | Open | be |
| tablet | not be | Open | not be | Open | not be |
| mobile | not be | Open | not be | Open | not be |
@javascript
Scenario: Course index is refreshed when we change role.
When I am on the "C1" "Course" page logged in as "teacher1"
And I turn editing mode on
And I hide section "1"
And I turn editing mode off
And I should see "Section 1" in the "courseindex-content" "region"
And I follow "Switch role to..." in the user menu
And I press "Student"
Then I should not see "Section 1" in the "courseindex-content" "region"
@javascript
Scenario: Course index behaviour for activities without url
# Add a label to the course (labels doesn't have URL, because they can't be displayed in a separate page).
Given the following "activities" exist:
| activity | name | intro | course | idnumber | section |
| label | Activity sample 4 | Test label | C1 | sample4 | 2 |
# Check resources without URL, as labels, are displayed in the CI and the link goes to the main page when it is clicked.
When I am on the "sample1" "Activity" page logged in as "student1"
Then I should see "Activity sample 4" in the "#courseindex" "css_element"
And I click on "Activity sample 4" "link" in the "#courseindex" "css_element"
And I should see "Test label" in the "region-main" "region"
And I should see "Activity sample 2" in the "region-main" "region"
# Check resources without URL, as labels, are displayed for teachers too, and the link is working even when edit mode is on.
And I am on the "sample1" "Activity" page logged in as "teacher1"
And I should see "Activity sample 4" in the "#courseindex" "css_element"
And I turn editing mode on
And I should see "Activity sample 4" in the "#courseindex" "css_element"
And I click on "Activity sample 4" "link" in the "#courseindex" "css_element"
And I should see "Test label" in the "region-main" "region"
And I should see "Activity sample 2" in the "region-main" "region"
@javascript
Scenario: Course index behaviour for labels with name or without name
# Add two labels to the course (one with name and one without name).
Given the following "activities" exist:
| activity | name | intro | course | idnumber | section |
| label | Activity sample 5 | Test label 1 | C1 | sample4 | 2 |
| label | | Test label 2 | C1 | sample5 | 2 |
When I am on the "Course 1" course page logged in as teacher1
And I should see "Section 2" in the "courseindex-content" "region"
# Label name should be displayed if it is set.
And I should see "Activity sample 5" in the "courseindex-content" "region"
# Label intro text should be displayed if label name is not set.
And I should see "Test label 2" in the "courseindex-content" "region"
@javascript
Scenario: Change the section name inline in section page
When I am on the "Course 1 > Section 2" "course > section" page logged in as "teacher1"
And I turn editing mode on
When I set the field "Edit section name" in the "page-header" "region" to "Custom section name"
Then I should see "Custom section name" in the "courseindex-content" "region"
@@ -0,0 +1,79 @@
@core @core_courseformat @show_editor
Feature: Verify edit utils availability
In order to edit the course activities
As a student with capability 'moodle/course:manageactivities'
I need to be able to use the edit utils.
Background:
Given the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
| category | 0 |
| numsections | 3 |
| initsections | 1 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section |
| assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
| author1 | Author | 1 | author1@example.com |
And the following "roles" exist:
| shortname | name | archetype |
| author | Author | student |
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/course:manageactivities | Allow | author | Course | C1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| author1 | C1 | author |
| student1 | C1 | student |
@javascript
Scenario: Edit tools should be available to teachers.
Given I log in as "teacher1"
When I am on "Course 1" course homepage
And I turn editing mode on
Then I should see "Add an activity or resource"
And I open "Activity sample 1" actions menu
And I should see "Edit settings"
And ".section_action_menu" "css_element" should exist in the "Section 1" "section"
And I click on ".section_action_menu" "css_element" in the "Section 1" "section"
And I should see "Edit settings"
@javascript
Scenario: Edit mode should not be available to students.
Given I log in as "student1"
When I am on "Course 1" course homepage
Then I should not see "Edit mode"
@javascript
Scenario: Edit tools should be available to students with manageactivities capability but not allowed to add sections without course:update
Given I log in as "author1"
When I am on "Course 1" course homepage
And I turn editing mode on
Then I should see "Add an activity or resource"
But I should not see "Add section"
And I open "Activity sample 1" actions menu
And I should see "Edit settings"
And I open section "1" edit menu
And I should not see "Edit settings"
And I should see "View"
@javascript
Scenario: Section adding should be available to students if they also have the capability 'moodle/course:update'.
Given the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/course:update | Allow | author | Course | C1 |
And I log in as "author1"
When I am on "Course 1" course homepage
And I turn editing mode on
Then I should see "Add an activity or resource"
And I should see "Add section"
And I open "Activity sample 1" actions menu
And I should see "Edit settings"
And ".section_action_menu" "css_element" should exist in the "Section 1" "section"
And I click on ".section_action_menu" "css_element" in the "Section 1" "section"
And I should see "Edit settings"
@@ -0,0 +1,184 @@
@core @core_course @core_courseformat
Feature: Course index completion icons
In order to quickly check my activities completions
As a student
I need to see the activity completion in the course index.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
| category | 0 |
| enablecompletion | 1 |
| numsections | 4 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section | completion |
| assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 | 1 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| teacher1 | C1 | editingteacher |
# The course index is hidden by default in small devices.
And I change window size to "large"
@javascript
Scenario: Teacher does not see completion icons.
When I am on the "C1" "Course" page logged in as "teacher1"
Then I should see "New section" in the "courseindex-content" "region"
And I should see "Activity sample 1" in the "courseindex-content" "region"
And "To do" "icon" should not exist in the "courseindex-content" "region"
@javascript
Scenario: User should see the completion icons
When I am on the "C1" "Course" page logged in as "student1"
Then I should see "New section" in the "courseindex-content" "region"
And I should see "Activity sample 1" in the "courseindex-content" "region"
And "To do" "icon" should exist in the "courseindex-content" "region"
@javascript
Scenario: Manual completion should update the course index completion
Given I am on the "C1" "Course" page logged in as "student1"
And "To do" "icon" should exist in the "courseindex-content" "region"
When I press "Mark as done"
And I wait until "Done" "button" exists
Then "Done" "icon" should exist in the "courseindex-content" "region"
And I press "Done"
And I wait until "Mark as done" "button" exists
And "To do" "icon" should exist in the "courseindex-content" "region"
@javascript
Scenario: Manual completion in an activity page should update the course index
Given I am on the "sample1" "Activity" page logged in as "student1"
And "To do" "icon" should exist in the "courseindex-content" "region"
When I press "Mark as done"
And I wait until "Done" "button" exists
Then "Done" "icon" should exist in the "courseindex-content" "region"
And I press "Done"
And I wait until "Mark as done" "button" exists
And "To do" "icon" should exist in the "courseindex-content" "region"
@javascript
Scenario: Refresh the page should keep the completion consistent
Given I am on the "C1" "Course" page logged in as "student1"
And "To do" "icon" should exist in the "courseindex-content" "region"
When I press "Mark as done"
And I wait until "Done" "button" exists
And I reload the page
Then the manual completion button of "Activity sample 1" is displayed as "Done"
@javascript
Scenario: Auto completion should appear in the course index
Given the following "activities" exist:
| activity | name | intro | course | idnumber | section | completion | completionview |
| assign | Activity sample 2 | Test assignment description | C1 | sample2 | 1 | 1 | 1 |
When I am on the "sample2" "Activity" page logged in as "student1"
And I am on the "C1" "Course" page
Then "Done" "icon" should exist in the "courseindex-content" "region"
@javascript
Scenario: Completion failed should appear in the course index
Given the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | First question | Answer the first question |
And the following "activities" exist:
| activity | name | course | idnumber | attempts | gradepass | completion | completionusegrade | completionpassgrade | completionattemptsexhausted |
| quiz | Test quiz name | C1 | quiz1 | 1 | 5.00 | 2 | 1 | 1 | 1 |
And quiz "Test quiz name" contains the following questions:
| question | page |
| First question | 1 |
And user "student1" has attempted "Test quiz name" with responses:
| slot | response |
| 1 | False |
When I am on the "C1" "Course" page logged in as "student1"
And "Failed" "icon" should exist in the "courseindex-content" "region"
@javascript
Scenario: Completion passed should appear in the course index
Given the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | First question | Answer the first question |
And the following "activities" exist:
| activity | name | course | idnumber | attempts | gradepass | completion | completionusegrade | completionpassgrade | completionattemptsexhausted |
| quiz | Test quiz name | C1 | quiz1 | 1 | 5.00 | 2 | 1 | 1 | 1 |
And quiz "Test quiz name" contains the following questions:
| question | page |
| First question | 1 |
And user "student1" has attempted "Test quiz name" with responses:
| slot | response |
| 1 | True |
When I am on the "C1" "Course" page logged in as "student1"
And "Done" "icon" should exist in the "courseindex-content" "region"
@javascript
Scenario: Completion done should appear in the course index when the requirement is any grade
Given the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | First question | Answer the first question |
And the following "activities" exist:
| activity | name | course | idnumber | attempts | gradepass | completion | completionusegrade | completionpassgrade |
| quiz | Test quiz name | C1 | quiz1 | 1 | 5.00 | 2 | 1 | 0 |
And quiz "Test quiz name" contains the following questions:
| question | page |
| First question | 1 |
And user "student1" has attempted "Test quiz name" with responses:
| slot | response |
| 1 | False |
When I am on the "C1" "Course" page logged in as "student1"
And "Done" "icon" should exist in the "courseindex-content" "region"
@javascript
Scenario: Activities with custom completion rules could fail
Given the following "activity" exists:
| activity | scorm |
| course | C1 |
| name | Music history |
| packagefilepath | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip |
| maxattempt | 1 |
| latattemptlock | 1 |
# Add requirements
| completion | 2 |
| completionscorerequired | 90 |
Given I am on the "Music history" "scorm activity" page logged in as student1
# We need a little taller window because Firefox is, apparently, unable to auto-scroll within
# an iframe, so we need to ensure that the "Save changes" button is visible in the viewport.
And I change window size to "large"
And I press "Enter"
And I switch to the main frame
And I click on "Par?" "list_item"
And I switch to "scorm_object" iframe
And I wait until the page is ready
And I switch to the main frame
And I click on "Keeping Score" "list_item"
And I switch to "scorm_object" iframe
And I wait until the page is ready
And I switch to the main frame
And I click on "Other Scoring Systems" "list_item"
And I switch to "scorm_object" iframe
And I wait until the page is ready
And I switch to the main frame
And I click on "The Rules of Golf" "list_item"
And I switch to "scorm_object" iframe
And I wait until the page is ready
And I switch to the main frame
And I click on "Playing Golf Quiz" "list_item"
And I switch to "scorm_object" iframe
And I wait until the page is ready
And I click on "[id='question_com.scorm.golfsamples.interactions.playing_1_1']" "css_element"
And I press "Submit Answers"
And I wait until "Score: 20" "text" exists
And I switch to the main frame
And I click on "Exit activity" "link"
And "Failed" "icon" should exist in the "courseindex-content" "region"
@@ -0,0 +1,118 @@
@core @core_courseformat
Feature: Verify that courseindex is usable with the keyboard
In order to use the course index
As a user
I need to be able to navigate it without a mouse
Background:
Given the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
| category | 0 |
| enablecompletion | 1 |
| numsections | 3 |
| initsections | 1 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section |
| assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 |
| book | Activity sample 2 | | C1 | sample2 | 2 |
| choice | Activity sample 3 | Test choice description | C1 | sample3 | 3 |
Given I am on the "C1" "Course" page logged in as "admin"
And I change window size to "large"
And I click on "Close course index" "button"
And I click on "Open course index" "button"
And I should see "Section 1" in the "courseindex-content" "region"
And the focused element is "[data-preference='drawer-open-index'] .drawertoggle" "css_element"
And I press the tab key
And I press the tab key
And the focused element is ".courseindex-section" "css_element"
@javascript
Scenario: General focus on open course index.
When I press the shift tab key
And I press the shift tab key
And the focused element is "[data-preference='drawer-open-index'] .drawertoggle" "css_element"
And I press enter
Then I should not see "Section 1" in the "courseindex-content" "region"
@javascript @accessibility
Scenario: Course index should be accessible.
When I press the shift tab key
And I press the shift tab key
And I press enter
Then the page should meet accessibility standards with "wcag143" extra tests
And I press enter
And the page should meet accessibility standards with "wcag143" extra tests
@javascript
Scenario: Opening and closing sections.
When I press the down key
And I should see "Activity sample 1" in the "courseindex-content" "region"
# Close section with left key.
Then I press the left key
And I should not see "Activity sample 1" in the "courseindex-content" "region"
# Open a section with right key
And I press the right key
And I should see "Activity sample 1" in the "courseindex-content" "region"
# Key down to focus the module and close the section with two left keys.
And I press the down key
And I press the left key
And I press the left key
And I should not see "Activity sample 1" in the "courseindex-content" "region"
# Open a section using enter key.
And I press the down key
And I press the left key
And I should not see "Activity sample 2" in the "courseindex-content" "region"
And I press enter
And I should see "Activity sample 2" in the "courseindex-content" "region"
@javascript
Scenario: Enter key should not collapse sections.
When I press the down key
And I press enter
And I should see "Activity sample 1" in the "courseindex-content" "region"
@javascript
Scenario: Navigate to an activity.
When I press the down key
And I press the right key
And I press enter
Then I should see "Activity sample 1" in the "page-header" "region"
@javascript
Scenario: Navigate to first and last element.
# Close sections 1 and 3.
Given I press the down key
And I press the left key
And I should not see "Activity sample 1" in the "courseindex-content" "region"
And I press the down key
And I press the down key
And I press the down key
And I press the left key
And I should not see "Activity sample 3" in the "courseindex-content" "region"
# Use end key to go to the last element.
When I press the end key
And I press the right key
And I should see "Activity sample 3" in the "courseindex-content" "region"
And I press the left key
Then I should not see "Activity sample 3" in the "courseindex-content" "region"
# Use home key to go to the first element.
And I press the home key
And I press the down key
And I press the right key
And I should see "Activity sample 1" in the "courseindex-content" "region"
@javascript
Scenario: Asterisc to open all sections.
# Close sections 1 and 2.
Given I press the down key
And I press the left key
And I should not see "Activity sample 1" in the "courseindex-content" "region"
And I press the down key
And I press the left key
And I should not see "Activity sample 2" in the "courseindex-content" "region"
And I should see "Activity sample 3" in the "courseindex-content" "region"
When I press the multiply key
Then I should see "Activity sample 1" in the "courseindex-content" "region"
And I should see "Activity sample 2" in the "courseindex-content" "region"
And I should see "Activity sample 3" in the "courseindex-content" "region"
@@ -0,0 +1,36 @@
@core @core_course @core_courseformat
Feature: The course index language should change according to user preferences.
As a user in a course
I should see the same language in the course index and the course content when I switch languages.
Background:
Given remote langimport tests are enabled
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
| category | 0 |
| enablecompletion | 1 |
| numsections | 4 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| teacher1 | C1 | editingteacher |
And the following "language pack" exists:
| language | fr |
And I change window size to "large"
@javascript
Scenario Outline: Course index is refreshed when we change language.
Given I am on the "C1" "Course" page logged in as "<user>"
When I follow "Language" in the user menu
Then I should see "Language selector" user submenu
And I follow "Français (fr)"
Then I should see "Généralités" in the "courseindex-content" "region"
Examples:
| user |
| student1 |
| teacher1 |
@@ -0,0 +1,192 @@
@core @core_course @core_courseformat @core_completion
Feature: Course page activities completion
In order to check activities completions
As a student
I need to see the activity completion criterias dropdown.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| shortname | fullname | enablecompletion |
| C1 | Course 1 | 1 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| teacher1 | C1 | editingteacher |
Scenario: Teacher does not see manual completion button
Given the following "activity" exists:
| activity | assign |
| name | Activity sample |
| course | C1 |
| completion | 1 |
| completionview | 0 |
When I am on the "C1" "Course" page logged in as "teacher1"
Then "Mark as done" "button" should not exist in the "Activity sample" "activity"
And the "Mark as done" item should exist in the "Completion" dropdown of the "Activity sample" "activity"
@javascript
Scenario: Student should see the manual completion button
Given the following "activity" exists:
| activity | assign |
| name | Activity sample |
| course | C1 |
| completion | 1 |
| completionview | 0 |
When I am on the "C1" "Course" page logged in as "student1"
Then the manual completion button for "Activity sample" should exist
And the manual completion button of "Activity sample" is displayed as "Mark as done"
And I toggle the manual completion state of "Activity sample"
And the manual completion button of "Activity sample" is displayed as "Done"
Scenario: Teacher should see the automatic completion criterias of activities
Given the following "activity" exists:
| activity | assign |
| name | Activity sample |
| course | C1 |
| completion | 2 |
| completionview | 1 |
When I am on the "C1" "Course" page logged in as "teacher1"
And the "View" item should exist in the "Completion" dropdown of the "Activity sample" "activity"
# After viewing the activity, the completion criteria dropdown should still display "Completion".
And I am on the "Activity sample" "assign Activity" page
And I am on the "Course 1" course page
And "Completion" "button" should exist in the "Activity sample" "activity"
Scenario: Student should see the automatic completion criterias statuses of activities with completion view
Given the following "activity" exists:
| activity | assign |
| name | Activity sample |
| course | C1 |
| completion | 2 |
| completionview | 1 |
When I am on the "C1" "Course" page logged in as "student1"
And the "View" item should exist in the "To do" dropdown of the "Activity sample" "activity"
# After viewing the activity, the completion criteria dropdown should display "Done" instead of "To do".
And I am on the "Activity sample" "assign Activity" page
And I am on the "Course 1" course page
And "To do" "button" should not exist in the "Activity sample" "activity"
And the "View" item should exist in the "Done" dropdown of the "Activity sample" "activity"
Scenario: Student should see the automatic completion criterias statuses of activities with completion grade
Given the following "activities" exist:
| activity | name | course | idnumber | gradepass | completion | completionusegrade |
| quiz | Activity sample 1 | C1 | quiz1 | 5.00 | 2 | 1 |
| quiz | Activity sample 2 | C1 | quiz2 | 5.00 | 2 | 1 |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | First question | Answer the first question |
And quiz "Activity sample 1" contains the following questions:
| question | page |
| First question | 1 |
And quiz "Activity sample 2" contains the following questions:
| question | page |
| First question | 1 |
When I am on the "C1" "Course" page logged in as "student1"
Then the "Receive a grade" item should exist in the "To do" dropdown of the "Activity sample 1" "activity"
And the "Receive a grade" item should exist in the "To do" dropdown of the "Activity sample 2" "activity"
# Pass grade.
And user "student1" has attempted "Activity sample 1" with responses:
| slot | response |
| 1 | True |
# Fail grade.
And user "student1" has attempted "Activity sample 2" with responses:
| slot | response |
| 1 | False |
# After receiving a grade, the completion criteria dropdown should display "Done" instead of "To do", regardless of pass/fail.
And I am on the "Course 1" course page
And "To do" "button" should not exist in the "Activity sample 1" "activity"
And the "Receive a grade" item should exist in the "Done" dropdown of the "Activity sample 1" "activity"
And "To do" "button" should not exist in the "Activity sample 2" "activity"
And the "Receive a grade" item should exist in the "Done" dropdown of the "Activity sample 2" "activity"
Scenario: Student should see the automatic completion criterias statuses of activities with completion passgrade
Given the following "activities" exist:
| activity | name | course | idnumber | gradepass | completion | completionusegrade | completionpassgrade |
| quiz | Activity sample 1 | C1 | quiz1 | 5.00 | 2 | 1 | 1 |
| quiz | Activity sample 2 | C1 | quiz2 | 5.00 | 2 | 1 | 1 |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | questiontext |
| Test questions | truefalse | First question | Answer the first question |
And quiz "Activity sample 1" contains the following questions:
| question | page |
| First question | 1 |
And quiz "Activity sample 2" contains the following questions:
| question | page |
| First question | 1 |
When I am on the "C1" "Course" page logged in as "student1"
Then the "Receive a grade" item should exist in the "To do" dropdown of the "Activity sample 1" "activity"
And the "Receive a grade" item should exist in the "To do" dropdown of the "Activity sample 2" "activity"
# Pass grade.
And user "student1" has attempted "Activity sample 1" with responses:
| slot | response |
| 1 | True |
# Fail grade.
And user "student1" has attempted "Activity sample 2" with responses:
| slot | response |
| 1 | False |
# After receiving a grade, the completion criteria dropdown should display "Done" only for the passing grade.
And I am on the "Course 1" course page
And "To do" "button" should not exist in the "Activity sample 1" "activity"
And the "Receive a grade" item should exist in the "Done" dropdown of the "Activity sample 1" "activity"
But "To do" "button" should exist in the "Activity sample 2" "activity"
And the "Receive a grade" item should exist in the "To do" dropdown of the "Activity sample 2" "activity"
Scenario: Teacher can edit activity completion using completion dialog link
Given the following "activity" exists:
| activity | assign |
| name | Activity sample |
| course | C1 |
| completion | 2 |
| completionview | 1 |
When I am on the "C1" "Course" page logged in as "teacher1"
# Edit conditions link should not be displayed when editing mode is off.
Then "Edit conditions" "link" should not exist in the "Activity sample" "core_courseformat > Activity completion"
# Edit conditions link should be displayed when editing mode is on.
But I am on "C1" course homepage with editing mode on
And I click on "Edit conditions" "link" in the "Activity sample" "core_courseformat > Activity completion"
And I should see "Activity sample" in the "page-header" "region"
And I should see "Edit settings"
And I should see "Activity completion"
Scenario: Completion dialog shows warning message if there are no criterias
# Create an activity with automatic completion but without completion criterias.
Given the following "activity" exists:
| activity | assign |
| name | Activity sample |
| course | C1 |
| completion | 2 |
# Teacher view.
When I am on the "C1" "Course" page logged in as "teacher1"
And I turn editing mode on
And "You have to add at least one completion condition." "text" should exist in the "Activity sample" "core_courseformat > Activity completion"
And "Add conditions" "link" should exist in the "Activity sample" "core_courseformat > Activity completion"
And I log out
# Student view.
And I am on the "C1" "Course" page logged in as "student1"
And "There are no completion conditions set for this activity." "text" should exist in the "Activity sample" "core_courseformat > Activity completion"
@javascript
Scenario: Completion edit button should be hidden in bulk editing
Given the following "activity" exists:
| activity | assign |
| name | Activity sample |
| course | C1 |
| completion | 2 |
| completionview | 1 |
And I log in as "teacher1"
And I am on "C1" course homepage with editing mode on
And I click on "Completion" "button"
And "Edit conditions" "link" in the "Activity sample" "activity" should be visible
When I click on "Bulk actions" "button"
And I click on "Completion" "button"
Then "Edit conditions" "link" in the "Activity sample" "activity" should not be visible
@@ -0,0 +1,38 @@
@core @core_course @core_courseformat
Feature: Duplicate a section
In order to set up my course contents quickly
As a teacher
I need to duplicate sections inside the same course
Background:
Given the following "courses" exist:
| fullname | shortname | category | enablecompletion | numsections | initsections |
| Course 1 | C1 | 0 | 1 | 1 | 0 |
| Course 2 | C2 | 0 | 1 | 4 | 1 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section |
| assign | Activity sample 1.1 | Test assignment description | C1 | sample11 | 1 |
| book | Activity sample 1.2 | Test book description | C1 | sample12 | 1 |
| assign | Activity sample 2.1 | Test assignment description | C2 | sample21 | 1 |
| book | Activity sample 2.2 | Test book description | C2 | sample22 | 1 |
| choice | Activity sample 2.3 | Test choice description | C2 | sample23 | 2 |
And I log in as "admin"
@javascript
Scenario: Duplicate unnamed section
Given I am on "Course 1" course homepage with editing mode on
When I open section "1" edit menu
And I click on "Duplicate" "link" in the "New section" "section"
# The duplicated section has section number 2.
And I am on the "Course 1 > Section 2" "course > section" page
Then I should see "Activity sample 1.2"
And I should see "New section"
@javascript
Scenario: Duplicate a named section
Given I am on "Course 2" course homepage with editing mode on
And I set the field "Edit section name" in the "Section 1" "section" to "New name"
And I should see "New name" in the "New name" "section"
When I open section "1" edit menu
And I click on "Duplicate" "link" in the "New name" "section"
Then I should see "Activity sample 2.2" in the "New name (copy)" "section"
@@ -0,0 +1,122 @@
@core @core_course @core_courseformat
Feature: Move activity using keyboard
In order to move activities without a mouse
As a user
I need to select the activity destination with the keyboard.
Background:
Given the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
| category | 0 |
| enablecompletion | 1 |
| numsections | 4 |
| initsections | 1 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section |
| assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 |
| book | Activity sample 2 | | C1 | sample2 | 2 |
| choice | Activity sample 3 | Test choice description | C1 | sample3 | 3 |
| choice | Other sample 3 | Test choice description | C1 | sample31 | 3 |
And I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
@javascript
Scenario: Move activity to another section selecting the section name
Given I open "Activity sample 3" actions menu
And I click on "Move" "link" in the "Activity sample 3" activity
# Focus on the modal content tree.
When I press the tab key
And I press the tab key
# Select the section 2.
And I press the down key
And I press the down key
And I press enter
Then I should see "Activity sample 3" in the "Section 2" "section"
@javascript
Scenario: Move activity to another section selecting an inner activity
Given I open "Activity sample 3" actions menu
And I click on "Move" "link" in the "Activity sample 3" activity
# Focus on the modal content tree.
When I press the tab key
And I press the tab key
# Open section 2.
And I press the down key
And I press the down key
And I press the right key
# Select first activity.
And I press the down key
And I press enter
Then I should see "Activity sample 3" in the "Section 2" "section"
@javascript
Scenario: Close a section in the move modal
Given I open "Activity sample 3" actions menu
And I click on "Move" "link" in the "Activity sample 3" activity
And I should see "Activity sample 3" in the ".modal-body" "css_element"
# Focus on the modal content tree.
When I press the tab key
And I press the tab key
# Close section 3.
And I press the down key
And I press the down key
And I press the down key
And I press the left key
# Move to section 4.
And I press the down key
And I press enter
Then I should see "Activity sample 3" in the "Section 4" "section"
@javascript
Scenario: Move activity using open all sections
Given I open "Activity sample 3" actions menu
And I click on "Move" "link" in the "Activity sample 3" activity
And I should see "Activity sample 3" in the ".modal-body" "css_element"
# Focus on the modal content tree.
When I press the tab key
And I press the tab key
# Open all sections.
And I press the multiply key
# Move down to section 4
And I press the down key
And I press the down key
And I press the down key
And I press the down key
And I press the down key
And I press the down key
And I press the down key
And I press the down key
And I press enter
Then I should see "Activity sample 3" in the "Section 4" "section"
@javascript
Scenario: Move activity using go to the last element
Given I open "Activity sample 3" actions menu
And I click on "Move" "link" in the "Activity sample 3" activity
# Focus on the modal content tree.
When I press the tab key
And I press the tab key
# Go to the last section.
And I press the end key
# Move down to section 4
And I press enter
Then I should see "Activity sample 3" in the "Section 4" "section"
@javascript
Scenario: Move activity using go to the first element
Given I open "Activity sample 3" actions menu
And I click on "Move" "link" in the "Activity sample 3" activity
And I should see "Activity sample 3" in the ".modal-body" "css_element"
# Focus on the modal content tree.
When I press the tab key
And I press the tab key
# Move some sections down.
And I press the down key
And I press the down key
And I press the down key
# Go to the first section.
And I press the home key
# Move down to general section
And I press enter
Then I should see "Activity sample 3" in the "General" "section"
@@ -0,0 +1,63 @@
@core @core_course @core_courseformat
Feature: Move a section using keyboard
In order to move sections without a mouse
As a user
I need to select the section destination with the keyboard.
Background:
Given the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
| category | 0 |
| enablecompletion | 1 |
| numsections | 4 |
| initsections | 1 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section |
| assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 |
| book | Activity sample 2 | | C1 | sample2 | 2 |
| choice | Activity sample 3 | Test choice description | C1 | sample3 | 3 |
| choice | Other sample 3 | Test choice description | C1 | sample31 | 3 |
And I log in as "admin"
And I am on "Course 1" course homepage with editing mode on
@javascript
Scenario: Move section above another section
Given I open section "3" edit menu
And I click on "Move" "link" in the "Section 3" "section"
# Focus on the modal content tree.
When I press the tab key
And I press the tab key
# Select the section 1.
And I press the down key
And I press enter
Then "Section 2" "section" should appear after "Section 3" "section"
@javascript
Scenario: Move section using go to the last element
Given I open section "2" edit menu
And I click on "Move" "link" in the "Section 2" "section"
# Focus on the modal content tree.
When I press the tab key
And I press the tab key
# Go to the last section.
And I press the end key
# Move down to section 4
And I press enter
Then "Section 2" "section" should appear after "Section 4" "section"
@javascript
Scenario: Move section using go to the first element
Given I open section "3" edit menu
And I click on "Move" "link" in the "Section 3" "section"
# Focus on the modal content tree.
When I press the tab key
And I press the tab key
# Move some sections down.
And I press the down key
And I press the down key
And I press the down key
# Go to the first section.
And I press the home key
And I press enter
Then "Section 1" "section" should appear after "Section 3" "section"
@@ -0,0 +1,120 @@
@core @core_courseformat
Feature: Single section course page
In order to improve the course page
As a user
I need to be able to see a section in a single page
Background:
Given the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
| category | 0 |
| numsections | 3 |
| initsections | 1 |
And the following "activities" exist:
| activity | name | course | idnumber | section |
| assign | Activity sample 0.1 | C1 | sample1 | 0 |
| assign | Activity sample 1.1 | C1 | sample1 | 1 |
| assign | Activity sample 1.2 | C1 | sample2 | 1 |
| assign | Activity sample 1.3 | C1 | sample3 | 1 |
| assign | Activity sample 2.1 | C1 | sample3 | 2 |
| assign | Activity sample 2.2 | C1 | sample3 | 2 |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
Given I am on the "C1" "Course" page logged in as "teacher1"
@javascript
Scenario: Collapsed sections are always expanded in the single section page
Given I press "Collapse all"
And I should not see "Activity sample 1.1" in the "region-main" "region"
When I am on the "Course 1 > Section 1" "course > section" page
Then I should see "Activity sample 1.1"
And I should see "Activity sample 1.2"
And I should see "Activity sample 1.3"
And I should not see "Activity sample 2.1" in the "region-main" "region"
And I should not see "Activity sample 2.1" in the "region-main" "region"
Scenario: General section is not displayed in the single section page
When I am on the "Course 1 > Section 1" "course > section" page
Then I should not see "General" in the "#section-1" "css_element"
And I should not see "Activity sample 0.1" in the "region-main" "region"
And I should see "Activity sample 1.1"
And I should see "Activity sample 1.2"
And I should see "Activity sample 1.3"
And I should not see "Activity sample 2.1" in the "region-main" "region"
And I should not see "Activity sample 2.1" in the "region-main" "region"
@javascript
Scenario: The view action for sections displays the single section page
Given I turn editing mode on
And I open section "1" edit menu
When I click on "View" "link" in the "Section 1" "section"
Then I should not see "General" in the "#section-1" "css_element"
And I should not see "Activity sample 0.1" in the "region-main" "region"
And I should see "Activity sample 1.1"
And I should see "Activity sample 1.2"
And I should see "Activity sample 1.3"
And I should not see "Activity sample 2.1" in the "region-main" "region"
And I should not see "Activity sample 2.1" in the "region-main" "region"
And I am on "Course 1" course homepage
And I open section "2" edit menu
And I click on "View" "link" in the "Section 2" "section"
And I should not see "General" in the "#section-2" "css_element"
And I should not see "Activity sample 0.1" in the "region-main" "region"
And I should not see "Activity sample 1.1"
And I should not see "Activity sample 1.2"
And I should not see "Activity sample 1.3"
And I should see "Activity sample 2.1" in the "region-main" "region"
And I should see "Activity sample 2.1" in the "region-main" "region"
# The General section is also displayed in isolation.
But I am on "Course 1" course homepage
And I open section "0" edit menu
And I click on "View" "link" in the "General" "section"
And I should see "General" in the "page" "region"
And I should see "Activity sample 0.1" in the "region-main" "region"
And I should not see "Activity sample 1.1" in the "region-main" "region"
And I should not see "Activity sample 1.2" in the "region-main" "region"
And I should not see "Activity sample 1.3" in the "region-main" "region"
And I should not see "Activity sample 2.1" in the "region-main" "region"
And I should not see "Activity sample 2.1" in the "region-main" "region"
# The section viewed has been trigered.
And I am on "Course 1" course homepage
And I navigate to "Reports > Live logs" in current page administration
And I should see "Section viewed"
Scenario: The add section button is not displayed in the single section page
Given I turn editing mode on
When I click on "View" "link" in the "Section 1" "section"
Then "Add section" "link" should not exist in the "region-main" "region"
@javascript
Scenario: Change the section name inline
# The course index is hidden by default in small devices.
Given I change window size to "large"
And I turn editing mode on
And I open section "1" edit menu
And I click on "View" "link" in the "Section 1" "section"
When I set the field "Edit section name" in the "page-header" "region" to "Custom section name"
Then "Custom section name" "text" should exist in the ".breadcrumb" "css_element"
@javascript
Scenario: Copy section page permalink URL to clipboard
Given I am on the "Course 1 > Section 1" "course > section" page
And I turn editing mode on
When I choose the "Permalink" item in the "Edit" action menu of the "page-header" "region"
And I click on "Copy to clipboard" "link" in the "Permalink" "dialogue"
Then I should see "Text copied to clipboard"
Scenario: Blocks are displayed in section page too
Given I log out
And the following "blocks" exist:
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
| online_users | Course | C1 | course-view-* | site-pre |
When I am on the "C1" "Course" page logged in as "teacher1"
Then I should see "Online users"
And I am on the "Course 1 > Section 1" "course > section" page
And I should see "Online users"
@@ -0,0 +1,96 @@
@core @core_courseformat
Feature: Varify section visibility interface
In order to edit the course sections visibility
As a teacher
I need to be able to see the updated visibility information
Background:
Given the following "course" exists:
| fullname | Course 1 |
| shortname | C1 |
| category | 0 |
| numsections | 3 |
| initsections | 1 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | section |
| assign | Activity sample 1 | Test assignment description | C1 | sample1 | 1 |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
Given I am on the "C1" "Course" page logged in as "teacher1"
And I turn editing mode on
@javascript
Scenario: Activities available but not shown on course page only apply to hidden sections.
Given I hide section "1"
And I open "Activity sample 1" actions menu
And I choose "Availability > Make available but don't show on course page" in the open action menu
And I should see "Available but not shown on course page" in the "Activity sample 1" "activity"
When I show section "1"
Then I should not see "Available but not shown on course page" in the "Activity sample 1" "activity"
@javascript
Scenario: Hide a section also hides the activities.
When I hide section "1"
Then I should see "Hidden from students" in the "Section 1" "section"
And I should see "Hidden from students" in the "Activity sample 1" "activity"
And I show section "1"
And I should not see "Hidden from students" in the "Section 1" "section"
And I should not see "Hidden from students" in the "Activity sample 1" "activity"
@javascript
Scenario: Hidden activities in hidden sections stay hidden when the section is shown.
Given I open "Activity sample 1" actions menu
And I choose "Hide" in the open action menu
And I should see "Hidden from students" in the "Activity sample 1" "activity"
And I hide section "1"
And I should see "Hidden from students" in the "Activity sample 1" "activity"
When I show section "1"
Then I should see "Hidden from students" in the "Activity sample 1" "activity"
@javascript
Scenario: Hidden sections can be shown and hidden using the drop down menu in the activity card.
Given I hide section "1"
When I click on "Hidden from students" "button" in the "Section 1" "section"
And I should see "Show on course page" in the "Section 1" "section"
And I should see "Hide on course page" in the "Section 1" "section"
And I click on "Show on course page" "link" in the "Section 1" "section"
Then I should not see "Hidden from students" in the "Section 1" "section"
@javascript
Scenario: Hidden sections are shown as "not available" to student when
the course is set to show hidden sections "not available". They are shown
as "hidden from students" to editing teachers.
Given the following "courses" exist:
| fullname | shortname | format | hiddensections | numsections | initsections |
| Course 2 | C2 | topics | 0 | 3 | 1 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C2 | student |
| teacher1 | C2 | editingteacher |
And I am on the "C2" "Course" page logged in as "admin"
And I turn editing mode on
And I hide section "1"
And I log out
When I am on the "C2" "Course" page logged in as "student1"
Then I should see "Not available" in the "Section 1" "section"
And I log out
And I am on the "C2" "Course" page logged in as "teacher1"
And I should see "Hidden from students" in the "Section 1" "section"
@javascript
Scenario: The visibility badge can show a hidden section in a the section page
Given I hide section "1"
When I am on the "C1 > Section 1" "course > section" page
And I click on "Hidden from students" "button" in the "[data-region='sectionbadges']" "css_element"
And I should see "Show on course page" in the "[data-region='sectionbadges']" "css_element"
And I should see "Hide on course page" in the "[data-region='sectionbadges']" "css_element"
And I click on "Show on course page" "link" in the "[data-region='sectionbadges']" "css_element"
Then I should not see "Hidden from students" in the "[data-region='sectionbadges']" "css_element"
And I open the action menu in "page-header" "region"
And I choose "Hide" in the open action menu
And I should see "Hidden from students" in the "[data-region='sectionbadges']" "css_element"
+85
View File
@@ -0,0 +1,85 @@
<?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_courseformat\external;
use core_external\external_api;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
use dndupload_handler;
/**
* Tests for the file_hanlders class.
*
* @package core_course
* @category test
* @copyright 2022 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core_courseformat\external\file_handlers
*/
class file_handlers_test extends \externallib_advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setupBeforeClass(): void { // phpcs:ignore
global $CFG;
require_once($CFG->dirroot . '/course/lib.php');
require_once($CFG->dirroot . '/course/dnduploadlib.php');
}
/**
* Test the behaviour of get_state::execute().
*
* @covers ::execute
*/
public function test_execute(): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(['numsections' => 3, 'format' => 'topics']);
$this->setAdminUser();
$result = file_handlers::execute($course->id);
$result = external_api::clean_returnvalue(file_handlers::execute_returns(), $result);
$handlers = new dndupload_handler($course, null);
$expected = $handlers->get_js_data();
$this->assertCount(count($expected->filehandlers), $result);
foreach ($expected->filehandlers as $key => $handler) {
$tocompare = $result[$key];
$this->assertEquals($handler->extension, $tocompare['extension']);
}
}
/**
* Test the behaviour of get_state::execute() in a wrong course.
*
* @covers ::execute
*/
public function test_execute_wrong_course(): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(['numsections' => 3, 'format' => 'topics']);
$this->setAdminUser();
$this->expectException('dml_missing_record_exception');
$result = file_handlers::execute(-1);
$result = external_api::clean_returnvalue(file_handlers::execute_returns(), $result);
}
}
+283
View File
@@ -0,0 +1,283 @@
<?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_courseformat\external;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
use core_external\external_api;
/**
* Tests for the get_state class.
*
* @package core_course
* @category test
* @copyright 2021 Sara Arjona (sara@moodle.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core_courseformat\external\get_state
*/
class get_state_test extends \externallib_advanced_testcase {
/** @var array Sections in the testing course. */
private $sections;
/** @var array Activities in the testing course. */
private $activities;
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setupBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/course/lib.php');
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest.php');
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest_output_course_format_state.php');
}
/**
* Setup testcase.
*/
public function setUp(): void {
$this->resetAfterTest();
$this->sections = [];
$this->activities = [];
}
/**
* Test tearDown.
*/
public function tearDown(): void {
unset($this->sections);
unset($this->activities);
}
/**
* Test the behaviour of get_state::execute().
*
* @dataProvider get_state_provider
* @covers ::execute
*
* @param string $role The role of the user that will execute the method.
* @param string $format The course format of the course where the method will be executed.
* @param string|null $expectedexception If this call will raise an exception, this is its name.
*/
public function test_get_state(string $role, string $format = 'topics', ?string $expectedexception = null): void {
$this->resetAfterTest();
// Create a course.
$numsections = 6;
$visiblesections = $numsections + 1; // Include topic 0.
$course = $this->getDataGenerator()->create_course(['numsections' => $numsections, 'format' => $format]);
$hiddensections = [4, 6];
foreach ($hiddensections as $section) {
set_section_visible($course->id, $section, 0);
}
// Create and enrol user.
$isadmin = ($role == 'admin');
$canedit = $isadmin || ($role == 'editingteacher');
if ($isadmin) {
$this->setAdminUser();
} else {
if (!$canedit) {
// User won't see the hidden sections. Remove them from the total.
$visiblesections = $visiblesections - count($hiddensections);
}
$user = $this->getDataGenerator()->create_user();
if ($role != 'unenroled') {
$this->getDataGenerator()->enrol_user($user->id, $course->id, $role);
}
$this->setUser($user);
}
// Social course format automatically creates a forum activity.
if (course_get_format($course)->get_format() === 'social') {
$cms = get_fast_modinfo($course)->get_cms();
// Let's add this assertion just to ensure course format has only one activity.
$this->assertCount(1, $cms);
$activitycm = reset($cms);
// And that activity is a forum.
$this->assertEquals('forum', $activitycm->modname);
// Assign the activity cm to the activities array.
$this->activities[$activitycm->id] = $activitycm;
} else {
// Add some activities to the course.
$this->create_activity($course->id, 'page', 1, true, $canedit);
$this->create_activity($course->id, 'forum', 1, true, $canedit);
$this->create_activity($course->id, 'book', 1, false, $canedit);
$this->create_activity($course->id, 'assign', 2, false, $canedit);
$this->create_activity($course->id, 'glossary', 4, true, $canedit);
$this->create_activity($course->id, 'label', 5, false, $canedit);
$this->create_activity($course->id, 'feedback', 5, true, $canedit);
}
if ($expectedexception) {
$this->expectException($expectedexception);
}
// Get course state.
$result = get_state::execute($course->id);
$result = external_api::clean_returnvalue(get_state::execute_returns(), $result);
$result = json_decode($result);
if ($format == 'theunittest') {
// These course format's hasn't the renderer file, so a debugging message will be displayed.
$this->assertDebuggingCalled();
}
// Check course information.
$this->assertEquals($numsections, $result->course->numsections);
$this->assertCount($visiblesections, $result->section);
$this->assertCount(count($this->activities), $result->cm);
$this->assertCount(count($result->course->sectionlist), $result->section);
if ($format == 'theunittest') {
$this->assertTrue(property_exists($result->course, 'newfancyelement'));
} else {
$this->assertFalse(property_exists($result->course, 'newfancyelement'));
}
// Check sections information.
foreach ($result->section as $section) {
if (in_array($section->number, $hiddensections)) {
$this->assertFalse($section->visible);
} else {
$this->assertTrue($section->visible);
}
// Check section is defined in course->sectionlist.
$this->assertContains($section->id, $result->course->sectionlist);
// Check course modules list for this section is the expected.
if (array_key_exists($section->number, $this->sections)) {
$this->assertEquals($this->sections[$section->number], $section->cmlist);
}
}
// Check course modules information.
foreach ($result->cm as $cm) {
$this->assertEquals($this->activities[$cm->id]->name, $cm->name);
$this->assertEquals((bool) $this->activities[$cm->id]->visible, $cm->visible);
}
}
/**
* Data provider for test_get_state().
*
* @return array
*/
public function get_state_provider(): array {
return [
// ROLES. Testing behaviour depending on the user role calling the method.
'Admin user should work' => [
'role' => 'admin',
],
'Editing teacher should work' => [
'role' => 'editingteacher',
],
'Student should work' => [
'role' => 'student',
],
'Unenroled user should raise an exception' => [
'role' => 'unenroled',
'format' => 'topics',
'expectedexception' => 'moodle_exception',
],
// COURSEFORMAT. Test behaviour depending on course formats.
'Single activity format should work (admin)' => [
'role' => 'admin',
'format' => 'singleactivity',
],
'Social format should work (admin)' => [
'role' => 'admin',
'format' => 'social',
],
'Weeks format should work (admin)' => [
'role' => 'admin',
'format' => 'weeks',
],
'The unit tests format should work (admin)' => [
'role' => 'admin',
'format' => 'theunittest',
],
'Single activity format should work (student)' => [
'role' => 'student',
'format' => 'singleactivity',
],
'Social format should work (student)' => [
'role' => 'student',
'format' => 'social',
],
'Weeks format should work (student)' => [
'role' => 'student',
'format' => 'weeks',
],
'The unit tests format should work (student)' => [
'role' => 'student',
'format' => 'theunittest',
],
'Single activity format should raise an exception (unenroled)' => [
'role' => 'unenroled',
'format' => 'singleactivity',
'expectedexception' => 'moodle_exception',
],
'Social format should raise an exception (unenroled)' => [
'role' => 'unenroled',
'format' => 'social',
'expectedexception' => 'moodle_exception',
],
'Weeks format should raise an exception (unenroled)' => [
'role' => 'unenroled',
'format' => 'weeks',
'expectedexception' => 'moodle_exception',
],
'The unit tests format should raise an exception (unenroled)' => [
'role' => 'unenroled',
'format' => 'theunittest',
'expectedexception' => 'moodle_exception',
],
];
}
/**
* Helper method to create an activity into a section and add it to the $sections and $activities arrays.
* For non-admin users, only visible activities will be added to the activities and sections arrays.
*
* @param int $courseid Course identifier where the activity will be added.
* @param string $type Activity type ('forum', 'assign', ...).
* @param int $section Section number where the activity will be added.
* @param bool $visible Whether the activity will be visible or not.
* @param bool $canedit Whether the activity will be accessed later by a user with editing capabilities
*/
private function create_activity(int $courseid, string $type, int $section, bool $visible = true, bool $canedit = true): void {
$activity = $this->getDataGenerator()->create_module(
$type,
['course' => $courseid],
['section' => $section, 'visible' => $visible]
);
list(, $activitycm) = get_course_and_cm_from_instance($activity->id, $type);
if ($visible || $canedit) {
$this->activities[$activitycm->id] = $activitycm;
$this->sections[$section][] = $activitycm->id;
}
}
}
+236
View File
@@ -0,0 +1,236 @@
<?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_courseformat\external;
use stdClass;
use moodle_exception;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
/**
* Tests for the update_course class.
*
* @package core_course
* @category test
* @copyright 2021 Sara Arjona (sara@moodle.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core_courseformat\external\update_course
*/
class update_course_test extends \externallib_advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setupBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest.php');
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest_output_course_format_state.php');
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest_stateactions.php');
}
/**
* Test the webservice can execute a core state action (cm_state).
*
* @dataProvider execute_course_state_provider
* @covers ::execute
*
* @param string $format the course format
* @param string $action the state action name
* @param array $expected the expected results
* @param bool $expectexception if an exception should happen.
* @param bool $assertdebug if an debug message should happen.
*/
public function test_execute_course_state(
string $format,
string $action,
array $expected,
bool $expectexception,
bool $assertdebug
): void {
$this->resetAfterTest();
// Create a course with two activities.
$course = $this->getDataGenerator()->create_course(['format' => $format]);
$activity = $this->getDataGenerator()->create_module('book', ['course' => $course->id]);
$this->setAdminUser();
// Expect exception.
if ($expectexception) {
$this->expectException(moodle_exception::class);
}
// Execute course action.
$results = json_decode(update_course::execute($action, $course->id, [$activity->cmid]));
if ($assertdebug) {
// Some course formats hasn't the renderer file, so a debugging message will be displayed.
$this->assertDebuggingCalled();
}
// Check result.
$this->assertCount($expected['count'], $results);
$update = $this->find_update($results, $expected['action'], 'cm', $activity->cmid);
$this->assertNotEmpty($update);
if ($expected['visible'] === null) {
$this->assertObjectNotHasProperty('visible', $update->fields);
} else {
$this->assertEquals($expected['visible'], $update->fields->visible);
}
}
/**
* Data provider for test_execute_course_state
*
* @return array of testing scenarios
*/
public function execute_course_state_provider(): array {
return [
'Execute a core state action (cm_state)' => [
'format' => 'topics',
'action' => 'cm_state',
'expected' => [
'count' => 2,
'action' => 'put',
'visible' => 1,
],
'expectexception' => false,
'assertdebug' => false,
],
'Formats can override core state actions' => [
'format' => 'theunittest',
'action' => 'cm_state',
'expected' => [
'count' => 1,
'action' => 'create',
'visible' => 1,
],
'expectexception' => false,
'assertdebug' => true,
],
'Formats can create new state actions' => [
'format' => 'theunittest',
'action' => 'format_do_something',
'expected' => [
'count' => 1,
'action' => 'remove',
'visible' => null,
],
'expectexception' => false,
'assertdebug' => true,
],
'Innexisting state action' => [
'format' => 'topics',
'action' => 'Wrong_State_Action_Name',
'expected' => [],
'expectexception' => true,
'assertdebug' => false,
],
];
}
/**
* Helper methods to find a specific update in the updadelist.
*
* @param array $updatelist the update list
* @param string $action the action to find
* @param string $name the element name to find
* @param int $identifier the element id value
* @return stdClass|null the object found, if any.
*/
private function find_update(
array $updatelist,
string $action,
string $name,
int $identifier
): ?stdClass {
foreach ($updatelist as $update) {
if ($update->action != $action || $update->name != $name) {
continue;
}
if (!isset($update->fields->id)) {
continue;
}
if ($update->fields->id == $identifier) {
return $update;
}
}
return null;
}
/**
* Test a wrong course id.
*
* @covers ::execute
*
*/
public function test_execute_wrong_courseid(): void {
$this->resetAfterTest();
// Create a course with two activities.
$course = $this->getDataGenerator()->create_course(['format' => 'topics']);
$activity = $this->getDataGenerator()->create_module('book', ['course' => $course->id]);
$this->setAdminUser();
// Expect exception.
$this->expectException(moodle_exception::class);
// Execute course action.
$results = json_decode(update_course::execute('cm_state', $course->id + 1, [$activity->cmid]));
}
/**
* Test target params are passed to the state actions.
*
* @covers ::execute
*/
public function test_execute_target_params(): void {
$this->resetAfterTest();
// Create a course with two activities.
$course = $this->getDataGenerator()->create_course(['format' => 'theunittest', 'numsections' => 2]);
$activity = $this->getDataGenerator()->create_module('book', ['course' => $course->id]);
$modinfo = get_fast_modinfo($course);
$section = $modinfo->get_section_info(1);
$this->setAdminUser();
// Execute action with targetsectionid.
$results = json_decode(update_course::execute('targetsection_test', $course->id, [], $section->id));
$this->assertDebuggingCalled();
$this->assertCount(1, $results);
$update = $this->find_update($results, 'put', 'section', $section->id);
$this->assertNotEmpty($update);
// Execute action with targetcmid.
$results = json_decode(update_course::execute('targetcm_test', $course->id, [], null, $activity->cmid));
$this->assertDebuggingCalled();
$this->assertCount(1, $results);
$update = $this->find_update($results, 'put', 'cm', $activity->cmid);
$this->assertNotEmpty($update);
}
}
+85
View File
@@ -0,0 +1,85 @@
<?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/>.
/**
* Fixture for fake course format testing course format API.
*
* @package core_course
* @copyright 2014 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class format_theunittest extends core_courseformat\base {
/**
* Definitions of the additional options that format uses
*
* @param bool $foreditform
* @return array of options
*/
public function course_format_options($foreditform = false) {
static $courseformatoptions = false;
if ($courseformatoptions === false) {
$courseformatoptions = array(
'hideoddsections' => array(
'default' => 0,
'type' => PARAM_INT,
),
'summary_editor' => array(
'default' => '',
'type' => PARAM_RAW,
),
);
}
if ($foreditform && !isset($courseformatoptions['hideoddsections']['label'])) {
$sectionmenu = array(
0 => 'Never',
1 => 'Hide without notice',
2 => 'Hide with notice'
);
$courseformatoptionsedit = array(
'hideoddsections' => array(
'label' => 'Hide odd sections',
'element_type' => 'select',
'element_attributes' => array($sectionmenu),
),
'summary_editor' => array(
'label' => 'Summary Text',
'element_type' => 'editor',
),
);
$courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsedit);
}
return $courseformatoptions;
}
/**
* Allows to specify for modinfo that section is not available even when it is visible and conditionally available.
*
* @param section_info $section
* @param bool $available
* @param string $availableinfo
*/
public function section_get_available_hook(section_info $section, &$available, &$availableinfo) {
if (($section->section % 2) && ($hideoddsections = $this->get_course()->hideoddsections)) {
$available = false;
if ($hideoddsections == 2) {
$availableinfo = 'Odd sections are oddly hidden';
} else {
$availableinfo = '';
}
}
}
}
@@ -0,0 +1,29 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace format_theunittest\courseformat;
use core_courseformat\local\cmactions as core_cm_actions;
/**
* Fixture for fake course module actions testing.
*
* @package core_course
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cmactions extends core_cm_actions {
}
@@ -0,0 +1,29 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace format_theunittest\courseformat;
use core_courseformat\local\courseactions as core_course_actions;
/**
* Fixture for fake course actions testing.
*
* @package core_course
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class courseactions extends core_course_actions {
}
@@ -0,0 +1,43 @@
<?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 format_theunittest\output\courseformat\state;
use renderable;
use templatable;
use stdClass;
/**
* Fixture for an invalid output for testing get_output_classname.
*
* @package core_course
* @copyright 2021 Ferran Recio (ferran@moodle.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class invalidoutput implements renderable, templatable {
/**
* Export some data.
*
* @param renderer_base $output typically, the renderer that's calling this function
* @return stdClass data context for a mustache template
*/
public function export_for_template(\renderer_base $output): stdClass {
return (object)[
'something' => 'invalid',
];
}
}
@@ -0,0 +1,42 @@
<?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 format_theunittest\output\courseformat\state;
use core_courseformat\output\local\state\course as course_state;
/**
* Fixture for fake course format testing course format API.
*
* @package core_course
* @copyright 2021 Sara Arjona (sara@moodle.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course extends course_state {
/**
* Export this data so it can be used as state object in the course editor.
*
* @param renderer_base $output typically, the renderer that's calling this function
* @return stdClass data context for a mustache template
*/
public function export_for_template(\renderer_base $output): \stdClass {
$data = parent::export_for_template($output);
$data->newfancyelement = 'thatsme';
return $data;
}
}
@@ -0,0 +1,29 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace format_theunittest\courseformat;
use core_courseformat\local\sectionactions as core_section_actions;
/**
* Fixture for fake section actions testing.
*
* @package core_course
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class sectionactions extends core_section_actions {
}
@@ -0,0 +1,111 @@
<?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 format_theunittest\courseformat;
use core_courseformat\stateupdates;
use core_courseformat\stateactions as core_actions;
use stdClass;
/**
* Fixture for fake course stateactions testing.
*
* @package core_course
* @copyright 2021 Sara Arjona (sara@moodle.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class stateactions extends core_actions {
/**
* Alternative cm_state state action for testing.
*
* @param stateupdates $updates the affected course elements track
* @param stdClass $course the course object
* @param int[] $ids the list of affected course module ids
* @param int $targetsectionid optional target section id
* @param int $targetcmid optional target cm id
*/
public function cm_state(
stateupdates $updates,
stdClass $course,
array $ids,
?int $targetsectionid = null,
?int $targetcmid = null
): void {
$updates->add_cm_create(array_pop($ids));
}
/**
* Course format custom state action.
*
* @param stateupdates $updates the affected course elements track
* @param stdClass $course the course object
* @param int[] $ids the list of affected course module ids
* @param int $targetsectionid optional target section id
* @param int $targetcmid optional target cm id
*/
public function format_do_something(
stateupdates $updates,
stdClass $course,
array $ids,
?int $targetsectionid = null,
?int $targetcmid = null
): void {
$updates->add_cm_remove(array_pop($ids));
}
/**
* Course format target section testing action.
*
* @param stateupdates $updates the affected course elements track
* @param stdClass $course the course object
* @param int[] $ids the list of affected course module ids
* @param int $targetsectionid optional target section id
* @param int $targetcmid optional target cm id
*/
public function targetsection_test(
stateupdates $updates,
stdClass $course,
array $ids,
?int $targetsectionid = null,
?int $targetcmid = null
): void {
$updates->add_section_put($targetsectionid);
}
/**
* Course format target cm testing action.
*
* @param stateupdates $updates the affected course elements track
* @param stdClass $course the course object
* @param int[] $ids the list of affected course module ids
* @param int $targetsectionid optional target section id
* @param int $targetcmid optional target cm id
*/
public function targetcm_test(
stateupdates $updates,
stdClass $course,
array $ids,
?int $targetsectionid = null,
?int $targetcmid = null
): void {
$updates->add_cm_put($targetcmid);
}
}
+146
View File
@@ -0,0 +1,146 @@
<?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_courseformat;
/**
* Course format actions class tests.
*
* @package core_courseformat
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core_courseformat\base
*/
class formatactions_test extends \advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest.php');
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest_courseactions.php');
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest_sectionactions.php');
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest_cmactions.php');
}
/**
* Test for get_instance static method.
* @dataProvider provider_classname_action
* @covers ::instance
* @param string $format
* @param array $classnames
*/
public function test_instance(string $format, array $classnames): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(['format' => $format]);
$instance1 = formatactions::instance($course);
$this->assertInstanceOf('\core_courseformat\formatactions', $instance1);
$instance2 = formatactions::instance($course->id);
$this->assertInstanceOf('\core_courseformat\formatactions', $instance2);
// Validate the method is caching the result.
$this->assertEquals($instance1, $instance2);
// Validate public attribute classes.
$this->assertInstanceOf($classnames['course'], $instance1->course);
$this->assertInstanceOf($classnames['section'], $instance1->section);
$this->assertInstanceOf($classnames['cm'], $instance1->cm);
}
/**
* Test that the course action instance is created correctly.
* @dataProvider provider_classname_action
* @covers ::course
* @param string $format
* @param array $classnames
*/
public function test_course_action_instance(string $format, array $classnames): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(['format' => $format]);
$instance1 = formatactions::course($course);
$this->assertInstanceOf($classnames['course'], $instance1);
$instance2 = formatactions::course($course->id);
$this->assertInstanceOf($classnames['course'], $instance2);
}
/**
* Test that the section action instance is created correctly.
* @dataProvider provider_classname_action
* @covers ::section
*
* @param string $format
* @param array $classnames
*/
public function test_static_sectionactions_instance(string $format, array $classnames): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(['format' => $format]);
$instance1 = formatactions::section($course);
$this->assertInstanceOf($classnames['section'], $instance1);
$instance2 = formatactions::section($course->id);
$this->assertInstanceOf($classnames['section'], $instance2);
}
/**
* Test that the cm action instance is created correctly.
* @dataProvider provider_classname_action
* @covers ::cm
*
* @param string $format
* @param array $classnames
*/
public function test_static_cmactions_instance(string $format, array $classnames): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(['format' => $format]);
$instance1 = formatactions::cm($course);
$this->assertInstanceOf($classnames['cm'], $instance1);
$instance2 = formatactions::cm($course->id);
$this->assertInstanceOf($classnames['cm'], $instance2);
}
/**
* Data provider for format class names scenarios.
* @return array
*/
public static function provider_classname_action(): array {
return [
'Topics format' => [
'format' => 'topics',
'classnames' => [
'course' => '\core_courseformat\local\courseactions',
'section' => '\core_courseformat\local\sectionactions',
'cm' => '\core_courseformat\local\cmactions',
],
],
'The unit test fixture format' => [
'format' => 'theunittest',
'classnames' => [
'course' => '\format_theunittest\courseformat\courseactions',
'section' => '\format_theunittest\courseformat\sectionactions',
'cm' => '\format_theunittest\courseformat\cmactions',
],
],
];
}
}
@@ -0,0 +1,148 @@
<?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_courseformat\local;
use ReflectionMethod;
use section_info;
use cm_info;
/**
* Base format actions class tests.
*
* @package core_courseformat
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core_courseformat\base
*/
class baseactions_test extends \advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/course/lib.php');
}
/**
* Get the reflection method for a base class instance.
* @param baseactions $baseinstance
* @param string $methodname
* @return ReflectionMethod
*/
private function get_base_reflection_method(baseactions $baseinstance, string $methodname): ReflectionMethod {
$reflectionclass = new \reflectionclass($baseinstance);
$method = $reflectionclass->getMethod($methodname);
return $method;
}
/**
* Test for get_instance static method.
* @covers ::get_format
*/
public function test_get_format(): void {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(['format' => 'topics']);
$baseactions = new baseactions($course);
$method = $this->get_base_reflection_method($baseactions, 'get_format');
$format = $method->invoke($baseactions);
$this->assertEquals('topics', $format->get_format());
$this->assertEquals('format_topics', $format::class);
// Format should be always the most updated one.
$course->format = 'weeks';
$DB->update_record('course', $course);
$format = $method->invoke($baseactions);
$this->assertEquals('weeks', $format->get_format());
$this->assertEquals('format_weeks', $format::class);
}
/**
* Test for get_instance static method.
* @covers ::get_section_info
*/
public function test_get_section_info(): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(
['format' => 'topics', 'numsections' => 4],
['createsections' => true]
);
$modinfo = get_fast_modinfo($course->id);
$originalsection = $modinfo->get_section_info(1);
$baseactions = new baseactions($course);
$method = $this->get_base_reflection_method($baseactions, 'get_section_info');
$sectioninfo = $method->invoke($baseactions, $originalsection->id);
$this->assertInstanceOf(section_info::class, $sectioninfo);
$this->assertEquals($originalsection->id, $sectioninfo->id);
$this->assertEquals($originalsection->section, $sectioninfo->section);
// Section info should be always the most updated one.
course_update_section($course, $originalsection, (object)['name' => 'New name']);
move_section_to($course, 1, 3);
$sectioninfo = $method->invoke($baseactions, $originalsection->id);
$this->assertInstanceOf(section_info::class, $sectioninfo);
$this->assertEquals($originalsection->id, $sectioninfo->id);
$this->assertEquals(3, $sectioninfo->section);
$this->assertEquals('New name', $sectioninfo->name);
}
/**
* Test for get_instance static method.
* @covers ::get_cm_info
*/
public function test_get_cm_info(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course(
['format' => 'topics', 'numsections' => 4],
['createsections' => true]
);
$activity = $this->getDataGenerator()->create_module('label', ['course' => $course->id]);
$modinfo = get_fast_modinfo($course->id);
$destinationsection = $modinfo->get_section_info(3);
$originalcm = $modinfo->get_cm($activity->cmid);
$baseactions = new baseactions($course);
$method = $this->get_base_reflection_method($baseactions, 'get_cm_info');
$cm = $method->invoke($baseactions, $originalcm->id);
$this->assertInstanceOf(cm_info::class, $cm);
$this->assertEquals($originalcm->id, $cm->id);
$this->assertEquals($originalcm->sectionnum, $cm->sectionnum);
$this->assertEquals($originalcm->name, $cm->name);
// CM info should be always the most updated one.
moveto_module($originalcm, $destinationsection);
$cm = $method->invoke($baseactions, $originalcm->id);
$this->assertInstanceOf(cm_info::class, $cm);
$this->assertEquals($originalcm->id, $cm->id);
$this->assertEquals($destinationsection->section, $cm->sectionnum);
}
}
@@ -0,0 +1,224 @@
<?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_courseformat\local;
use core_courseformat\hook\after_cm_name_edited;
/**
* Course module format actions class tests.
*
* @package core_courseformat
* @copyright 2024 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core_courseformat\cmactions
*/
final class cmactions_test extends \advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/course/lib.php');
}
/**
* Test renaming a course module.
*
* @dataProvider provider_test_rename
* @covers ::rename
* @param string $newname The new name for the course module.
* @param bool $expected Whether the course module was renamed.
* @param bool $expectexception Whether an exception is expected.
*/
public function test_rename(string $newname, bool $expected, bool $expectexception): void {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(['format' => 'topics']);
$activity = $this->getDataGenerator()->create_module(
'assign',
['course' => $course->id, 'name' => 'Old name']
);
$cmactions = new cmactions($course);
if ($expectexception) {
$this->expectException(\moodle_exception::class);
}
$result = $cmactions->rename($activity->cmid, $newname);
$this->assertEquals($expected, $result);
$cminfo = get_fast_modinfo($course)->get_cm($activity->cmid);
if ($result) {
$this->assertEquals('New name', $cminfo->name);
} else {
$this->assertEquals('Old name', $cminfo->name);
}
}
/**
* Data provider for test_rename.
*
* @return array
*/
public static function provider_test_rename(): array {
return [
'Empty name' => [
'newname' => '',
'expected' => false,
'expectexception' => false,
],
'Maximum length' => [
'newname' => str_repeat('a', 256),
'expected' => false,
'expectexception' => true,
],
'Valid name' => [
'newname' => 'New name',
'expected' => true,
'expectexception' => false,
],
];
}
/**
* Test rename an activity also rename the calendar events.
*
* @covers ::rename
*/
public function test_rename_calendar_events(): void {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
set_config('enablecompletion', 1);
$course = $this->getDataGenerator()->create_course(['enablecompletion' => COMPLETION_ENABLED]);
$activity = $this->getDataGenerator()->create_module(
'assign',
[
'name' => 'Old name',
'course' => $course,
'completionexpected' => time(),
'duedate' => time(),
]
);
$cm = get_coursemodule_from_instance('assign', $activity->id, $course->id);
// Validate course events naming.
$this->assertEquals(2, $DB->count_records('event'));
$event = $DB->get_record(
'event',
['modulename' => 'assign', 'instance' => $activity->id, 'eventtype' => 'due']
);
$this->assertEquals(
get_string('calendardue', 'assign', 'Old name'),
$event->name
);
$event = $DB->get_record(
'event',
['modulename' => 'assign', 'instance' => $activity->id, 'eventtype' => 'expectcompletionon']
);
$this->assertEquals(
get_string('completionexpectedfor', 'completion', (object) ['instancename' => 'Old name']),
$event->name
);
// Rename activity.
$cmactions = new cmactions($course);
$result = $cmactions->rename($activity->cmid, 'New name');
$this->assertTrue($result);
// Validate event renaming.
$event = $DB->get_record(
'event',
['modulename' => 'assign', 'instance' => $activity->id, 'eventtype' => 'due']
);
$this->assertEquals(
get_string('calendardue', 'assign', 'New name'),
$event->name
);
$event = $DB->get_record(
'event',
['modulename' => 'assign', 'instance' => $activity->id, 'eventtype' => 'expectcompletionon']
);
$this->assertEquals(
get_string('completionexpectedfor', 'completion', (object) ['instancename' => 'New name']),
$event->name
);
}
/**
* Test renaming an activity trigger a course update log event.
*
* @covers ::rename
*/
public function test_rename_course_module_updated_event(): void {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module(
'assign',
['course' => $course->id, 'name' => 'Old name']
);
$sink = $this->redirectEvents();
$cmactions = new cmactions($course);
$result = $cmactions->rename($activity->cmid, 'New name');
$this->assertTrue($result);
$events = $sink->get_events();
$event = reset($events);
// Check that the event data is valid.
$this->assertInstanceOf('\core\event\course_module_updated', $event);
$this->assertEquals(\context_module::instance($activity->cmid), $event->get_context());
}
/**
* Test renaming an activity triggers the after_cm_name_edited hook.
* @covers ::rename
*/
public function test_rename_after_cm_name_edited_hook(): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module(
'assign',
['course' => $course->id, 'name' => 'Old name']
);
$executedhook = null;
$testcallback = function(after_cm_name_edited $hook) use (&$executedhook): void {
$executedhook = $hook;
};
$this->redirectHook(after_cm_name_edited::class, $testcallback);
$cmactions = new cmactions($course);
$result = $cmactions->rename($activity->cmid, 'New name');
$this->assertTrue($result);
$this->assertEquals($activity->cmid, $executedhook->get_cm()->id);
$this->assertEquals('New name', $executedhook->get_newname());
}
}
@@ -0,0 +1,942 @@
<?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_courseformat\local;
use stdClass;
/**
* Section format actions class tests.
*
* @package core_courseformat
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core_courseformat\sectionactions
*/
class sectionactions_test extends \advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/course/lib.php');
}
/**
* Test for create_delegated method.
* @covers ::create_delegated
* @dataProvider create_delegated_provider
* @param string $component the name of the plugin
* @param int|null $itemid the id of the delegated section
* @param stdClass|null $fields the fields to set on the section
*/
public function test_create_delegated(string $component, ?int $itemid, ?stdClass $fields): void {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 1]);
$sectionactions = new sectionactions($course);
$section = $sectionactions->create_delegated($component, $itemid, $fields);
$this->assertEquals($component, $section->component);
$this->assertEquals($itemid, $section->itemid);
if (!empty($fields)) {
foreach ($fields as $field => $value) {
$this->assertEquals($value, $section->$field);
}
}
}
/**
* Data provider for test_create_delegated.
* @return array
*/
public static function create_delegated_provider(): array {
return [
'component with no itemid or fields' => [
'mod_assign',
null,
null,
],
'component with itemid but no fields' => [
'mod_assign',
1,
null,
],
'component with itemid and empty fields' => [
'mod_assign',
1,
new stdClass(),
],
'component with itemid and name field' => [
'mod_assign',
1,
(object) ['name' => 'new name'],
],
'component with no itemid but name field' => [
'mod_assign',
null,
(object) ['name' => 'new name'],
],
'component with itemid and summary' => [
'mod_assign',
1,
(object) ['summary' => 'summary'],
],
'component with itemid and summary, summaryformat ' => [
'mod_assign',
1,
(object) ['summary' => 'summary', 'summaryformat' => 1],
],
'component with itemid and section number' => [
'mod_assign',
1,
(object) ['section' => 2],
],
'component with itemid and visible 1' => [
'mod_assign',
1,
(object) ['visible' => 1],
],
'component with itemid and visible 0' => [
'mod_assign',
1,
(object) ['visible' => 0],
],
];
}
/**
* Test for create method.
* @covers ::create
* @dataProvider create_provider
* @param int $sectionnum the name of the plugin
* @param bool $skip if the validation should be skipped
* @param bool $expectexception if the method should throw an exception
* @param int $expected the expected section number
*/
public function test_create(int $sectionnum, bool $skip, bool $expectexception, int $expected): void {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 1]);
$sectionactions = new sectionactions($course);
if ($expectexception) {
$this->expectException(\dml_write_exception::class);
}
$section = $sectionactions->create($sectionnum, $skip);
$this->assertEquals($expected, $section->section);
}
/**
* Data provider for test_create_delegated.
* @return array
*/
public static function create_provider(): array {
return [
'section 1' => [
'sectionnum' => 1,
'skip' => false,
'expectexception' => false,
'expected' => 1,
],
'section 2' => [
'sectionnum' => 2,
'skip' => false,
'expectexception' => false,
'expected' => 2,
],
'section 3' => [
'sectionnum' => 3,
'skip' => false,
'expectexception' => false,
'expected' => 2,
],
'section 4' => [
'sectionnum' => 4,
'skip' => false,
'expectexception' => false,
'expected' => 2,
],
'section 1 with exception' => [
'sectionnum' => 1,
'skip' => true,
'expectexception' => true,
'expected' => 0,
],
'section 2 with skip validation' => [
'sectionnum' => 2,
'skip' => true,
'expectexception' => false,
'expected' => 2,
],
'section 5 with skip validation' => [
'sectionnum' => 5,
'skip' => true,
'expectexception' => false,
'expected' => 5,
],
];
}
/**
* Test create sections when there are sections with comonent (delegated sections) in the course.
* @covers ::create
* @covers ::create_delegated
*/
public function test_create_with_delegated_sections(): void {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(
['format' => 'topics', 'numsections' => 1],
['createsections' => true],
);
$sectionactions = new sectionactions($course);
$section = $sectionactions->create_delegated('mod_forum', 1);
$this->assertEquals(2, $section->section);
$delegateid = $section->id;
// Regular sections are created before delegated ones.
$section = $sectionactions->create(2);
$this->assertEquals(2, $section->section);
$regularid = $section->id;
$modinfo = get_fast_modinfo($course);
$section2 = $modinfo->get_section_info(2);
$this->assertEquals($regularid, $section2->id);
$this->assertEquals(2, $section2->section);
$sectiondelegated = $modinfo->get_section_info_by_component('mod_forum', 1);
$this->assertEquals($delegateid, $sectiondelegated->id);
$this->assertEquals(3, $sectiondelegated->section);
// New delegates should be after the current delegate sections.
$section = $sectionactions->create_delegated('mod_forum', 2);
$this->assertEquals(4, $section->section);
}
/**
* Test for create_if_missing method.
* @covers ::create_if_missing
* @dataProvider create_if_missing_provider
* @param array $sectionnums the section numbers to create
* @param bool $expected the expected result
*/
public function test_create_if_missing(array $sectionnums, bool $expected): void {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 2]);
$sectionactions = new sectionactions($course);
$result = $sectionactions->create_if_missing($sectionnums);
$this->assertEquals($expected, $result);
$modinfo = get_fast_modinfo($course);
foreach ($sectionnums as $sectionnum) {
$section = $modinfo->get_section_info($sectionnum);
$this->assertEquals($sectionnum, $section->section);
}
}
/**
* Data provider for test_create_delegated.
* @return array
*/
public static function create_if_missing_provider(): array {
return [
'existing section' => [
'sectionnum' => [1],
'expected' => false,
],
'unexisting section' => [
'sectionnum' => [3],
'expected' => true,
],
'several existing sections' => [
'sectionnum' => [1, 2],
'expected' => false,
],
'several unexisting sections' => [
'sectionnum' => [3, 4],
'expected' => true,
],
'empty array' => [
'sectionnum' => [],
'expected' => false,
],
'existent and unexistent sections' => [
'sectionnum' => [1, 2, 3, 4],
'expected' => true,
],
];
}
/**
* Test create if missing when the course has delegated sections.
* @covers ::create_if_missing
* @covers ::create_delegated
*/
public function test_create_if_missing_with_delegated_sections(): void {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(
['format' => 'topics', 'numsections' => 1],
['createsections' => true],
);
$sectionactions = new sectionactions($course);
$section = $sectionactions->create_delegated('mod_forum', 1);
$delegateid = $section->id;
$result = $sectionactions->create_if_missing([1, 2]);
$this->assertTrue($result);
$modinfo = get_fast_modinfo($course);
$section = $modinfo->get_section_info(2);
$this->assertEquals(2, $section->section);
$this->assertNotEquals($delegateid, $section->id);
$delegatedsection = $modinfo->get_section_info_by_id($delegateid);
$this->assertEquals(3, $delegatedsection->section);
$result = $sectionactions->create_if_missing([1, 2]);
$this->assertFalse($result);
$result = $sectionactions->create_if_missing([1, 2, 3]);
$this->assertTrue($result);
$modinfo = get_fast_modinfo($course);
$section = $modinfo->get_section_info(3);
$this->assertEquals(3, $section->section);
$this->assertNotEquals($delegateid, $section->id);
$delegatedsection = $modinfo->get_section_info_by_id($delegateid);
$this->assertEquals(4, $delegatedsection->section);
$result = $sectionactions->create_if_missing([1, 2, 3]);
$this->assertFalse($result);
}
/**
* Test for delete method.
* @covers ::delete
*/
public function test_delete(): void {
global $DB;
$this->resetAfterTest(true);
$generator = $this->getDataGenerator();
$course = $generator->create_course(
['numsections' => 6, 'format' => 'topics'],
['createsections' => true]
);
$assign0 = $generator->create_module('assign', ['course' => $course, 'section' => 0]);
$assign1 = $generator->create_module('assign', ['course' => $course, 'section' => 1]);
$assign21 = $generator->create_module('assign', ['course' => $course, 'section' => 2]);
$assign22 = $generator->create_module('assign', ['course' => $course, 'section' => 2]);
$assign3 = $generator->create_module('assign', ['course' => $course, 'section' => 3]);
$assign5 = $generator->create_module('assign', ['course' => $course, 'section' => 5]);
$assign6 = $generator->create_module('assign', ['course' => $course, 'section' => 6]);
$this->setAdminUser();
$sectionactions = new sectionactions($course);
$sections = get_fast_modinfo($course)->get_section_info_all();
// Attempt to delete 0-section.
$this->assertFalse($sectionactions->delete($sections[0], true));
$this->assertTrue($DB->record_exists('course_modules', ['id' => $assign0->cmid]));
$this->assertEquals(6, course_get_format($course)->get_last_section_number());
// Delete last section.
$this->assertTrue($sectionactions->delete($sections[6], true));
$this->assertFalse($DB->record_exists('course_modules', ['id' => $assign6->cmid]));
$this->assertEquals(5, course_get_format($course)->get_last_section_number());
// Delete empty section.
$this->assertTrue($sectionactions->delete($sections[4], false));
$this->assertEquals(4, course_get_format($course)->get_last_section_number());
// Delete section in the middle (2).
$this->assertFalse($sectionactions->delete($sections[2], false));
$this->assertEquals(4, course_get_format($course)->get_last_section_number());
$sections = get_fast_modinfo($course)->get_section_info_all();
$this->assertTrue($sectionactions->delete($sections[2], true));
$this->assertFalse($DB->record_exists('course_modules', ['id' => $assign21->cmid]));
$this->assertFalse($DB->record_exists('course_modules', ['id' => $assign22->cmid]));
$this->assertEquals(3, course_get_format($course)->get_last_section_number());
$this->assertEquals(
[
0 => [$assign0->cmid],
1 => [$assign1->cmid],
2 => [$assign3->cmid],
3 => [$assign5->cmid],
],
get_fast_modinfo($course)->sections
);
// Remove marked section.
course_set_marker($course->id, 1);
$this->assertTrue(course_get_format($course)->is_section_current(1));
$this->assertTrue($sectionactions->delete(
get_fast_modinfo($course)->get_section_info(1),
true
));
$this->assertFalse(course_get_format($course)->is_section_current(1));
}
/**
* Test that triggering a course_section_deleted event works as expected.
* @covers ::delete
*/
public function test_section_deleted_event(): void {
global $USER, $DB;
$this->resetAfterTest();
$sink = $this->redirectEvents();
// Create the course with sections.
$course = $this->getDataGenerator()->create_course(['numsections' => 10], ['createsections' => true]);
$coursecontext = \context_course::instance($course->id);
$section = get_fast_modinfo($course)->get_section_info(10);
$sectionrecord = $DB->get_record('course_sections', ['id' => $section->id]);
$sectionactions = new sectionactions($course);
$sectionactions->delete($section);
$events = $sink->get_events();
$event = array_pop($events); // Delete section event.
$sink->close();
// Validate event data.
$this->assertInstanceOf('\core\event\course_section_deleted', $event);
$this->assertEquals('course_sections', $event->objecttable);
$this->assertEquals($section->id, $event->objectid);
$this->assertEquals($course->id, $event->courseid);
$this->assertEquals($coursecontext->id, $event->contextid);
$this->assertEquals($section->section, $event->other['sectionnum']);
$expecteddesc = "The user with id '{$event->userid}' deleted section number '{$event->other['sectionnum']}' " .
"(section name '{$event->other['sectionname']}') for the course with id '{$event->courseid}'";
$this->assertEquals($expecteddesc, $event->get_description());
$this->assertEquals($sectionrecord, $event->get_record_snapshot('course_sections', $event->objectid));
$this->assertNull($event->get_url());
$this->assertEventContextNotUsed($event);
}
/**
* Test async section deletion hook.
* @covers ::delete
*/
public function test_async_section_deletion_hook_implemented(): void {
// Async section deletion (provided section contains modules), depends on the 'true' being returned by at least one plugin
// implementing the 'course_module_adhoc_deletion_recommended' hook. In core, is implemented by the course recyclebin,
// which will only return true if the plugin is enabled. To make sure async deletion occurs, this test enables recyclebin.
global $DB, $USER;
$this->resetAfterTest(true);
$this->setAdminUser();
// Ensure recyclebin is enabled.
set_config('coursebinenable', true, 'tool_recyclebin');
// Create course, module and context.
$generator = $this->getDataGenerator();
$course = $generator->create_course(['numsections' => 4, 'format' => 'topics'], ['createsections' => true]);
$assign0 = $generator->create_module('assign', ['course' => $course, 'section' => 2]);
$assign1 = $generator->create_module('assign', ['course' => $course, 'section' => 2]);
$assign2 = $generator->create_module('assign', ['course' => $course, 'section' => 2]);
$assign3 = $generator->create_module('assign', ['course' => $course, 'section' => 0]);
$sectionactions = new sectionactions($course);
// Delete empty section. No difference from normal, synchronous behaviour.
$this->assertTrue($sectionactions->delete(get_fast_modinfo($course)->get_section_info(4), false, true));
$this->assertEquals(3, course_get_format($course)->get_last_section_number());
// Delete a module in section 2 (using async). Need to verify this doesn't generate two tasks when we delete
// the section in the next step.
course_delete_module($assign2->cmid, true);
// Confirm that the module is pending deletion in its current section.
$section = $DB->get_record('course_sections', ['course' => $course->id, 'section' => '2']); // For event comparison.
$this->assertEquals(true, $DB->record_exists('course_modules', ['id' => $assign2->cmid, 'deletioninprogress' => 1,
'section' => $section->id]));
// Non-empty section, no forcedelete, so no change.
$this->assertFalse($sectionactions->delete(get_fast_modinfo($course)->get_section_info(2), false, true));
$sink = $this->redirectEvents();
$this->assertTrue($sectionactions->delete(get_fast_modinfo($course)->get_section_info(2), true, true));
// Now, confirm that:
// a) the section's modules have been flagged for deletion and moved to section 0 and;
// b) the section has been deleted and;
// c) course_section_deleted event has been fired. The course_module_deleted events will only fire once they have been
// removed from section 0 via the adhoc task.
// Modules should have been flagged for deletion and moved to section 0.
$sectionid = $DB->get_field('course_sections', 'id', ['course' => $course->id, 'section' => 0]);
$this->assertEquals(
3,
$DB->count_records('course_modules', ['section' => $sectionid, 'deletioninprogress' => 1])
);
// Confirm the section has been deleted.
$this->assertEquals(2, course_get_format($course)->get_last_section_number());
// Check event fired.
$events = $sink->get_events();
$event = array_pop($events);
$sink->close();
$this->assertInstanceOf('\core\event\course_section_deleted', $event);
$this->assertEquals($section->id, $event->objectid);
$this->assertEquals($USER->id, $event->userid);
$this->assertEquals('course_sections', $event->objecttable);
$this->assertEquals(null, $event->get_url());
$this->assertEquals($section, $event->get_record_snapshot('course_sections', $section->id));
// Now, run the adhoc task to delete the modules from section 0.
$sink = $this->redirectEvents(); // To capture the events.
\phpunit_util::run_all_adhoc_tasks();
// Confirm the modules have been deleted.
list($insql, $assignids) = $DB->get_in_or_equal([$assign0->cmid, $assign1->cmid, $assign2->cmid]);
$cmcount = $DB->count_records_select('course_modules', 'id ' . $insql, $assignids);
$this->assertEmpty($cmcount);
// Confirm other modules in section 0 still remain.
$this->assertEquals(1, $DB->count_records('course_modules', ['id' => $assign3->cmid]));
// Confirm that events were generated for all 3 of the modules.
$events = $sink->get_events();
$sink->close();
$count = 0;
while (!empty($events)) {
$event = array_pop($events);
if ($event instanceof \core\event\course_module_deleted &&
in_array($event->objectid, [$assign0->cmid, $assign1->cmid, $assign2->cmid])) {
$count++;
}
}
$this->assertEquals(3, $count);
}
/**
* Test section update method.
*
* @covers ::update
* @dataProvider update_provider
* @param string $fieldname the name of the field to update
* @param int|string $value the value to set
* @param int|string $expected the expected value after the update ('=' to specify the same value as original field)
* @param bool $expectexception if the method should throw an exception
*/
public function test_update(
string $fieldname,
int|string $value,
int|string $expected,
bool $expectexception
): void {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(
['format' => 'topics', 'numsections' => 1],
['createsections' => true]
);
$section = get_fast_modinfo($course)->get_section_info(1);
$sectionrecord = $DB->get_record('course_sections', ['id' => $section->id]);
$this->assertNotEquals($value, $sectionrecord->$fieldname);
$this->assertNotEquals($value, $section->$fieldname);
if ($expectexception) {
$this->expectException(\moodle_exception::class);
}
if ($expected === '=') {
$expected = $section->$fieldname;
}
$sectionactions = new sectionactions($course);
$sectionactions->update($section, [$fieldname => $value]);
$sectionrecord = $DB->get_record('course_sections', ['id' => $section->id]);
$this->assertEquals($expected, $sectionrecord->$fieldname);
$section = get_fast_modinfo($course)->get_section_info(1);
$this->assertEquals($expected, $section->$fieldname);
}
/**
* Data provider for test_update.
* @return array
*/
public static function update_provider(): array {
return [
'Id will not be updated' => [
'fieldname' => 'id',
'value' => -1,
'expected' => '=',
'expectexception' => false,
],
'Course will not be updated' => [
'fieldname' => 'course',
'value' => -1,
'expected' => '=',
'expectexception' => false,
],
'Section number will not be updated' => [
'fieldname' => 'section',
'value' => -1,
'expected' => '=',
'expectexception' => false,
],
'Sequence will be updated' => [
'fieldname' => 'name',
'value' => 'new name',
'expected' => 'new name',
'expectexception' => false,
],
'Summary can be updated' => [
'fieldname' => 'summary',
'value' => 'new summary',
'expected' => 'new summary',
'expectexception' => false,
],
'Visible can be updated' => [
'fieldname' => 'visible',
'value' => 0,
'expected' => 0,
'expectexception' => false,
],
'component can be updated' => [
'fieldname' => 'component',
'value' => 'mod_assign',
'expected' => 'mod_assign',
'expectexception' => false,
],
'itemid can be updated' => [
'fieldname' => 'itemid',
'value' => 1,
'expected' => 1,
'expectexception' => false,
],
'Long names throws and exception' => [
'fieldname' => 'name',
'value' => str_repeat('a', 256),
'expected' => '=',
'expectexception' => true,
],
];
}
/**
* Test section update method updating several values at once.
*
* @covers ::update
*/
public function test_update_multiple_fields(): void {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(
['format' => 'topics', 'numsections' => 1],
['createsections' => true]
);
$section = get_fast_modinfo($course)->get_section_info(1);
$sectionrecord = $DB->get_record('course_sections', ['id' => $section->id]);
$this->assertEquals(1, $sectionrecord->visible);
$this->assertNull($section->name);
$sectionactions = new sectionactions($course);
$sectionactions->update($section, ['name' => 'New name', 'visible' => 0]);
$sectionrecord = $DB->get_record('course_sections', ['id' => $section->id]);
$this->assertEquals('New name', $sectionrecord->name);
$this->assertEquals(0, $sectionrecord->visible);
$section = get_fast_modinfo($course)->get_section_info(1);
$this->assertEquals('New name', $section->name);
$this->assertEquals(0, $section->visible);
}
/**
* Test updating a section trigger a course section update log event.
*
* @covers ::update
*/
public function test_course_section_updated_event(): void {
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(
['format' => 'topics', 'numsections' => 1],
['createsections' => true]
);
$section = get_fast_modinfo($course)->get_section_info(1);
$sink = $this->redirectEvents();
$sectionactions = new sectionactions($course);
$sectionactions->update($section, ['name' => 'New name', 'visible' => 0]);
$events = $sink->get_events();
$event = reset($events);
// Check that the event data is valid.
$this->assertInstanceOf('\core\event\course_section_updated', $event);
$data = $event->get_data();
$this->assertEquals(\context_course::instance($course->id), $event->get_context());
$this->assertEquals($section->id, $data['objectid']);
}
/**
* Test section update change the modified date.
*
* @covers ::update
*/
public function test_update_time_modified(): void {
global $DB;
$this->resetAfterTest();
// Create the course with sections.
$course = $this->getDataGenerator()->create_course(
['format' => 'topics', 'numsections' => 1],
['createsections' => true]
);
$section = get_fast_modinfo($course)->get_section_info(1);
$sectionrecord = $DB->get_record('course_sections', ['id' => $section->id]);
$oldtimemodified = $sectionrecord->timemodified;
$sectionactions = new sectionactions($course);
// Ensuring that the section update occurs at a different timestamp.
$this->waitForSecond();
// The timemodified should only be updated if the section is actually updated.
$result = $sectionactions->update($section, []);
$this->assertFalse($result);
$sectionrecord = $DB->get_record('course_sections', ['id' => $section->id]);
$this->assertEquals($oldtimemodified, $sectionrecord->timemodified);
// Now update something to prove timemodified changes.
$result = $sectionactions->update($section, ['name' => 'New name']);
$this->assertTrue($result);
$sectionrecord = $DB->get_record('course_sections', ['id' => $section->id]);
$this->assertGreaterThan($oldtimemodified, $sectionrecord->timemodified);
}
/**
* Test section updating visibility will hide or show section activities.
*
* @covers ::update
*/
public function test_update_hide_section_activities(): void {
global $DB;
$this->resetAfterTest();
// Create 4 activities (visible, visible, hidden, hidden).
$course = $this->getDataGenerator()->create_course(
['format' => 'topics', 'numsections' => 1],
['createsections' => true]
);
$activity1 = $this->getDataGenerator()->create_module(
'assign',
['course' => $course->id, 'section' => 1]
);
$activity2 = $this->getDataGenerator()->create_module(
'assign',
['course' => $course->id, 'section' => 1]
);
$activity3 = $this->getDataGenerator()->create_module(
'assign',
['course' => $course->id, 'section' => 1, 'visible' => 0]
);
$activity4 = $this->getDataGenerator()->create_module(
'assign',
['course' => $course->id, 'section' => 1, 'visible' => 0]
);
$modinfo = get_fast_modinfo($course);
$cm1 = $modinfo->get_cm($activity1->cmid);
$cm2 = $modinfo->get_cm($activity2->cmid);
$cm3 = $modinfo->get_cm($activity3->cmid);
$cm4 = $modinfo->get_cm($activity4->cmid);
$this->assertEquals(1, $cm1->visible);
$this->assertEquals(1, $cm2->visible);
$this->assertEquals(0, $cm3->visible);
$this->assertEquals(0, $cm4->visible);
$sectionactions = new sectionactions($course);
// Validate hidding section hides all activities.
$section = $modinfo->get_section_info(1);
$sectionactions->update($section, ['visible' => 0]);
$modinfo = get_fast_modinfo($course);
$cm1 = $modinfo->get_cm($activity1->cmid);
$cm2 = $modinfo->get_cm($activity2->cmid);
$cm3 = $modinfo->get_cm($activity3->cmid);
$cm4 = $modinfo->get_cm($activity4->cmid);
$this->assertEquals(0, $cm1->visible);
$this->assertEquals(0, $cm2->visible);
$this->assertEquals(0, $cm3->visible);
$this->assertEquals(0, $cm4->visible);
// Validate showing the section restores the previous visibility.
$section = $modinfo->get_section_info(1);
$sectionactions->update($section, ['visible' => 1]);
$modinfo = get_fast_modinfo($course);
$cm1 = $modinfo->get_cm($activity1->cmid);
$cm2 = $modinfo->get_cm($activity2->cmid);
$cm3 = $modinfo->get_cm($activity3->cmid);
$cm4 = $modinfo->get_cm($activity4->cmid);
$this->assertEquals(1, $cm1->visible);
$this->assertEquals(1, $cm2->visible);
$this->assertEquals(0, $cm3->visible);
$this->assertEquals(0, $cm4->visible);
// Swap two activities visibility to alter visible values.
set_coursemodule_visible($cm2->id, 0, 0, true);
set_coursemodule_visible($cm4->id, 1, 1, true);
$modinfo = get_fast_modinfo($course);
$cm1 = $modinfo->get_cm($activity1->cmid);
$cm2 = $modinfo->get_cm($activity2->cmid);
$cm3 = $modinfo->get_cm($activity3->cmid);
$cm4 = $modinfo->get_cm($activity4->cmid);
$this->assertEquals(1, $cm1->visible);
$this->assertEquals(0, $cm2->visible);
$this->assertEquals(0, $cm3->visible);
$this->assertEquals(1, $cm4->visible);
// Validate hidding the section again.
$section = $modinfo->get_section_info(1);
$sectionactions->update($section, ['visible' => 0]);
$modinfo = get_fast_modinfo($course);
$cm1 = $modinfo->get_cm($activity1->cmid);
$cm2 = $modinfo->get_cm($activity2->cmid);
$cm3 = $modinfo->get_cm($activity3->cmid);
$cm4 = $modinfo->get_cm($activity4->cmid);
$this->assertEquals(0, $cm1->visible);
$this->assertEquals(0, $cm2->visible);
$this->assertEquals(0, $cm3->visible);
$this->assertEquals(0, $cm4->visible);
// Validate showing the section once more to check previous state is restored.
$section = $modinfo->get_section_info(1);
$sectionactions->update($section, ['visible' => 1]);
$modinfo = get_fast_modinfo($course);
$cm1 = $modinfo->get_cm($activity1->cmid);
$cm2 = $modinfo->get_cm($activity2->cmid);
$cm3 = $modinfo->get_cm($activity3->cmid);
$cm4 = $modinfo->get_cm($activity4->cmid);
$this->assertEquals(1, $cm1->visible);
$this->assertEquals(0, $cm2->visible);
$this->assertEquals(0, $cm3->visible);
$this->assertEquals(1, $cm4->visible);
}
/**
* Test that the preprocess_section_name method can alter the section rename value.
*
* @covers ::update
* @covers ::preprocess_delegated_section_fields
*/
public function test_preprocess_section_name(): void {
global $DB, $CFG;
$this->resetAfterTest();
require_once($CFG->libdir . '/tests/fixtures/sectiondelegatetest.php');
$course = $this->getDataGenerator()->create_course();
$sectionactions = new sectionactions($course);
$section = $sectionactions->create_delegated('test_component', 1);
$result = $sectionactions->update($section, ['name' => 'new_name']);
$this->assertTrue($result);
$section = $DB->get_record('course_sections', ['id' => $section->id]);
$this->assertEquals('new_name_suffix', $section->name);
$sectioninfo = get_fast_modinfo($course->id)->get_section_info_by_id($section->id);
$this->assertEquals('new_name_suffix', $sectioninfo->name);
// Validate null name.
$section = $sectionactions->create_delegated('test_component', 1, (object)['name' => 'sample']);
$result = $sectionactions->update($section, ['name' => null]);
$this->assertTrue($result);
$section = $DB->get_record('course_sections', ['id' => $section->id]);
$this->assertEquals('null_name', $section->name);
$sectioninfo = get_fast_modinfo($course->id)->get_section_info_by_id($section->id);
$this->assertEquals('null_name', $sectioninfo->name);
}
/**
* Test that the position of a new section in a course with deleghated sections.
* @covers ::create
* @covers ::create_delegated
*/
public function test_create_position(): void {
global $DB, $CFG;
$this->resetAfterTest();
require_once($CFG->libdir . '/tests/fixtures/sectiondelegatetest.php');
$course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 1]);
$section1 = get_fast_modinfo($course->id)->get_section_info(1);
$sectionactions = new sectionactions($course);
$delegatedsection1 = $sectionactions->create_delegated('test_component', 1);
$delegatedsection2 = $sectionactions->create_delegated('test_component', 2);
$this->assertEquals(2, $delegatedsection1->section);
$this->assertEquals(3, $delegatedsection2->section);
// Create some regular sections with zero and none param.
$newsection1 = $sectionactions->create(0);
$newsection2 = $sectionactions->create();
$this->assertEquals(2, $newsection1->section);
$this->assertEquals(3, $newsection2->section);
// Check the section order.
$section = $sectioninfo = get_fast_modinfo($course->id)->get_section_info_all();
$this->assertEquals($section1->id, $section[1]->id);
$this->assertEquals($newsection1->id, $section[2]->id);
$this->assertEquals($newsection2->id, $section[3]->id);
$this->assertEquals($delegatedsection1->id, $section[4]->id);
$this->assertEquals($delegatedsection2->id, $section[5]->id);
}
}
@@ -0,0 +1,208 @@
<?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_courseformat\output;
use stdClass;
/**
* Tests for activitybadge class.
*
* @package core_courseformat
* @copyright 2023 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core_courseformat\output\activitybadge
*/
class activitybadge_test extends \advanced_testcase {
/**
* Test the behaviour of create_instance() and export_for_template() attributes.
* @runInSeparateProcess
*
* @covers ::export_for_template
* @covers ::create_instance
*/
public function test_activitybadge_export_for_template(): void {
$this->resetAfterTest();
$this->setAdminUser();
$data = $this->setup_scenario();
$user = $this->getDataGenerator()->create_user(['trackforums' => 1]);
$this->getDataGenerator()->enrol_user(
$user->id,
$data->course->id,
'student'
);
$this->setUser($user);
$renderer = $data->renderer;
// The activitybadge for a file with all options enabled shouldn't be empty.
$class = activitybadge::create_instance($data->fileshowtype);
$result = $class->export_for_template($renderer);
$this->check_activitybadge($result, 'TXT', 'badge-none');
// The activitybadge for a file with Show type option disabled should be empty.
$class = activitybadge::create_instance($data->filehidetype);
$result = $class->export_for_template($renderer);
$this->check_activitybadge($result);
// The activitybadge for a forum with unread messages shouldn't be empty.
$class = activitybadge::create_instance($data->forumunread);
$result = $class->export_for_template($renderer);
$this->check_activitybadge($result, '1 unread post', 'bg-dark text-white');
// The activitybadge for a forum without unread messages should be empty.
$class = activitybadge::create_instance($data->forumread);
$result = $class->export_for_template($renderer);
$this->check_activitybadge($result);
// The activitybadge for an assignment should be empty.
$class = activitybadge::create_instance($data->assign);
$this->assertNull($class);
// The activitybadge for a label should be empty.
$class = activitybadge::create_instance($data->label);
$this->assertNull($class);
}
/**
* Setup the default scenario, creating some activities:
* - A forum with one unread message from the teacher.
* - Another forum without unread messages.
* - A file with all the appearance options enabled.
* - A file with the "Show type" option disabled.
* - An assignment.
* - A label.
*
* @return stdClass the scenario instances.
*/
private function setup_scenario(): stdClass {
global $PAGE;
$course = $this->getDataGenerator()->create_course(['numsections' => 1]);
// Enrol editing teacher to the course.
$teacher = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user(
$teacher->id,
$course->id,
'editingteacher'
);
$this->setUser($teacher);
// Create a forum with tracking forced and add a discussion.
$record = new stdClass();
$record->introformat = FORMAT_HTML;
$record->course = $course->id;
$record->trackingtype = FORUM_TRACKING_FORCED;
$forumread = $this->getDataGenerator()->create_module('forum', $record);
$forumunread = $this->getDataGenerator()->create_module('forum', $record);
$record = new stdClass();
$record->course = $course->id;
$record->userid = $teacher->id;
$record->forum = $forumunread->id;
$discussion = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
// Create a file with all the options enabled.
$record = (object)[
'course' => $course->id,
'showsize' => 1,
'showtype' => 1,
'showdate' => 1,
];
$fileshowtype = self::getDataGenerator()->create_module('resource', $record);
// Create a file with Show type disabled.
$record = (object)[
'course' => $course->id,
'showsize' => 1,
'showtype' => 0,
'showdate' => 1,
];
$filehidetype = self::getDataGenerator()->create_module('resource', $record);
// Create an assignment and a label.
$assign = $this->getDataGenerator()->create_module('assign', ['course' => $course->id]);
$label = $this->getDataGenerator()->create_module('label', ['course' => $course->id]);
rebuild_course_cache($course->id, true);
$renderer = course_get_format($course->id)->get_renderer($PAGE);
$modinfo = get_fast_modinfo($course->id);
return (object)[
'course' => $course,
'forumunread' => $modinfo->get_cm($forumunread->cmid),
'discussion' => $discussion,
'forumread' => $modinfo->get_cm($forumread->cmid),
'fileshowtype' => $modinfo->get_cm($fileshowtype->cmid),
'filehidetype' => $modinfo->get_cm($filehidetype->cmid),
'assign' => $modinfo->get_cm($assign->cmid),
'label' => $modinfo->get_cm($label->cmid),
'renderer' => $renderer,
];
}
/**
* Method to check if the result of the export_from_template is the expected.
*
* @param stdClass $result The result of the export_from_template() call.
* @param string|null $content The expected activitybadge content.
* @param string|null $style The expected activitybadge style.
* @param string|null $url The expected activitybadge url.
* @param string|null $elementid The expected activitybadge element id.
* @param array|null $extra The expected activitybadge extra attributes.
*/
private function check_activitybadge(
stdClass $result,
?string $content = null,
?string $style = null,
?string $url = null,
?string $elementid = null,
?array $extra = null
): void {
if (is_null($content)) {
$this->assertObjectNotHasProperty('badgecontent', $result);
} else {
$this->assertEquals($content, $result->badgecontent);
}
if (is_null($style)) {
$this->assertObjectNotHasProperty('badgestyle', $result);
} else {
$this->assertEquals($style, $result->badgestyle);
}
if (is_null($url)) {
$this->assertObjectNotHasProperty('badgeurl', $result);
} else {
$this->assertEquals($url, $result->badgeurl);
}
if (is_null($elementid)) {
$this->assertObjectNotHasProperty('badgeelementid', $result);
} else {
$this->assertEquals($elementid, $result->badgeelementid);
}
if (is_null($extra)) {
$this->assertObjectNotHasProperty('badgeextraattributes', $result);
} else {
$this->assertEquals($extra, $result->badgeextraattributes);
}
}
}
@@ -0,0 +1,356 @@
<?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_courseformat\output\local\state;
use availability_date\condition;
use core_availability\tree;
use stdClass;
/**
* Tests for cm state class.
*
* @package core_courseformat
* @copyright 2022 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core_courseformat\output\local\state\cm
*/
class cm_test extends \advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setupBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/course/lib.php');
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest.php');
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest_output_course_format_state.php');
}
/**
* Test the behaviour of state\cm hasavailability attribute.
*
* @dataProvider hasrestrictions_state_provider
* @covers ::export_for_template
*
* @param string $format the course format
* @param string $rolename the user role name (editingteacher or student)
* @param bool $hasavailability if the activity|section has availability
* @param bool $available if the activity availability condition is available or not to the user
* @param bool $expected the expected result
*/
public function test_cm_hasrestrictions_state(
string $format = 'topics',
string $rolename = 'editingteacher',
bool $hasavailability = false,
bool $available = false,
bool $expected = false
): void {
$data = $this->setup_hasrestrictions_scenario($format, $rolename, $hasavailability, $available);
// Get the cm state.
$courseformat = $data->courseformat;
$renderer = $data->renderer;
$cmclass = $courseformat->get_output_classname('state\\cm');
$cmstate = new $cmclass(
$courseformat,
$data->section,
$data->cm
);
$state = $cmstate->export_for_template($renderer);
$this->assertEquals($expected, $state->hascmrestrictions);
}
/**
* Setup section or cm has restrictions scenario.
*
* @param string $format the course format
* @param string $rolename the user role name (editingteacher or student)
* @param bool $hasavailability if the activity|section has availability
* @param bool $available if the activity availability condition is available or not to the user
* @return stdClass the scenario instances.
*/
private function setup_hasrestrictions_scenario(
string $format = 'topics',
string $rolename = 'editingteacher',
bool $hasavailability = false,
bool $available = false
): stdClass {
global $PAGE, $DB;
$this->resetAfterTest();
set_config('enableavailability', 1);
$course = $this->getDataGenerator()->create_course(['numsections' => 1, 'format' => $format]);
// Create and enrol user.
$user = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user(
$user->id,
$course->id,
$rolename
);
$this->setUser($user);
// Create an activity.
$activity = $this->getDataGenerator()->create_module('page', ['course' => $course->id], [
'section' => 1,
'visible' => 1
]);
// Set up the availability settings.
if ($hasavailability) {
$operation = ($available) ? condition::DIRECTION_UNTIL : condition::DIRECTION_FROM;
$availabilityjson = json_encode(tree::get_root_json(
[
condition::get_json($operation, time() + 3600),
],
'&',
true
));
$selector = ['id' => $activity->cmid];
$DB->set_field('course_modules', 'availability', trim($availabilityjson), $selector);
}
// Get the cm state.
$courseformat = course_get_format($course->id);
$modinfo = $courseformat->get_modinfo();
$renderer = $courseformat->get_renderer($PAGE);
if ($format == 'theunittest') {
// These course format's hasn't the renderer file, so a debugging message will be displayed.
$this->assertDebuggingCalled();
}
return (object)[
'courseformat' => $courseformat,
'section' => $modinfo->get_section_info(1),
'cm' => $modinfo->get_cm($activity->cmid),
'renderer' => $renderer,
];
}
/**
* Data provider for test_state().
*
* @return array
*/
public function hasrestrictions_state_provider(): array {
return [
// Teacher scenarios (topics).
'Teacher, Topics, can edit, has availability and is available' => [
'format' => 'topics',
'rolename' => 'editingteacher',
'hasavailability' => true,
'available' => true,
'expected' => true,
],
'Teacher, Topics, can edit, has availability and is not available' => [
'format' => 'topics',
'rolename' => 'editingteacher',
'hasavailability' => true,
'available' => false,
'expected' => true,
],
'Teacher, Topics, can edit and has not availability' => [
'format' => 'topics',
'rolename' => 'editingteacher',
'hasavailability' => false,
'available' => true,
'expected' => false,
],
// Teacher scenarios (weeks).
'Teacher, Weeks, can edit, has availability and is available' => [
'format' => 'weeks',
'rolename' => 'editingteacher',
'hasavailability' => true,
'available' => true,
'expected' => true,
],
'Teacher, Weeks, can edit, has availability and is not available' => [
'format' => 'weeks',
'rolename' => 'editingteacher',
'hasavailability' => true,
'available' => false,
'expected' => true,
],
'Teacher, Weeks, can edit and has not availability' => [
'format' => 'weeks',
'rolename' => 'editingteacher',
'hasavailability' => false,
'available' => true,
'expected' => false,
],
// Teacher scenarios (mock format).
'Teacher, Mock format, can edit, has availability and is available' => [
'format' => 'theunittest',
'rolename' => 'editingteacher',
'hasavailability' => true,
'available' => true,
'expected' => true,
],
'Teacher, Mock format, can edit, has availability and is not available' => [
'format' => 'theunittest',
'rolename' => 'editingteacher',
'hasavailability' => true,
'available' => false,
'expected' => true,
],
'Teacher, Mock format, can edit and has not availability' => [
'format' => 'theunittest',
'rolename' => 'editingteacher',
'hasavailability' => false,
'available' => true,
'expected' => false,
],
// Non editing teacher scenarios (topics).
'Non editing teacher, Topics, can edit, has availability and is available' => [
'format' => 'topics',
'rolename' => 'teacher',
'hasavailability' => true,
'available' => true,
'expected' => true,
],
'Non editing teacher, Topics, can edit, has availability and is not available' => [
'format' => 'topics',
'rolename' => 'teacher',
'hasavailability' => true,
'available' => false,
'expected' => true,
],
'Non editing teacher, Topics, can edit and has not availability' => [
'format' => 'topics',
'rolename' => 'teacher',
'hasavailability' => false,
'available' => true,
'expected' => false,
],
// Non editing teacher scenarios (weeks).
'Non editing teacher, Weeks, can edit, has availability and is available' => [
'format' => 'weeks',
'rolename' => 'teacher',
'hasavailability' => true,
'available' => true,
'expected' => true,
],
'Non editing teacher, Weeks, can edit, has availability and is not available' => [
'format' => 'weeks',
'rolename' => 'teacher',
'hasavailability' => true,
'available' => false,
'expected' => true,
],
'Non editing teacher, Weeks, can edit and has not availability' => [
'format' => 'weeks',
'rolename' => 'teacher',
'hasavailability' => false,
'available' => true,
'expected' => false,
],
// Non editing teacher scenarios (mock format).
'Non editing teacher, Mock format, can edit, has availability and is available' => [
'format' => 'theunittest',
'rolename' => 'teacher',
'hasavailability' => true,
'available' => true,
'expected' => true,
],
'Non editing teacher, Mock format, can edit, has availability and is not available' => [
'format' => 'theunittest',
'rolename' => 'teacher',
'hasavailability' => true,
'available' => false,
'expected' => true,
],
'Non editing teacher, Mock format, can edit and has not availability' => [
'format' => 'theunittest',
'rolename' => 'teacher',
'hasavailability' => false,
'available' => true,
'expected' => false,
],
// Student scenarios (topics).
'Student, Topics, cannot edit, has availability and is available' => [
'format' => 'topics',
'rolename' => 'student',
'hasavailability' => true,
'available' => true,
'expected' => false,
],
'Student, Topics, cannot edit, has availability and is not available' => [
'format' => 'topics',
'rolename' => 'student',
'hasavailability' => true,
'available' => false,
'expected' => true,
],
'Student, Topics, cannot edit and has not availability' => [
'format' => 'topics',
'rolename' => 'student',
'hasavailability' => false,
'available' => true,
'expected' => false,
],
// Student scenarios (weeks).
'Student, Weeks, cannot edit, has availability and is available' => [
'format' => 'weeks',
'rolename' => 'student',
'hasavailability' => true,
'available' => true,
'expected' => false,
],
'Student, Weeks, cannot edit, has availability and is not available' => [
'format' => 'weeks',
'rolename' => 'student',
'hasavailability' => true,
'available' => false,
'expected' => true,
],
'Student, Weeks, cannot edit and has not availability' => [
'format' => 'weeks',
'rolename' => 'student',
'hasavailability' => false,
'available' => true,
'expected' => false,
],
// Student scenarios (mock format).
'Student, Mock format, cannot edit, has availability and is available' => [
'format' => 'theunittest',
'rolename' => 'student',
'hasavailability' => true,
'available' => true,
'expected' => false,
],
'Student, Mock format, cannot edit, has availability and is not available' => [
'format' => 'theunittest',
'rolename' => 'student',
'hasavailability' => true,
'available' => false,
'expected' => true,
],
'Student, Mock format, cannot edit and has not availability' => [
'format' => 'theunittest',
'rolename' => 'student',
'hasavailability' => false,
'available' => true,
'expected' => false,
],
];
}
}
@@ -0,0 +1,348 @@
<?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_courseformat\output\local\state;
use availability_date\condition;
use core_availability\tree;
use stdClass;
/**
* Tests for section state class.
*
* @package core_courseformat
* @copyright 2022 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core_courseformat\output\local\state\section
*/
class section_test extends \advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setupBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/course/lib.php');
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest.php');
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest_output_course_format_state.php');
}
/**
* Test the behaviour of state\section hasavailability attribute.
*
* @dataProvider hasrestrictions_state_provider
* @covers ::export_for_template
*
* @param string $format the course format
* @param string $rolename the user role name (editingteacher or student)
* @param bool $hasavailability if the activity|section has availability
* @param bool $available if the activity availability condition is available or not to the user
* @param bool $expected the expected result
*/
public function test_section_hasrestrictions_state(
string $format = 'topics',
string $rolename = 'editingteacher',
bool $hasavailability = false,
bool $available = false,
bool $expected = false
): void {
$data = $this->setup_hasrestrictions_scenario($format, $rolename, $hasavailability, $available);
// Get the cm state.
$courseformat = $data->courseformat;
$renderer = $data->renderer;
$sectionclass = $courseformat->get_output_classname('state\\section');
$sectionstate = new $sectionclass(
$courseformat,
$data->section
);
$state = $sectionstate->export_for_template($renderer);
$this->assertEquals($expected, $state->hasrestrictions);
}
/**
* Setup section or cm has restrictions scenario.
*
* @param string $format the course format
* @param string $rolename the user role name (editingteacher or student)
* @param bool $hasavailability if the section has availability
* @param bool $available if the section availability condition is available or not to the user
* @return stdClass the scenario instances.
*/
private function setup_hasrestrictions_scenario(
string $format = 'topics',
string $rolename = 'editingteacher',
bool $hasavailability = false,
bool $available = false
): stdClass {
global $PAGE, $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(['numsections' => 1, 'format' => $format]);
// Create and enrol user.
$user = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->enrol_user(
$user->id,
$course->id,
$rolename
);
$this->setUser($user);
// Set up the availability settings.
if ($hasavailability) {
$operation = ($available) ? condition::DIRECTION_UNTIL : condition::DIRECTION_FROM;
$availabilityjson = json_encode(tree::get_root_json(
[
condition::get_json($operation, time() + 3600),
],
'&',
true
));
$modinfo = get_fast_modinfo($course);
$sectioninfo = $modinfo->get_section_info(1);
$selector = ['id' => $sectioninfo->id];
$DB->set_field('course_sections', 'availability', trim($availabilityjson), $selector);
}
rebuild_course_cache($course->id, true);
$courseformat = course_get_format($course->id);
$modinfo = $courseformat->get_modinfo();
$renderer = $courseformat->get_renderer($PAGE);
if ($format == 'theunittest') {
// These course format's hasn't the renderer file, so a debugging message will be displayed.
$this->assertDebuggingCalled();
}
return (object)[
'courseformat' => $courseformat,
'section' => $modinfo->get_section_info(1),
'renderer' => $renderer,
];
}
/**
* Data provider for test_state().
*
* @return array
*/
public function hasrestrictions_state_provider(): array {
return [
// Teacher scenarios (topics).
'Teacher, Topics, can edit, has availability and is available' => [
'format' => 'topics',
'rolename' => 'editingteacher',
'hasavailability' => true,
'available' => true,
'expected' => true,
],
'Teacher, Topics, can edit, has availability and is not available' => [
'format' => 'topics',
'rolename' => 'editingteacher',
'hasavailability' => true,
'available' => false,
'expected' => true,
],
'Teacher, Topics, can edit and has not availability' => [
'format' => 'topics',
'rolename' => 'editingteacher',
'hasavailability' => false,
'available' => true,
'expected' => false,
],
// Teacher scenarios (weeks).
'Teacher, Weeks, can edit, has availability and is available' => [
'format' => 'weeks',
'rolename' => 'editingteacher',
'hasavailability' => true,
'available' => true,
'expected' => true,
],
'Teacher, Weeks, can edit, has availability and is not available' => [
'format' => 'weeks',
'rolename' => 'editingteacher',
'hasavailability' => true,
'available' => false,
'expected' => true,
],
'Teacher, Weeks, can edit and has not availability' => [
'format' => 'weeks',
'rolename' => 'editingteacher',
'hasavailability' => false,
'available' => true,
'expected' => false,
],
// Teacher scenarios (mock format).
'Teacher, Mock format, can edit, has availability and is available' => [
'format' => 'theunittest',
'rolename' => 'editingteacher',
'hasavailability' => true,
'available' => true,
'expected' => true,
],
'Teacher, Mock format, can edit, has availability and is not available' => [
'format' => 'theunittest',
'rolename' => 'editingteacher',
'hasavailability' => true,
'available' => false,
'expected' => true,
],
'Teacher, Mock format, can edit and has not availability' => [
'format' => 'theunittest',
'rolename' => 'editingteacher',
'hasavailability' => false,
'available' => true,
'expected' => false,
],
// Non editing teacher scenarios (topics).
'Non editing teacher, Topics, can edit, has availability and is available' => [
'format' => 'topics',
'rolename' => 'teacher',
'hasavailability' => true,
'available' => true,
'expected' => false,
],
'Non editing teacher, Topics, can edit, has availability and is not available' => [
'format' => 'topics',
'rolename' => 'teacher',
'hasavailability' => true,
'available' => false,
'expected' => false,
],
'Non editing teacher, Topics, can edit and has not availability' => [
'format' => 'topics',
'rolename' => 'teacher',
'hasavailability' => false,
'available' => true,
'expected' => false,
],
// Non editing teacher scenarios (weeks).
'Non editing teacher, Weeks, can edit, has availability and is available' => [
'format' => 'weeks',
'rolename' => 'teacher',
'hasavailability' => true,
'available' => true,
'expected' => false,
],
'Non editing teacher, Weeks, can edit, has availability and is not available' => [
'format' => 'weeks',
'rolename' => 'teacher',
'hasavailability' => true,
'available' => false,
'expected' => false,
],
'Non editing teacher, Weeks, can edit and has not availability' => [
'format' => 'weeks',
'rolename' => 'teacher',
'hasavailability' => false,
'available' => true,
'expected' => false,
],
// Non editing teacher scenarios (mock format).
'Non editing teacher, Mock format, can edit, has availability and is available' => [
'format' => 'theunittest',
'rolename' => 'teacher',
'hasavailability' => true,
'available' => true,
'expected' => false,
],
'Non editing teacher, Mock format, can edit, has availability and is not available' => [
'format' => 'theunittest',
'rolename' => 'teacher',
'hasavailability' => true,
'available' => false,
'expected' => false,
],
'Non editing teacher, Mock format, can edit and has not availability' => [
'format' => 'theunittest',
'rolename' => 'teacher',
'hasavailability' => false,
'available' => true,
'expected' => false,
],
// Student scenarios (topics).
'Topics, cannot edit, has availability and is available' => [
'format' => 'topics',
'rolename' => 'student',
'hasavailability' => true,
'available' => true,
'expected' => false,
],
'Topics, cannot edit, has availability and is not available' => [
'format' => 'topics',
'rolename' => 'student',
'hasavailability' => true,
'available' => false,
'expected' => true,
],
'Topics, cannot edit and has not availability' => [
'format' => 'topics',
'rolename' => 'student',
'hasavailability' => false,
'available' => true,
'expected' => false,
],
// Student scenarios (weeks).
'Weeks, cannot edit, has availability and is available' => [
'format' => 'weeks',
'rolename' => 'student',
'hasavailability' => true,
'available' => true,
'expected' => false,
],
'Weeks, cannot edit, has availability and is not available' => [
'format' => 'weeks',
'rolename' => 'student',
'hasavailability' => true,
'available' => false,
'expected' => true,
],
'Weeks, cannot edit and has not availability' => [
'format' => 'weeks',
'rolename' => 'student',
'hasavailability' => false,
'available' => true,
'expected' => false,
],
// Student scenarios (mock format).
'Mock format, cannot edit, has availability and is available' => [
'format' => 'theunittest',
'rolename' => 'student',
'hasavailability' => true,
'available' => true,
'expected' => false,
],
'Mock format, cannot edit, has availability and is not available' => [
'format' => 'theunittest',
'rolename' => 'student',
'hasavailability' => true,
'available' => false,
'expected' => true,
],
'Mock format, cannot edit and has not availability' => [
'format' => 'theunittest',
'rolename' => 'student',
'hasavailability' => false,
'available' => true,
'expected' => false,
],
];
}
}
@@ -0,0 +1,169 @@
<?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_courseformat\output\local\state;
/**
* Tests for state classes (course, section, cm).
*
* @package core
* @subpackage course
* @copyright 2021 Ilya Tregubov <ilya@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class state_test extends \advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setupBeforeClass(): void {
global $CFG;
require_once($CFG->dirroot . '/course/lib.php');
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest.php');
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest_output_course_format_state.php');
}
/**
* Test the behaviour of state::export_for_template().
*
* @dataProvider state_provider
* @covers \core_courseformat\output\local\state
*
* @param string $format The course format of the course where the method will be executed.
*/
public function test_state(string $format = 'topics'): void {
global $PAGE;
$this->resetAfterTest();
// Create a course.
$numsections = 6;
$course = $this->getDataGenerator()->create_course(['numsections' => $numsections, 'format' => $format]);
$hiddensections = [4, 6];
foreach ($hiddensections as $section) {
set_section_visible($course->id, $section, 0);
}
// Create and enrol user.
$this->setAdminUser();
$courseformat = course_get_format($course->id);
$modinfo = $courseformat->get_modinfo();
$issocialformat = $courseformat->get_format() === 'social';
// Only create activities if the course format is not social.
// There's no course home page (and sections) for social course format.
if (!$issocialformat || $format == 'theunittest') {
// Add some activities to the course.
$this->getDataGenerator()->create_module('page', ['course' => $course->id], ['section' => 1,
'visible' => 1]);
$this->getDataGenerator()->create_module('forum', ['course' => $course->id], ['section' => 1,
'visible' => 1]);
$this->getDataGenerator()->create_module('assign', ['course' => $course->id], ['section' => 2,
'visible' => 0]);
$this->getDataGenerator()->create_module('glossary', ['course' => $course->id], ['section' => 4,
'visible' => 1]);
$this->getDataGenerator()->create_module('label', ['course' => $course->id], ['section' => 5,
'visible' => 0]);
$this->getDataGenerator()->create_module('feedback', ['course' => $course->id], ['section' => 5,
'visible' => 1]);
}
$courseclass = $courseformat->get_output_classname('state\\course');
$sectionclass = $courseformat->get_output_classname('state\\section');
$cmclass = $courseformat->get_output_classname('state\\cm');
// Get the proper renderer.
$renderer = $courseformat->get_renderer($PAGE);
$result = (object)[
'course' => (object)[],
'section' => [],
'cm' => [],
];
// General state.
$coursestate = new $courseclass($courseformat);
$result->course = $coursestate->export_for_template($renderer);
if ($format == 'theunittest') {
// These course format's hasn't the renderer file, so a debugging message will be displayed.
$this->assertDebuggingCalled();
}
$this->assertEquals($course->id, $result->course->id);
$this->assertEquals($numsections, $result->course->numsections);
$this->assertFalse($result->course->editmode);
$sections = $modinfo->get_section_info_all();
foreach ($sections as $key => $section) {
$this->assertEquals($section->id, $result->course->sectionlist[$key]);
if (!$issocialformat || $format == 'theunittest') {
if (!empty($section->uservisible)) {
$sectionstate = new $sectionclass($courseformat, $section);
$result->section[$key] = $sectionstate->export_for_template($renderer);
$this->assertEquals($section->id, $result->section[$key]->id);
$this->assertEquals($section->section, $result->section[$key]->section);
$this->assertTrue($section->visible == $result->section[$key]->visible);
if ($key === 0 || $key === 3 || $key === 6) {
$this->assertEmpty($result->section[$key]->cmlist);
} else if ($key === 1) {
$this->assertEquals(2, count($result->section[$key]->cmlist));
} else if ($key === 2 || $key === 4) {
$this->assertEquals(1, count($result->section[$key]->cmlist));
} else if ($key === 5) {
$this->assertEquals(2, count($result->section[$key]->cmlist));
}
}
} else {
// Social course format doesn't have sections.
$this->assertEmpty($result->section);
}
}
foreach ($modinfo->cms as $key => $cm) {
$section = $sections[$cm->sectionnum];
$cmstate = new $cmclass($courseformat, $section, $cm);
$result->cm[$key] = $cmstate->export_for_template($renderer);
$this->assertEquals($cm->id, $result->cm[$key]->id);
$this->assertEquals($cm->name, $result->cm[$key]->name);
$this->assertTrue($cm->visible == $result->cm[$key]->visible);
}
}
/**
* Data provider for test_state().
*
* @return array
*/
public function state_provider(): array {
return [
// COURSEFORMAT. Test behaviour depending on course formats.
'Single activity format' => [
'format' => 'singleactivity',
],
'Social format' => [
'format' => 'social',
],
'Weeks format' => [
'format' => 'weeks',
],
'The unit tests format' => [
'format' => 'theunittest',
],
];
}
}
@@ -0,0 +1,69 @@
<?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_courseformat\privacy;
use context_course;
use core_privacy\local\request\writer;
/**
* Privacy tests for core_courseformat.
*
* @package core_courseformat
* @category test
* @copyright 2021 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_test extends \core_privacy\tests\provider_testcase {
/**
* Test for provider::test_export_user_preferences().
*/
public function test_export_user_preferences(): void {
$this->resetAfterTest();
// Test setup.
$generator = $this->getDataGenerator();
$course = $generator->create_course();
course_create_sections_if_missing($course, [0, 1, 2]);
$user = $generator->create_and_enrol($course, 'student');
$prefix = provider::SECTION_PREFERENCES_PREFIX;
$preference = "{$prefix}_{$course->id}";
$value = "Something";
$preferencestring = get_string("preference:$prefix", 'courseformat', $course->fullname);
// Add a user home page preference for the User.
set_user_preference($preference , $value, $user);
// Test the user preferences export contains 1 user preference record for the User.
provider::export_user_preferences($user->id);
$coursecontext = context_course::instance($course->id);
$writer = writer::with_context($coursecontext);
$this->assertTrue($writer->has_any_data());
$exportedpreferences = $writer->get_user_preferences('core_courseformat');
$this->assertCount(1, (array) $exportedpreferences);
$this->assertEquals(
$value,
$exportedpreferences->$preference->value
);
$this->assertEquals(
$preferencestring,
$exportedpreferences->{$preference}->description
);
}
}
@@ -0,0 +1,144 @@
<?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_courseformat;
use test_component\courseformat\sectiondelegate as testsectiondelegate;
/**
* Section delegate tests.
*
* @package core_courseformat
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \core_courseformat\sectiondelegate
* @coversDefaultClass \core_courseformat\sectiondelegate
*/
class sectiondelegate_test extends \advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setUpBeforeClass(): void {
global $CFG;
require_once($CFG->libdir . '/tests/fixtures/sectiondelegatetest.php');
}
/**
* Test that the instance method returns the correct class.
* @covers ::instance
*/
public function test_instance(): void {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 3]);
// Section 2 has an existing delegate class.
course_update_section(
$course,
$DB->get_record('course_sections', ['course' => $course->id, 'section' => 2]),
[
'component' => 'test_component',
'itemid' => 1,
]
);
// Section 3 has a missing delegate class.
course_update_section(
$course,
$DB->get_record('course_sections', ['course' => $course->id, 'section' => 3]),
[
'component' => 'missing_component',
'itemid' => 1,
]
);
$modinfo = get_fast_modinfo($course->id);
$sectioninfos = $modinfo->get_section_info_all();
$this->assertNull(sectiondelegate::instance($sectioninfos[1]));
$this->assertInstanceOf('\test_component\courseformat\sectiondelegate', sectiondelegate::instance($sectioninfos[2]));
$this->assertNull(sectiondelegate::instance($sectioninfos[3]));
}
/**
* Test has_delegate_class().
*
* @covers ::has_delegate_class
*/
public function test_has_delegate_class(): void {
$this->assertFalse(sectiondelegate::has_delegate_class('missing_component'));
$this->assertFalse(sectiondelegate::has_delegate_class('mod_label'));
$this->assertTrue(sectiondelegate::has_delegate_class('test_component'));
}
/**
* Test get_section_action_menu().
*
* @covers ::get_section_action_menu
*/
public function test_get_section_action_menu(): void {
global $DB, $PAGE;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 1]);
$sectioninfo = formatactions::section($course)->create_delegated('test_component', 1);
/** @var testsectiondelegate */
$delegated = $sectioninfo->get_component_instance();
$format = course_get_format($course);
$outputclass = $format->get_output_classname('content\\section\\controlmenu');
/** @var \core_courseformat\output\local\content\section\controlmenu */
$controlmenu = new $outputclass($format, $sectioninfo);
$renderer = $PAGE->get_renderer('format_' . $course->format);
$sectionmenu = $controlmenu->get_action_menu($renderer);
// When the delegate class returns the same action menu, calculated from the given $controlmenu.
$result = $delegated->get_section_action_menu($format, $controlmenu, $renderer);
// The $result and $sectionmenu are the same but can't be compared directly because they have different ids.
$this->assertEquals(
count($result->get_primary_actions()),
count($sectionmenu->get_primary_actions()),
);
$this->assertEquals(
count($result->get_secondary_actions()),
count($sectionmenu->get_secondary_actions())
);
$this->assertEquals(
$result->get_secondary_actions()[0]->url,
$sectionmenu->get_secondary_actions()[0]->url
);
// When the delegated class returns an empty action menu.
$delegated->set_section_action_menu(testsectiondelegate::MENUEMPTY);
$result = $delegated->get_section_action_menu($format, $controlmenu, $renderer);
// The $result and $sectionmenu are different.
$this->assertNotEquals(
count($result->get_secondary_actions()),
count($sectionmenu->get_secondary_actions())
);
// When the delegated class return a null action menu.
$delegated->set_section_action_menu(null);
$result = $delegated->get_section_action_menu($format, $controlmenu, $renderer);
$this->assertNull($result);
}
}
File diff suppressed because it is too large Load Diff
+425
View File
@@ -0,0 +1,425 @@
<?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_courseformat;
use stdClass;
/**
* Tests for the stateupdates class.
*
* @package core_courseformat
* @category test
* @copyright 2021 Sara Arjona (sara@moodle.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \core_courseformat\stateupdates
*/
class stateupdates_test extends \advanced_testcase {
/**
* Test for add_course_put.
*
* @dataProvider add_course_put_provider
* @covers ::add_course_put
*
* @param string $role the user role in the course
*/
public function test_add_course_put(string $role): void {
global $PAGE;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(['format' => 'topics']);
// Create and enrol user using given role.
if ($role == 'admin') {
$this->setAdminUser();
} else {
$user = $this->getDataGenerator()->create_user();
if ($role != 'unenroled') {
$this->getDataGenerator()->enrol_user($user->id, $course->id, $role);
}
$this->setUser($user);
}
// Initialise stateupdates.
$format = course_get_format($course);
$updates = new stateupdates($format);
// Get the expected export.
$renderer = $format->get_renderer($PAGE);
$stateclass = $format->get_output_classname("state\\course");
$currentstate = new $stateclass($format);
$expected = $currentstate->export_for_template($renderer);
$updates->add_course_put();
$updatelist = $updates->jsonSerialize();
$this->assertCount(1, $updatelist);
$update = array_pop($updatelist);
$this->assertEquals('put', $update->action);
$this->assertEquals('course', $update->name);
$this->assertEquals($expected, $update->fields);
}
/**
* Data provider for test_add_course_put.
*
* @return array testing scenarios
*/
public function add_course_put_provider() {
return [
'Admin role' => [
'admin',
],
'Teacher role' => [
'editingteacher',
],
'Student role' => [
'student',
],
];
}
/**
* Helper methods to find a specific update in the updadelist.
*
* @param array $updatelist the update list
* @param string $action the action to find
* @param string $name the element name to find
* @param int $identifier the element id value
* @return stdClass|null the object found, if any.
*/
private function find_update(
array $updatelist,
string $action,
string $name,
int $identifier
): ?stdClass {
foreach ($updatelist as $update) {
if ($update->action != $action || $update->name != $name) {
continue;
}
if (!isset($update->fields->id)) {
continue;
}
if ($update->fields->id == $identifier) {
return $update;
}
}
return null;
}
/**
* Add track about a section state update.
*
* @dataProvider add_section_provider
* @covers ::add_section_create
* @covers ::add_section_remove
* @covers ::add_section_put
*
* @param string $action the action name
* @param string $role the user role name
* @param array $expected the expected results
*/
public function test_add_section(string $action, string $role, array $expected): void {
global $PAGE, $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(['numsections' => 2, 'format' => 'topics']);
// Set section 2 hidden.
set_section_visible($course->id, 2, 0);
// Create and enrol user using given role.
if ($role == 'admin') {
$this->setAdminUser();
} else {
$user = $this->getDataGenerator()->create_user();
if ($role != 'unenroled') {
$this->getDataGenerator()->enrol_user($user->id, $course->id, $role);
}
$this->setUser($user);
}
// Initialise stateupdates.
$format = course_get_format($course);
$updates = new stateupdates($format);
$modinfo = $format->get_modinfo();
// Get the expected export.
$renderer = $format->get_renderer($PAGE);
$stateclass = $format->get_output_classname("state\\section");
// Execute method for both sections.
$method = "add_section_{$action}";
$sections = $modinfo->get_section_info_all();
foreach ($sections as $section) {
$updates->$method($section->id);
}
$updatelist = $updates->jsonSerialize();
$this->assertCount(count($expected), $updatelist);
foreach ($expected as $sectionnum) {
$section = $sections[$sectionnum];
$currentstate = new $stateclass($format, $section);
$expected = $currentstate->export_for_template($renderer);
$update = $this->find_update($updatelist, $action, 'section', $section->id);
$this->assertEquals($action, $update->action);
$this->assertEquals('section', $update->name);
// Delete does not provide all fields.
if ($action == 'remove') {
$this->assertEquals($section->id, $update->fields->id);
} else {
$this->assertEquals($expected, $update->fields);
}
}
}
/**
* Data provider for test_add_section.
*
* @return array testing scenarios
*/
public function add_section_provider(): array {
return array_merge(
$this->add_section_provider_helper('put'),
$this->add_section_provider_helper('create'),
$this->add_section_provider_helper('remove'),
);
}
/**
* Helper for add_section_provider scenarios.
*
* @param string $action the action to perform
* @return array testing scenarios
*/
private function add_section_provider_helper(string $action): array {
// Delete does not depends on user permissions.
if ($action == 'remove') {
$studentsections = [0, 1, 2];
} else {
$studentsections = [0, 1];
}
return [
"$action admin role" => [
'action' => $action,
'role' => 'admin',
'expected' => [0, 1, 2],
],
"$action teacher role" => [
'action' => $action,
'role' => 'editingteacher',
'expected' => [0, 1, 2],
],
"$action student role" => [
'action' => $action,
'role' => 'student',
'expected' => $studentsections,
],
];
}
/**
* Add track about a course module state update.
*
* @dataProvider add_cm_provider
* @covers ::add_cm_put
* @covers ::add_cm_create
* @covers ::add_cm_remove
*
* @param string $action the action name
* @param string $role the user role name
* @param array $expected the expected results
*/
public function test_add_cm(string $action, string $role, array $expected): void {
global $PAGE, $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course(['numsections' => 2, 'format' => 'topics']);
// Set section 2 hidden.
set_section_visible($course->id, 2, 0);
// Create 2 activities on each section.
$activities = [];
$activities[] = $this->getDataGenerator()->create_module(
'book',
['course' => $course->id],
['section' => 1, 'visible' => true]
);
$activities[] = $this->getDataGenerator()->create_module(
'book',
['course' => $course->id],
['section' => 1, 'visible' => false]
);
$activities[] = $this->getDataGenerator()->create_module(
'book',
['course' => $course->id],
['section' => 2, 'visible' => true]
);
$activities[] = $this->getDataGenerator()->create_module(
'book',
['course' => $course->id],
['section' => 2, 'visible' => false]
);
// Create and enrol user using given role.
if ($role == 'admin') {
$this->setAdminUser();
} else {
$user = $this->getDataGenerator()->create_user();
if ($role != 'unenroled') {
$this->getDataGenerator()->enrol_user($user->id, $course->id, $role);
}
$this->setUser($user);
}
// Initialise stateupdates.
$format = course_get_format($course);
$updates = new stateupdates($format);
$modinfo = $format->get_modinfo();
// Get the expected export.
$renderer = $format->get_renderer($PAGE);
$stateclass = $format->get_output_classname("state\\cm");
// Execute method for both sections.
$method = "add_cm_{$action}";
foreach ($activities as $activity) {
$updates->$method($activity->cmid);
}
$updatelist = $updates->jsonSerialize();
$this->assertCount(count($expected), $updatelist);
foreach ($expected as $cmnum) {
$activity = $activities[$cmnum];
$cm = $modinfo->get_cm($activity->cmid);
$section = $modinfo->get_section_info($cm->sectionnum);
$currentstate = new $stateclass($format, $section, $cm);
$expected = $currentstate->export_for_template($renderer);
$update = $this->find_update($updatelist, $action, 'cm', $cm->id);
$this->assertEquals($action, $update->action);
$this->assertEquals('cm', $update->name);
// Delete does not provide all fields.
if ($action == 'remove') {
$this->assertEquals($cm->id, $update->fields->id);
} else {
$this->assertEquals($expected, $update->fields);
}
}
}
/**
* Data provider for test_add_cm.
*
* @return array testing scenarios
*/
public function add_cm_provider(): array {
return array_merge(
$this->add_cm_provider_helper('put'),
$this->add_cm_provider_helper('create'),
$this->add_cm_provider_helper('remove'),
);
}
/**
* Helper for add_cm_provider scenarios.
*
* @param string $action the action to perform
* @return array testing scenarios
*/
private function add_cm_provider_helper(string $action): array {
// Delete does not depends on user permissions.
if ($action == 'remove') {
$studentcms = [0, 1, 2, 3];
} else {
$studentcms = [0];
}
return [
"$action admin role" => [
'action' => $action,
'role' => 'admin',
'expected' => [0, 1, 2, 3],
],
"$action teacher role" => [
'action' => $action,
'role' => 'editingteacher',
'expected' => [0, 1, 2, 3],
],
"$action student role" => [
'action' => $action,
'role' => 'student',
'expected' => $studentcms,
],
];
}
/**
* Test components can add data to delegated section state updates.
* @covers ::add_section_put
*/
public function test_put_section_state_extra_updates(): void {
global $DB, $CFG;
$this->resetAfterTest();
require_once($CFG->libdir . '/tests/fixtures/sectiondelegatetest.php');
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module(
'assign',
['course' => $course->id]
);
// The test component section delegate will add the activity cm info into the state.
$section = formatactions::section($course)->create_delegated('test_component', $activity->cmid);
$format = course_get_format($course);
$updates = new \core_courseformat\stateupdates($format);
$updates->add_section_put($section->id);
$data = $updates->jsonSerialize();
$this->assertCount(2, $data);
$sectiondata = $data[0];
$this->assertEquals('section', $sectiondata->name);
$this->assertEquals('put', $sectiondata->action);
$this->assertEquals($section->id, $sectiondata->fields->id);
$cmdata = $data[1];
$this->assertEquals('cm', $cmdata->name);
$this->assertEquals('put', $cmdata->action);
$this->assertEquals($activity->cmid, $cmdata->fields->id);
}
}