diff --git a/admin/roles/tests/behat/override_roles_highlighting.feature b/admin/roles/tests/behat/override_roles_highlighting.feature index f7e358ffdc7..8fa6f0fd191 100644 --- a/admin/roles/tests/behat/override_roles_highlighting.feature +++ b/admin/roles/tests/behat/override_roles_highlighting.feature @@ -9,11 +9,10 @@ Feature: Highlight non-inherited permissions | fullname | shortname | | Course fullname | C_shortname | And I log in as "admin" - And I am on site homepage @javascript Scenario: Override a permission - Given I follow "Course fullname" + Given I am on "Course fullname" course homepage And I navigate to "Users > Permissions" in current page administration And I select "Manager (0)" from the "roleid" singleselect And I click on "Prohibit" "radio" in the "View added and updated modules in recent activity block" "table_row" diff --git a/admin/settings/courses.php b/admin/settings/courses.php index 0f1d394f5dc..5f83df78721 100644 --- a/admin/settings/courses.php +++ b/admin/settings/courses.php @@ -133,7 +133,7 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) { // Completion tracking. $temp->add(new admin_setting_heading('progress', new lang_string('completion','completion'), '')); $temp->add(new admin_setting_configselect('moodlecourse/enablecompletion', new lang_string('completion', 'completion'), - new lang_string('enablecompletion_help', 'completion'), 0, array(0 => new lang_string('no'), 1 => new lang_string('yes')))); + new lang_string('enablecompletion_help', 'completion'), 1, array(0 => new lang_string('no'), 1 => new lang_string('yes')))); // Groups. $temp->add(new admin_setting_heading('groups', new lang_string('groups', 'group'), '')); diff --git a/admin/tool/availabilityconditions/tests/behat/manage_conditions.feature b/admin/tool/availabilityconditions/tests/behat/manage_conditions.feature index c0107b3e875..97b5b20b8e2 100644 --- a/admin/tool/availabilityconditions/tests/behat/manage_conditions.feature +++ b/admin/tool/availabilityconditions/tests/behat/manage_conditions.feature @@ -49,9 +49,7 @@ Feature: Manage availability conditions # OK, toggling works. Set the grade one to Hide and we'll go see if it actually worked. And I click on "Hide" "icon" in the "Restriction by grade" "table_row" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Page" to section "1" And I expand all fieldsets And I click on "Add restriction..." "button" diff --git a/admin/tool/behat/tests/behat/data_generators.feature b/admin/tool/behat/tests/behat/data_generators.feature index 5560c833be8..d8a325b2714 100644 --- a/admin/tool/behat/tests/behat/data_generators.feature +++ b/admin/tool/behat/tests/behat/data_generators.feature @@ -57,8 +57,7 @@ Feature: Set up contextual data for tests | Grouping 1 | C1 | GG1 | | Grouping 2 | C1 | GG2 | When I log in as "admin" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Users > Groups" in current page administration Then I should see "Group 1" And I should see "Group 2" @@ -87,8 +86,7 @@ Feature: Set up contextual data for tests | mod/forum:editanypost | Allow | student | Course | C1 | | mod/forum:replynews | Prevent | editingteacher | Course | C1 | When I log in as "admin" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Users > Permissions" in current page administration And I set the field "Advanced role override" to "Student (1)" Then "mod/forum:editanypost" capability has "Allow" permission @@ -108,7 +106,7 @@ Feature: Set up contextual data for tests | user | course | role | | student1 | C1 | student | When I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage Then I should see "Topic 1" Scenario: Add role assigns @@ -144,24 +142,20 @@ Feature: Set up contextual data for tests Then "Edit settings" "link" should exist in current page administration And I log out And I log in as "user2" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And "Turn editing on" "link" should exist in current page administration And I log out And I log in as "user3" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And "Turn editing on" "link" should exist in current page administration And I log out And I log in as "user4" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And "Turn editing on" "link" should exist in current page administration And I log out And I log in as "user5" And I should see "You are logged in as" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "You can not enrol yourself in this course." Scenario: Add modules @@ -199,8 +193,7 @@ Feature: Set up contextual data for tests | activity | name | intro | course | idnumber | grade | | assign | Test assignment name with scale | Test assignment description | C1 | assign1 | Test Scale 1 | When I log in as "admin" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage Then I should see "Test assignment name" # Assignment 2.2 module type is disabled by default # And I should see "Test assignment22 name" @@ -261,8 +254,7 @@ Feature: Set up contextual data for tests | grouping | group | | GG1 | G1 | When I log in as "admin" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Users > Groups" in current page administration Then the "groups" select box should contain "Group 1 (1)" And the "groups" select box should contain "Group 2 (1)" @@ -321,7 +313,7 @@ Feature: Set up contextual data for tests | Grade sub category 2 | C1 | Grade category 1 | When I log in as "admin" And I am on course index - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook Then I should see "Grade category 1" And I should see "Grade sub category 2" @@ -344,8 +336,7 @@ Feature: Set up contextual data for tests | Test Grade Item 2 | C1 | Grade category 1 | | Test Grade Item 3 | C1 | Grade sub category 2 | When I log in as "admin" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Setup > Gradebook setup" in the course gradebook Then I should see "Test Grade Item 1" And I follow "Edit Test Grade Item 1" @@ -373,8 +364,7 @@ Feature: Set up contextual data for tests | name | scale | | Test Scale 1 | Disappointing, Good, Very good, Excellent | When I log in as "admin" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Scales" in the course gradebook Then I should see "Test Scale 1" And I should see "Disappointing, Good, Very good, Excellent" @@ -395,8 +385,7 @@ Feature: Set up contextual data for tests And the following config values are set as admin: | enableoutcomes | 1 | When I log in as "admin" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Outcomes" Then I should see "Grade outcome 1" in the "#addoutcomes" "css_element" And I should see "Grade outcome 2" in the "#removeoutcomes" "css_element" @@ -424,8 +413,7 @@ Feature: Set up contextual data for tests And the following config values are set as admin: | enableoutcomes | 1 | When I log in as "admin" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Setup > Gradebook setup" in the course gradebook Then I should see "Test Outcome Grade Item 1" And I follow "Edit Test Outcome Grade Item 1" @@ -442,6 +430,5 @@ Feature: Set up contextual data for tests | blockname | contextlevel | reference | pagetypepattern | defaultregion | | online_users | Course | C1 | course-view-* | site-pre | When I log in as "admin" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage Then I should see "Online users" diff --git a/admin/tool/behat/tests/behat/datetime_strings.feature b/admin/tool/behat/tests/behat/datetime_strings.feature index 0b4e7e55744..1e15acc262f 100644 --- a/admin/tool/behat/tests/behat/datetime_strings.feature +++ b/admin/tool/behat/tests/behat/datetime_strings.feature @@ -19,7 +19,7 @@ Feature: Transform date time string arguments | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment name" And I should see "##yesterday##l, j F Y##" And I log out diff --git a/admin/tool/behat/tests/behat/edit_permissions.feature b/admin/tool/behat/tests/behat/edit_permissions.feature index c0e7cc045f6..5de8e0a0c86 100644 --- a/admin/tool/behat/tests/behat/edit_permissions.feature +++ b/admin/tool/behat/tests/behat/edit_permissions.feature @@ -31,7 +31,7 @@ Feature: Edit capabilities Scenario: Course capabilities overrides Given I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Users > Permissions" in current page administration And I override the system permissions of "Student" role with: | mod/forum:deleteanypost | Prohibit | @@ -45,8 +45,7 @@ Feature: Edit capabilities Scenario: Module capabilities overrides Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Forum" to section "1" and I fill the form with: | Forum name | I'm the name | | Description | I'm the introduction | diff --git a/admin/tool/behat/tests/behat/get_and_set_fields.feature b/admin/tool/behat/tests/behat/get_and_set_fields.feature index 4b467bc1529..151a59deb43 100644 --- a/admin/tool/behat/tests/behat/get_and_set_fields.feature +++ b/admin/tool/behat/tests/behat/get_and_set_fields.feature @@ -33,8 +33,7 @@ Feature: Verify that all form fields values can be get and set | activity | course | idnumber | name | intro | firstpagetitle | wikimode | visible | | wiki | C1 | wiki1 | Test this one | Test this one | Test this one | collaborative | 0 | And I log in as "admin" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Reset" node in "Course administration" # Select (multi-select) - Checking "the select box should contain". And I expand all fieldsets @@ -48,9 +47,7 @@ Feature: Verify that all form fields values can be get and set And the "Unenrol users" select box should not contain "President" And the "Unenrol users" select box should not contain "Baker" And the "Unenrol users" select box should not contain "President, Baker" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I follow "Test this one" And I press "Create page" # Text (textarea & editor) & Select (multi-select) - Checking "I set the following fields to these values". @@ -100,7 +97,7 @@ Feature: Verify that all form fields values can be get and set | Default format | HTML | | Force format | 1 | And I press "Cancel" - And I follow "Course 1" + And I am on "Course 1" course homepage # Radio - Checking "I set the field" and "the field matches value". And I add a "Choice" to section "1" and I fill the form with: | Choice name | Test choice name | @@ -115,8 +112,7 @@ Feature: Verify that all form fields values can be get and set And the field "one" matches value "1" And the field "two" matches value "" # Check if field xpath set/match works. - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Edit settings" node in "Course administration" And I set the field with xpath "//input[@id='id_idnumber']" to "Course id number" And the field with xpath "//input[@name='idnumber']" matches value "Course id number" @@ -129,7 +125,7 @@ Feature: Verify that all form fields values can be get and set @javascript Scenario: with JS enabled all form fields getters and setters works as expected - Then I follow "Course 1" + Then I am on "Course 1" course homepage And I navigate to "Users > Groups" in current page administration # Select (multi-select & AJAX) - Checking "I set the field" and "select box should contain". And I set the field "groups" to "Group 2" @@ -141,7 +137,7 @@ Feature: Verify that all form fields values can be get and set And the "members" select box should contain "Student 2" And the "members" select box should not contain "Student 3" # Checkbox (AJAX) - Checking "I set the field" and "I set the following fields to these values". - And I follow "Course 1" + And I am on "Course 1" course homepage And I add a "Lesson" to section "1" And I set the following fields to these values: | Name | Test lesson | diff --git a/admin/tool/behat/tests/behat/manipulate_forms.feature b/admin/tool/behat/tests/behat/manipulate_forms.feature index 5f5efdf6f01..85f52de84de 100644 --- a/admin/tool/behat/tests/behat/manipulate_forms.feature +++ b/admin/tool/behat/tests/behat/manipulate_forms.feature @@ -23,9 +23,7 @@ Feature: Forms manipulation | fullname | shortname | category | | Course 1 | C1 | 0 | And I log in as "admin" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Quiz" to section "1" When I expand all fieldsets Then I should see "Close the quiz" diff --git a/admin/tool/filetypes/tests/behat/add_filetypes.feature b/admin/tool/filetypes/tests/behat/add_filetypes.feature index 0a9f39f712e..5c464bacaa9 100644 --- a/admin/tool/filetypes/tests/behat/add_filetypes.feature +++ b/admin/tool/filetypes/tests/behat/add_filetypes.feature @@ -120,9 +120,7 @@ Feature: Add customised file types | Custom description | Froggy file | And I press "Save changes" # Create a resource activity and add it to a course - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I add a "File" to section "1" And I set the following fields to these values: | Name | An example of customised file type | diff --git a/admin/tool/monitor/tests/behat/rule.feature b/admin/tool/monitor/tests/behat/rule.feature index 0d25c54be30..a38a75d3090 100644 --- a/admin/tool/monitor/tests/behat/rule.feature +++ b/admin/tool/monitor/tests/behat/rule.feature @@ -18,7 +18,7 @@ Feature: tool_monitor_rule And I navigate to "Event monitoring rules" node in "Site administration > Reports" And I click on "Enable" "link" And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Event monitoring rules" node in "Course administration > Reports" And I press "Add a new rule" And I set the following fields to these values: @@ -45,8 +45,7 @@ Feature: tool_monitor_rule Scenario: Add a rule on course level Given I log in as "teacher1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Event monitoring rules" node in "Course administration > Reports" When I press "Add a new rule" And I set the following fields to these values: @@ -66,7 +65,7 @@ Feature: tool_monitor_rule Scenario: Delete a rule on course level Given I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Event monitoring rules" node in "Course administration > Reports" When I click on "Delete rule" "link" Then I should see "Are you sure you want to delete the rule \"New rule course level\"?" @@ -76,7 +75,7 @@ Feature: tool_monitor_rule Scenario: Edit a rule on course level Given I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Event monitoring rules" node in "Course administration > Reports" When I click on "Edit rule" "link" And I set the following fields to these values: @@ -95,7 +94,7 @@ Feature: tool_monitor_rule Scenario: Duplicate a rule on course level Given I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Event monitoring rules" node in "Course administration > Reports" When I click on "Duplicate rule" "link" in the "New rule course level" "table_row" Then I should see "Rule successfully duplicated" @@ -154,7 +153,7 @@ Feature: tool_monitor_rule Scenario: Duplicate a rule on site level Given I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Event monitoring rules" node in "Course administration > Reports" When I click on "Duplicate rule" "link" in the "New rule site level" "table_row" Then I should see "Rule successfully duplicated" diff --git a/admin/tool/monitor/tests/behat/subscription.feature b/admin/tool/monitor/tests/behat/subscription.feature index 9acc86d30c4..2766705faaa 100644 --- a/admin/tool/monitor/tests/behat/subscription.feature +++ b/admin/tool/monitor/tests/behat/subscription.feature @@ -22,8 +22,7 @@ Feature: tool_monitor_subscriptions And I log in as "admin" And I navigate to "Event monitoring rules" node in "Site administration > Reports" And I click on "Enable" "link" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Event monitoring rules" node in "Course administration > Reports" And I press "Add a new rule" And I set the following fields to these values: @@ -132,8 +131,7 @@ Feature: tool_monitor_subscriptions And I follow "Subscribe to rule \"New rule course level\"" And I should see "Subscription successfully created" And "#toolmonitorsubs_r0" "css_element" should exist - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I trigger cron And I am on site homepage When I click on ".popover-region-notifications" "css_element" diff --git a/admin/tool/recyclebin/tests/behat/backup_user_data.feature b/admin/tool/recyclebin/tests/behat/backup_user_data.feature index 31065d0ae95..7df1cb2b866 100644 --- a/admin/tool/recyclebin/tests/behat/backup_user_data.feature +++ b/admin/tool/recyclebin/tests/behat/backup_user_data.feature @@ -23,8 +23,7 @@ Feature: Backup user data @javascript Scenario: Delete and restore a quiz with user data Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Quiz" to section "1" and I fill the form with: | Name | Quiz 1 | | Description | Test quiz description | @@ -44,7 +43,7 @@ Feature: Backup user data | Feedback for the response 'False'. | So you think it is false | And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Quiz 1" And I press "Attempt quiz now" And I click on "True" "radio" in the "First question" "question" @@ -55,8 +54,7 @@ Feature: Backup user data And I should see "5.00 out of 10.00" And I log out And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I delete "Quiz 1" activity And I run all adhoc tasks And I navigate to "Recycle bin" node in "Course administration" @@ -64,7 +62,7 @@ Feature: Backup user data And I click on "Restore" "link" in the "region-main" "region" And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage When I navigate to "User report" in the course gradebook Then "Quiz 1" row "Grade" column of "user-grade" table should contain "5" And "Quiz 1" row "Percentage" column of "user-grade" table should contain "50" diff --git a/admin/tool/recyclebin/tests/behat/basic_functionality.feature b/admin/tool/recyclebin/tests/behat/basic_functionality.feature index 6b4ea5bf2e5..26a00960514 100644 --- a/admin/tool/recyclebin/tests/behat/basic_functionality.feature +++ b/admin/tool/recyclebin/tests/behat/basic_functionality.feature @@ -25,8 +25,7 @@ Feature: Basic recycle bin functionality Scenario: Restore a deleted assignment Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assign | | Description | Test | @@ -37,8 +36,7 @@ Feature: Basic recycle bin functionality And I click on "Restore" "link" in the "region-main" "region" And I should see "'Test assign' has been restored" And I wait to be redirected - And I am on homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Test assign" in the "Topic 1" "section" Scenario: Restore a deleted course @@ -64,8 +62,7 @@ Feature: Basic recycle bin functionality @javascript Scenario: Deleting a single item from the recycle bin Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assign | | Description | Test | @@ -84,8 +81,7 @@ Feature: Basic recycle bin functionality @javascript Scenario: Deleting all the items from the recycle bin Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assign 1 | | Description | Test 1 | diff --git a/admin/tool/uploaduser/tests/behat/upload_users.feature b/admin/tool/uploaduser/tests/behat/upload_users.feature index 7ee6235889f..e217cf13765 100644 --- a/admin/tool/uploaduser/tests/behat/upload_users.feature +++ b/admin/tool/uploaduser/tests/behat/upload_users.feature @@ -33,8 +33,7 @@ Feature: Upload users And I should see "Tom Jones" And I should see "Trent Reznor" And I should see "reznor@example.com" - And I am on site homepage - And I follow "Maths" + And I am on "Maths" course homepage And I navigate to "Users > Groups" in current page administration And I set the field "groups" to "Section 1 (1)" And the "members" select box should contain "Tom Jones" diff --git a/admin/tool/usertours/tests/behat/create_tour.feature b/admin/tool/usertours/tests/behat/create_tour.feature index 33c7db7cc77..6a5f152d9f6 100644 --- a/admin/tool/usertours/tests/behat/create_tour.feature +++ b/admin/tool/usertours/tests/behat/create_tour.feature @@ -20,7 +20,7 @@ Feature: Add a new user tour | Display in middle of page | Welcome | Welcome to your personal learning space. We'd like to give you a quick tour to show you some of the areas you may find helpful | And I add steps to the "First tour" tour: | targettype | targetvalue_block | Title | Content | - | Block | Course overview | Course overview | This area shows you what's happening in some of your courses | + | Block | My overview | My overview | This area shows you what's happening in some of your courses | | Block | Calendar | Calendar | This is the Calendar. All of your assignments and due dates can be found here | And I add steps to the "First tour" tour: | targettype | targetvalue_selector | Title | Content | diff --git a/admin/tool/usertours/tests/behat/tour_filter.feature b/admin/tool/usertours/tests/behat/tour_filter.feature index a0104d454be..46f226a9c06 100644 --- a/admin/tool/usertours/tests/behat/tour_filter.feature +++ b/admin/tool/usertours/tests/behat/tour_filter.feature @@ -46,17 +46,14 @@ Feature: Apply tour filters to a tour | Display in middle of page | Welcome | Welcome to your course tour.| And I log out And I log in as "editor1" - And I am on site homepage - When I follow "Course 1" + When I am on "Course 1" course homepage Then I should not see "Welcome to your course tour." And I log out And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Welcome to your course tour." And I click on "End tour" "button" And I log out And I log in as "teacher1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Welcome to your course tour." diff --git a/availability/condition/completion/tests/behat/availability_completion.feature b/availability/condition/completion/tests/behat/availability_completion.feature index 9ac9f370c7a..a57094fb26a 100644 --- a/availability/condition/completion/tests/behat/availability_completion.feature +++ b/availability/condition/completion/tests/behat/availability_completion.feature @@ -21,9 +21,7 @@ Feature: availability_completion Scenario: Test condition # Basic setup. Given I log in as "teacher1" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on # Add a Page with a completion tickbox. And I add a "Page" to section "1" and I fill the form with: @@ -48,8 +46,7 @@ Feature: availability_completion # Log back in as student. When I log out And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage # Page 2 should not appear yet. Then I should not see "Page 2" in the "region-main" "region" diff --git a/availability/condition/completion/tests/behat/conditional_bug.feature b/availability/condition/completion/tests/behat/conditional_bug.feature index b2d39004c5f..f0b80941f86 100644 --- a/availability/condition/completion/tests/behat/conditional_bug.feature +++ b/availability/condition/completion/tests/behat/conditional_bug.feature @@ -19,7 +19,7 @@ Feature: Confirm that conditions on completion no longer cause a bug Scenario: Multiple completion conditions on glossary # Set up course. Given I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Edit settings" node in "Course administration" And I expand all fieldsets And I set the field "Enable completion tracking" to "Yes" diff --git a/availability/condition/date/tests/behat/availability_date.feature b/availability/condition/date/tests/behat/availability_date.feature index 161080fe725..b4ba1475658 100644 --- a/availability/condition/date/tests/behat/availability_date.feature +++ b/availability/condition/date/tests/behat/availability_date.feature @@ -21,9 +21,7 @@ Feature: availability_date Scenario: Test condition # Basic setup. Given I log in as "teacher1" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on # Add a Page with a date condition that does match (from the past). And I add a "Page" to section "1" @@ -55,8 +53,7 @@ Feature: availability_date # Log back in as student. When I log out And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage # Page 1 should appear, but page 2 does not. Then I should see "Page 1" in the "region-main" "region" diff --git a/availability/condition/grade/tests/behat/availability_grade.feature b/availability/condition/grade/tests/behat/availability_grade.feature index 60f79208996..a9845257a9c 100644 --- a/availability/condition/grade/tests/behat/availability_grade.feature +++ b/availability/condition/grade/tests/behat/availability_grade.feature @@ -21,9 +21,7 @@ Feature: availability_grade Scenario: Test condition # Basic setup. Given I log in as "teacher1" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on # Add an assignment. And I add a "Assignment" to section "1" and I fill the form with: @@ -71,7 +69,7 @@ Feature: availability_grade And I click on "Edit settings" "link" in the "P3" activity And I expand all fieldsets And the field "Maximum grade percentage (exclusive)" matches value "" - And I follow "Course 1" + And I am on "Course 1" course homepage # Add a Page with a grade condition for 10%. And I add a "Page" to section "4" @@ -91,8 +89,7 @@ Feature: availability_grade # Log in as student without a grade yet. When I log out And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage # Do the assignment. And I follow "A1" @@ -110,8 +107,7 @@ Feature: availability_grade # Log back in as teacher. When I log out And I log in as "teacher1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage # Give the assignment 40%. And I follow "A1" @@ -126,8 +122,7 @@ Feature: availability_grade # Log back in as student. And I log out And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage # Check pages are visible. Then I should see "P2" in the "region-main" "region" diff --git a/availability/condition/group/tests/behat/availability_group.feature b/availability/condition/group/tests/behat/availability_group.feature index 31d65d21026..ceae232b520 100644 --- a/availability/condition/group/tests/behat/availability_group.feature +++ b/availability/condition/group/tests/behat/availability_group.feature @@ -21,9 +21,7 @@ Feature: availability_group Scenario: Test condition # Basic setup. Given I log in as "teacher1" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on # Start to add a Page. If there aren't any groups, there's no Group option. And I add a "Page" to section "1" @@ -39,8 +37,7 @@ Feature: availability_group | G2 | C1 | GI2 | # This step used to be 'And I follow "C1"', but Chrome thinks the breadcrumb # is not clickable, so we'll go via the home page instead. - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I add a "Page" to section "1" And I expand all fieldsets And I click on "Add restriction..." "button" @@ -85,8 +82,7 @@ Feature: availability_group # Log back in as student. When I log out And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage # No pages should appear yet. Then I should not see "P1" in the "region-main" "region" @@ -99,8 +95,7 @@ Feature: availability_group | student1 | GI1 | And I log out And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage # P1 (any groups) and P2 should show but not P3. Then I should see "P1" in the "region-main" "region" diff --git a/availability/condition/grouping/tests/behat/availability_grouping.feature b/availability/condition/grouping/tests/behat/availability_grouping.feature index 3c1ccb1ad38..91f4d045d57 100644 --- a/availability/condition/grouping/tests/behat/availability_grouping.feature +++ b/availability/condition/grouping/tests/behat/availability_grouping.feature @@ -27,9 +27,7 @@ Feature: availability_grouping Scenario: Test condition # Basic setup. Given I log in as "teacher1" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on # Start to add a Page. If there aren't any groupings, there's no Grouping option. And I add a "Page" to section "1" @@ -41,8 +39,7 @@ Feature: availability_grouping # Back to course page but add groups. # This step used to be 'And I follow "C1"', but Chrome thinks the breadcrumb # is not clickable, so we'll go via the home page instead. - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And the following "groupings" exist: | name | course | idnumber | | GX1 | C1 | GXI1 | @@ -78,7 +75,7 @@ Feature: availability_grouping # Log back in as student. When I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage # No pages should appear yet. Then I should not see "P1" in the "region-main" "region" @@ -90,8 +87,7 @@ Feature: availability_grouping | grouping | group | | GXI1 | GI1 | And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage # P1 should show but not B2. Then I should see "P1" in the "region-main" "region" diff --git a/availability/condition/profile/tests/behat/availability_profile.feature b/availability/condition/profile/tests/behat/availability_profile.feature index a5b01b7cfa9..682f59f761f 100644 --- a/availability/condition/profile/tests/behat/availability_profile.feature +++ b/availability/condition/profile/tests/behat/availability_profile.feature @@ -21,9 +21,7 @@ Feature: availability_profile Scenario: Test condition # Basic setup. Given I log in as "teacher1" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on # Add And I add a "Page" to section "1" @@ -56,8 +54,7 @@ Feature: availability_profile # Log back in as student. When I log out And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage # I see P1 but not P2. Then I should see "P1" in the "region-main" "region" @@ -82,9 +79,7 @@ Feature: availability_profile And I click on "Update profile" "button" # Set Page activity which has requirement on this field. - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Page" to section "1" And I set the following fields to these values: | Name | P1 | @@ -109,6 +104,5 @@ Feature: availability_profile # Log out and back in as student. Should be able to see activity. And I log out And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage Then I should see "P1" in the "region-main" "region" diff --git a/availability/tests/behat/display_availability.feature b/availability/tests/behat/display_availability.feature index 0daeee26e5b..60e7150b1e2 100644 --- a/availability/tests/behat/display_availability.feature +++ b/availability/tests/behat/display_availability.feature @@ -39,9 +39,7 @@ Feature: display_availability Scenario: Activity availability display # Set up. Given I log in as "teacher1" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on # Add a Page with 1 restriction. When I add a "Page" to section "1" @@ -102,8 +100,7 @@ Feature: display_availability # Change to student view. Given I log out And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage # Page 1 display still there but should be dimmed and not a link. Then I should see "Page 1" in the "#section-1 .dimmed_text" "css_element" @@ -123,9 +120,7 @@ Feature: display_availability Scenario: Section availability display # Set up. Given I log in as "teacher1" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on # Add a restriction to section 1 (visible to students). When I edit the section "1" @@ -147,8 +142,7 @@ Feature: display_availability And I press "Save changes" # This is necessary because otherwise it fails in Chrome, see MDL-44959 - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage # Add Pages to each section. And I add a "Page" to section "1" and I fill the form with: @@ -172,8 +166,7 @@ Feature: display_availability # Change to student view. Given I log out And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage # The contents of both sections should be hidden. Then I should not see "Page 1" in the "region-main" "region" diff --git a/availability/tests/behat/edit_availability.feature b/availability/tests/behat/edit_availability.feature index 7193088a9b4..ce0392e715a 100644 --- a/availability/tests/behat/edit_availability.feature +++ b/availability/tests/behat/edit_availability.feature @@ -32,9 +32,7 @@ Feature: edit_availability Given the following config values are set as admin: | enableavailability | 0 | When I log in as "teacher1" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Page" to section "1" Then "Restrict access" "fieldset" should not exist @@ -45,8 +43,7 @@ Feature: edit_availability And the following config values are set as admin: | enableavailability | 1 | - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I add a "Page" to section "1" Then "Restrict access" "fieldset" should exist @@ -58,10 +55,7 @@ Feature: edit_availability Scenario: Edit availability using settings in activity form # Set up. Given I log in as "teacher1" - And I follow "Course 1" - - # Add a Page and check it has None in so far. - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Page" to section "1" And I expand all fieldsets Then I should see "None" in the "Restrict access" "fieldset" @@ -149,9 +143,7 @@ Feature: edit_availability Scenario: Edit availability using settings in section form # Set up. Given I log in as "teacher1" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on # Edit a section When I edit the section "1" @@ -171,9 +163,7 @@ Feature: edit_availability Given the following config values are set as admin: | enableavailability | 0 | And I log in as "admin" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Forum" to section "1" When I expand all fieldsets Then "Add group/grouping access restriction" "button" should not exist @@ -185,9 +175,7 @@ Feature: edit_availability | name | course | idnumber | | GX1 | C1 | GXI1 | And I log in as "admin" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Forum" to section "1" And I set the following fields to these values: | Forum name | MyForum | diff --git a/backup/util/ui/tests/behat/duplicate_activities.feature b/backup/util/ui/tests/behat/duplicate_activities.feature index 4c252d096a7..1207eb77b15 100644 --- a/backup/util/ui/tests/behat/duplicate_activities.feature +++ b/backup/util/ui/tests/behat/duplicate_activities.feature @@ -15,8 +15,7 @@ Feature: Duplicate activities | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Database" to section "1" and I fill the form with: | Name | Test database name | | Description | Test database description | diff --git a/backup/util/ui/tests/behat/import_course.feature b/backup/util/ui/tests/behat/import_course.feature index 111c339b60c..5adde7f3d8e 100644 --- a/backup/util/ui/tests/behat/import_course.feature +++ b/backup/util/ui/tests/behat/import_course.feature @@ -17,8 +17,7 @@ Feature: Import course's contents into another course | teacher1 | C1 | editingteacher | | teacher1 | C2 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Database" to section "1" and I fill the form with: | Name | Test database name | | Description | Test database description | diff --git a/backup/util/ui/tests/behat/import_groups.feature b/backup/util/ui/tests/behat/import_groups.feature index d5745421672..32ff5d1904e 100644 --- a/backup/util/ui/tests/behat/import_groups.feature +++ b/backup/util/ui/tests/behat/import_groups.feature @@ -25,7 +25,7 @@ Feature: Option to include groups and groupings when importing a course to anoth | Grouping 1 | C1 | GROUPING1 | | Grouping 2 | C1 | GROUPING2 | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage Scenario: Include groups and groupings when importing a course to another course Given I import "Course 1" course into "Course 2" course using this options: diff --git a/backup/util/ui/tests/behat/restore_moodle2_courses.feature b/backup/util/ui/tests/behat/restore_moodle2_courses.feature index 32c1cd7e7f1..b02e25bd314 100644 --- a/backup/util/ui/tests/behat/restore_moodle2_courses.feature +++ b/backup/util/ui/tests/behat/restore_moodle2_courses.feature @@ -15,9 +15,7 @@ Feature: Restore Moodle 2 course backups | assign | C3 | assign1 | Test assign name | Assign description | 1 | | data | C3 | data1 | Test database name | Database description | 2 | And I log in as "admin" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Forum" to section "1" and I fill the form with: | Forum name | Test forum name | | Description | Test forum description | @@ -63,7 +61,7 @@ Feature: Restore Moodle 2 course backups Scenario: Restore a backup into the same course removing it's contents before that When I backup "Course 1" course using this options: | Confirmation | Filename | test_backup.mbz | - And I follow "Course 1" + And I am on "Course 1" course homepage And I add a "Forum" to section "1" and I fill the form with: | Forum name | Test forum post backup name | | Description | Test forum post backup description | diff --git a/badges/tests/behat/award_badge.feature b/badges/tests/behat/award_badge.feature index a1d2fb4a63e..13c040a17b9 100644 --- a/badges/tests/behat/award_badge.feature +++ b/badges/tests/behat/award_badge.feature @@ -86,7 +86,7 @@ Feature: Award badges | student1 | C1 | student | | student2 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Add a new badge" node in "Course administration > Badges" And I follow "Add a new badge" And I set the following fields to these values: @@ -128,7 +128,7 @@ Feature: Award badges | teacher1 | C1 | editingteacher | | student1 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Edit settings" node in "Course administration" And I set the following fields to these values: | Enable completion tracking | Yes | @@ -137,7 +137,8 @@ Feature: Award badges And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment name | | Description | Submit your online text | - And I follow "Course 1" + | id_completion | 1 | + And I am on "Course 1" course homepage And I navigate to "Add a new badge" node in "Course administration > Badges" And I follow "Add a new badge" And I set the following fields to these values: @@ -156,9 +157,8 @@ Feature: Award badges And I follow "Profile" in the user menu And I click on "Course 1" "link" in the "region-main" "region" Then I should not see "badges" - And I am on homepage - And I follow "Course 1" - And I click on "Not completed: Test assignment name. Select to mark as complete." "icon" + And I am on "Course 1" course homepage + And I click on "Not completed: Test assignment name" "icon" And I follow "Profile" in the user menu And I click on "Course 1" "link" in the "region-main" "region" Then I should see "Course Badge" @@ -177,7 +177,7 @@ Feature: Award badges | teacher1 | C1 | editingteacher | | student1 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Edit settings" node in "Course administration" And I set the following fields to these values: | Enable completion tracking | Yes | @@ -187,12 +187,13 @@ Feature: Award badges | Assignment name | Test assignment name | | Description | Submit your online text | | assignsubmission_onlinetext_enabled | 1 | + | id_completion | 1 | And I navigate to "Course completion" node in "Course administration" And I set the field "id_overall_aggregation" to "2" And I click on "Condition: Activity completion" "link" And I set the field "Assignment - Test assignment name" to "1" And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Add a new badge" node in "Course administration > Badges" And I follow "Add a new badge" And I set the following fields to these values: @@ -211,9 +212,8 @@ Feature: Award badges And I follow "Profile" in the user menu And I click on "Course 1" "link" in the "region-main" "region" Then I should not see "badges" - And I am on homepage - And I follow "Course 1" - And I click on "Not completed: Test assignment name. Select to mark as complete." "icon" + And I am on "Course 1" course homepage + And I click on "Not completed: Test assignment name" "icon" And I log out # Completion cron won't mark the whole course completed unless the # individual criteria was marked completed more than a second ago. So @@ -242,7 +242,7 @@ Feature: Award badges | student1 | C1 | student | | student2 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage # Create course badge 1. And I navigate to "Add a new badge" node in "Course administration > Badges" And I follow "Add a new badge" @@ -325,7 +325,7 @@ Feature: Award badges | student1 | C1 | student | | student2 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Add a new badge" node in "Course administration > Badges" And I follow "Add a new badge" And I set the following fields to these values: diff --git a/blocks/activity_modules/tests/behat/block_activity_modules.feature b/blocks/activity_modules/tests/behat/block_activity_modules.feature index 462ef805ccb..2f24252aa8d 100644 --- a/blocks/activity_modules/tests/behat/block_activity_modules.feature +++ b/blocks/activity_modules/tests/behat/block_activity_modules.feature @@ -110,48 +110,47 @@ Feature: Block activity modules When I log in as "admin" And I am on course index - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Activities" block And I click on "Assignments" "link" in the "Activities" "block" Then I should see "Test assignment name" - And I follow "Course 1" + And I am on "Course 1" course homepage And I click on "Chats" "link" in the "Activities" "block" And I should see "Test chat name" - And I follow "Course 1" + And I am on "Course 1" course homepage And I click on "Choices" "link" in the "Activities" "block" And I should see "Test choice name" - And I follow "Course 1" + And I am on "Course 1" course homepage And I click on "Databases" "link" in the "Activities" "block" And I should see "Test database name" - And I follow "Course 1" + And I am on "Course 1" course homepage And I click on "Feedback" "link" in the "Activities" "block" And I should see "Test feedback name" - And I follow "Course 1" + And I am on "Course 1" course homepage And I click on "Forums" "link" in the "Activities" "block" And I should see "Test forum name" - And I follow "Course 1" + And I am on "Course 1" course homepage And I click on "External tools" "link" in the "Activities" "block" And I should see "Test lti name" - And I follow "Course 1" + And I am on "Course 1" course homepage And I click on "Quizzes" "link" in the "Activities" "block" And I should see "Test quiz name" - And I follow "Course 1" + And I am on "Course 1" course homepage And I click on "Glossaries" "link" in the "Activities" "block" And I should see "Test glossary name" - And I follow "Course 1" + And I am on "Course 1" course homepage And I click on "SCORM packages" "link" in the "Activities" "block" And I should see "Test scorm name" - And I follow "Course 1" + And I am on "Course 1" course homepage And I click on "Lessons" "link" in the "Activities" "block" And I should see "Test lesson name" - And I follow "Course 1" + And I am on "Course 1" course homepage And I click on "Wikis" "link" in the "Activities" "block" And I should see "Test wiki name" - And I follow "Course 1" + And I am on "Course 1" course homepage And I click on "Workshop" "link" in the "Activities" "block" And I should see "Test workshop name" - And I follow "Course 1" + And I am on "Course 1" course homepage And I click on "Resources" "link" in the "Activities" "block" And I should see "Test book name" And I should see "Test page name" diff --git a/blocks/activity_results/tests/behat/addblockinactivity.feature b/blocks/activity_results/tests/behat/addblockinactivity.feature index de4a14926fd..3191e13411d 100644 --- a/blocks/activity_results/tests/behat/addblockinactivity.feature +++ b/blocks/activity_results/tests/behat/addblockinactivity.feature @@ -25,30 +25,29 @@ Feature: The activity results block displays student scores | student4 | C1 | student | | student5 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment 1 | | Description | Offline text | | assignsubmission_file_enabled | 0 | - And I follow "Course 1" + And I am on "Course 1" course homepage And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment 2 | | Description | Offline text | | assignsubmission_file_enabled | 0 | - And I follow "Course 1" + And I am on "Course 1" course homepage And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment 3 | | Description | Offline text | | assignsubmission_file_enabled | 0 | - And I follow "Course 1" + And I am on "Course 1" course homepage And I add a "Page" to section "1" And I set the following fields to these values: | Name | Test page name | | Description | Test page description | | Page content | This is a page | And I press "Save and return to course" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Test page name" And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on @@ -58,7 +57,7 @@ Feature: The activity results block displays student scores And I give the grade "60.00" to the user "Student 4" for the grade item "Test assignment 1" And I give the grade "50.00" to the user "Student 5" for the grade item "Test assignment 1" And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage Scenario: Configure the block on a non-graded activity to show 3 high scores Given I follow "Test page name" @@ -84,19 +83,19 @@ Feature: The activity results block displays student scores And I configure the "Activity results" block Then the field "id_config_activitygradeitemid" matches value "Test assignment 1" And I press "Cancel" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 2" And I add the "Activity results" block And I configure the "Activity results" block And the field "id_config_activitygradeitemid" matches value "Test assignment 2" And I press "Cancel" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 3" And I add the "Activity results" block And I configure the "Activity results" block And the field "id_config_activitygradeitemid" matches value "Test assignment 3" And I press "Cancel" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test page name" And I add the "Activity results" block And I configure the "Activity results" block diff --git a/blocks/activity_results/tests/behat/addunconfiguredblock.feature b/blocks/activity_results/tests/behat/addunconfiguredblock.feature index f53d8eae84e..0570dbddfea 100644 --- a/blocks/activity_results/tests/behat/addunconfiguredblock.feature +++ b/blocks/activity_results/tests/behat/addunconfiguredblock.feature @@ -15,8 +15,7 @@ Feature: The activity results block doesn't displays student scores for unconfig | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on Scenario: Add the block to a the course Given I add the "Activity results" block diff --git a/blocks/activity_results/tests/behat/addunsupportedactivity.feature b/blocks/activity_results/tests/behat/addunsupportedactivity.feature index d3f5c991ef8..5103f23804c 100644 --- a/blocks/activity_results/tests/behat/addunsupportedactivity.feature +++ b/blocks/activity_results/tests/behat/addunsupportedactivity.feature @@ -15,8 +15,7 @@ Feature: The activity results block doesn't display student scores for unsupport | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on Scenario: Try to configure the block to use an activity without grades Given I add a "Assignment" to section "1" and I fill the form with: diff --git a/blocks/activity_results/tests/behat/defaultsettings.feature b/blocks/activity_results/tests/behat/defaultsettings.feature index ca45c84449a..3af9d0e39fc 100644 --- a/blocks/activity_results/tests/behat/defaultsettings.feature +++ b/blocks/activity_results/tests/behat/defaultsettings.feature @@ -24,13 +24,12 @@ Feature: The activity results block can have administrator set defaults | config_gradeformat | 2 | block_activity_results | | config_nameformat | 2 | block_activity_results | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment | | Description | Offline text | | assignsubmission_file_enabled | 0 | - And I follow "Course 1" + And I am on "Course 1" course homepage And I add the "Activity results" block When I configure the "Activity results" block And the following fields match these values: @@ -48,13 +47,12 @@ Feature: The activity results block can have administrator set defaults | config_showworst | 0 | block_activity_results | | config_showworst_locked | 1 | block_activity_results | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment | | Description | Offline text | | assignsubmission_file_enabled | 0 | - And I follow "Course 1" + And I am on "Course 1" course homepage And I add the "Activity results" block When I configure the "Activity results" block And the following fields match these values: diff --git a/blocks/activity_results/tests/behat/highscoreswithoutgroups.feature b/blocks/activity_results/tests/behat/highscoreswithoutgroups.feature index 620aac1cead..eb750a05aa0 100644 --- a/blocks/activity_results/tests/behat/highscoreswithoutgroups.feature +++ b/blocks/activity_results/tests/behat/highscoreswithoutgroups.feature @@ -25,13 +25,12 @@ Feature: The activity results block displays student high scores | student4 | C1 | student | | student5 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment | | Description | Offline text | | assignsubmission_file_enabled | 0 | - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I give the grade "90.00" to the user "Student 1" for the grade item "Test assignment" @@ -40,7 +39,7 @@ Feature: The activity results block displays student high scores And I give the grade "60.00" to the user "Student 4" for the grade item "Test assignment" And I give the grade "50.00" to the user "Student 5" for the grade item "Test assignment" And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage Scenario: Configure the block on the course page to show 0 high scores Given I add the "Activity results" block diff --git a/blocks/activity_results/tests/behat/highscoreswithscales.feature b/blocks/activity_results/tests/behat/highscoreswithscales.feature index d121c97b9da..2a8bcf99fd0 100644 --- a/blocks/activity_results/tests/behat/highscoreswithscales.feature +++ b/blocks/activity_results/tests/behat/highscoreswithscales.feature @@ -25,22 +25,21 @@ Feature: The activity results block displays students high scores in group as sc | student4 | C1 | student | | student5 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Scales" in the course gradebook And I press "Add a new scale" And I set the following fields to these values: | Name | My Scale | | Scale | Disappointing, Not good enough, Average, Good, Very good, Excellent! | And I press "Save changes" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment | | Description | Offline text | | assignsubmission_file_enabled | 0 | | id_grade_modgrade_type | Scale | | id_grade_modgrade_scale | My Scale | - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I give the grade "Excellent!" to the user "Student 1" for the grade item "Test assignment" @@ -49,7 +48,7 @@ Feature: The activity results block displays students high scores in group as sc And I give the grade "Average" to the user "Student 4" for the grade item "Test assignment" And I give the grade "Not good enough" to the user "Student 5" for the grade item "Test assignment" And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage Scenario: Configure the block on the course page to show 1 high score Given I add the "Activity results" block diff --git a/blocks/activity_results/tests/behat/highscoreswithscalesandgroups.feature b/blocks/activity_results/tests/behat/highscoreswithscalesandgroups.feature index 6b9c9727594..95008623b02 100644 --- a/blocks/activity_results/tests/behat/highscoreswithscalesandgroups.feature +++ b/blocks/activity_results/tests/behat/highscoreswithscalesandgroups.feature @@ -42,15 +42,14 @@ Feature: The activity results block displays student in group high scores as sca | student5 | G3 | | student6 | G3 | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Scales" in the course gradebook And I press "Add a new scale" And I set the following fields to these values: | Name | My Scale | | Scale | Disappointing, Not good enough, Average, Good, Very good, Excellent! | And I press "Save changes" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment | | Description | Offline text | @@ -58,7 +57,7 @@ Feature: The activity results block displays student in group high scores as sca | id_grade_modgrade_type | Scale | | id_grade_modgrade_scale | My Scale | | Group mode | Separate groups | - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I give the grade "Excellent!" to the user "Student 1" for the grade item "Test assignment" @@ -68,7 +67,7 @@ Feature: The activity results block displays student in group high scores as sca And I give the grade "Good" to the user "Student 5" for the grade item "Test assignment" And I give the grade "Average" to the user "Student 6" for the grade item "Test assignment" And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage Scenario: Try to configure the block on the course page to show 1 high score Given I add the "Activity results" block @@ -83,7 +82,7 @@ Feature: The activity results block displays student in group high scores as sca And I should see "Excellent!" in the "Activity results" "block" And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Student 1" in the "Activity results" "block" And I should see "Excellent!" in the "Activity results" "block" @@ -104,7 +103,7 @@ Feature: The activity results block displays student in group high scores as sca And I should see "Good" in the "Activity results" "block" And I log out And I log in as "student3" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Student 3" in the "Activity results" "block" And I should see "Very good" in the "Activity results" "block" And I should see "Student 4" in the "Activity results" "block" @@ -125,7 +124,7 @@ Feature: The activity results block displays student in group high scores as sca And I should see "Good" in the "Activity results" "block" And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "User S1" in the "Activity results" "block" And I should see "Excellent!" in the "Activity results" "block" And I should see "User S2" in the "Activity results" "block" @@ -146,7 +145,7 @@ Feature: The activity results block displays student in group high scores as sca And I should see "Good" in the "Activity results" "block" And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "User" in the "Activity results" "block" And I should see "Excellent!" in the "Activity results" "block" And I should see "Very good" in the "Activity results" "block" diff --git a/blocks/activity_results/tests/behat/highscoreswithseperategroups.feature b/blocks/activity_results/tests/behat/highscoreswithseperategroups.feature index 0c1ccdd077d..ccc223db26d 100644 --- a/blocks/activity_results/tests/behat/highscoreswithseperategroups.feature +++ b/blocks/activity_results/tests/behat/highscoreswithseperategroups.feature @@ -42,14 +42,13 @@ Feature: The activity results block displays student in separate groups scores | student5 | G3 | | student6 | G3 | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment | | Description | Offline text | | assignsubmission_file_enabled | 0 | | Group mode | Separate groups | - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I give the grade "100.00" to the user "Student 1" for the grade item "Test assignment" @@ -59,7 +58,7 @@ Feature: The activity results block displays student in separate groups scores And I give the grade "80.00" to the user "Student 5" for the grade item "Test assignment" And I give the grade "70.00" to the user "Student 6" for the grade item "Test assignment" And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage Scenario: Configure the block on the course page to show 1 high score Given I add the "Activity results" block @@ -89,7 +88,7 @@ Feature: The activity results block displays student in separate groups scores And I should see "95.00/100.00" in the "Activity results" "block" And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Student 1" in the "Activity results" "block" And I should see "100.00/100.00" in the "Activity results" "block" @@ -107,7 +106,7 @@ Feature: The activity results block displays student in separate groups scores And I should see "95.00" in the "Activity results" "block" And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Student 1" in the "Activity results" "block" And I should see "100.00" in the "Activity results" "block" @@ -130,7 +129,7 @@ Feature: The activity results block displays student in separate groups scores And I should see "75%" in the "Activity results" "block" And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Student 1" in the "Activity results" "block" And I should see "100%" in the "Activity results" "block" And I should see "Student 2" in the "Activity results" "block" @@ -154,7 +153,7 @@ Feature: The activity results block displays student in separate groups scores And I should see "75.00/100.00" in the "Activity results" "block" And I log out And I log in as "student3" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Student 3" in the "Activity results" "block" And I should see "90.00/100.00" in the "Activity results" "block" And I should see "Student 4" in the "Activity results" "block" @@ -178,7 +177,7 @@ Feature: The activity results block displays student in separate groups scores And I should see "75.00" in the "Activity results" "block" And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Student 1" in the "Activity results" "block" And I should see "100.00" in the "Activity results" "block" And I should see "Student 2" in the "Activity results" "block" @@ -200,7 +199,7 @@ Feature: The activity results block displays student in separate groups scores And I should see "75.00%" in the "Activity results" "block" And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "User S1" in the "Activity results" "block" And I should see "100.00%" in the "Activity results" "block" And I should see "User S2" in the "Activity results" "block" @@ -222,7 +221,7 @@ Feature: The activity results block displays student in separate groups scores And I should see "75.00%" in the "Activity results" "block" And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "User" in the "Activity results" "block" And I should see "100.00%" in the "Activity results" "block" And I should see "90.00%" in the "Activity results" "block" diff --git a/blocks/activity_results/tests/behat/highscoreswithvisiblegroups.feature b/blocks/activity_results/tests/behat/highscoreswithvisiblegroups.feature index 0e64d0544fe..11ee5a579a4 100644 --- a/blocks/activity_results/tests/behat/highscoreswithvisiblegroups.feature +++ b/blocks/activity_results/tests/behat/highscoreswithvisiblegroups.feature @@ -42,14 +42,13 @@ Feature: The activity results block displays student in visible groups scores | student5 | G3 | | student6 | G3 | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment | | Description | Offline text | | assignsubmission_file_enabled | 0 | | Group mode | Visible groups | - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I give the grade "100.00" to the user "Student 1" for the grade item "Test assignment" @@ -59,7 +58,7 @@ Feature: The activity results block displays student in visible groups scores And I give the grade "80.00" to the user "Student 5" for the grade item "Test assignment" And I give the grade "70.00" to the user "Student 6" for the grade item "Test assignment" And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage Scenario: Configure the block on the course page to show 1 high score Given I add the "Activity results" block @@ -87,7 +86,7 @@ Feature: The activity results block displays student in visible groups scores And I press "Save changes" And I log out Then I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Group 1" in the "Activity results" "block" And I should see "95.00/100.00" in the "Activity results" "block" @@ -103,7 +102,7 @@ Feature: The activity results block displays student in visible groups scores And I press "Save changes" And I log out Then I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Group 1" in the "Activity results" "block" And I should see "95.00" in the "Activity results" "block" @@ -120,7 +119,7 @@ Feature: The activity results block displays student in visible groups scores And I press "Save changes" And I log out Then I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Group 1" in the "Activity results" "block" And I should see "95%" in the "Activity results" "block" And I should see "Group 2" in the "Activity results" "block" @@ -140,7 +139,7 @@ Feature: The activity results block displays student in visible groups scores And I press "Save changes" And I log out Then I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Group 1" in the "Activity results" "block" And I should see "95.00/100.00" in the "Activity results" "block" And I should see "Group 2" in the "Activity results" "block" @@ -160,7 +159,7 @@ Feature: The activity results block displays student in visible groups scores And I press "Save changes" And I log out Then I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Group 1" in the "Activity results" "block" And I should see "95.00" in the "Activity results" "block" And I should see "Group 2" in the "Activity results" "block" @@ -180,7 +179,7 @@ Feature: The activity results block displays student in visible groups scores And I press "Save changes" And I log out Then I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Group" in the "Activity results" "block" And I should see "95.00%" in the "Activity results" "block" And I should see "85.00%" in the "Activity results" "block" @@ -198,7 +197,7 @@ Feature: The activity results block displays student in visible groups scores And I press "Save changes" And I log out Then I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Group" in the "Activity results" "block" And I should see "95.00%" in the "Activity results" "block" And I should see "85.00%" in the "Activity results" "block" diff --git a/blocks/activity_results/tests/behat/lowscoreswithoutgroups.feature b/blocks/activity_results/tests/behat/lowscoreswithoutgroups.feature index e2c8729b985..f632d95e1db 100644 --- a/blocks/activity_results/tests/behat/lowscoreswithoutgroups.feature +++ b/blocks/activity_results/tests/behat/lowscoreswithoutgroups.feature @@ -25,13 +25,12 @@ Feature: The activity results block displays student low scores | student4 | C1 | student | | student5 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment | | Description | Offline text | | assignsubmission_file_enabled | 0 | - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I give the grade "90.00" to the user "Student 1" for the grade item "Test assignment" @@ -40,7 +39,7 @@ Feature: The activity results block displays student low scores And I give the grade "60.00" to the user "Student 4" for the grade item "Test assignment" And I give the grade "50.00" to the user "Student 5" for the grade item "Test assignment" And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage Scenario: Configure the block on the course page to show 1 low score Given I add the "Activity results" block diff --git a/blocks/activity_results/tests/behat/lowscoreswithscales.feature b/blocks/activity_results/tests/behat/lowscoreswithscales.feature index 5068a7b3642..8885d70202d 100644 --- a/blocks/activity_results/tests/behat/lowscoreswithscales.feature +++ b/blocks/activity_results/tests/behat/lowscoreswithscales.feature @@ -25,22 +25,21 @@ Feature: The activity results block displays student low scores as scales | student4 | C1 | student | | student5 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Scales" in the course gradebook And I press "Add a new scale" And I set the following fields to these values: | Name | My Scale | | Scale | Disappointing, Not good enough, Average, Good, Very good, Excellent! | And I press "Save changes" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment | | Description | Offline text | | assignsubmission_file_enabled | 0 | | id_grade_modgrade_type | Scale | | id_grade_modgrade_scale | My Scale | - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I give the grade "Excellent!" to the user "Student 1" for the grade item "Test assignment" @@ -49,7 +48,7 @@ Feature: The activity results block displays student low scores as scales And I give the grade "Average" to the user "Student 4" for the grade item "Test assignment" And I give the grade "Not good enough" to the user "Student 5" for the grade item "Test assignment" And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage Scenario: Configure the block on the course page to show 1 low score Given I add the "Activity results" block diff --git a/blocks/activity_results/tests/behat/lowscoreswithscalesandgroups.feature b/blocks/activity_results/tests/behat/lowscoreswithscalesandgroups.feature index 30602ff0746..a8967146169 100644 --- a/blocks/activity_results/tests/behat/lowscoreswithscalesandgroups.feature +++ b/blocks/activity_results/tests/behat/lowscoreswithscalesandgroups.feature @@ -42,15 +42,14 @@ Feature: The activity results block displays students in groups low scores as sc | student5 | G3 | | student6 | G3 | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Scales" in the course gradebook And I press "Add a new scale" And I set the following fields to these values: | Name | My Scale | | Scale | Disappointing, Not good enough, Average, Good, Very good, Excellent! | And I press "Save changes" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment | | Description | Offline text | @@ -58,7 +57,7 @@ Feature: The activity results block displays students in groups low scores as sc | id_grade_modgrade_type | Scale | | id_grade_modgrade_scale | My Scale | | Group mode | Separate groups | - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I give the grade "Excellent!" to the user "Student 1" for the grade item "Test assignment" @@ -68,7 +67,7 @@ Feature: The activity results block displays students in groups low scores as sc And I give the grade "Good" to the user "Student 5" for the grade item "Test assignment" And I give the grade "Average" to the user "Student 6" for the grade item "Test assignment" And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage Scenario: Try to configure the block on the course page to show 1 low score Given I add the "Activity results" block @@ -83,7 +82,7 @@ Feature: The activity results block displays students in groups low scores as sc And I should see "Good" in the "Activity results" "block" And I log out And I log in as "student5" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Student 6" in the "Activity results" "block" And I should see "Average" in the "Activity results" "block" @@ -102,7 +101,7 @@ Feature: The activity results block displays students in groups low scores as sc And I should see "Good" in the "Activity results" "block" And I log out And I log in as "student3" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Student 3" in the "Activity results" "block" And I should see "Very good" in the "Activity results" "block" And I should see "Student 4" in the "Activity results" "block" @@ -122,7 +121,7 @@ Feature: The activity results block displays students in groups low scores as sc And I should see "Good" in the "Activity results" "block" And I log out And I log in as "student5" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "User S5" in the "Activity results" "block" And I should see "Good" in the "Activity results" "block" And I should see "User S6" in the "Activity results" "block" @@ -142,7 +141,7 @@ Feature: The activity results block displays students in groups low scores as sc And I should see "Good" in the "Activity results" "block" And I log out And I log in as "student5" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "User" in the "Activity results" "block" And I should see "Good" in the "Activity results" "block" And I should see "Average" in the "Activity results" "block" diff --git a/blocks/activity_results/tests/behat/lowscoreswithseperategroups.feature b/blocks/activity_results/tests/behat/lowscoreswithseperategroups.feature index 5d73692b9da..264eb8fa3f0 100644 --- a/blocks/activity_results/tests/behat/lowscoreswithseperategroups.feature +++ b/blocks/activity_results/tests/behat/lowscoreswithseperategroups.feature @@ -42,14 +42,13 @@ Feature: The activity results block displays students in separate groups scores | student5 | G3 | | student6 | G3 | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment | | Description | Offline text | | assignsubmission_file_enabled | 0 | | Group mode | Separate groups | - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I give the grade "100.00" to the user "Student 1" for the grade item "Test assignment" @@ -59,7 +58,7 @@ Feature: The activity results block displays students in separate groups scores And I give the grade "80.00" to the user "Student 5" for the grade item "Test assignment" And I give the grade "70.00" to the user "Student 6" for the grade item "Test assignment" And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage Scenario: Configure the block on the course page to show 1 low score Given I add the "Activity results" block @@ -89,7 +88,7 @@ Feature: The activity results block displays students in separate groups scores And I should see "75.00/100.00" in the "Activity results" "block" And I log out And I log in as "student5" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Student 6" in the "Activity results" "block" And I should see "70.00/100.00" in the "Activity results" "block" @@ -107,7 +106,7 @@ Feature: The activity results block displays students in separate groups scores And I should see "75.00" in the "Activity results" "block" And I log out And I log in as "student5" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Student 6" in the "Activity results" "block" And I should see "70.00" in the "Activity results" "block" @@ -128,7 +127,7 @@ Feature: The activity results block displays students in separate groups scores And I should see "75%" in the "Activity results" "block" And I log out And I log in as "student5" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Student 6" in the "Activity results" "block" And I should see "70%" in the "Activity results" "block" And I should see "Student 5" in the "Activity results" "block" @@ -150,7 +149,7 @@ Feature: The activity results block displays students in separate groups scores And I should see "75.00/100.00" in the "Activity results" "block" And I log out And I log in as "student3" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Student 3" in the "Activity results" "block" And I should see "90.00/100.00" in the "Activity results" "block" And I should see "Student 4" in the "Activity results" "block" @@ -172,7 +171,7 @@ Feature: The activity results block displays students in separate groups scores And I should see "75.00" in the "Activity results" "block" And I log out And I log in as "student5" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Student 5" in the "Activity results" "block" And I should see "80.00" in the "Activity results" "block" And I should see "Student 6" in the "Activity results" "block" @@ -193,7 +192,7 @@ Feature: The activity results block displays students in separate groups scores And I should see "75.00%" in the "Activity results" "block" And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "User S1" in the "Activity results" "block" And I should see "100.00%" in the "Activity results" "block" And I should see "User S2" in the "Activity results" "block" @@ -214,7 +213,7 @@ Feature: The activity results block displays students in separate groups scores And I should see "75.00%" in the "Activity results" "block" And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "User" in the "Activity results" "block" And I should see "100.00%" in the "Activity results" "block" And I should see "90.00%" in the "Activity results" "block" diff --git a/blocks/activity_results/tests/behat/lowscoreswithvisiblegroups.feature b/blocks/activity_results/tests/behat/lowscoreswithvisiblegroups.feature index e137f929104..46525007998 100644 --- a/blocks/activity_results/tests/behat/lowscoreswithvisiblegroups.feature +++ b/blocks/activity_results/tests/behat/lowscoreswithvisiblegroups.feature @@ -42,14 +42,13 @@ Feature: The activity results block displays student in visible groups low score | student5 | G3 | | student6 | G3 | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment | | Description | Offline text | | assignsubmission_file_enabled | 0 | | Group mode | Visible groups | - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I give the grade "100.00" to the user "Student 1" for the grade item "Test assignment" @@ -59,7 +58,7 @@ Feature: The activity results block displays student in visible groups low score And I give the grade "80.00" to the user "Student 5" for the grade item "Test assignment" And I give the grade "70.00" to the user "Student 6" for the grade item "Test assignment" And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage Scenario: Configure the block on the course page to show 1 low score Given I add the "Activity results" block @@ -87,7 +86,7 @@ Feature: The activity results block displays student in visible groups low score And I press "Save changes" And I log out Then I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Group 3" in the "Activity results" "block" And I should see "75.00/100.00" in the "Activity results" "block" @@ -103,7 +102,7 @@ Feature: The activity results block displays student in visible groups low score And I press "Save changes" And I log out Then I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Group 3" in the "Activity results" "block" And I should see "75.00" in the "Activity results" "block" @@ -124,7 +123,7 @@ Feature: The activity results block displays student in visible groups low score And I should see "75%" in the "Activity results" "block" And I log out And I log in as "student5" - And I follow "Course 1" + And I am on "Course 1" course homepage Then I should see "Group 2" in the "Activity results" "block" And I should see "85%" in the "Activity results" "block" And I should see "Group 3" in the "Activity results" "block" @@ -142,7 +141,7 @@ Feature: The activity results block displays student in visible groups low score And I press "Save changes" And I log out Then I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Group 2" in the "Activity results" "block" And I should see "85.00/100.00" in the "Activity results" "block" And I should see "Group 3" in the "Activity results" "block" @@ -160,7 +159,7 @@ Feature: The activity results block displays student in visible groups low score And I press "Save changes" And I log out Then I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Group 2" in the "Activity results" "block" And I should see "85.00" in the "Activity results" "block" And I should see "Group 3" in the "Activity results" "block" @@ -178,7 +177,7 @@ Feature: The activity results block displays student in visible groups low score And I press "Save changes" And I log out Then I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Group" in the "Activity results" "block" And I should see "85.00%" in the "Activity results" "block" And I should see "75.00%" in the "Activity results" "block" @@ -195,7 +194,7 @@ Feature: The activity results block displays student in visible groups low score And I press "Save changes" And I log out Then I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Group" in the "Activity results" "block" And I should see "85.00%" in the "Activity results" "block" And I should see "75.00%" in the "Activity results" "block" diff --git a/blocks/badges/tests/behat/block_badges.feature b/blocks/badges/tests/behat/block_badges.feature index e9da78ac5ae..6586d5fa62d 100644 --- a/blocks/badges/tests/behat/block_badges.feature +++ b/blocks/badges/tests/behat/block_badges.feature @@ -21,14 +21,12 @@ Feature: Enable Block Badges in a course without badges | enablebadges | 0 | And I log out And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I add the "Latest badges" block Then I should see "Badges are not enabled on this site." in the "Latest badges" "block" Scenario: Add the block to a the course when badges are enabled Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I add the "Latest badges" block Then I should see "You have no badges to display" in the "Latest badges" "block" diff --git a/blocks/badges/tests/behat/block_badges_course.feature b/blocks/badges/tests/behat/block_badges_course.feature index 3f16fa421e0..33fe576425c 100644 --- a/blocks/badges/tests/behat/block_badges_course.feature +++ b/blocks/badges/tests/behat/block_badges_course.feature @@ -15,7 +15,7 @@ Feature: Enable Block Badges in a course | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage # Issue badge 1 of 2 And I navigate to "Add a new badge" node in "Course administration > Badges" And I set the following fields to these values: @@ -54,16 +54,14 @@ Feature: Enable Block Badges in a course Scenario: Add the recent badges block to a course. Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I add the "Latest badges" block Then I should see "Badge 1" in the "Latest badges" "block" And I should see "Badge 2" in the "Latest badges" "block" Scenario: Add the recent badges block to a course and limit it to only display 1 badge. Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I add the "Latest badges" block And I configure the "Latest badges" block And I set the following fields to these values: diff --git a/blocks/badges/tests/behat/block_badges_dashboard.feature b/blocks/badges/tests/behat/block_badges_dashboard.feature index 3f87995725e..2928bf3508f 100644 --- a/blocks/badges/tests/behat/block_badges_dashboard.feature +++ b/blocks/badges/tests/behat/block_badges_dashboard.feature @@ -15,7 +15,7 @@ Feature: Enable Block Badges on the dashboard and view awarded badges | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage # Issue badge 1 of 2 And I navigate to "Add a new badge" node in "Course administration > Badges" And I set the following fields to these values: diff --git a/blocks/badges/tests/behat/block_badges_frontpage.feature b/blocks/badges/tests/behat/block_badges_frontpage.feature index c6b0425861e..d1daf80c63b 100644 --- a/blocks/badges/tests/behat/block_badges_frontpage.feature +++ b/blocks/badges/tests/behat/block_badges_frontpage.feature @@ -20,7 +20,7 @@ Feature: Enable Block Badges on the frontpage and view awarded badges And I add the "Latest badges" block And I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage # Issue badge 1 of 2 And I navigate to "Add a new badge" node in "Course administration > Badges" And I set the following fields to these values: diff --git a/blocks/blog_menu/tests/behat/block_blog_menu.feature b/blocks/blog_menu/tests/behat/block_blog_menu.feature index b93728f81be..419406b29fc 100644 --- a/blocks/blog_menu/tests/behat/block_blog_menu.feature +++ b/blocks/blog_menu/tests/behat/block_blog_menu.feature @@ -21,8 +21,7 @@ Feature: Enable Block blog menu in a course | enableblogs | 0 | And I log out And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I add the "Blog menu" block Then I should see "Blogging is disabled!" in the "Blog menu" "block" @@ -32,8 +31,7 @@ Feature: Enable Block blog menu in a course | useblogassociations | 0 | And I log out And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I add the "Blog menu" block Then I should see "Blog entries" in the "Blog menu" "block" And I should see "Add a new entry" in the "Blog menu" "block" @@ -47,8 +45,7 @@ Feature: Enable Block blog menu in a course | useblogassociations | 1 | And I log out And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I add the "Blog menu" block Then I should see "Blog entries" in the "Blog menu" "block" And I should see "Add a new entry" in the "Blog menu" "block" @@ -62,8 +59,7 @@ Feature: Enable Block blog menu in a course | enablerssfeeds | 0 | And I log out And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I add the "Blog menu" block Then I should not see "Blog RSS feed" in the "Blog menu" "block" And I should see "Add a new entry" in the "Blog menu" "block" @@ -74,8 +70,7 @@ Feature: Enable Block blog menu in a course | enablerssfeeds | 1 | And I log out And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I add the "Blog menu" block Then I should see "Blog RSS feed" in the "Blog menu" "block" And I should see "Add a new entry" in the "Blog menu" "block" diff --git a/blocks/blog_menu/tests/behat/block_blog_menu_activity.feature b/blocks/blog_menu/tests/behat/block_blog_menu_activity.feature index 6b8001f6c09..9a342d231c4 100644 --- a/blocks/blog_menu/tests/behat/block_blog_menu_activity.feature +++ b/blocks/blog_menu/tests/behat/block_blog_menu_activity.feature @@ -19,8 +19,7 @@ Feature: Enable Block blog menu in an activity | student1 | C1 | student | | student2 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment 1 | | Description | Offline text | @@ -31,7 +30,7 @@ Feature: Enable Block blog menu in an activity Scenario: Students use the blog menu block to post blogs Given I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1" And I follow "Add a new entry" When I set the following fields to these values: @@ -40,8 +39,7 @@ Feature: Enable Block blog menu in an activity And I press "Save changes" Then I should see "S1 First Blog" And I should see "This is my awesome blog!" - And I follow "Dashboard" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1" And I follow "Blog entries" And I should see "S1 First Blog" @@ -49,7 +47,7 @@ Feature: Enable Block blog menu in an activity Scenario: Students use the blog menu block to view their blogs about the activity Given I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1" And I follow "Add an entry about this Assignment" And I set the following fields to these values: @@ -61,7 +59,7 @@ Feature: Enable Block blog menu in an activity And I should see "Associated Assignment: Test assignment 1" And I log out And I log in as "student2" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1" And I follow "Add a new entry" And I set the following fields to these values: @@ -71,8 +69,7 @@ Feature: Enable Block blog menu in an activity And I should see "S2 Second Blog" And I should see "My unrelated blog!" And I should not see "Associated Assignment: Test assignment 1" - And I follow "Dashboard" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1" And I follow "Add an entry about this Assignment" And I set the following fields to these values: @@ -82,8 +79,7 @@ Feature: Enable Block blog menu in an activity And I should see "S2 First Blog" And I should see "My course blog is better!" And I should see "Associated Assignment: Test assignment 1" - And I follow "Dashboard" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1" When I follow "View my entries about this Assignment" Then I should see "S2 First Blog" @@ -92,7 +88,7 @@ Feature: Enable Block blog menu in an activity Scenario: Students use the blog menu block to view all blogs about the assignment Given I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1" And I follow "Add an entry about this Assignment" And I set the following fields to these values: @@ -104,7 +100,7 @@ Feature: Enable Block blog menu in an activity And I should see "Associated Assignment: Test assignment 1" And I log out And I log in as "student2" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1" And I follow "Add a new entry" And I set the following fields to these values: @@ -114,8 +110,7 @@ Feature: Enable Block blog menu in an activity And I should see "S2 Second Blog" And I should see "My unrelated blog!" And I should not see "Associated Assignment: Test assignment 1" - And I follow "Dashboard" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1" And I follow "Add an entry about this Assignment" And I set the following fields to these values: @@ -125,8 +120,7 @@ Feature: Enable Block blog menu in an activity And I should see "S2 First Blog" And I should see "My course blog is better!" And I should see "Associated Assignment: Test assignment 1" - And I follow "Dashboard" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1" When I follow "View all entries about this Assignment" Then I should see "S1 First Blog" @@ -135,7 +129,7 @@ Feature: Enable Block blog menu in an activity Scenario: Students use the blog menu block to view all their blog entries Given I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1" And I follow "Add an entry about this Assignment" And I set the following fields to these values: @@ -147,7 +141,7 @@ Feature: Enable Block blog menu in an activity And I should see "Associated Assignment: Test assignment 1" And I log out And I log in as "student2" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1" And I follow "Add a new entry" And I set the following fields to these values: @@ -157,8 +151,7 @@ Feature: Enable Block blog menu in an activity And I should see "S2 Second Blog" And I should see "My unrelated blog!" And I should not see "Associated Assignment: Test assignment 1" - And I follow "Dashboard" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1" And I follow "Add an entry about this Assignment" And I set the following fields to these values: @@ -168,8 +161,7 @@ Feature: Enable Block blog menu in an activity And I should see "S2 First Blog" And I should see "My course blog is better!" And I should see "Associated Assignment: Test assignment 1" - And I follow "Dashboard" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1" When I follow "Blog entries" Then I should see "S2 First Blog" @@ -178,7 +170,7 @@ Feature: Enable Block blog menu in an activity Scenario: Teacher searches for student blogs Given I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1" And I follow "Add an entry about this Assignment" And I set the following fields to these values: @@ -190,7 +182,7 @@ Feature: Enable Block blog menu in an activity And I should see "Associated Assignment: Test assignment 1" And I log out And I log in as "student2" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1" And I follow "Add a new entry" And I set the following fields to these values: @@ -200,8 +192,7 @@ Feature: Enable Block blog menu in an activity And I should see "S2 Second Blog" And I should see "My unrelated blog!" And I should not see "Associated Assignment: Test assignment 1" - And I follow "Dashboard" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1" And I follow "Add an entry about this Assignment" And I set the following fields to these values: @@ -213,7 +204,7 @@ Feature: Enable Block blog menu in an activity And I should see "Associated Assignment: Test assignment 1" And I log out When I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1" And I set the field "blogsearchquery" to "First" And I press "Search" diff --git a/blocks/blog_menu/tests/behat/block_blog_menu_course.feature b/blocks/blog_menu/tests/behat/block_blog_menu_course.feature index 76ceb37c78c..39821d29276 100644 --- a/blocks/blog_menu/tests/behat/block_blog_menu_course.feature +++ b/blocks/blog_menu/tests/behat/block_blog_menu_course.feature @@ -19,14 +19,13 @@ Feature: Students can use block blog menu in a course | student1 | C1 | student | | student2 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Blog menu" block And I log out Scenario: Students use the blog menu block to post blogs Given I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Add a new entry" When I set the following fields to these values: | Entry title | S1 First Blog | @@ -34,15 +33,14 @@ Feature: Students can use block blog menu in a course And I press "Save changes" Then I should see "S1 First Blog" And I should see "This is my awesome blog!" - And I follow "Dashboard" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Blog entries" And I should see "S1 First Blog" And I should see "This is my awesome blog!" Scenario: Students use the blog menu block to view their blogs about the course Given I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Add an entry about this course" And I set the following fields to these values: | Entry title | S1 First Blog | @@ -53,7 +51,7 @@ Feature: Students can use block blog menu in a course And I should see "Associated Course: C1" And I log out And I log in as "student2" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Add a new entry" And I set the following fields to these values: | Entry title | S2 Second Blog | @@ -62,8 +60,7 @@ Feature: Students can use block blog menu in a course And I should see "S2 Second Blog" And I should see "My unrelated blog!" And I should not see "Associated Course: C1" - And I follow "Dashboard" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Add an entry about this course" And I set the following fields to these values: | Entry title | S2 First Blog | @@ -72,8 +69,7 @@ Feature: Students can use block blog menu in a course And I should see "S2 First Blog" And I should see "My course blog is better!" And I should see "Associated Course: C1" - And I follow "Dashboard" - And I follow "Course 1" + And I am on "Course 1" course homepage When I follow "View my entries about this course" Then I should see "S2 First Blog" And I should not see "S2 Second Blog" @@ -81,7 +77,7 @@ Feature: Students can use block blog menu in a course Scenario: Students use the blog menu block to view all blogs about the course Given I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Add an entry about this course" And I set the following fields to these values: | Entry title | S1 First Blog | @@ -92,7 +88,7 @@ Feature: Students can use block blog menu in a course And I should see "Associated Course: C1" And I log out And I log in as "student2" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Add a new entry" And I set the following fields to these values: | Entry title | S2 Second Blog | @@ -101,8 +97,7 @@ Feature: Students can use block blog menu in a course And I should see "S2 Second Blog" And I should see "My unrelated blog!" And I should not see "Associated Course: C1" - And I follow "Dashboard" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Add an entry about this course" And I set the following fields to these values: | Entry title | S2 First Blog | @@ -111,8 +106,7 @@ Feature: Students can use block blog menu in a course And I should see "S2 First Blog" And I should see "My course blog is better!" And I should see "Associated Course: C1" - And I follow "Dashboard" - And I follow "Course 1" + And I am on "Course 1" course homepage When I follow "View all entries for this course" Then I should see "S1 First Blog" And I should see "S2 First Blog" @@ -120,7 +114,7 @@ Feature: Students can use block blog menu in a course Scenario: Students use the blog menu block to view all their blog entries Given I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Add an entry about this course" And I set the following fields to these values: | Entry title | S1 First Blog | @@ -131,7 +125,7 @@ Feature: Students can use block blog menu in a course And I should see "Associated Course: C1" And I log out And I log in as "student2" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Add a new entry" And I set the following fields to these values: | Entry title | S2 Second Blog | @@ -140,8 +134,7 @@ Feature: Students can use block blog menu in a course And I should see "S2 Second Blog" And I should see "My unrelated blog!" And I should not see "Associated Course: C1" - And I follow "Dashboard" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Add an entry about this course" And I set the following fields to these values: | Entry title | S2 First Blog | @@ -150,8 +143,7 @@ Feature: Students can use block blog menu in a course And I should see "S2 First Blog" And I should see "My course blog is better!" And I should see "Associated Course: C1" - And I follow "Dashboard" - And I follow "Course 1" + And I am on "Course 1" course homepage When I follow "Blog entries" Then I should see "S2 First Blog" And I should see "S2 Second Blog" @@ -159,7 +151,7 @@ Feature: Students can use block blog menu in a course Scenario: Teacher searches for student blogs Given I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Add an entry about this course" And I set the following fields to these values: | Entry title | S1 First Blog | @@ -170,7 +162,7 @@ Feature: Students can use block blog menu in a course And I should see "Associated Course: C1" And I log out And I log in as "student2" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Add a new entry" And I set the following fields to these values: | Entry title | S2 Second Blog | @@ -179,8 +171,7 @@ Feature: Students can use block blog menu in a course And I should see "S2 Second Blog" And I should see "My unrelated blog!" And I should not see "Associated Course: C1" - And I follow "Dashboard" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Add an entry about this course" And I set the following fields to these values: | Entry title | S2 First Blog | @@ -191,7 +182,7 @@ Feature: Students can use block blog menu in a course And I should see "Associated Course: C1" And I log out When I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I set the field "blogsearchquery" to "First" And I press "Search" Then I should see "S1 First Blog" diff --git a/blocks/blog_recent/tests/behat/block_blog_recent.feature b/blocks/blog_recent/tests/behat/block_blog_recent.feature index 6c3f73b7735..ccb4c64efb3 100644 --- a/blocks/blog_recent/tests/behat/block_blog_recent.feature +++ b/blocks/blog_recent/tests/behat/block_blog_recent.feature @@ -21,14 +21,12 @@ Feature: Feature: Users can use the recent blog entries block to view recent blo | enableblogs | 0 | And I log out And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I add the "Recent blog entries" block Then I should see "Blogging is disabled!" in the "Recent blog entries" "block" Scenario: Add the recent blogs block to a course when there are not any blog posts Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I add the "Recent blog entries" block Then I should see "No recent entries" in the "Recent blog entries" "block" diff --git a/blocks/blog_recent/tests/behat/block_blog_recent_activity.feature b/blocks/blog_recent/tests/behat/block_blog_recent_activity.feature index fae3f4a35dc..09112b7162c 100644 --- a/blocks/blog_recent/tests/behat/block_blog_recent_activity.feature +++ b/blocks/blog_recent/tests/behat/block_blog_recent_activity.feature @@ -19,8 +19,7 @@ Feature: Students can use the recent blog entries block to view recent entries o | student1 | C1 | student | | student2 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment 1 | | Description | Offline text | @@ -32,7 +31,7 @@ Feature: Students can use the recent blog entries block to view recent entries o Scenario: Students use the recent blog entries block to view blogs Given I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1" And I follow "Add an entry about this Assignment" When I set the following fields to these values: @@ -48,7 +47,7 @@ Feature: Students can use the recent blog entries block to view recent entries o Scenario: Students only see a few entries in the recent blog entries block Given I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1" And I follow "Add an entry about this Assignment" # Blog 1 of 5 @@ -106,8 +105,7 @@ Feature: Students can use the recent blog entries block to view recent entries o And I should see "This is my awesome blog!" Then I log out And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I follow "Test assignment 1" And I configure the "Recent blog entries" block And I set the following fields to these values: diff --git a/blocks/blog_recent/tests/behat/block_blog_recent_course.feature b/blocks/blog_recent/tests/behat/block_blog_recent_course.feature index f06fad337e9..67ee460cfff 100644 --- a/blocks/blog_recent/tests/behat/block_blog_recent_course.feature +++ b/blocks/blog_recent/tests/behat/block_blog_recent_course.feature @@ -17,15 +17,14 @@ Feature: Students can use the recent blog entries block to view recent entries o | teacher1 | C1 | editingteacher | | student1 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Blog menu" block And I add the "Recent blog entries" block And I log out Scenario: Students use the recent blog entries block to view blogs Given I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Add an entry about this course" When I set the following fields to these values: | Entry title | S1 First Blog | @@ -40,7 +39,7 @@ Feature: Students can use the recent blog entries block to view recent entries o Scenario: Students only see a few entries in the recent blog entries block Given I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Add an entry about this course" # Blog 1 of 5 And I set the following fields to these values: @@ -97,8 +96,7 @@ Feature: Students can use the recent blog entries block to view recent entries o And I should see "This is my awesome blog!" Then I log out And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I configure the "Recent blog entries" block And I set the following fields to these values: | id_config_numberofrecentblogentries | 2 | diff --git a/blocks/blog_tags/tests/behat/blogtag.feature b/blocks/blog_tags/tests/behat/blogtag.feature index 60572e16a7f..8cf6b27804a 100644 --- a/blocks/blog_tags/tests/behat/blogtag.feature +++ b/blocks/blog_tags/tests/behat/blogtag.feature @@ -21,8 +21,7 @@ Feature: Adding blog tag block | teacher1 | c1 | editingteacher | | student1 | c1 | student | When I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Blog tags" block # TODO MDL-57120 site "Blogs" link not accessible without navigation block. And I add the "Navigation" block if not present @@ -37,7 +36,7 @@ Feature: Adding blog tag block And I press "Save changes" And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to course participants And I click on "Course blogs" "link" in the "Navigation" "block" And I follow "Blog about this Course" diff --git a/blocks/calendar_month/block_calendar_month.php b/blocks/calendar_month/block_calendar_month.php index ace839e7b66..cc3d6f69f61 100644 --- a/blocks/calendar_month/block_calendar_month.php +++ b/blocks/calendar_month/block_calendar_month.php @@ -79,13 +79,16 @@ class block_calendar_month extends block_base { list($courses, $group, $user) = calendar_set_filters($filtercourse); if ($issite) { // For the front page. - $this->content->text .= calendar_get_mini($courses, $group, $user, false, false, 'frontpage', $courseid, $time); + $this->content->text .= calendar_get_mini($courses, $group, $user, false, false, + 'frontpage', $courseid, $time); // No filters for now. } else { // For any other course. - $this->content->text .= calendar_get_mini($courses, $group, $user, false, false, 'course', $courseid, $time); + $this->content->text .= calendar_get_mini($courses, $group, $user, false, false, + 'course', $courseid, $time); $this->content->text .= '

'.get_string('eventskey', 'calendar').'

'; - $this->content->text .= '
'.calendar_filter_controls($this->page->url).'
'; + $this->content->text .= '
' . + calendar_filter_controls($this->page->url) . '
'; } return $this->content; diff --git a/blocks/calendar_month/tests/behat/block_calendar_month.feature b/blocks/calendar_month/tests/behat/block_calendar_month.feature index e4952e8829f..db6f058c71a 100644 --- a/blocks/calendar_month/tests/behat/block_calendar_month.feature +++ b/blocks/calendar_month/tests/behat/block_calendar_month.feature @@ -21,8 +21,7 @@ Feature: Enable the calendar block in a course and test it's functionality Scenario: Add the block to a the course Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I add the "Calendar" block Then I should see "Events key" in the "Calendar" "block" @@ -34,8 +33,7 @@ Feature: Enable the calendar block in a course and test it's functionality | id_name | Site Event | And I log out When I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Calendar" block And I hover over today in the calendar Then I should see "Site Event" @@ -48,13 +46,12 @@ Feature: Enable the calendar block in a course and test it's functionality | id_name | Site Event | And I log out When I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Calendar" block And I create a calendar event with form data: | id_eventtype | Course | | id_name | Course Event | - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Hide global events" And I hover over today in the calendar Then I should not see "Site Event" @@ -63,31 +60,29 @@ Feature: Enable the calendar block in a course and test it's functionality @javascript Scenario: View a course event in the calendar block Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Calendar" block And I create a calendar event with form data: | id_eventtype | Course | | id_name | Course Event | - When I follow "Course 1" + When I am on "Course 1" course homepage And I hover over today in the calendar Then I should see "Course Event" @javascript Scenario: Filter course events in the calendar block Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Calendar" block And I create a calendar event with form data: | id_eventtype | Course | | id_name | Course Event | - And I follow "Course 1" + And I am on "Course 1" course homepage And I create a calendar event with form data: | id_eventtype | User | | id_name | User Event | When I am on homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Hide course events" And I hover over today in the calendar Then I should not see "Course Event" @@ -96,32 +91,30 @@ Feature: Enable the calendar block in a course and test it's functionality @javascript Scenario: View a user event in the calendar block Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Calendar" block And I create a calendar event with form data: | id_eventtype | User | | id_name | User Event | When I am on homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I hover over today in the calendar Then I should see "User Event" @javascript Scenario: Filter user events in the calendar block Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Calendar" block And I create a calendar event with form data: | id_eventtype | Course | | id_name | Course Event | - And I follow "Course 1" + And I am on "Course 1" course homepage And I create a calendar event with form data: | id_eventtype | User | | id_name | User Event | When I am on homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Hide user events" And I hover over today in the calendar Then I should not see "User Event" @@ -138,7 +131,7 @@ Feature: Enable the calendar block in a course and test it's functionality | student1 | G1 | | student2 | G2 | When I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Edit settings" node in "Course administration" And I set the following fields to these values: | id_groupmode | Separate groups | @@ -152,12 +145,12 @@ Feature: Enable the calendar block in a course and test it's functionality | id_name | Group Event | And I log out Then I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I hover over today in the calendar And I should see "Group Event" And I log out And I log in as "student2" - And I follow "Course 1" + And I am on "Course 1" course homepage And I hover over today in the calendar And I should not see "Group Event" @@ -172,7 +165,7 @@ Feature: Enable the calendar block in a course and test it's functionality | student1 | G1 | | student2 | G2 | When I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Edit settings" node in "Course administration" And I set the following fields to these values: | id_groupmode | Separate groups | @@ -183,14 +176,14 @@ Feature: Enable the calendar block in a course and test it's functionality And I create a calendar event with form data: | id_eventtype | Course | | id_name | Course Event 1 | - And I follow "Course 1" + And I am on "Course 1" course homepage And I create a calendar event with form data: | id_eventtype | Group | | id_groupid | Group 1 | | id_name | Group Event 1 | And I log out Then I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Hide group events" And I hover over today in the calendar And I should not see "Group Event 1" diff --git a/blocks/calendar_month/tests/behat/block_calendar_month_course.feature b/blocks/calendar_month/tests/behat/block_calendar_month_course.feature index 1c59a6021f4..23605721544 100644 --- a/blocks/calendar_month/tests/behat/block_calendar_month_course.feature +++ b/blocks/calendar_month/tests/behat/block_calendar_month_course.feature @@ -21,8 +21,7 @@ Feature: Enable the calendar block in a course | id_name | Site Event | And I log out Then I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Calendar" block And I hover over today in the calendar And I should see "Site Event" diff --git a/blocks/calendar_upcoming/block_calendar_upcoming.php b/blocks/calendar_upcoming/block_calendar_upcoming.php index 335bcc4e7e2..858a0e42baf 100644 --- a/blocks/calendar_upcoming/block_calendar_upcoming.php +++ b/blocks/calendar_upcoming/block_calendar_upcoming.php @@ -90,7 +90,7 @@ class block_calendar_upcoming extends block_base { if (!empty($this->instance)) { $link = 'view.php?view=day&course='.$courseshown.'&'; $showcourselink = ($this->page->course->id == SITEID); - $this->content->text = calendar_get_block_upcoming($events, $link, $showcourselink); + $this->content->text = self::get_upcoming_content($events, $link, $showcourselink); } if (empty($this->content->text)) { @@ -99,6 +99,54 @@ class block_calendar_upcoming extends block_base { return $this->content; } + + /** + * Get the upcoming event block content. + * + * @param array $events list of events + * @param \moodle_url|string $linkhref link to event referer + * @param boolean $showcourselink whether links to courses should be shown + * @return string|null $content html block content + */ + public static function get_upcoming_content($events, $linkhref = null, $showcourselink = false) { + $content = ''; + $lines = count($events); + + if (!$lines) { + return $content; + } + + for ($i = 0; $i < $lines; ++$i) { + if (!isset($events[$i]->time)) { + continue; + } + $events[$i] = calendar_add_event_metadata($events[$i]); + $content .= '
' . $events[$i]->icon . ''; + if (!empty($events[$i]->referer)) { + // That's an activity event, so let's provide the hyperlink. + $content .= $events[$i]->referer; + } else { + if (!empty($linkhref)) { + $href = calendar_get_link_href(new \moodle_url(CALENDAR_URL . $linkhref), 0, 0, 0, + $events[$i]->timestart); + $href->set_anchor('event_' . $events[$i]->id); + $content .= \html_writer::link($href, $events[$i]->name); + } else { + $content .= $events[$i]->name; + } + } + $events[$i]->time = str_replace('»', '
»', $events[$i]->time); + if ($showcourselink && !empty($events[$i]->courselink)) { + $content .= \html_writer::div($events[$i]->courselink, 'course'); + } + $content .= '
' . $events[$i]->time . '
'; + if ($i < $lines - 1) { + $content .= '
'; + } + } + + return $content; + } } diff --git a/blocks/comments/tests/behat/add_comment.feature b/blocks/comments/tests/behat/add_comment.feature index 7aa437f546a..02f73f41047 100644 --- a/blocks/comments/tests/behat/add_comment.feature +++ b/blocks/comments/tests/behat/add_comment.feature @@ -17,12 +17,11 @@ Feature: Add a comment to the comments block | teacher1 | C1 | editingteacher | | student1 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Comments" block And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage @javascript Scenario: Add a comment with Javascript enabled @@ -69,7 +68,7 @@ Feature: Add a comment to the comments block And I add "Super test comment 31" comment to comments block Then I should see "Super test comment 01" And I should see "Super test comment 31" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should not see "Super test comment 01" And I should not see "Super test comment 02" And I should not see "Super test comment 16" diff --git a/blocks/comments/tests/behat/block_comment_activity.feature b/blocks/comments/tests/behat/block_comment_activity.feature index d2a3180351f..4aa94bdded0 100644 --- a/blocks/comments/tests/behat/block_comment_activity.feature +++ b/blocks/comments/tests/behat/block_comment_activity.feature @@ -20,15 +20,14 @@ Feature: Enable Block comments on an activity page and view comments | activity | course | idnumber | name | intro | | page | C1 | page1 | Test page name | Test page description | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I follow "Test page name" And I add the "Comments" block And I follow "Show comments" And I add "I'm a comment from the teacher" comment to comments block And I log out When I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test page name" And I follow "Show comments" Then I should see "I'm a comment from the teacher" diff --git a/blocks/comments/tests/behat/block_comment_course.feature b/blocks/comments/tests/behat/block_comment_course.feature index c3acbc43a62..3589da498e0 100644 --- a/blocks/comments/tests/behat/block_comment_course.feature +++ b/blocks/comments/tests/behat/block_comment_course.feature @@ -17,13 +17,12 @@ Feature: Enable Block comments on a course page and view comments | teacher1 | C1 | editingteacher | | student1 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Comments" block And I follow "Show comments" And I add "I'm a comment from the teacher" comment to comments block And I log out When I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Show comments" Then I should see "I'm a comment from the teacher" diff --git a/blocks/comments/tests/behat/delete_comment.feature b/blocks/comments/tests/behat/delete_comment.feature index 73f78dd4670..f8939f42ae3 100644 --- a/blocks/comments/tests/behat/delete_comment.feature +++ b/blocks/comments/tests/behat/delete_comment.feature @@ -18,16 +18,15 @@ Feature: Delete comment block messages | teacher1 | C1 | editingteacher | | student1 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Comments" block And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I add "Comment from student1" comment to comments block And I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I add "Comment from teacher1" comment to comments block When I delete "Comment from student1" comment from comments block Then I should not see "Comment from student1" diff --git a/blocks/completionstatus/tests/behat/block_completionstatus.feature b/blocks/completionstatus/tests/behat/block_completionstatus.feature index 9fd829510aa..f5e24a149d8 100644 --- a/blocks/completionstatus/tests/behat/block_completionstatus.feature +++ b/blocks/completionstatus/tests/behat/block_completionstatus.feature @@ -19,8 +19,7 @@ Feature: Enable Block Completion in a course Scenario: Add the block to a the course where completion is disabled Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I navigate to "Edit settings" node in "Course administration" And I set the following fields to these values: | Enable completion tracking | No | @@ -30,8 +29,7 @@ Feature: Enable Block Completion in a course Scenario: Add the block to a the course where completion is not set Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I add the "Course completion status" block Then I should see "No completion criteria set for this course" in the "Course completion status" "block" @@ -40,8 +38,7 @@ Feature: Enable Block Completion in a course | activity | course | idnumber | name | intro | | page | C1 | page1 | Test page name | Test page description | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I follow "Test page name" And I navigate to "Edit settings" in current page administration And I set the following fields to these values: diff --git a/blocks/completionstatus/tests/behat/block_completionstatus_activity_completion.feature b/blocks/completionstatus/tests/behat/block_completionstatus_activity_completion.feature index 0a31bb7bb36..a92c042bd1f 100644 --- a/blocks/completionstatus/tests/behat/block_completionstatus_activity_completion.feature +++ b/blocks/completionstatus/tests/behat/block_completionstatus_activity_completion.feature @@ -22,8 +22,7 @@ Feature: Enable Block Completion in a course using activity completion Scenario: Add the block to a the course and add course completion items Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I follow "Test page name" And I navigate to "Edit settings" in current page administration And I set the following fields to these values: @@ -38,14 +37,13 @@ Feature: Enable Block Completion in a course using activity completion And I press "Save changes" And I log out When I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage Then I should see "Status: Not yet started" in the "Course completion status" "block" And I should see "0 of 1" in the "Activity completion" "table_row" Scenario: Add the block to a the course and add course completion items Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I follow "Test page name" And I navigate to "Edit settings" in current page administration And I set the following fields to these values: @@ -60,14 +58,13 @@ Feature: Enable Block Completion in a course using activity completion And I press "Save changes" And I log out When I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test page name" And I follow "C1" Then I should see "Status: Pending" in the "Course completion status" "block" And I should see "0 of 1" in the "Activity completion" "table_row" And I trigger cron - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "1 of 1" in the "Activity completion" "table_row" And I follow "More details" And I should see "Yes" in the "Activity completion" "table_row" diff --git a/blocks/completionstatus/tests/behat/block_completionstatus_manual_other.feature b/blocks/completionstatus/tests/behat/block_completionstatus_manual_other.feature index 7d4c3f6c0f0..c0219148769 100644 --- a/blocks/completionstatus/tests/behat/block_completionstatus_manual_other.feature +++ b/blocks/completionstatus/tests/behat/block_completionstatus_manual_other.feature @@ -21,8 +21,7 @@ Feature: Enable Block Completion in a course using manual completion by others Scenario: Add the block to a the course and mark a student complete. Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Course completion status" block And I navigate to "Course completion" node in "Course administration" And I expand all fieldsets @@ -31,12 +30,12 @@ Feature: Enable Block Completion in a course using manual completion by others And I press "Save changes" And I log out When I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Status: Not yet started" in the "Course completion status" "block" And I should see "No" in the "Teacher" "table_row" And I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Course completion" node in "Course administration > Reports" And I follow "Click to mark user complete" # Running completion task just after clicking sometimes fail, as record @@ -46,7 +45,7 @@ Feature: Enable Block Completion in a course using manual completion by others And I am on site homepage And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage Then I should see "Status: Complete" in the "Course completion status" "block" And I should see "Yes" in the "Teacher" "table_row" And I follow "More details" @@ -54,8 +53,7 @@ Feature: Enable Block Completion in a course using manual completion by others Scenario: Add the block to a the course and require multiple roles to mark a student complete. Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Course completion status" block And I navigate to "Course completion" node in "Course administration" And I expand all fieldsets @@ -66,18 +64,18 @@ Feature: Enable Block Completion in a course using manual completion by others And I press "Save changes" And I log out When I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Status: Not yet started" in the "Course completion status" "block" And I should see "No" in the "Teacher" "table_row" And I should see "No" in the "Non-editing teacher" "table_row" And I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Course completion" node in "Course administration > Reports" And I follow "Click to mark user complete" And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Status: In progress" in the "Course completion status" "block" And I should see "Yes" in the "Teacher" "table_row" And I should see "No" in the "Non-editing teacher" "table_row" @@ -86,7 +84,7 @@ Feature: Enable Block Completion in a course using manual completion by others And I should see "No" in the "Marked complete by Non-editing teacher" "table_row" And I log out And I log in as "teacher2" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Course completion" node in "Course administration > Reports" And I follow "Click to mark user complete" # Running completion task just after clicking sometimes fail, as record @@ -96,7 +94,7 @@ Feature: Enable Block Completion in a course using manual completion by others And I am on site homepage And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage Then I should see "Status: Complete" in the "Course completion status" "block" And I should see "Yes" in the "Teacher" "table_row" And I should see "Yes" in the "Non-editing teacher" "table_row" diff --git a/blocks/completionstatus/tests/behat/block_completionstatus_manual_self.feature b/blocks/completionstatus/tests/behat/block_completionstatus_manual_self.feature index 81b02980442..d9b73c5eeb9 100644 --- a/blocks/completionstatus/tests/behat/block_completionstatus_manual_self.feature +++ b/blocks/completionstatus/tests/behat/block_completionstatus_manual_self.feature @@ -17,8 +17,7 @@ Feature: Enable Block Completion in a course using manual self completion | teacher1 | C1 | editingteacher | | student1 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Course completion status" block And I add the "Self completion" block And I navigate to "Course completion" node in "Course administration" @@ -28,7 +27,7 @@ Feature: Enable Block Completion in a course using manual self completion And I press "Save changes" And I log out When I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Status: Not yet started" in the "Course completion status" "block" And I should see "No" in the "Self completion" "table_row" And I follow "Complete course" @@ -39,8 +38,7 @@ Feature: Enable Block Completion in a course using manual self completion # should be created before the task runs. And I wait "1" seconds And I run the scheduled task "core\task\completion_regular_task" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage Then I should see "Status: Complete" in the "Course completion status" "block" And I should see "Yes" in the "Self completion" "table_row" And I follow "More details" diff --git a/blocks/course_list/tests/behat/block_course_list_category.feature b/blocks/course_list/tests/behat/block_course_list_category.feature index c4273008f28..3c03721f725 100644 --- a/blocks/course_list/tests/behat/block_course_list_category.feature +++ b/blocks/course_list/tests/behat/block_course_list_category.feature @@ -58,7 +58,7 @@ Feature: Enable the course_list block on a category page and view it's contents And I should see "Course 2" in the "My courses" "block" And I should see "Course 3" in the "My courses" "block" And I should not see "Course 4" in the "My courses" "block" - And I follow "Course 3" + And I am on "Course 3" course homepage And I should see "Course 3" Scenario: Add the course list block on category page and view as an admin diff --git a/blocks/course_list/tests/behat/block_course_list_course.feature b/blocks/course_list/tests/behat/block_course_list_course.feature index 0441dfbb825..8a14d9ef7bf 100644 --- a/blocks/course_list/tests/behat/block_course_list_course.feature +++ b/blocks/course_list/tests/behat/block_course_list_course.feature @@ -27,8 +27,7 @@ Feature: Enable the course_list block on a course page and view it's contents Scenario: Add the course list block on course page and navigate to the course listing Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I add the "Courses" block Then I should see "Course 1" in the "My courses" "block" And I should see "Course 2" in the "My courses" "block" @@ -39,21 +38,18 @@ Feature: Enable the course_list block on a course page and view it's contents Scenario: Add the course list block on course page and navigate to another course Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I add the "Courses" block Then I should see "Course 1" in the "My courses" "block" And I should see "Course 2" in the "My courses" "block" And I should see "Course 3" in the "My courses" "block" And I should not see "Course 4" in the "My courses" "block" - And I follow "Course 3" + And I am on "Course 3" course homepage And I should see "Course 3" Scenario: Add the course list block on course page and view as an admin Given I log in as "admin" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I add the "Courses" block Then I should see "Miscellaneous" in the "Course categories" "block" And I should see "Category 1" in the "Course categories" "block" @@ -68,8 +64,7 @@ Feature: Enable the course_list block on a course page and view it's contents Given the following config values are set as admin: | block_course_list_hideallcourseslink | 1 | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I add the "Courses" block Then I should not see "All courses" in the "My courses" "block" @@ -80,9 +75,7 @@ Feature: Enable the course_list block on a course page and view it's contents | user | course | role | | admin | C1 | editingteacher | And I log in as "admin" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I add the "Courses" block Then I should not see "Miscellaneous" in the "My courses" "block" And I should not see "Category 1" in the "My courses" "block" diff --git a/blocks/course_list/tests/behat/block_course_list_dashboard.feature b/blocks/course_list/tests/behat/block_course_list_dashboard.feature index 33f50594cf5..f31ae948072 100644 --- a/blocks/course_list/tests/behat/block_course_list_dashboard.feature +++ b/blocks/course_list/tests/behat/block_course_list_dashboard.feature @@ -44,7 +44,7 @@ Feature: Enable the course_list block on the dashboard and view it's contents And I should see "Course 2" in the "My courses" "block" And I should see "Course 3" in the "My courses" "block" And I should not see "Course 4" in the "My courses" "block" - And I follow "Course 3" + And I am on "Course 3" course homepage And I should see "Course 3" Scenario: Add the course list block on the dashboard and view as an admin diff --git a/blocks/course_list/tests/behat/block_course_list_frontpage.feature b/blocks/course_list/tests/behat/block_course_list_frontpage.feature index e13a38019bd..b10cf27e1cc 100644 --- a/blocks/course_list/tests/behat/block_course_list_frontpage.feature +++ b/blocks/course_list/tests/behat/block_course_list_frontpage.feature @@ -52,7 +52,7 @@ Feature: Enable the course_list block on the frontpage and view it's contents And I should see "Course 2" in the "My courses" "block" And I should see "Course 3" in the "My courses" "block" And I should not see "Course 4" in the "My courses" "block" - And I follow "Course 3" + And I am on "Course 3" course homepage And I should see "Course 3" Scenario: Add the course list block on the frontpage page and view as an admin diff --git a/blocks/course_overview/block_course_overview.php b/blocks/course_overview/block_course_overview.php deleted file mode 100644 index ae244ef1082..00000000000 --- a/blocks/course_overview/block_course_overview.php +++ /dev/null @@ -1,128 +0,0 @@ -. - -/** - * Course overview block - * - * @package block_course_overview - * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -require_once($CFG->dirroot.'/blocks/course_overview/locallib.php'); - -/** - * Course overview block - * - * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class block_course_overview extends block_base { - /** - * If this is passed as mynumber then showallcourses, irrespective of limit by user. - */ - const SHOW_ALL_COURSES = -2; - - /** - * Block initialization - */ - public function init() { - $this->title = get_string('pluginname', 'block_course_overview'); - } - - /** - * Return contents of course_overview block - * - * @return stdClass contents of block - */ - public function get_content() { - global $USER, $CFG, $DB; - require_once($CFG->dirroot.'/user/profile/lib.php'); - - if($this->content !== NULL) { - return $this->content; - } - - $config = get_config('block_course_overview'); - - $this->content = new stdClass(); - $this->content->text = ''; - $this->content->footer = ''; - - $content = array(); - - $updatemynumber = optional_param('mynumber', -1, PARAM_INT); - if ($updatemynumber >= 0) { - block_course_overview_update_mynumber($updatemynumber); - } - - profile_load_custom_fields($USER); - - $showallcourses = ($updatemynumber === self::SHOW_ALL_COURSES); - list($sortedcourses, $sitecourses, $totalcourses) = block_course_overview_get_sorted_courses($showallcourses); - $overviews = block_course_overview_get_overviews($sitecourses); - - $renderer = $this->page->get_renderer('block_course_overview'); - if (!empty($config->showwelcomearea)) { - require_once($CFG->dirroot.'/message/lib.php'); - $msgcount = message_count_unread_messages(); - $this->content->text = $renderer->welcome_area($msgcount); - } - - // Number of sites to display. - if ($this->page->user_is_editing() && empty($config->forcedefaultmaxcourses)) { - $this->content->text .= $renderer->editing_bar_head($totalcourses); - } - - if (empty($sortedcourses)) { - $this->content->text .= get_string('nocourses','my'); - } else { - // For each course, build category cache. - $this->content->text .= $renderer->course_overview($sortedcourses, $overviews); - $this->content->text .= $renderer->hidden_courses($totalcourses - count($sortedcourses)); - } - - return $this->content; - } - - /** - * Allow the block to have a configuration page - * - * @return boolean - */ - public function has_config() { - return true; - } - - /** - * Locations where block can be displayed - * - * @return array - */ - public function applicable_formats() { - return array('my' => true); - } - - /** - * Sets block header to be hidden or visible - * - * @return bool if true then header will be visible. - */ - public function hide_header() { - // Hide header if welcome area is show. - $config = get_config('block_course_overview'); - return !empty($config->showwelcomearea); - } -} diff --git a/blocks/course_overview/lang/en/block_course_overview.php b/blocks/course_overview/lang/en/block_course_overview.php deleted file mode 100644 index d92f3dfdb05..00000000000 --- a/blocks/course_overview/lang/en/block_course_overview.php +++ /dev/null @@ -1,67 +0,0 @@ -. - -/** - * Lang strings for course_overview block - * - * @package block_course_overview - * @copyright 2012 Adam Olley - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -$string['activityoverview'] = 'You have {$a}s that need attention'; -$string['alwaysshowall'] = 'Always show all'; -$string['collapseall'] = 'Collapse all course lists'; -$string['configotherexpanded'] = 'If enabled, other courses will be expanded by default unless overridden by user preferences.'; -$string['configpreservestates'] = 'If enabled, the collapsed/expanded states set by the user are stored and used on each load.'; -$string['course_overview:addinstance'] = 'Add a new course overview block'; -$string['course_overview:myaddinstance'] = 'Add a new course overview block to Dashboard'; -$string['defaultmaxcourses'] = 'Default maximum courses'; -$string['defaultmaxcoursesdesc'] = 'Maximum courses which should be displayed on course overview block, 0 will show all courses'; -$string['expandall'] = 'Expand all course lists'; -$string['forcedefaultmaxcourses'] = 'Force maximum courses'; -$string['forcedefaultmaxcoursesdesc'] = 'If set then user will not be able to change his/her personal setting'; -$string['fullpath'] = 'All categories and subcategories'; -$string['hiddencoursecount'] = 'You have {$a} hidden course'; -$string['hiddencoursecountplural'] = 'You have {$a} hidden courses'; -$string['hiddencoursecountwithshowall'] = 'You have {$a->coursecount} hidden course ({$a->showalllink})'; -$string['hiddencoursecountwithshowallplural'] = 'You have {$a->coursecount} hidden courses ({$a->showalllink})'; -$string['message'] = 'message'; -$string['messages'] = 'messages'; -$string['movecourse'] = 'Move course: {$a}'; -$string['movecoursehere'] = 'Move course here'; -$string['movetofirst'] = 'Move {$a} course to top'; -$string['moveafterhere'] = 'Move {$a->movingcoursename} course after {$a->currentcoursename}'; -$string['movingcourse'] = 'You are moving: {$a->fullname} ({$a->cancellink})'; -$string['none'] = 'None'; -$string['numtodisplay'] = 'Number of courses to display: '; -$string['onlyparentname'] = 'Parent category only'; -$string['otherexpanded'] = 'Other courses expanded'; -$string['pluginname'] = 'Course overview'; -$string['preservestates'] = 'Preserve expanded states'; -$string['shortnameprefix'] = 'Includes {$a}'; -$string['shortnamesufixsingular'] = ' (and {$a} other)'; -$string['shortnamesufixprural'] = ' (and {$a} others)'; -$string['showcategories'] = 'Categories to show'; -$string['showcategoriesdesc'] = 'Should course categories be displayed below each course?'; -$string['showchildren'] = 'Show children'; -$string['showchildrendesc'] = 'Should child courses be listed underneath the main course title?'; -$string['showwelcomearea'] = 'Show welcome area'; -$string['showwelcomeareadesc'] = 'Show the welcome area above the course list?'; -$string['view_edit_profile'] = '(View and edit your profile.)'; -$string['welcome'] = 'Welcome {$a}'; -$string['youhavemessages'] = 'You have {$a} unread '; -$string['youhavenomessages'] = 'You have no unread '; diff --git a/blocks/course_overview/locallib.php b/blocks/course_overview/locallib.php deleted file mode 100644 index 06e6896ce3d..00000000000 --- a/blocks/course_overview/locallib.php +++ /dev/null @@ -1,233 +0,0 @@ -. - -/** - * Helper functions for course_overview block - * - * @package block_course_overview - * @copyright 2012 Adam Olley - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -define('BLOCKS_COURSE_OVERVIEW_SHOWCATEGORIES_NONE', '0'); -define('BLOCKS_COURSE_OVERVIEW_SHOWCATEGORIES_ONLY_PARENT_NAME', '1'); -define('BLOCKS_COURSE_OVERVIEW_SHOWCATEGORIES_FULL_PATH', '2'); - -/** - * Display overview for courses - * - * @param array $courses courses for which overview needs to be shown - * @return array html overview - */ -function block_course_overview_get_overviews($courses) { - $htmlarray = array(); - if ($modules = get_plugin_list_with_function('mod', 'print_overview')) { - // Split courses list into batches with no more than MAX_MODINFO_CACHE_SIZE courses in one batch. - // Otherwise we exceed the cache limit in get_fast_modinfo() and rebuild it too often. - if (defined('MAX_MODINFO_CACHE_SIZE') && MAX_MODINFO_CACHE_SIZE > 0 && count($courses) > MAX_MODINFO_CACHE_SIZE) { - $batches = array_chunk($courses, MAX_MODINFO_CACHE_SIZE, true); - } else { - $batches = array($courses); - } - foreach ($batches as $courses) { - foreach ($modules as $fname) { - $fname($courses, $htmlarray); - } - } - } - return $htmlarray; -} - -/** - * Sets user preference for maximum courses to be displayed in course_overview block - * - * @param int $number maximum courses which should be visible - */ -function block_course_overview_update_mynumber($number) { - set_user_preference('course_overview_number_of_courses', $number); -} - -/** - * Sets user course sorting preference in course_overview block - * - * @param array $sortorder list of course ids - */ -function block_course_overview_update_myorder($sortorder) { - $value = implode(',', $sortorder); - if (core_text::strlen($value) > 1333) { - // The value won't fit into the user preference. Remove courses in the end of the list (mostly likely user won't even notice). - $value = preg_replace('/,[\d]*$/', '', core_text::substr($value, 0, 1334)); - } - set_user_preference('course_overview_course_sortorder', $value); -} - -/** - * Gets user course sorting preference in course_overview block - * - * @return array list of course ids - */ -function block_course_overview_get_myorder() { - if ($value = get_user_preferences('course_overview_course_sortorder')) { - return explode(',', $value); - } - // If preference was not found, look in the old location and convert if found. - $order = array(); - if ($value = get_user_preferences('course_overview_course_order')) { - $order = unserialize_array($value); - block_course_overview_update_myorder($order); - unset_user_preference('course_overview_course_order'); - } - return $order; -} - -/** - * Returns shortname of activities in course - * - * @param int $courseid id of course for which activity shortname is needed - * @return string|bool list of child shortname - */ -function block_course_overview_get_child_shortnames($courseid) { - global $DB; - $ctxselect = context_helper::get_preload_record_columns_sql('ctx'); - $sql = "SELECT c.id, c.shortname, $ctxselect - FROM {enrol} e - JOIN {course} c ON (c.id = e.customint1) - JOIN {context} ctx ON (ctx.instanceid = e.customint1) - WHERE e.courseid = :courseid AND e.enrol = :method AND ctx.contextlevel = :contextlevel ORDER BY e.sortorder"; - $params = array('method' => 'meta', 'courseid' => $courseid, 'contextlevel' => CONTEXT_COURSE); - - if ($results = $DB->get_records_sql($sql, $params)) { - $shortnames = array(); - // Preload the context we will need it to format the category name shortly. - foreach ($results as $res) { - context_helper::preload_from_record($res); - $context = context_course::instance($res->id); - $shortnames[] = format_string($res->shortname, true, $context); - } - $total = count($shortnames); - $suffix = ''; - if ($total > 10) { - $shortnames = array_slice($shortnames, 0, 10); - $diff = $total - count($shortnames); - if ($diff > 1) { - $suffix = get_string('shortnamesufixprural', 'block_course_overview', $diff); - } else { - $suffix = get_string('shortnamesufixsingular', 'block_course_overview', $diff); - } - } - $shortnames = get_string('shortnameprefix', 'block_course_overview', implode('; ', $shortnames)); - $shortnames .= $suffix; - } - - return isset($shortnames) ? $shortnames : false; -} - -/** - * Returns maximum number of courses which will be displayed in course_overview block - * - * @param bool $showallcourses if set true all courses will be visible. - * @return int maximum number of courses - */ -function block_course_overview_get_max_user_courses($showallcourses = false) { - // Get block configuration - $config = get_config('block_course_overview'); - $limit = $config->defaultmaxcourses; - - // If max course is not set then try get user preference - if (empty($config->forcedefaultmaxcourses)) { - if ($showallcourses) { - $limit = 0; - } else { - $limit = get_user_preferences('course_overview_number_of_courses', $limit); - } - } - return $limit; -} - -/** - * Return sorted list of user courses - * - * @param bool $showallcourses if set true all courses will be visible. - * @return array list of sorted courses and count of courses. - */ -function block_course_overview_get_sorted_courses($showallcourses = false) { - global $USER; - - $limit = block_course_overview_get_max_user_courses($showallcourses); - - $courses = enrol_get_my_courses(); - $site = get_site(); - - if (array_key_exists($site->id,$courses)) { - unset($courses[$site->id]); - } - - foreach ($courses as $c) { - if (isset($USER->lastcourseaccess[$c->id])) { - $courses[$c->id]->lastaccess = $USER->lastcourseaccess[$c->id]; - } else { - $courses[$c->id]->lastaccess = 0; - } - } - - // Get remote courses. - $remotecourses = array(); - if (is_enabled_auth('mnet')) { - $remotecourses = get_my_remotecourses(); - } - // Remote courses will have -ve remoteid as key, so it can be differentiated from normal courses - foreach ($remotecourses as $id => $val) { - $remoteid = $val->remoteid * -1; - $val->id = $remoteid; - $courses[$remoteid] = $val; - } - - $order = block_course_overview_get_myorder(); - - $sortedcourses = array(); - $counter = 0; - // Get courses in sort order into list. - foreach ($order as $key => $cid) { - if (($counter >= $limit) && ($limit != 0)) { - break; - } - - // Make sure user is still enroled. - if (isset($courses[$cid])) { - $sortedcourses[$cid] = $courses[$cid]; - $counter++; - } - } - // Append unsorted courses if limit allows - foreach ($courses as $c) { - if (($limit != 0) && ($counter >= $limit)) { - break; - } - if (!in_array($c->id, $order)) { - $sortedcourses[$c->id] = $c; - $counter++; - } - } - - // From list extract site courses for overview - $sitecourses = array(); - foreach ($sortedcourses as $key => $course) { - if ($course->id > 0) { - $sitecourses[$key] = $course; - } - } - return array($sortedcourses, $sitecourses, count($courses)); -} diff --git a/blocks/course_overview/module.js b/blocks/course_overview/module.js deleted file mode 100644 index e900df806fe..00000000000 --- a/blocks/course_overview/module.js +++ /dev/null @@ -1,230 +0,0 @@ -M.block_course_overview = {} - -M.block_course_overview.add_handles = function(Y) { - M.block_course_overview.Y = Y; - var MOVEICON = { - pix: "i/move_2d", - component: 'moodle' - }; - - YUI().use('dd-constrain', 'dd-proxy', 'dd-drop', 'dd-plugin', function(Y) { - //Static Vars - var goingUp = false, lastY = 0; - - var list = Y.Node.all('.course_list .coursebox'); - list.each(function(v, k) { - // Replace move link and image with move_2d image. - var imagenode = v.one('.course_title .move a img'); - imagenode.setAttribute('src', M.util.image_url(MOVEICON.pix, MOVEICON.component)); - imagenode.addClass('cursor'); - v.one('.course_title .move a').replace(imagenode); - - var dd = new Y.DD.Drag({ - node: v, - target: { - padding: '0 0 0 20' - } - }).plug(Y.Plugin.DDProxy, { - moveOnEnd: false - }).plug(Y.Plugin.DDConstrained, { - constrain2node: '.course_list' - }); - dd.addHandle('.course_title .move'); - }); - - Y.DD.DDM.on('drag:start', function(e) { - //Get our drag object - var drag = e.target; - //Set some styles here - drag.get('node').setStyle('opacity', '.25'); - drag.get('dragNode').addClass('block_course_overview'); - drag.get('dragNode').set('innerHTML', drag.get('node').get('innerHTML')); - drag.get('dragNode').setStyles({ - opacity: '.5', - borderColor: drag.get('node').getStyle('borderColor'), - backgroundColor: drag.get('node').getStyle('backgroundColor') - }); - }); - - Y.DD.DDM.on('drag:end', function(e) { - var drag = e.target; - //Put our styles back - drag.get('node').setStyles({ - visibility: '', - opacity: '1' - }); - M.block_course_overview.save(Y); - }); - - Y.DD.DDM.on('drag:drag', function(e) { - //Get the last y point - var y = e.target.lastXY[1]; - //is it greater than the lastY var? - if (y < lastY) { - //We are going up - goingUp = true; - } else { - //We are going down. - goingUp = false; - } - //Cache for next check - lastY = y; - }); - - Y.DD.DDM.on('drop:over', function(e) { - //Get a reference to our drag and drop nodes - var drag = e.drag.get('node'), - drop = e.drop.get('node'); - - //Are we dropping on a li node? - if (drop.hasClass('coursebox')) { - //Are we not going up? - if (!goingUp) { - drop = drop.get('nextSibling'); - } - //Add the node to this list - e.drop.get('node').get('parentNode').insertBefore(drag, drop); - //Resize this nodes shim, so we can drop on it later. - e.drop.sizeShim(); - } - }); - - Y.DD.DDM.on('drag:drophit', function(e) { - var drop = e.drop.get('node'), - drag = e.drag.get('node'); - - //if we are not on an li, we must have been dropped on a ul - if (!drop.hasClass('coursebox')) { - if (!drop.contains(drag)) { - drop.appendChild(drag); - } - } - }); - }); -} - -M.block_course_overview.save = function() { - var Y = M.block_course_overview.Y; - var sortorder = Y.one('.course_list').get('children').getAttribute('id'); - for (var i = 0; i < sortorder.length; i++) { - sortorder[i] = sortorder[i].substring(7); - } - var params = { - sesskey : M.cfg.sesskey, - sortorder : sortorder - }; - Y.io(M.cfg.wwwroot+'/blocks/course_overview/save.php', { - method: 'POST', - data: build_querystring(params), - context: this - }); -} - -/** - * Init a collapsible region, see print_collapsible_region in weblib.php - * @param {YUI} Y YUI3 instance with all libraries loaded - * @param {String} id the HTML id for the div. - * @param {String} userpref the user preference that records the state of this box. false if none. - * @param {String} strtooltip - */ -M.block_course_overview.collapsible = function(Y, id, userpref, strtooltip) { - if (userpref) { - M.block_course_overview.userpref = true; - } - Y.use('anim', function(Y) { - new M.block_course_overview.CollapsibleRegion(Y, id, userpref, strtooltip); - }); -}; - -/** - * Object to handle a collapsible region : instantiate and forget styled object - * - * @class - * @constructor - * @param {YUI} Y YUI3 instance with all libraries loaded - * @param {String} id The HTML id for the div. - * @param {String} userpref The user preference that records the state of this box. false if none. - * @param {String} strtooltip - */ -M.block_course_overview.CollapsibleRegion = function(Y, id, userpref, strtooltip) { - // Record the pref name - this.userpref = userpref; - - // Find the divs in the document. - this.div = Y.one('#'+id); - - // Get the caption for the collapsible region - var caption = this.div.one('#'+id + '_caption'); - caption.setAttribute('title', strtooltip); - - // Create a link - var a = Y.Node.create(''); - // Create a local scoped lamba function to move nodes to a new link - var movenode = function(node){ - node.remove(); - a.append(node); - }; - // Apply the lamba function on each of the captions child nodes - caption.get('children').each(movenode, this); - caption.prepend(a); - - // Get the height of the div at this point before we shrink it if required - var height = this.div.get('offsetHeight'); - if (this.div.hasClass('collapsed')) { - // Shrink the div as it is collapsed by default - this.div.setStyle('height', caption.get('offsetHeight')+'px'); - } - - // Create the animation. - var animation = new Y.Anim({ - node: this.div, - duration: 0.3, - easing: Y.Easing.easeBoth, - to: {height:caption.get('offsetHeight')}, - from: {height:height} - }); - - // Handler for the animation finishing. - animation.on('end', function() { - this.div.toggleClass('collapsed'); - }, this); - - // Hook up the event handler. - caption.on('click', function(e, animation) { - e.preventDefault(); - // Animate to the appropriate size. - if (animation.get('running')) { - animation.stop(); - } - animation.set('reverse', this.div.hasClass('collapsed')); - // Update the user preference. - if (this.userpref) { - M.util.set_user_preference(this.userpref, !this.div.hasClass('collapsed')); - } - animation.run(); - }, this, animation); -}; - -M.block_course_overview.userpref = false; - -/** - * The user preference that stores the state of this box. - * @property userpref - * @type String - */ -M.block_course_overview.CollapsibleRegion.prototype.userpref = null; - -/** - * The key divs that make up this - * @property div - * @type Y.Node - */ -M.block_course_overview.CollapsibleRegion.prototype.div = null; - -/** - * The key divs that make up this - * @property icon - * @type Y.Node - */ -M.block_course_overview.CollapsibleRegion.prototype.icon = null; - diff --git a/blocks/course_overview/move.php b/blocks/course_overview/move.php deleted file mode 100644 index b6f042dd7a1..00000000000 --- a/blocks/course_overview/move.php +++ /dev/null @@ -1,60 +0,0 @@ -. - -/** - * Move/order course functionality for course_overview block. - * - * @package block_course_overview - * @copyright 2012 Adam Olley - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -require_once(__DIR__ . '/../../config.php'); -require_once(__DIR__ . '/locallib.php'); - -require_sesskey(); -require_login(); - -$coursetomove = required_param('courseid', PARAM_INT); -$moveto = required_param('moveto', PARAM_INT); - -list($courses, $sitecourses, $coursecount) = block_course_overview_get_sorted_courses(); -$sortedcourses = array_keys($courses); - -$currentcourseindex = array_search($coursetomove, $sortedcourses); -// If coursetomove is not found or moveto < 0 or > count($sortedcourses) then throw error. -if ($currentcourseindex === false) { - print_error("invalidcourseid", null, null, $coursetomove); -} else if (($moveto < 0) || ($moveto >= count($sortedcourses))) { - print_error("invalidaction"); -} - -// If current course index is same as destination index then don't do anything. -if ($currentcourseindex === $moveto) { - redirect(new moodle_url('/my/index.php')); -} - -// Create neworder list for courses. -$neworder = array(); - -unset($sortedcourses[$currentcourseindex]); -$neworder = array_slice($sortedcourses, 0, $moveto, true); -$neworder[] = $coursetomove; -$remaningcourses = array_slice($sortedcourses, $moveto); -foreach ($remaningcourses as $courseid) { - $neworder[] = $courseid; -} -block_course_overview_update_myorder(array_values($neworder)); -redirect(new moodle_url('/my/index.php')); diff --git a/blocks/course_overview/renderer.php b/blocks/course_overview/renderer.php deleted file mode 100644 index 78c8b0ef33a..00000000000 --- a/blocks/course_overview/renderer.php +++ /dev/null @@ -1,348 +0,0 @@ -. - -/** - * course_overview block rendrer - * - * @package block_course_overview - * @copyright 2012 Adam Olley - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -defined('MOODLE_INTERNAL') || die; - -/** - * Course_overview block rendrer - * - * @copyright 2012 Adam Olley - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class block_course_overview_renderer extends plugin_renderer_base { - - /** - * Construct contents of course_overview block - * - * @param array $courses list of courses in sorted order - * @param array $overviews list of course overviews - * @return string html to be displayed in course_overview block - */ - public function course_overview($courses, $overviews) { - $html = ''; - $config = get_config('block_course_overview'); - if ($config->showcategories != BLOCKS_COURSE_OVERVIEW_SHOWCATEGORIES_NONE) { - global $CFG; - require_once($CFG->libdir.'/coursecatlib.php'); - } - $ismovingcourse = false; - $courseordernumber = 0; - $maxcourses = count($courses); - $userediting = false; - // Intialise string/icon etc if user is editing and courses > 1 - if ($this->page->user_is_editing() && (count($courses) > 1)) { - $userediting = true; - $this->page->requires->js_init_call('M.block_course_overview.add_handles'); - - // Check if course is moving - $ismovingcourse = optional_param('movecourse', FALSE, PARAM_BOOL); - $movingcourseid = optional_param('courseid', 0, PARAM_INT); - } - - // Render first movehere icon. - if ($ismovingcourse) { - // Remove movecourse param from url. - $this->page->ensure_param_not_in_url('movecourse'); - - // Show moving course notice, so user knows what is being moved. - $html .= $this->output->box_start('notice'); - $a = new stdClass(); - $a->fullname = $courses[$movingcourseid]->fullname; - $a->cancellink = html_writer::link($this->page->url, get_string('cancel')); - $html .= get_string('movingcourse', 'block_course_overview', $a); - $html .= $this->output->box_end(); - - $moveurl = new moodle_url('/blocks/course_overview/move.php', - array('sesskey' => sesskey(), 'moveto' => 0, 'courseid' => $movingcourseid)); - // Create move icon, so it can be used. - $name = $courses[$movingcourseid]->fullname; - $movetofirsticon = $this->output->pix_icon('movehere', get_string('movetofirst', 'block_course_overview', $name)); - $moveurl = html_writer::link($moveurl, $movetofirsticon); - $html .= html_writer::tag('div', $moveurl, array('class' => 'movehere')); - } - - foreach ($courses as $key => $course) { - // If moving course, then don't show course which needs to be moved. - if ($ismovingcourse && ($course->id == $movingcourseid)) { - continue; - } - $html .= $this->output->box_start('coursebox', "course-{$course->id}"); - $html .= html_writer::start_tag('div', array('class' => 'course_title')); - // If user is editing, then add move icons. - if ($userediting && !$ismovingcourse) { - $moveicon = $this->output->pix_icon('t/move', get_string('movecourse', 'block_course_overview', $course->fullname)); - $moveurl = new moodle_url($this->page->url, array('sesskey' => sesskey(), 'movecourse' => 1, 'courseid' => $course->id)); - $moveurl = html_writer::link($moveurl, $moveicon); - $html .= html_writer::tag('div', $moveurl, array('class' => 'move')); - - } - - // No need to pass title through s() here as it will be done automatically by html_writer. - $attributes = array('title' => $course->fullname); - if ($course->id > 0) { - if (empty($course->visible)) { - $attributes['class'] = 'dimmed'; - } - $courseurl = new moodle_url('/course/view.php', array('id' => $course->id)); - $coursefullname = format_string(get_course_display_name_for_list($course), true, $course->id); - $link = html_writer::link($courseurl, $coursefullname, $attributes); - $html .= $this->output->heading($link, 2, 'title'); - } else { - $html .= $this->output->heading(html_writer::link( - new moodle_url('/auth/mnet/jump.php', array('hostid' => $course->hostid, 'wantsurl' => '/course/view.php?id='.$course->remoteid)), - format_string($course->shortname, true), $attributes) . ' (' . format_string($course->hostname) . ')', 2, 'title'); - } - $html .= $this->output->container('', 'flush'); - $html .= html_writer::end_tag('div'); - - if (!empty($config->showchildren) && ($course->id > 0)) { - // List children here. - if ($children = block_course_overview_get_child_shortnames($course->id)) { - $html .= html_writer::tag('span', $children, array('class' => 'coursechildren')); - } - } - - // If user is moving courses, then down't show overview. - if (isset($overviews[$course->id]) && !$ismovingcourse) { - $html .= $this->activity_display($course->id, $overviews[$course->id]); - } - - if ($config->showcategories != BLOCKS_COURSE_OVERVIEW_SHOWCATEGORIES_NONE) { - // List category parent or categories path here. - $currentcategory = coursecat::get($course->category, IGNORE_MISSING); - if ($currentcategory !== null) { - $html .= html_writer::start_tag('div', array('class' => 'categorypath')); - if ($config->showcategories == BLOCKS_COURSE_OVERVIEW_SHOWCATEGORIES_FULL_PATH) { - foreach ($currentcategory->get_parents() as $categoryid) { - $category = coursecat::get($categoryid, IGNORE_MISSING); - if ($category !== null) { - $html .= $category->get_formatted_name().' / '; - } - } - } - $html .= $currentcategory->get_formatted_name(); - $html .= html_writer::end_tag('div'); - } - } - - $html .= $this->output->container('', 'flush'); - $html .= $this->output->box_end(); - $courseordernumber++; - if ($ismovingcourse) { - $moveurl = new moodle_url('/blocks/course_overview/move.php', - array('sesskey' => sesskey(), 'moveto' => $courseordernumber, 'courseid' => $movingcourseid)); - $a = new stdClass(); - $a->movingcoursename = $courses[$movingcourseid]->fullname; - $a->currentcoursename = $course->fullname; - $movehereicon = $this->output->pix_icon('movehere', get_string('moveafterhere', 'block_course_overview', $a)); - $moveurl = html_writer::link($moveurl, $movehereicon); - $html .= html_writer::tag('div', $moveurl, array('class' => 'movehere')); - } - } - // Wrap course list in a div and return. - return html_writer::tag('div', $html, array('class' => 'course_list')); - } - - /** - * Coustuct activities overview for a course - * - * @param int $cid course id - * @param array $overview overview of activities in course - * @return string html of activities overview - */ - protected function activity_display($cid, $overview) { - $output = html_writer::start_tag('div', array('class' => 'activity_info')); - foreach (array_keys($overview) as $module) { - $output .= html_writer::start_tag('div', array('class' => 'activity_overview')); - $url = new moodle_url("/mod/$module/index.php", array('id' => $cid)); - $modulename = get_string('modulename', $module); - $icontext = html_writer::link($url, $this->output->image_icon('icon', $modulename, 'mod_'.$module, array('class'=>'iconlarge'))); - if (get_string_manager()->string_exists("activityoverview", $module)) { - $icontext .= get_string("activityoverview", $module); - } else { - $icontext .= get_string("activityoverview", 'block_course_overview', $modulename); - } - - // Add collapsible region with overview text in it. - $output .= $this->collapsible_region($overview[$module], '', 'region_'.$cid.'_'.$module, $icontext, '', true); - - $output .= html_writer::end_tag('div'); - } - $output .= html_writer::end_tag('div'); - return $output; - } - - /** - * Constructs header in editing mode - * - * @param int $max maximum number of courses - * @return string html of header bar. - */ - public function editing_bar_head($max = 0) { - $output = $this->output->box_start('notice'); - - $options = array('0' => get_string('alwaysshowall', 'block_course_overview')); - for ($i = 1; $i <= $max; $i++) { - $options[$i] = $i; - } - $url = new moodle_url('/my/index.php'); - $select = new single_select($url, 'mynumber', $options, block_course_overview_get_max_user_courses(), array()); - $select->set_label(get_string('numtodisplay', 'block_course_overview')); - $output .= $this->output->render($select); - - $output .= $this->output->box_end(); - return $output; - } - - /** - * Show hidden courses count - * - * @param int $total count of hidden courses - * @return string html - */ - public function hidden_courses($total) { - if ($total <= 0) { - return; - } - $output = $this->output->box_start('notice'); - $plural = $total > 1 ? 'plural' : ''; - $config = get_config('block_course_overview'); - // Show view all course link to user if forcedefaultmaxcourses is not empty. - if (!empty($config->forcedefaultmaxcourses)) { - $output .= get_string('hiddencoursecount'.$plural, 'block_course_overview', $total); - } else { - $a = new stdClass(); - $a->coursecount = $total; - $a->showalllink = html_writer::link(new moodle_url('/my/index.php', array('mynumber' => block_course_overview::SHOW_ALL_COURSES)), - get_string('showallcourses')); - $output .= get_string('hiddencoursecountwithshowall'.$plural, 'block_course_overview', $a); - } - - $output .= $this->output->box_end(); - return $output; - } - - /** - * Creates collapsable region - * - * @param string $contents existing contents - * @param string $classes class names added to the div that is output. - * @param string $id id added to the div that is output. Must not be blank. - * @param string $caption text displayed at the top. Clicking on this will cause the region to expand or contract. - * @param string $userpref the name of the user preference that stores the user's preferred default state. - * (May be blank if you do not wish the state to be persisted. - * @param bool $default Initial collapsed state to use if the user_preference it not set. - * @return bool if true, return the HTML as a string, rather than printing it. - */ - protected function collapsible_region($contents, $classes, $id, $caption, $userpref = '', $default = false) { - $output = $this->collapsible_region_start($classes, $id, $caption, $userpref, $default); - $output .= $contents; - $output .= $this->collapsible_region_end(); - - return $output; - } - - /** - * Print (or return) the start of a collapsible region, that has a caption that can - * be clicked to expand or collapse the region. If JavaScript is off, then the region - * will always be expanded. - * - * @param string $classes class names added to the div that is output. - * @param string $id id added to the div that is output. Must not be blank. - * @param string $caption text displayed at the top. Clicking on this will cause the region to expand or contract. - * @param string $userpref the name of the user preference that stores the user's preferred default state. - * (May be blank if you do not wish the state to be persisted. - * @param bool $default Initial collapsed state to use if the user_preference it not set. - * @return bool if true, return the HTML as a string, rather than printing it. - */ - protected function collapsible_region_start($classes, $id, $caption, $userpref = '', $default = false) { - // Work out the initial state. - if (!empty($userpref) and is_string($userpref)) { - user_preference_allow_ajax_update($userpref, PARAM_BOOL); - $collapsed = get_user_preferences($userpref, $default); - } else { - $collapsed = $default; - $userpref = false; - } - - if ($collapsed) { - $classes .= ' collapsed'; - } - - $output = ''; - $output .= '
'; - $output .= '
'; - $output .= '
'; - $output .= $caption . ' '; - $output .= '
'; - $this->page->requires->js_init_call('M.block_course_overview.collapsible', array($id, $userpref, get_string('clicktohideshow'))); - - return $output; - } - - /** - * Close a region started with print_collapsible_region_start. - * - * @return string return the HTML as a string, rather than printing it. - */ - protected function collapsible_region_end() { - $output = '
'; - return $output; - } - - /** - * Cretes html for welcome area - * - * @param int $msgcount number of messages - * @return string html string for welcome area. - */ - public function welcome_area($msgcount) { - global $CFG, $USER; - $output = $this->output->box_start('welcome_area'); - - $picture = $this->output->user_picture($USER, array('size' => 75, 'class' => 'welcome_userpicture')); - $output .= html_writer::tag('div', $picture, array('class' => 'profilepicture')); - - $output .= $this->output->box_start('welcome_message'); - $output .= $this->output->heading(get_string('welcome', 'block_course_overview', $USER->firstname)); - - if (!empty($CFG->messaging)) { - $plural = 's'; - if ($msgcount > 0) { - $output .= get_string('youhavemessages', 'block_course_overview', $msgcount); - if ($msgcount == 1) { - $plural = ''; - } - } else { - $output .= get_string('youhavenomessages', 'block_course_overview'); - } - $output .= html_writer::link(new moodle_url('/message/index.php'), - get_string('message'.$plural, 'block_course_overview')); - } - $output .= $this->output->box_end(); - $output .= $this->output->container('', 'flush'); - $output .= $this->output->box_end(); - - return $output; - } -} diff --git a/blocks/course_overview/settings.php b/blocks/course_overview/settings.php deleted file mode 100644 index d1c42758135..00000000000 --- a/blocks/course_overview/settings.php +++ /dev/null @@ -1,42 +0,0 @@ -. - -/** - * course_overview block settings - * - * @package block_course_overview - * @copyright 2012 Adam Olley - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -defined('MOODLE_INTERNAL') || die; - -if ($ADMIN->fulltree) { - $settings->add(new admin_setting_configtext('block_course_overview/defaultmaxcourses', new lang_string('defaultmaxcourses', 'block_course_overview'), - new lang_string('defaultmaxcoursesdesc', 'block_course_overview'), 10, PARAM_INT)); - $settings->add(new admin_setting_configcheckbox('block_course_overview/forcedefaultmaxcourses', new lang_string('forcedefaultmaxcourses', 'block_course_overview'), - new lang_string('forcedefaultmaxcoursesdesc', 'block_course_overview'), 1, PARAM_INT)); - $settings->add(new admin_setting_configcheckbox('block_course_overview/showchildren', new lang_string('showchildren', 'block_course_overview'), - new lang_string('showchildrendesc', 'block_course_overview'), 1, PARAM_INT)); - $settings->add(new admin_setting_configcheckbox('block_course_overview/showwelcomearea', new lang_string('showwelcomearea', 'block_course_overview'), - new lang_string('showwelcomeareadesc', 'block_course_overview'), 1, PARAM_INT)); - $showcategories = array( - BLOCKS_COURSE_OVERVIEW_SHOWCATEGORIES_NONE => new lang_string('none', 'block_course_overview'), - BLOCKS_COURSE_OVERVIEW_SHOWCATEGORIES_ONLY_PARENT_NAME => new lang_string('onlyparentname', 'block_course_overview'), - BLOCKS_COURSE_OVERVIEW_SHOWCATEGORIES_FULL_PATH => new lang_string('fullpath', 'block_course_overview') - ); - $settings->add(new admin_setting_configselect('block_course_overview/showcategories', new lang_string('showcategories', 'block_course_overview'), - new lang_string('showcategoriesdesc', 'block_course_overview'), BLOCKS_COURSE_OVERVIEW_SHOWCATEGORIES_NONE, $showcategories)); -} diff --git a/blocks/course_overview/styles.css b/blocks/course_overview/styles.css deleted file mode 100644 index 2aab2a35d06..00000000000 --- a/blocks/course_overview/styles.css +++ /dev/null @@ -1,87 +0,0 @@ -.block_course_overview .coursechildren { - font-weight: normal; - font-style: italic; -} - -.block_course_overview .categorypath { - text-align: right; -} - -.block_course_overview .content { - margin: 0 20px; -} - -.block_course_overview .content .notice { - margin: 5px 0; -} - -.block_course_overview .coursebox { - padding: 15px; - width: auto; -} - -.block_course_overview .profilepicture { - float: left; -} - -.block_course_overview .welcome_area { - width: 100%; - padding-bottom: 5px; -} - -.block_course_overview .welcome_message { - float: left; - padding: 10px; - border-collapse: separate; - clear: none; -} - -.block_course_overview .content h2.title { - float: left; - margin: 0 0 .5em 0; - position: relative; -} - -.block_course_overview .course_title { - position: relative; -} - -.editing .block_course_overview .coursebox .cursor { - cursor: move; - margin-bottom: 2px; -} - -.editing .block_course_overview .move { - float: left; - padding: 2px 10px 0 0; -} - -.block_course_overview .course_list { - width: 100%; -} - -.block_course_overview div.flush { - clear: both; -} - -.block_course_overview .activity_info { - clear: both; -} - -.block_course_overview .activity_overview { - padding: 2px; -} - -.block_course_overview .activity_overview img.iconlarge { - vertical-align: text-bottom; - margin-right: 6px; -} - -.block_course_overview .singleselect { - text-align: left; - margin: 0; -} - -.block_course_overview .content .course_list .movehere { - margin-bottom: 15px; -} diff --git a/blocks/course_overview/tests/behat/block_course_overview.feature b/blocks/course_overview/tests/behat/block_course_overview.feature deleted file mode 100644 index 389b6c5353b..00000000000 --- a/blocks/course_overview/tests/behat/block_course_overview.feature +++ /dev/null @@ -1,162 +0,0 @@ -@block @block_course_overview -Feature: View the course overview block on the dashboard and test it's functionality - In order to view the course overview block on the dashboard - As an admin - I can configure the course overview block - - Background: - Given the following "users" exist: - | username | firstname | lastname | email | idnumber | - | student1 | Student | 1 | student1@example.com | S1 | - | teacher1 | Teacher | 1 | teacher1@example.com | T1 | - And the following "categories" exist: - | name | category | idnumber | - | Category 1 | 0 | CAT1 | - | Category 2 | CAT1 | CAT2 | - And the following "courses" exist: - | fullname | shortname | category | - | Course 1 | C1 | 0 | - | Course 2 | C2 | CAT1 | - | Course 3 | C3 | CAT2 | - - Scenario: View the block by a user without any enrolments - Given I log in as "student1" - Then I should see "No course information to show" in the "Course overview" "block" - - Scenario: View the block by a user with several enrolments - Given the following "course enrolments" exist: - | user | course | role | - | student1 | C1 | student | - | student1 | C2 | student | - When I log in as "student1" - Then I should see "Course 1" in the "Course overview" "block" - And I should see "Course 2" in the "Course overview" "block" - - Scenario: View the block by a user with several enrolments and limit the number of courses. - Given the following "course enrolments" exist: - | user | course | role | - | student1 | C1 | student | - | student1 | C2 | student | - | student1 | C3 | student | - When I log in as "student1" - And I press "Customise this page" - And I select "1" from the "Number of courses to display:" singleselect - Then I should see "Course 1" in the "Course overview" "block" - And I should see "You have 2 hidden courses" - And I should not see "Course 2" in the "Course overview" "block" - And I should not see "Course 3" in the "Course overview" "block" - And I follow "Show all courses" - And I should see "Course 1" in the "Course overview" "block" - And I should see "Course 2" in the "Course overview" "block" - And I should see "Course 3" in the "Course overview" "block" - - Scenario: View the block by a user with several enrolments and an admin set default max courses. - Given the following config values are set as admin: - | defaultmaxcourses | 2 | block_course_overview | - And the following "course enrolments" exist: - | user | course | role | - | student1 | C1 | student | - | student1 | C2 | student | - | student1 | C3 | student | - When I log in as "student1" - Then I should see "Course 1" in the "Course overview" "block" - And I should see "Course 2" in the "Course overview" "block" - And I should see "You have 1 hidden course" - And I press "Customise this page" - And I select "Always show all" from the "Number of courses to display:" singleselect - And I should see "Course 3" in the "Course overview" "block" - And I should not see "You have 1 hidden course" - - Scenario: View the block by a user with several enrolments and an admin enforced maximum displayed courses. - Given the following config values are set as admin: - | defaultmaxcourses | 2 | block_course_overview | - | forcedefaultmaxcourses | 1 | block_course_overview | - And the following "course enrolments" exist: - | user | course | role | - | student1 | C1 | student | - | student1 | C2 | student | - | student1 | C3 | student | - When I log in as "student1" - Then I should see "Course 1" in the "Course overview" "block" - And I should see "Course 2" in the "Course overview" "block" - And I should see "You have 1 hidden course" - And I press "Customise this page" - And I should not see "Always show all" - - Scenario: View the block by a user with the welcome area enabled and messaging disabled. - Given the following config values are set as admin: - | showwelcomearea | 1 | block_course_overview | - | messaging | 0 | | - When I log in as "student1" - Then I should see "Welcome Student" in the "Course overview" "block" - And I should not see "messages" in the "Course overview" "block" - - Scenario: View the block by a user with both the welcome area and messaging enabled. - Given the following config values are set as admin: - | showwelcomearea | 1 | block_course_overview | - When I log in as "student1" - Then I should see "Welcome Student" in the "Course overview" "block" - And I should see "You have no unread messages" in the "Course overview" "block" - And I follow "messages" - And I should see "No messages" - - @javascript - Scenario: View the block by a user with the welcome area and the user having messages. - Given the following config values are set as admin: - | showwelcomearea | 1 | block_course_overview | - And I log in as "student1" - And I should see "Welcome Student" in the "Course overview" "block" - And I should see "You have no unread messages" in the "Course overview" "block" - And I follow "messages" - And I send "This is message 1" message to "Teacher 1" user - And I send "This is message 2" message to "Teacher 1" user - When I log out - And I log in as "teacher1" - Then I should see "Welcome Teacher" in the "Course overview" "block" - And I should see "You have 2 unread messages" in the "Course overview" "block" - - Scenario: View the block by a user with the parent categories displayed. - Given the following config values are set as admin: - | showcategories | Parent category only | block_course_overview | - And the following "course enrolments" exist: - | user | course | role | - | student1 | C1 | student | - | student1 | C2 | student | - | student1 | C3 | student | - When I log in as "student1" - Then I should see "Miscellaneous" in the "Course overview" "block" - And I should see "Category 1" in the "Course overview" "block" - And I should see "Category 2" in the "Course overview" "block" - And I should not see "Category 1 / Category 1" in the "Course overview" "block" - - Scenario: View the block by a user with the full categories displayed. - Given the following config values are set as admin: - | showcategories | 2 | block_course_overview | - And the following "course enrolments" exist: - | user | course | role | - | student1 | C1 | student | - | student1 | C2 | student | - | student1 | C3 | student | - When I log in as "student1" - Then I should see "Miscellaneous" in the "Course overview" "block" - And I should see "Category 1 / Category 2" in the "Course overview" "block" - - @javascript - Scenario: View the block by a user with the show children option enabled. - Given the following config values are set as admin: - | showchildren | 1 | block_course_overview | - And the following "course enrolments" exist: - | user | course | role | - | student1 | C1 | student | - And I log in as "admin" - And I navigate to "Manage enrol plugins" node in "Site administration > Plugins > Enrolments" - And I click on "Enable" "link" in the "Course meta link" "table_row" - And I am on site homepage - And I follow "Course 2" - And I add "Course meta link" enrolment method with: - | Link course | C1 | - And I log out - When I log in as "student1" - Then I should see "Course 1" in the "Course overview" "block" - And I should see "Course 2" in the "Course overview" "block" - And I should see "Includes C1" in the "Course overview" "block" diff --git a/blocks/course_overview/tests/behat/quiz_overview.feature b/blocks/course_overview/tests/behat/quiz_overview.feature deleted file mode 100644 index c91c551fcf1..00000000000 --- a/blocks/course_overview/tests/behat/quiz_overview.feature +++ /dev/null @@ -1,93 +0,0 @@ -@block @block_course_overview @mod_quiz -Feature: View the quiz being due - In order to know what quizzes are due - As a student - I can visit my dashboard - - Background: - Given the following "users" exist: - | username | firstname | lastname | email | - | student1 | Student | 1 | student1@example.com | - | student2 | Student | 2 | student2@example.com | - | teacher1 | Teacher | 1 | teacher1@example.com | - And the following "courses" exist: - | fullname | shortname | - | Course 1 | C1 | - | Course 2 | C2 | - And the following "course enrolments" exist: - | user | course | role | - | student1 | C1 | student | - | student2 | C2 | student | - | teacher1 | C1 | editingteacher | - | teacher1 | C2 | editingteacher | - And the following "activities" exist: - | activity | course | idnumber | name | timeclose | - | quiz | C1 | Q1A | Quiz 1A No deadline | 0 | - | quiz | C1 | Q1B | Quiz 1B Past deadline | 1337 | - | quiz | C1 | Q1C | Quiz 1C Future deadline | 9000000000 | - | quiz | C1 | Q1D | Quiz 1D Future deadline | 9000000000 | - | quiz | C1 | Q1E | Quiz 1E Future deadline | 9000000000 | - | quiz | C2 | Q2A | Quiz 2A Future deadline | 9000000000 | - And the following "question categories" exist: - | contextlevel | reference | name | - | Course | C1 | Test questions | - And the following "questions" exist: - | qtype | name | questiontext | questioncategory | - | truefalse | First question | Answer the first question | Test questions | - And quiz "Quiz 1A No deadline" contains the following questions: - | question | page | - | First question | 1 | - And quiz "Quiz 1B Past deadline" contains the following questions: - | question | page | - | First question | 1 | - And quiz "Quiz 1C Future deadline" contains the following questions: - | question | page | - | First question | 1 | - And quiz "Quiz 1D Future deadline" contains the following questions: - | question | page | - | First question | 1 | - And quiz "Quiz 1E Future deadline" contains the following questions: - | question | page | - | First question | 1 | - And quiz "Quiz 2A Future deadline" contains the following questions: - | question | page | - | First question | 1 | - - Scenario: View my quizzes that are due - Given I log in as "student1" - When I am on homepage - Then I should see "You have quizzes that are due" in the "Course overview" "block" - And I should see "Quiz 1C Future deadline" in the "Course overview" "block" - And I should see "Quiz 1D Future deadline" in the "Course overview" "block" - And I should see "Quiz 1E Future deadline" in the "Course overview" "block" - And I should not see "Quiz 1A No deadline" in the "Course overview" "block" - And I should not see "Quiz 1B Past deadline" in the "Course overview" "block" - And I should not see "Quiz 2A Future deadline" in the "Course overview" "block" - And I log out - And I log in as "student2" - And I should see "You have quizzes that are due" in the "Course overview" "block" - And I should not see "Quiz 1C Future deadline" in the "Course overview" "block" - And I should not see "Quiz 1D Future deadline" in the "Course overview" "block" - And I should not see "Quiz 1E Future deadline" in the "Course overview" "block" - And I should not see "Quiz 1A No deadline" in the "Course overview" "block" - And I should not see "Quiz 1B Past deadline" in the "Course overview" "block" - And I should see "Quiz 2A Future deadline" in the "Course overview" "block" - - Scenario: View my quizzes that are due and never finished - Given I log in as "student1" - And I follow "Course 1" - And I follow "Quiz 1D Future deadline" - And I press "Attempt quiz now" - And I follow "Finish attempt ..." - And I press "Submit all and finish" - And I follow "Course 1" - And I follow "Quiz 1E Future deadline" - And I press "Attempt quiz now" - When I am on homepage - Then I should see "You have quizzes that are due" in the "Course overview" "block" - And I should see "Quiz 1C Future deadline" in the "Course overview" "block" - And I should see "Quiz 1E Future deadline" in the "Course overview" "block" - And I should not see "Quiz 1A No deadline" in the "Course overview" "block" - And I should not see "Quiz 1B Past deadline" in the "Course overview" "block" - And I should not see "Quiz 1D Future deadline" in the "Course overview" "block" - And I should not see "Quiz 2A Future deadline" in the "Course overview" "block" diff --git a/blocks/course_summary/tests/behat/block_course_summary_course.feature b/blocks/course_summary/tests/behat/block_course_summary_course.feature index defb9f7f527..3b4b32cdd07 100644 --- a/blocks/course_summary/tests/behat/block_course_summary_course.feature +++ b/blocks/course_summary/tests/behat/block_course_summary_course.feature @@ -17,21 +17,20 @@ Feature: Course summary block used in a course | student1 | C101 | student | | teacher1 | C101 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Course/site summary" block And I log out Scenario: Student can view course summary When I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage Then "Course summary" "block" should exist And I should see "Course summary" in the "Course summary" "block" And I should see "Proved the course summary block works!" in the "Course summary" "block" Scenario: Teacher can not see edit icon when edit mode is off When I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage Then I should see "Proved the course summary block works!" in the "Course summary" "block" And I should see "Course summary" in the "Course summary" "block" And "Edit" "link" should not exist in the "Course summary" "block" diff --git a/blocks/glossary_random/tests/behat/glossary_random.feature b/blocks/glossary_random/tests/behat/glossary_random.feature index 974a1ffafe1..423c1952fae 100644 --- a/blocks/glossary_random/tests/behat/glossary_random.feature +++ b/blocks/glossary_random/tests/behat/glossary_random.feature @@ -19,13 +19,12 @@ Feature: Random glossary entry block is used in a course Scenario: Student can not see the block if it is not configured When I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Random glossary entry" block Then I should see "Please configure this block using the edit icon" in the "block_glossary_random" "block" And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And "block_glossary_random" "block" should not exist And I log out @@ -34,8 +33,7 @@ Feature: Random glossary entry block is used in a course | activity | name | intro | course | idnumber | defaultapproval | | glossary | GlossaryAuto | Test glossary description | C1 | glossary1 | 1 | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Random glossary entry" block And I configure the "block_glossary_random" block And I set the following fields to these values: @@ -45,14 +43,14 @@ Feature: Random glossary entry block is used in a course And I press "Save changes" And I log out When I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage Then I should see "There are no entries yet in the chosen glossary" in the "AutoGlossaryblock" "block" And I click on "Add a new entry" "link" in the "AutoGlossaryblock" "block" And I set the following fields to these values: | Concept | Concept1 | | Definition | Definition1 | And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Concept1" in the "AutoGlossaryblock" "block" And I should see "Definition1" in the "AutoGlossaryblock" "block" And I should not see "There are no entries yet in the chosen glossary" in the "AutoGlossaryblock" "block" @@ -61,7 +59,7 @@ Feature: Random glossary entry block is used in a course | Concept | Concept2 | | Definition | Definition2 | And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage # Only the last entry appears in the block And I should not see "Concept1" in the "AutoGlossaryblock" "block" And I should not see "Definition1" in the "AutoGlossaryblock" "block" @@ -78,8 +76,7 @@ Feature: Random glossary entry block is used in a course | activity | name | intro | course | idnumber | defaultapproval | | glossary | GlossaryManual | Test glossary description | C1 | glossary2 | 0 | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Random glossary entry" block And I configure the "block_glossary_random" block And I set the following fields to these values: @@ -89,18 +86,18 @@ Feature: Random glossary entry block is used in a course And I press "Save changes" And I log out When I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage Then I should see "There are no entries yet in the chosen glossary" in the "ManualGlossaryblock" "block" And I click on "Add a new entry" "link" in the "ManualGlossaryblock" "block" And I set the following fields to these values: | Concept | Concept1 | | Definition | Definition1 | And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "There are no entries yet in the chosen glossary" in the "ManualGlossaryblock" "block" And I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "There are no entries yet in the chosen glossary" in the "ManualGlossaryblock" "block" And I follow "GlossaryManual" And I follow "Waiting approval" diff --git a/blocks/glossary_random/tests/behat/glossary_random_global.feature b/blocks/glossary_random/tests/behat/glossary_random_global.feature index 2eea426a972..33297029893 100644 --- a/blocks/glossary_random/tests/behat/glossary_random_global.feature +++ b/blocks/glossary_random/tests/behat/glossary_random_global.feature @@ -23,8 +23,7 @@ Feature: Random glossary entry block linking to global glossary Scenario: View random (last) entry in the global glossary When I log in as "admin" - And I am on site homepage - And I follow "Course 2" + And I am on "Course 2" course homepage And I follow "Tips and Tricks" And I press "Add a new entry" And I set the following fields to these values: @@ -34,8 +33,7 @@ Feature: Random glossary entry block linking to global glossary And I log out # As a teacher add a block to the course page linking to the global glossary. And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Random glossary entry" block And I configure the "block_glossary_random" block And I set the following fields to these values: @@ -49,7 +47,7 @@ Feature: Random glossary entry block linking to global glossary And I log out # Student who can't see the module is still able to view entries in this block (because the glossary was marked as global) And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Never come late" in the "Tip of the day" "block" And I should not see "Add a new entry" in the "Tip of the day" "block" And I should see "View all entries" in the "Tip of the day" "block" @@ -57,8 +55,7 @@ Feature: Random glossary entry block linking to global glossary Scenario: Removing the global glossary that is used in random glossary block And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Random glossary entry" block And I configure the "block_glossary_random" block And I set the following fields to these values: @@ -68,17 +65,15 @@ Feature: Random glossary entry block linking to global glossary And I press "Save changes" And I log out And I log in as "admin" - And I am on site homepage - And I follow "Course 2" + And I am on "Course 2" course homepage And I follow "Tips and Tricks" And I follow "Edit settings" And I set the field "globalglossary" to "0" And I press "Save and return to course" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage Then I should see "Please configure this block using the edit icon." in the "Tip of the day" "block" And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And "Tip of the day" "block" should not exist And I log out diff --git a/blocks/html/tests/behat/course_block.feature b/blocks/html/tests/behat/course_block.feature index 0815badd43d..08e233e6416 100644 --- a/blocks/html/tests/behat/course_block.feature +++ b/blocks/html/tests/behat/course_block.feature @@ -17,8 +17,7 @@ Feature: HTML blocks in a course | teacher1 | C1 | editingteacher | | student1 | C1 | student | When I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "HTML" block And I configure the "(new HTML block)" block And I set the field "Content" to "First block content" @@ -31,6 +30,6 @@ Feature: HTML blocks in a course And I press "Save changes" And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "First block content" in the "First block header" "block" And I should see "Second block content" in the "Second block header" "block" \ No newline at end of file diff --git a/blocks/messages/tests/behat/block_messages_course.feature b/blocks/messages/tests/behat/block_messages_course.feature index 43446c2e82a..63dd4a511a3 100644 --- a/blocks/messages/tests/behat/block_messages_course.feature +++ b/blocks/messages/tests/behat/block_messages_course.feature @@ -21,15 +21,13 @@ Feature: The messages block allows users to list new messages an a course Given the following config values are set as admin: | messaging | 0 | And I log in as "teacher1" - And I follow "Course 1" - When I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Messages" block Then I should see "Messaging is disabled on this site" in the "Messages" "block" Scenario: View the block by a user who does not have any messages. Given I log in as "teacher1" - And I follow "Course 1" - When I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Messages" block Then I should see "No messages" in the "Messages" "block" @@ -41,20 +39,18 @@ Feature: The messages block allows users to list new messages an a course And I send "This is message 2" message to "Teacher 1" user And I log out And I log in as "teacher1" - And I follow "Course 1" - When I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Messages" block Then I should see "Student 1" in the "Messages" "block" @javascript Scenario: Use the block to send a message to a user. Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Messages" block And I click on "//a[normalize-space(.) = 'Messages']" "xpath_element" in the "Messages" "block" And I send "This is message 1" message to "Student 1" user And I log out When I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage Then I should see "Teacher 1" in the "Messages" "block" diff --git a/blocks/myoverview/amd/build/calendar_events_repository.min.js b/blocks/myoverview/amd/build/calendar_events_repository.min.js new file mode 100644 index 00000000000..5c6e35a1419 --- /dev/null +++ b/blocks/myoverview/amd/build/calendar_events_repository.min.js @@ -0,0 +1 @@ +define(["jquery","core/ajax","core/notification"],function(a,b,c){var d=20,e=function(a){a.hasOwnProperty("limit")||(a.limit=d),a.limitnum=a.limit,delete a.limit,a.hasOwnProperty("starttime")&&(a.timesortfrom=a.starttime,delete a.starttime),a.hasOwnProperty("endtime")&&(a.timesortto=a.endtime,delete a.endtime);var e={methodname:"core_calendar_get_action_events_by_course",args:a},f=b.call([e])[0];return f.fail(c.exception),f},f=function(a){a.hasOwnProperty("limit")||(a.limit=10),a.limitnum=a.limit,delete a.limit,a.hasOwnProperty("starttime")&&(a.timesortfrom=a.starttime,delete a.starttime),a.hasOwnProperty("endtime")&&(a.timesortto=a.endtime,delete a.endtime);var d={methodname:"core_calendar_get_action_events_by_courses",args:a},e=b.call([d])[0];return e.fail(c.exception),e},g=function(a){a.hasOwnProperty("limit")||(a.limit=d),a.limitnum=a.limit,delete a.limit,a.hasOwnProperty("starttime")&&(a.timesortfrom=a.starttime,delete a.starttime),a.hasOwnProperty("endtime")&&(a.timesortto=a.endtime,delete a.endtime);var e={methodname:"core_calendar_get_action_events_by_timesort",args:a},f=b.call([e])[0];return f.fail(c.exception),f};return{queryByTime:g,queryByCourse:e,queryByCourses:f}}); \ No newline at end of file diff --git a/blocks/myoverview/amd/build/event_list.min.js b/blocks/myoverview/amd/build/event_list.min.js new file mode 100644 index 00000000000..7ebbaf32a39 --- /dev/null +++ b/blocks/myoverview/amd/build/event_list.min.js @@ -0,0 +1 @@ +define(["jquery","core/notification","core/templates","core/custom_interaction_events","block_myoverview/calendar_events_repository"],function(a,b,c,d,e){var f=86400,g={EMPTY_MESSAGE:'[data-region="empty-message"]',ROOT:'[data-region="event-list-container"]',EVENT_LIST:'[data-region="event-list"]',EVENT_LIST_CONTENT:'[data-region="event-list-content"]',EVENT_LIST_GROUP_CONTAINER:'[data-region="event-list-group-container"]',LOADING_ICON_CONTAINER:'[data-region="loading-icon-container"]',VIEW_MORE_BUTTON:'[data-action="view-more"]'},h={EVENT_LIST_ITEMS:"block_myoverview/event-list-items",COURSE_EVENT_LIST_ITEMS:"block_myoverview/course-event-list-items"},i=function(a){a.attr("data-loaded-all",!0)},j=function(a){return!!a.attr("data-loaded-all")},k=function(a){var b=a.find(g.LOADING_ICON_CONTAINER),c=a.find(g.VIEW_MORE_BUTTON);a.addClass("loading"),b.removeClass("hidden"),c.prop("disabled",!0)},l=function(a){var b=a.find(g.LOADING_ICON_CONTAINER),c=a.find(g.VIEW_MORE_BUTTON);a.removeClass("loading"),b.addClass("hidden"),j(a)||c.prop("disabled",!1)},m=function(a){return a.hasClass("loading")},n=function(a){a.attr("data-has-events",!0)},o=function(a){return!!a.attr("data-has-events")},p=function(a,b){b?n(a):o(a)||q(a)},q=function(a){a.find(g.EVENT_LIST_CONTENT).addClass("hidden"),a.find(g.EMPTY_MESSAGE).removeClass("hidden")},r=function(a,b,d){return a.removeClass("hidden"),c.render(d,{events:b}).done(function(b,d){c.appendNodeContents(a.find(g.EVENT_LIST),b,d)})},s=function(a,b){var c=b.timesort||0;return c-a},t=function(a,b){var c=Math.floor((new Date).setHours(0,0,0,0)/1e3),d=+b.attr("data-start-day")*f,e=+b.attr("data-end-day")*f,g=s(c,a);return""===b.attr("data-end-day")?d<=g:d<=g&&g. + +/** + * A javascript module to retrieve calendar events from the server. + * + * @module block_myoverview/calendar_events_repository + * @class repository + * @package block_myoverview + * @copyright 2016 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notification) { + + var DEFAULT_LIMIT = 20; + + /** + * Retrieve a list of calendar events for the logged in user for the + * given course. + * + * Valid args are: + * int courseid Only get events for this course + * int starttime Only get events after this time + * int endtime Only get events before this time + * int limit Limit the number of results returned + * int aftereventid Offset the result set from the given id + * + * @method queryByCourse + * @param {object} args The request arguments + * @return {promise} Resolved with an array of the calendar events + */ + var queryByCourse = function(args) { + if (!args.hasOwnProperty('limit')) { + args.limit = DEFAULT_LIMIT; + } + + args.limitnum = args.limit; + delete args.limit; + + if (args.hasOwnProperty('starttime')) { + args.timesortfrom = args.starttime; + delete args.starttime; + } + + if (args.hasOwnProperty('endtime')) { + args.timesortto = args.endtime; + delete args.endtime; + } + + var request = { + methodname: 'core_calendar_get_action_events_by_course', + args: args + }; + + var promise = Ajax.call([request])[0]; + + promise.fail(Notification.exception); + + return promise; + }; + + /** + * Retrieve a list of calendar events for the given courses for the + * logged in user. + * + * Valid args are: + * array courseids Get events for these courses + * int starttime Only get events after this time + * int endtime Only get events before this time + * int limit Limit the number of results returned + * + * @method queryByCourses + * @param {object} args The request arguments + * @return {promise} Resolved with an array of the calendar events + */ + var queryByCourses = function(args) { + if (!args.hasOwnProperty('limit')) { + // This is intentionally smaller than the default limit. + args.limit = 10; + } + + args.limitnum = args.limit; + delete args.limit; + + if (args.hasOwnProperty('starttime')) { + args.timesortfrom = args.starttime; + delete args.starttime; + } + + if (args.hasOwnProperty('endtime')) { + args.timesortto = args.endtime; + delete args.endtime; + } + + var request = { + methodname: 'core_calendar_get_action_events_by_courses', + args: args + }; + + var promise = Ajax.call([request])[0]; + + promise.fail(Notification.exception); + + return promise; + }; + + /** + * Retrieve a list of calendar events for the logged in user after the given + * time. + * + * Valid args are: + * int starttime Only get events after this time + * int endtime Only get events before this time + * int limit Limit the number of results returned + * int aftereventid Offset the result set from the given id + * + * @method queryByTime + * @param {object} args The request arguments + * @return {promise} Resolved with an array of the calendar events + */ + var queryByTime = function(args) { + if (!args.hasOwnProperty('limit')) { + args.limit = DEFAULT_LIMIT; + } + + args.limitnum = args.limit; + delete args.limit; + + if (args.hasOwnProperty('starttime')) { + args.timesortfrom = args.starttime; + delete args.starttime; + } + + if (args.hasOwnProperty('endtime')) { + args.timesortto = args.endtime; + delete args.endtime; + } + + var request = { + methodname: 'core_calendar_get_action_events_by_timesort', + args: args + }; + + var promise = Ajax.call([request])[0]; + + promise.fail(Notification.exception); + + return promise; + }; + + return { + queryByTime: queryByTime, + queryByCourse: queryByCourse, + queryByCourses: queryByCourses, + }; +}); diff --git a/blocks/myoverview/amd/src/event_list.js b/blocks/myoverview/amd/src/event_list.js new file mode 100644 index 00000000000..ee665491bc5 --- /dev/null +++ b/blocks/myoverview/amd/src/event_list.js @@ -0,0 +1,413 @@ +// 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 . + +/** + * Javascript to load and render the list of calendar events for a + * given day range. + * + * @module block_myoverview/event_list + * @package block_myoverview + * @copyright 2016 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/notification', 'core/templates', + 'core/custom_interaction_events', + 'block_myoverview/calendar_events_repository'], + function($, Notification, Templates, CustomEvents, CalendarEventsRepository) { + + var SECONDS_IN_DAY = 60 * 60 * 24; + + var SELECTORS = { + EMPTY_MESSAGE: '[data-region="empty-message"]', + ROOT: '[data-region="event-list-container"]', + EVENT_LIST: '[data-region="event-list"]', + EVENT_LIST_CONTENT: '[data-region="event-list-content"]', + EVENT_LIST_GROUP_CONTAINER: '[data-region="event-list-group-container"]', + LOADING_ICON_CONTAINER: '[data-region="loading-icon-container"]', + VIEW_MORE_BUTTON: '[data-action="view-more"]' + }; + + var TEMPLATES = { + EVENT_LIST_ITEMS: 'block_myoverview/event-list-items', + COURSE_EVENT_LIST_ITEMS: 'block_myoverview/course-event-list-items' + }; + + /** + * Set a flag on the element to indicate that it has completed + * loading all event data. + * + * @method setLoadedAll + * @private + * @param {object} root The container element + */ + var setLoadedAll = function(root) { + root.attr('data-loaded-all', true); + }; + + /** + * Check if all event data has finished loading. + * + * @method hasLoadedAll + * @private + * @param {object} root The container element + * @return {bool} if the element has completed all loading + */ + var hasLoadedAll = function(root) { + return !!root.attr('data-loaded-all'); + }; + + /** + * Set the element state to loading. + * + * @method startLoading + * @private + * @param {object} root The container element + */ + var startLoading = function(root) { + var loadingIcon = root.find(SELECTORS.LOADING_ICON_CONTAINER), + viewMoreButton = root.find(SELECTORS.VIEW_MORE_BUTTON); + + root.addClass('loading'); + loadingIcon.removeClass('hidden'); + viewMoreButton.prop('disabled', true); + }; + + /** + * Remove the loading state from the element. + * + * @method stopLoading + * @private + * @param {object} root The container element + */ + var stopLoading = function(root) { + var loadingIcon = root.find(SELECTORS.LOADING_ICON_CONTAINER), + viewMoreButton = root.find(SELECTORS.VIEW_MORE_BUTTON); + + root.removeClass('loading'); + loadingIcon.addClass('hidden'); + + if (!hasLoadedAll(root)) { + // Only enable the button if we've got more events to load. + viewMoreButton.prop('disabled', false); + } + }; + + /** + * Check if the element is currently loading some event data. + * + * @method isLoading + * @private + * @param {object} root The container element + * @returns {Boolean} + */ + var isLoading = function(root) { + return root.hasClass('loading'); + }; + + /** + * Flag the root element to remember that it contains events. + * + * @method setHasContent + * @private + * @param {object} root The container element + */ + var setHasContent = function(root) { + root.attr('data-has-events', true); + }; + + /** + * Check if the root element has had events loaded. + * + * @method hasContent + * @private + * @param {object} root The container element + * @return {bool} + */ + var hasContent = function(root) { + return root.attr('data-has-events') ? true : false; + }; + + /** + * Update the visibility of the content area. The content area + * is hidden if we have no events. + * + * @method updateContentVisibility + * @private + * @param {object} root The container element + * @param {int} eventCount A count of the events we just received. + */ + var updateContentVisibility = function(root, eventCount) { + if (eventCount) { + // We've rendered some events, let's remember that. + setHasContent(root); + } else { + // If this is the first time trying to load events and + // we don't have any then there isn't any so let's show + // the empty message. + if (!hasContent(root)) { + hideContent(root); + } + } + }; + + /** + * Hide the content area and display the empty content message. + * + * @method hideContent + * @private + * @param {object} root The container element + */ + var hideContent = function(root) { + root.find(SELECTORS.EVENT_LIST_CONTENT).addClass('hidden'); + root.find(SELECTORS.EMPTY_MESSAGE).removeClass('hidden'); + }; + + /** + * Render a group of calendar events and add them to the event + * list. + * + * @method renderGroup + * @private + * @param {object} group The group container element + * @param {array} calendarEvents The list of calendar events + * @param {string} templateName The template name + * @return {promise} Resolved when the elements are attached to the DOM + */ + var renderGroup = function(group, calendarEvents, templateName) { + + group.removeClass('hidden'); + + return Templates.render( + templateName, + {events: calendarEvents} + ).done(function(html, js) { + Templates.appendNodeContents(group.find(SELECTORS.EVENT_LIST), html, js); + }); + }; + + /** + * Determine the time (in seconds) from the given timestamp until the calendar + * event will need actioning. + * + * @method timeUntilEvent + * @private + * @param {int} timestamp The time to compare with + * @param {object} event The calendar event + * @return {int} + */ + var timeUntilEvent = function(timestamp, event) { + var orderTime = event.timesort || 0; + return orderTime - timestamp; + }; + + /** + * Check if the given calendar event should be added to the given event + * list group container. The event list group container will specify a + * day range for the time boundary it is interested in. + * + * If only a start day is specified for the container then it will be treated + * as an open catchment for all events that begin after that time. + * + * @method eventBelongsInContainer + * @private + * @param {object} event The calendar event + * @param {object} container The group event list container + * @return {bool} + */ + var eventBelongsInContainer = function(event, container) { + var todayTime = Math.floor(new Date().setHours(0, 0, 0, 0) / 1000), + timeUntilContainerStart = +container.attr('data-start-day') * SECONDS_IN_DAY, + timeUntilContainerEnd = +container.attr('data-end-day') * SECONDS_IN_DAY, + timeUntilEventNeedsAction = timeUntilEvent(todayTime, event); + + if (container.attr('data-end-day') === '') { + return timeUntilContainerStart <= timeUntilEventNeedsAction; + } else { + return timeUntilContainerStart <= timeUntilEventNeedsAction && + timeUntilEventNeedsAction < timeUntilContainerEnd; + } + }; + + /** + * Return a function that can be used to filter a list of events based on the day + * range specified on the given event list group container. + * + * @method getFilterCallbackForContainer + * @private + * @param {object} container Event list group container + * @return {function} + */ + var getFilterCallbackForContainer = function(container) { + return function(event) { + return eventBelongsInContainer(event, $(container)); + }; + }; + + /** + * Render the given calendar events in the container element. The container + * elements must have a day range defined using data attributes that will be + * used to group the calendar events according to their order time. + * + * @method render + * @private + * @param {object} root The container element + * @param {array} calendarEvents A list of calendar events + * @return {promise} Resolved with a count of the number of rendered events + */ + var render = function(root, calendarEvents) { + var renderCount = 0; + var templateName = TEMPLATES.EVENT_LIST_ITEMS; + + if (root.attr('data-course-id')) { + templateName = TEMPLATES.COURSE_EVENT_LIST_ITEMS; + } + + // Loop over each of the element list groups and find the set of calendar events + // that belong to that group (as defined by the group's day range). The matching + // list of calendar events are rendered and added to the DOM within that group. + return $.when.apply($, $.map(root.find(SELECTORS.EVENT_LIST_GROUP_CONTAINER), function(container) { + var events = calendarEvents.filter(getFilterCallbackForContainer(container)); + + if (events.length) { + renderCount += events.length; + return renderGroup($(container), events, templateName); + } else { + return null; + } + })).then(function() { + return renderCount; + }); + }; + + /** + * Retrieve a list of calendar events, render and append them to the end of the + * existing list. The events will be loaded based on the set of data attributes + * on the root element. + * + * This function can be provided with a jQuery promise. If it is then it won't + * attempt to load data by itself, instead it will use the given promise. + * + * The provided promise must resolve with an an object that has an events key + * and value is an array of calendar events. + * E.g. + * { events: ['event 1', 'event 2'] } + * + * @method load + * @param {object} root The root element of the event list + * @param {object} promise A jQuery promise resolved with events + * @return {promise} A jquery promise + */ + var load = function(root, promise) { + root = $(root); + var limit = +root.attr('data-limit'), + courseId = +root.attr('data-course-id'), + lastId = root.attr('data-last-id'), + date = new Date(), + startTime; + + date.setDate(date.getDate() - 14); + date.setHours(0, 0, 0, 0); + startTime = Math.floor(date / 1000); + + // Don't load twice. + if (isLoading(root)) { + return $.Deferred().resolve(); + } + + startLoading(root); + + // If we haven't been provided a promise to resolve the + // data then we will load our own. + if (typeof promise == 'undefined') { + var args = { + starttime: startTime, + limit: limit, + }; + + if (lastId) { + args.aftereventid = lastId; + } + + // If we have a course id then we only want events from that course. + if (courseId) { + args.courseid = courseId; + promise = CalendarEventsRepository.queryByCourse(args); + } else { + // Otherwise we want events from any course. + promise = CalendarEventsRepository.queryByTime(args); + } + } + + // Request data from the server. + return promise.then(function(result) { + return result.events; + }).then(function(calendarEvents) { + if (!calendarEvents.length || (calendarEvents.length < limit)) { + // We have no more events so mark the list as done. + setLoadedAll(root); + } + + if (calendarEvents.length) { + // Remember the last id we've seen. + root.attr('data-last-id', calendarEvents[calendarEvents.length - 1].id); + + // Render the events. + return render(root, calendarEvents).then(function(renderCount) { + updateContentVisibility(root, calendarEvents.length); + + if (renderCount < calendarEvents.length) { + // if the number of events that was rendered is less than + // the number we sent for rendering we can assume that there + // are no groups to add them in. Since the ordering of the + // events is guaranteed it means that any future requests will + // also yield events that can't be rendered, so let's not bother + // sending any more requests. + setLoadedAll(root); + } + }); + } else { + updateContentVisibility(root, calendarEvents.length); + } + }).fail( + Notification.exception + ).always(function() { + stopLoading(root); + }); + }; + + /** + * Register the event listeners for the container element. + * + * @method registerEventListeners + * @param {object} root The root element of the event list + */ + var registerEventListeners = function(root) { + CustomEvents.define(root, [CustomEvents.events.activate]); + root.on(CustomEvents.events.activate, SELECTORS.VIEW_MORE_BUTTON, function() { + load(root); + }); + }; + + return { + init: function(root) { + root = $(root); + load(root); + registerEventListeners(root); + }, + registerEventListeners: registerEventListeners, + load: load, + rootSelector: SELECTORS.ROOT, + }; +}); diff --git a/blocks/myoverview/amd/src/event_list_by_course.js b/blocks/myoverview/amd/src/event_list_by_course.js new file mode 100644 index 00000000000..db182eeb3ad --- /dev/null +++ b/blocks/myoverview/amd/src/event_list_by_course.js @@ -0,0 +1,106 @@ +// 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 . + +/** + * Javascript to load and render the list of calendar events grouping by course. + * + * @module block_myoverview/events_by_course_list + * @package block_myoverview + * @copyright 2016 Simey Lameze + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define( +[ + 'jquery', + 'block_myoverview/event_list', + 'block_myoverview/calendar_events_repository' +], +function($, EventList, EventsRepository) { + + var SELECTORS = { + EVENTS_BY_COURSE_CONTAINER: '[data-region="course-events-container"]', + }; + + /** + * Loop through course events containers and load calendar events for that course. + * + * @method load + * @param {Object} root The root element of sort by course list. + */ + var load = function(root) { + var courseBlocks = root.find(SELECTORS.EVENTS_BY_COURSE_CONTAINER); + + if (!courseBlocks.length) { + return; + } + + var date = new Date(); + date.setDate(date.getDate() - 14); + date.setHours(0, 0, 0, 0); + var startTime = Math.floor(date / 1000); + var limit = courseBlocks.attr('data-limit'); + var courseIds = courseBlocks.map(function() { + return $(this).attr('data-course-id'); + }).get(); + + // Load the first set of events for each course in a single request. + // We want to avoid sending an individual request for each course because + // there could be lots of them. + var coursesPromise = EventsRepository.queryByCourses({ + courseids: courseIds, + starttime: startTime, + limit: limit + }); + + // Load the events into each course block. + courseBlocks.each(function(index, container) { + container = $(container); + var courseId = container.attr('data-course-id'); + var eventListContainer = container.find(EventList.rootSelector); + var promise = $.Deferred(); + + // Once all of the course events have been loaded then we need + // to extract just the ones relevant to this course block and + // hand them to the event list to render. + coursesPromise.done(function(result) { + var events = []; + // Get this course block's events from the collection returned + // from the server. + var courseGroup = result.groupedbycourse.filter(function(group) { + return group.courseid == courseId; + }); + + if (courseGroup.length) { + events = courseGroup[0].events; + } + + promise.resolve({events: events}); + }).fail(function(e) { + promise.reject(e); + }); + + // Provide the event list with a promise that will be resolved + // when we have received the events from the server. + EventList.load(eventListContainer, promise); + }); + }; + + return { + init: function(root) { + root = $(root); + load(root); + } + }; +}); diff --git a/blocks/myoverview/amd/src/paging_bar.js b/blocks/myoverview/amd/src/paging_bar.js new file mode 100644 index 00000000000..e153e2d5762 --- /dev/null +++ b/blocks/myoverview/amd/src/paging_bar.js @@ -0,0 +1,102 @@ +// 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 . + +/** + * Javascript to load and render the paging bar. + * + * @module block_myoverview/paging_bar + * @package block_myoverview + * @copyright 2016 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/custom_interaction_events'], + function($, CustomEvents) { + + var SELECTORS = { + ROOT: '[data-region="paging-bar"]', + PAGE_ITEM: '[data-region="page-item"]', + ACTIVE_PAGE_ITEM: '[data-region="page-item"].active' + }; + + var EVENTS = { + PAGE_SELECTED: 'block_myoverview-paging-bar-page-selected', + }; + + /** + * Get the page element by number. + * + * @param {object} root The root element. + * @param {Number} pageNumber The page number. + * @returns {*} + */ + var getPageByNumber = function(root, pageNumber) { + return root.find(SELECTORS.PAGE_ITEM + '[data-page-number="' + pageNumber + '"]'); + }; + + /** + * Get the page number. + * + * @param {object} root The root element. + * @param {object} page The page. + * @returns {*} the page number + */ + var getPageNumber = function(root, page) { + var pageNumber = page.attr('data-page-number'); + + if (pageNumber == 'first') { + pageNumber = 1; + } else if (pageNumber == 'last') { + pageNumber = root.attr('data-page-count'); + } + + return pageNumber; + }; + + /** + * Register event listeners for the module. + * @param {object} root The root element. + */ + var registerEventListeners = function(root) { + root = $(root); + CustomEvents.define(root, [ + CustomEvents.events.activate + ]); + + root.on(CustomEvents.events.activate, SELECTORS.PAGE_ITEM, function(e, data) { + var page = $(e.target).closest(SELECTORS.PAGE_ITEM); + var activePage = root.find(SELECTORS.ACTIVE_PAGE_ITEM); + var pageNumber = getPageNumber(root, page); + var isSamePage = pageNumber == getPageNumber(root, activePage); + + if (!isSamePage) { + root.find(SELECTORS.PAGE_ITEM).removeClass('active'); + getPageByNumber(root, pageNumber).addClass('active'); + } + + root.trigger(EVENTS.PAGE_SELECTED, [{ + pageNumber: pageNumber, + isSamePage: isSamePage, + }]); + + data.originalEvent.preventDefault(); + }); + }; + + return { + registerEventListeners: registerEventListeners, + events: EVENTS, + rootSelector: SELECTORS.ROOT, + }; +}); diff --git a/blocks/myoverview/amd/src/paging_content.js b/blocks/myoverview/amd/src/paging_content.js new file mode 100644 index 00000000000..1e33daef02d --- /dev/null +++ b/blocks/myoverview/amd/src/paging_content.js @@ -0,0 +1,105 @@ +// 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 . + +/** + * Paging content module. + * + * @module block_myoverview/paging_content + * @package block_myoverview + * @copyright 2016 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(['jquery', 'core/templates', 'block_myoverview/paging_bar'], + function($, Templates, PagingBar) { + + var SELECTORS = { + ROOT: '[data-region="paging-content"]', + PAGE_REGION: '[data-region="paging-content-item"]' + }; + + /** + * Constructor of the paging content module. + * + * @param {object} root + * @param {object} pagingBarElement + * @constructor + */ + var PagingContent = function(root, pagingBarElement) { + this.root = $(root); + this.pagingBar = $(pagingBarElement); + + }; + + PagingContent.rootSelector = SELECTORS.ROOT; + + /** + * Load content and create page. + * + * @param {Number} pageNumber + * @returns {*|Promise} + */ + PagingContent.prototype.createPage = function(pageNumber) { + + return this.loadContent(pageNumber).then(function(html, js) { + Templates.appendNodeContents(this.root, html, js); + }.bind(this)).then(function() { + return this.findPage(pageNumber); + }.bind(this) + ); + }; + + /** + * Find a page by the number. + * + * @param {Number} pageNumber The number of the page to be found. + * @returns {*} Page root + */ + PagingContent.prototype.findPage = function(pageNumber) { + return this.root.find('[data-page="' + pageNumber + '"]'); + }; + + /** + * Make a page visible. + * + * @param {Number} pageNumber The number of the page to be visible. + */ + PagingContent.prototype.showPage = function(pageNumber) { + + var existingPage = this.findPage(pageNumber); + this.root.find(SELECTORS.PAGE_REGION).addClass('hidden'); + + if (existingPage.length) { + existingPage.removeClass('hidden'); + } else { + this.createPage(pageNumber).done(function(newPage) { + newPage.removeClass('hidden'); + }); + } + }; + + /** + * Event listeners. + */ + PagingContent.prototype.registerEventListeners = function() { + + this.pagingBar.on(PagingBar.events.PAGE_SELECTED, function(e, data) { + if (!data.isSamePage) { + this.showPage(data.pageNumber); + } + }.bind(this)); + }; + + return PagingContent; +}); diff --git a/blocks/myoverview/block_myoverview.php b/blocks/myoverview/block_myoverview.php new file mode 100644 index 00000000000..f22ce15decb --- /dev/null +++ b/blocks/myoverview/block_myoverview.php @@ -0,0 +1,71 @@ +. + +/** + * Contains the class for the My overview block. + * + * @package block_myoverview + * @copyright Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * My overview block class. + * + * @package block_myoverview + * @copyright Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_myoverview extends block_base { + + /** + * Init. + */ + public function init() { + $this->title = get_string('pluginname', 'block_myoverview'); + } + + /** + * Returns the contents. + * + * @return stdClass contents of block + */ + public function get_content() { + if (isset($this->content)) { + return $this->content; + } + + $renderable = new \block_myoverview\output\main(); + $renderer = $this->page->get_renderer('block_myoverview'); + + $this->content = new stdClass(); + $this->content->text = $renderer->render($renderable); + $this->content->footer = ''; + + return $this->content; + } + + /** + * Locations where block can be displayed. + * + * @return array + */ + public function applicable_formats() { + return array('my' => true); + } +} diff --git a/blocks/myoverview/classes/output/course_summary.php b/blocks/myoverview/classes/output/course_summary.php new file mode 100644 index 00000000000..5155930b455 --- /dev/null +++ b/blocks/myoverview/classes/output/course_summary.php @@ -0,0 +1,82 @@ +. + +/** + * Class containing data for my overview block. + * + * @package block_myoverview + * @copyright 2017 Simey Lameze + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace block_myoverview\output; +defined('MOODLE_INTERNAL') || die(); + +use core_course\external\course_summary_exporter; +use renderable; +use renderer_base; +use templatable; +/** + * Class containing data for my overview block. + * + * @copyright 2017 Simey Lameze + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_summary implements renderable, templatable { + + /** @var array $courses List of courses the user is enrolled in. */ + protected $courses = []; + + /** @var array $coursesprogress List of progress percentage for each course. */ + protected $coursesprogress = []; + + /** + * The course_summary constructor. + * + * @param array $courses list of courses. + * @param array $coursesprogress list of courses progress. + */ + public function __construct($courses, $coursesprogress) { + $this->courses = $courses; + $this->coursesprogress = $coursesprogress; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param \renderer_base $output + * @return array + */ + public function export_for_template(renderer_base $output) { + + $data = []; + foreach ($this->courses as $courseid => $course) { + $context = \context_course::instance($courseid); + // Convert summary to plain text. + $course->summary = content_to_text($course->summary, false); + $exporter = new course_summary_exporter($course, array('context' => $context)); + $exportedcourse = $exporter->export($output); + + if (isset($this->coursesprogress[$courseid])) { + $courseprogress = $this->coursesprogress[$courseid]; + $exportedcourse->hasprogress = !is_null($courseprogress); + $exportedcourse->progress = $courseprogress; + } + + $data[] = $exportedcourse; + } + return $data; + } +} diff --git a/blocks/myoverview/classes/output/courses_view.php b/blocks/myoverview/classes/output/courses_view.php new file mode 100644 index 00000000000..86b0e49e239 --- /dev/null +++ b/blocks/myoverview/classes/output/courses_view.php @@ -0,0 +1,144 @@ +. + +/** + * Class containing data for courses view in the myoverview block. + * + * @package block_myoverview + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace block_myoverview\output; +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use renderer_base; +use templatable; +use core_course\external\course_summary_exporter; + +/** + * Class containing data for courses view in the myoverview block. + * + * @copyright 2017 Simey Lameze + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class courses_view implements renderable, templatable { + /** Quantity of courses per page. */ + const COURSES_PER_PAGE = 6; + + /** @var array $courses List of courses the user is enrolled in. */ + protected $courses = []; + + /** @var array $coursesprogress List of progress percentage for each course. */ + protected $coursesprogress = []; + + /** + * The courses_view constructor. + * + * @param array $courses list of courses. + * @param array $coursesprogress list of courses progress. + */ + public function __construct($courses, $coursesprogress) { + $this->courses = $courses; + $this->coursesprogress = $coursesprogress; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param \renderer_base $output + * @return array + */ + public function export_for_template(renderer_base $output) { + $today = time(); + + // Build courses view data structure. + $coursesview = [ + 'hascourses' => !empty($this->courses) + ]; + + // How many courses we have per status? + $coursesbystatus = ['past' => 0, 'inprogress' => 0, 'future' => 0]; + foreach ($this->courses as $course) { + $startdate = $course->startdate; + $enddate = $course->enddate; + $courseid = $course->id; + $context = \context_course::instance($courseid); + // Convert summary to plain text. + $course->summary = content_to_text($course->summary, false); + $exporter = new course_summary_exporter($course, [ + 'context' => $context + ]); + $exportedcourse = $exporter->export($output); + + if (isset($this->coursesprogress[$courseid])) { + $courseprogress = $this->coursesprogress[$courseid]; + $exportedcourse->hasprogress = !is_null($courseprogress); + $exportedcourse->progress = $courseprogress; + } + + if ($startdate > $today) { + // Courses that have not started yet. + $futurepages = floor($coursesbystatus['future'] / $this::COURSES_PER_PAGE); + + $coursesview['future']['pages'][$futurepages]['courses'][] = $exportedcourse; + $coursesview['future']['pages'][$futurepages]['active'] = ($futurepages == 0 ? true : false); + $coursesview['future']['pages'][$futurepages]['page'] = $futurepages + 1; + $coursesbystatus['future']++; + + } else if (!empty($enddate) && $enddate < $today) { + // Courses that have already ended. + $pastpages = floor($coursesbystatus['past'] / $this::COURSES_PER_PAGE); + + $coursesview['past']['pages'][$pastpages]['courses'][] = $exportedcourse; + $coursesview['past']['pages'][$pastpages]['active'] = ($pastpages == 0 ? true : false); + $coursesview['past']['pages'][$pastpages]['page'] = $pastpages + 1; + $coursesbystatus['past']++; + + } else { + // Courses still in progress. Either their end date is not set, or the end date is not yet past the current date. + $inprogresspages = floor($coursesbystatus['inprogress'] / $this::COURSES_PER_PAGE); + + $coursesview['inprogress']['pages'][$inprogresspages]['courses'][] = $exportedcourse; + $coursesview['inprogress']['pages'][$inprogresspages]['active'] = ($inprogresspages == 0 ? true : false); + $coursesview['inprogress']['pages'][$inprogresspages]['page'] = $inprogresspages + 1; + $coursesbystatus['inprogress']++; + } + } + + // Build courses view paging bar structure. + foreach ($coursesbystatus as $status => $total) { + $quantpages = ceil($total / $this::COURSES_PER_PAGE); + + if ($quantpages) { + $coursesview[$status]['pagingbar']['disabled'] = ($quantpages <= 1); + $coursesview[$status]['pagingbar']['pagecount'] = $quantpages; + $coursesview[$status]['pagingbar']['first'] = ['page' => '«', 'url' => '#']; + $coursesview[$status]['pagingbar']['last'] = ['page' => '»', 'url' => '#']; + for ($page = 0; $page < $quantpages; $page++) { + $coursesview[$status]['pagingbar']['pages'][$page] = [ + 'number' => $page + 1, + 'page' => $page + 1, + 'url' => '#', + 'active' => ($page == 0 ? true : false) + ]; + } + } + } + + return $coursesview; + } +} diff --git a/blocks/myoverview/classes/output/main.php b/blocks/myoverview/classes/output/main.php new file mode 100644 index 00000000000..bdc92d8968e --- /dev/null +++ b/blocks/myoverview/classes/output/main.php @@ -0,0 +1,74 @@ +. + +/** + * Class containing data for my overview block. + * + * @package block_myoverview + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace block_myoverview\output; +defined('MOODLE_INTERNAL') || die(); + +use renderable; +use renderer_base; +use templatable; +use core_completion\progress; + +/** + * Class containing data for my overview block. + * + * @copyright 2017 Simey Lameze + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class main implements renderable, templatable { + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param \renderer_base $output + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + $courses = enrol_get_my_courses('*', 'fullname ASC'); + $coursesprogress = []; + + foreach ($courses as $course) { + $percentage = progress::get_course_progress_percentage($course); + + if (!is_null($percentage)) { + $percentage = floor($percentage); + } + + $coursesprogress[$course->id] = $percentage; + } + + $coursesummary = new course_summary($courses, $coursesprogress); + $coursesview = new courses_view($courses, $coursesprogress); + $nocoursesurl = $output->image_url('courses', 'block_myoverview')->out(); + $noeventsurl = $output->image_url('activities', 'block_myoverview')->out(); + + return [ + 'courses' => $coursesummary->export_for_template($output), + 'coursesview' => $coursesview->export_for_template($output), + 'urls' => [ + 'nocourses' => $nocoursesurl, + 'noevents' => $noeventsurl + ] + ]; + } +} diff --git a/blocks/myoverview/classes/output/renderer.php b/blocks/myoverview/classes/output/renderer.php new file mode 100644 index 00000000000..606dd3bf16d --- /dev/null +++ b/blocks/myoverview/classes/output/renderer.php @@ -0,0 +1,48 @@ +. + +/** + * myoverview block rendrer + * + * @package block_myoverview + * @copyright 2016 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace block_myoverview\output; +defined('MOODLE_INTERNAL') || die; + +use plugin_renderer_base; +use renderable; + +/** + * myoverview block renderer + * + * @package block_myoverview + * @copyright 2016 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends plugin_renderer_base { + + /** + * Return the main content for the block overview. + * + * @param main $main The main renderable + * @return string HTML string + */ + public function render_main(main $main) { + return $this->render_from_template('block_myoverview/main', $main->export_for_template($this)); + } +} diff --git a/blocks/course_overview/db/access.php b/blocks/myoverview/db/access.php similarity index 88% rename from blocks/course_overview/db/access.php rename to blocks/myoverview/db/access.php index 95abb630e64..d05b432ded9 100644 --- a/blocks/course_overview/db/access.php +++ b/blocks/myoverview/db/access.php @@ -15,9 +15,9 @@ // along with Moodle. If not, see . /** - * Course overview block caps. + * Capabilities for the My overview block. * - * @package block_course_overview + * @package block_myoverview * @copyright Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -26,7 +26,7 @@ defined('MOODLE_INTERNAL') || die(); $capabilities = array( - 'block/course_overview:myaddinstance' => array( + 'block/myoverview:myaddinstance' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_SYSTEM, 'archetypes' => array( @@ -36,7 +36,7 @@ $capabilities = array( 'clonepermissionsfrom' => 'moodle/my:manageblocks' ), - 'block/course_overview:addinstance' => array( + 'block/myoverview:addinstance' => array( 'riskbitmask' => RISK_SPAM | RISK_XSS, 'captype' => 'write', diff --git a/blocks/myoverview/lang/en/block_myoverview.php b/blocks/myoverview/lang/en/block_myoverview.php new file mode 100644 index 00000000000..02b0832b354 --- /dev/null +++ b/blocks/myoverview/lang/en/block_myoverview.php @@ -0,0 +1,42 @@ +. + +/** + * Lang strings for the My overview block. + * + * @package block_myoverview + * @copyright Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['future'] = 'Future'; +$string['inprogress'] = 'In progress'; +$string['myoverview:addinstance'] = 'Add a new my overview block'; +$string['myoverview:myaddinstance'] = 'Add a new my overview block to Dashboard'; +$string['nocourses'] = 'No courses'; +$string['nocoursesinprogress'] = 'No in progress courses'; +$string['nocoursesfuture'] = 'No future courses'; +$string['nocoursespast'] = 'No past courses'; +$string['noevents'] = 'No upcoming activities due'; +$string['next30days'] = 'Next 30 days'; +$string['next7days'] = 'Next 7 days'; +$string['past'] = 'Past'; +$string['pluginname'] = 'My overview'; +$string['recentlyoverdue'] = 'Recently overdue'; +$string['sortbycourses'] = 'Sort by courses'; +$string['sortbydates'] = 'Sort by dates'; +$string['timeline'] = 'Timeline'; +$string['viewcourse'] = 'View course'; diff --git a/blocks/myoverview/pix/activities.svg b/blocks/myoverview/pix/activities.svg new file mode 100755 index 00000000000..3b5af3f0d00 --- /dev/null +++ b/blocks/myoverview/pix/activities.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/blocks/myoverview/pix/courses.svg b/blocks/myoverview/pix/courses.svg new file mode 100755 index 00000000000..674b6192e76 --- /dev/null +++ b/blocks/myoverview/pix/courses.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/blocks/myoverview/templates/course-event-list-item.mustache b/blocks/myoverview/templates/course-event-list-item.mustache new file mode 100644 index 00000000000..af7d5ee350f --- /dev/null +++ b/blocks/myoverview/templates/course-event-list-item.mustache @@ -0,0 +1,69 @@ +{{! + 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 . +}} +{{! + @template block_myoverview/course-event-list-item + + This template renders an event list item for the myoverview block + in the courses view. + + Example context (json): + { + "name": "Assignment due 1", + "url": "https://www.google.com", + "timesort": 1490320388, + "action": { + "name": "Submit assignment", + "url": "https://www.google.com", + "itemcount": 1, + "showitemcount": true, + "actionable": true + }, + "icon": { + "key": "icon", + "component": "mod_assign", + "alttext": "Assignment icon" + } + } +}} +
  • +
    +
    +
    + {{#icon}}{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}{{/icon}} +
    +
    + {{name}} +

    + {{#userdate}} {{timesort}}, {{#str}} strftimerecent {{/str}} {{/userdate}} +

    +
    +
    +
    + {{#action.actionable}} + {{action.name}} + {{#action.itemcount}} + {{#action.showitemcount}} + {{.}} + {{/action.showitemcount}} + {{/action.itemcount}} + {{/action.actionable}} + {{^action.actionable}} +
    {{action.name}}
    + {{/action.actionable}} +
    +
    +
  • diff --git a/blocks/myoverview/templates/course-event-list-items.mustache b/blocks/myoverview/templates/course-event-list-items.mustache new file mode 100644 index 00000000000..10a1c435dc0 --- /dev/null +++ b/blocks/myoverview/templates/course-event-list-items.mustache @@ -0,0 +1,63 @@ +{{! + 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 . +}} +{{! + @template block_myoverview/course-event-list-items + + This template renders a group of event list items for the myoverview block + sort by courses view. + + Example context (json): + { + "events": [ + { + "name": "Assignment due 1", + "url": "https://www.google.com", + "timesort": 1490320388, + "action": { + "name": "Submit assignment", + "url": "https://www.google.com", + "itemcount": 1, + "actionable": true + }, + "icon": { + "key": "icon", + "component": "mod_assign", + "alttext": "Assignment icon" + } + }, + { + "name": "Assignment due 2", + "url": "https://www.google.com", + "timesort": 1490320388, + "action": { + "name": "Submit assignment", + "url": "https://www.google.com", + "itemcount": 1, + "actionable": true + }, + "icon": { + "key": "icon", + "component": "mod_assign", + "alttext": "Assignment icon" + } + } + ] + } +}} +{{#events}} + {{> block_myoverview/course-event-list-item }} +{{/events}} diff --git a/blocks/myoverview/templates/course-event-list.mustache b/blocks/myoverview/templates/course-event-list.mustache new file mode 100644 index 00000000000..77cf57cbf9f --- /dev/null +++ b/blocks/myoverview/templates/course-event-list.mustache @@ -0,0 +1,104 @@ +{{! + 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 . +}} +{{! + @template block_myoverview/course-event-list + + This template renders a list of events for the myoverview block + sort by courses view. + + Example context (json): + { + } +}} +
    + +
    + {{< block_myoverview/event-list-group }} + {{$title}}{{#str}} recentlyoverdue, block_myoverview {{/str}}{{/title}} + {{$extratitleclasses}}text-danger{{/extratitleclasses}} + {{$startday}}-14{{/startday}} + {{$endday}}0{{/endday}} + {{$eventlistitems}} + {{> block_myoverview/course-event-list-items }} + {{/eventlistitems}} + {{/ block_myoverview/event-list-group }} + {{< block_myoverview/event-list-group }} + {{$title}}{{#str}} today {{/str}}{{/title}} + {{$extratitleclasses}}{{/extratitleclasses}} + {{$startday}}0{{/startday}} + {{$endday}}1{{/endday}} + {{$eventlistitems}} + {{> block_myoverview/course-event-list-items }} + {{/eventlistitems}} + {{/ block_myoverview/event-list-group }} + {{< block_myoverview/event-list-group }} + {{$title}}{{#str}} next7days, block_myoverview {{/str}}{{/title}} + {{$extratitleclasses}}{{/extratitleclasses}} + {{$startday}}1{{/startday}} + {{$endday}}7{{/endday}} + {{$eventlistitems}} + {{> block_myoverview/course-event-list-items }} + {{/eventlistitems}} + {{/ block_myoverview/event-list-group }} + {{< block_myoverview/event-list-group }} + {{$title}}{{#str}} next30days, block_myoverview {{/str}}{{/title}} + {{$extratitleclasses}}{{/extratitleclasses}} + {{$startday}}7{{/startday}} + {{$endday}}30{{/endday}} + {{$eventlistitems}} + {{> block_myoverview/course-event-list-items }} + {{/eventlistitems}} + {{/ block_myoverview/event-list-group }} + {{< block_myoverview/event-list-group }} + {{$title}}{{#str}} future, block_myoverview {{/str}}{{/title}} + {{$extratitleclasses}}{{/extratitleclasses}} + {{$startday}}30{{/startday}} + {{$endday}}{{/endday}} + {{$eventlistitems}} + {{> block_myoverview/course-event-list-items }} + {{/eventlistitems}} + {{/ block_myoverview/event-list-group }} + +
    + +
    +
    + +
    +{{#js}} +require(['jquery', 'block_myoverview/event_list'], function($, EventList) { + var root = $("#event-list-container-{{$courseid}}{{/courseid}}"); + EventList.registerEventListeners(root); +}); +{{/js}} diff --git a/blocks/myoverview/templates/course-item.mustache b/blocks/myoverview/templates/course-item.mustache new file mode 100644 index 00000000000..7d2250ba7e9 --- /dev/null +++ b/blocks/myoverview/templates/course-item.mustache @@ -0,0 +1,46 @@ +{{! + 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 . +}} +{{! + @template block_myoverview/course-item + + This template renders the each course block containing a summary and calendar events. + + Example context (json): + { + "shortname": "course 3", + "viewurl": "https://www.google.com", + "startdate": 1490320388, + "enddate": 1490320388, + "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout." + } +}} +
  • +
    +
    +
    + {{> block_myoverview/course-summary }} +
    +
    + {{< block_myoverview/course-event-list }} + {{$limit}}10{{/limit}} + {{$offset}}0{{/offset}} + {{$courseid}}{{id}}{{/courseid}} + {{/ block_myoverview/course-event-list }} +
    +
    +
    +
  • diff --git a/blocks/myoverview/templates/course-paging-content-item.mustache b/blocks/myoverview/templates/course-paging-content-item.mustache new file mode 100644 index 00000000000..c20a052f33f --- /dev/null +++ b/blocks/myoverview/templates/course-paging-content-item.mustache @@ -0,0 +1,51 @@ +{{! + 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 . +}} +{{! + @template block_myoverview/course-paging-content-item + + This template renders each course block. + + Example context (json): + { + "page": 1, + "active": true, + "courses": [ + { + "fullnamedisplay": "course 1", + "viewurl": "https://www.google.com", + "startdate": 1490252232, + "enddate": 1490252232, + "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout." + }, + { + "fullnamedisplay": "course 2", + "viewurl": "https://www.google.com", + "startdate": 1490252232, + "enddate": 1490252232, + "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout." + } + ] + } +}} +{{< block_myoverview/paging-content-item }} + {{$classes}}row{{/classes}} + {{$content}} + {{#courses}} + {{> block_myoverview/courses-view-course-item }} + {{/courses}} + {{/content}} +{{/ block_myoverview/paging-content-item }} diff --git a/blocks/myoverview/templates/course-paging-content.mustache b/blocks/myoverview/templates/course-paging-content.mustache new file mode 100644 index 00000000000..21a55611de8 --- /dev/null +++ b/blocks/myoverview/templates/course-paging-content.mustache @@ -0,0 +1,52 @@ +{{! + 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 . +}} +{{! + @template block_myoverview/course-paging-content + + This template renders the each course block containing a summary and calendar events. + + Example context (json): + { + "pages": [ + { + "page": 1, + "active": true, + "courses": [ + { + "fullnamedisplay": "course 1", + "viewurl": "https://www.google.com", + "startdate": 1490252232, + "enddate": 1490252232, + "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout." + }, + { + "fullnamedisplay": "course 2", + "viewurl": "https://www.google.com", + "startdate": 1490252232, + "enddate": 1490252232, + "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout." + } + ] + } + ] + } +}} +{{< block_myoverview/paging-content }} + {{$paging-content-item}} + {{> block_myoverview/course-paging-content-item }} + {{/paging-content-item}} +{{/ block_myoverview/paging-content }} diff --git a/blocks/myoverview/templates/course-summary.mustache b/blocks/myoverview/templates/course-summary.mustache new file mode 100644 index 00000000000..dcde9a38389 --- /dev/null +++ b/blocks/myoverview/templates/course-summary.mustache @@ -0,0 +1,61 @@ +{{! + 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 . +}} +{{! + @template block_myoverview/course-summary + + This template renders the course summary (view by courses) for the myoverview block. + + Example context (json): + { + "fullnamedisplay": "course 3", + "viewurl": "https://www.google.com", + "startdate": 1490320388, + "enddate": 1490320388, + "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout." + } +}} +
    +
    + {{> block_myoverview/progress-chart}} +

    {{fullnamedisplay}}

    +
    +
    + {{> block_myoverview/progress-chart}} +

    {{fullnamedisplay}}

    +
    +
    +
    +
    +
    + {{> block_myoverview/progress-chart}} +
    +
    + +
    +
    +

    + {{#userdate}}{{startdate}}, {{#str}}strftimedate, langconfig{{/str}}{{/userdate}} + {{#enddate}} + - {{#userdate}}{{enddate}}, {{#str}}strftimedate, langconfig{{/str}}{{/userdate}} + {{/enddate}} +

    +

    + {{#shortentext}} 140, {{summary}}{{/shortentext}} +

    +
    diff --git a/blocks/myoverview/templates/courses-view-by-status.mustache b/blocks/myoverview/templates/courses-view-by-status.mustache new file mode 100644 index 00000000000..3360d55db46 --- /dev/null +++ b/blocks/myoverview/templates/courses-view-by-status.mustache @@ -0,0 +1,45 @@ +{{! + 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 . +}} +{{! + @template block_myoverview/courses-view-by-status + + This template renders the courses view for the myoverview block. + + Example context (json): + {} +}} +
    + + {{> block_myoverview/course-paging-content }} + +
    + {{> block_myoverview/paging-bar }} +
    +
    +{{#js}} +require(['jquery', 'block_myoverview/paging_bar', 'block_myoverview/paging_content'], + function($, PagingBar, PagingContent) { + + var root = $('#{{$id}}courses-view-status-{{uniqid}}{{/id}}'); + var pagingBarElement = root.find(PagingBar.rootSelector); + var pagingContentElement = root.find(PagingContent.rootSelector); + + var content = new PagingContent(pagingContentElement, pagingBarElement); + content.registerEventListeners(); +}); +{{/js}} diff --git a/blocks/myoverview/templates/courses-view-course-item.mustache b/blocks/myoverview/templates/courses-view-course-item.mustache new file mode 100644 index 00000000000..aa77887593c --- /dev/null +++ b/blocks/myoverview/templates/courses-view-course-item.mustache @@ -0,0 +1,61 @@ +{{! + 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 . +}} +{{! + @template block_myoverview/courses-view-course-item + + This template renders the course summary (view by courses) for the myoverview block. + + Example context (json): + { + "fullnamedisplay": "course 3", + "viewurl": "https://www.google.com", + "startdate": 1490252232, + "enddate": 1490252232, + "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout." + } +}} +
    +
    +
    +
    + {{> block_myoverview/progress-chart}} +

    {{fullnamedisplay}}

    +
    +
    +
    +
    +
    + {{> block_myoverview/progress-chart}} +
    +
    + +
    +
    +

    + {{#userdate}}{{startdate}}, {{#str}}strftimedate, langconfig{{/str}}{{/userdate}} + {{#enddate}} + - {{#userdate}}{{enddate}}, {{#str}}strftimedate, langconfig{{/str}}{{/userdate}} + {{/enddate}} +

    +

    + {{#shortentext}} 140, {{summary}}{{/shortentext}} +

    +
    +
    +
    diff --git a/blocks/myoverview/templates/courses-view.mustache b/blocks/myoverview/templates/courses-view.mustache new file mode 100644 index 00000000000..baeb696a067 --- /dev/null +++ b/blocks/myoverview/templates/courses-view.mustache @@ -0,0 +1,114 @@ +{{! + 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 . +}} +{{! + @template block_myoverview/courses-view + + This template renders the courses view for the myoverview block. + + Example context (json): + {} +}} +
    + {{#hascourses}} + +
    +
    + {{#inprogress}} + {{< block_myoverview/courses-view-by-status }} + {{$id}}courses-view-in-progress{{/id}} + {{$status}}1{{/status}} + {{$pagingbarid}}pb-for-in-progress{{/pagingbarid}} + {{$pagingcontentid}}pc-for-in-progress{{/pagingcontentid}} + {{/ block_myoverview/courses-view-by-status }} + {{/inprogress}} + {{^inprogress}} +
    + {{#str}} nocoursesinprogress, block_myoverview {{/str}} +

    {{#str}} nocoursesinprogress, block_myoverview {{/str}}

    +
    + {{/inprogress}} +
    +
    + {{#future}} + {{< block_myoverview/courses-view-by-status }} + {{$id}}courses-view-future{{/id}} + {{$status}}2{{/status}} + {{$pagingbarid}}pb-for-future{{/pagingbarid}} + {{$pagingcontentid}}pc-for-in-progress{{/pagingcontentid}} + {{/ block_myoverview/courses-view-by-status }} + {{/future}} + {{^future}} +
    + {{#str}} nocoursesfuture, block_myoverview {{/str}} +

    {{#str}} nocoursesfuture, block_myoverview {{/str}}

    +
    + {{/future}} +
    +
    + {{#past}} + {{< block_myoverview/courses-view-by-status }} + {{$id}}courses-view-past{{/id}} + {{$status}}0{{/status}} + {{$pagingbarid}}pb-for-past{{/pagingbarid}} + {{$pagingcontentid}}pc-for-in-progress{{/pagingcontentid}} + {{/ block_myoverview/courses-view-by-status }} + {{/past}} + {{^past}} +
    + {{#str}} nocoursespast, block_myoverview {{/str}} +

    {{#str}} nocoursespast, block_myoverview {{/str}}

    +
    + {{/past}} +
    +
    + {{/hascourses}} + {{^hascourses}} +
    + {{#str}} nocourses, block_myoverview {{/str}} +

    {{#str}} nocourses, block_myoverview {{/str}}

    +
    + {{/hascourses}} +
    +{{#js}} +require(['jquery', 'core/custom_interaction_events'], function($, customEvents) { + var root = $('#courses-view-{{uniqid}}'); + customEvents.define(root, [customEvents.events.activate]); + root.on(customEvents.events.activate, '[data-toggle="btns"] > .btn', function() { + root.find('.btn.active').removeClass('active'); + }); +}); +{{/js}} diff --git a/blocks/myoverview/templates/event-list-group.mustache b/blocks/myoverview/templates/event-list-group.mustache new file mode 100644 index 00000000000..340fdcbb4a6 --- /dev/null +++ b/blocks/myoverview/templates/event-list-group.mustache @@ -0,0 +1,75 @@ +{{! + 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 . +}} +{{! + @template block_myoverview/event-list-group + + This template renders a list of events for the myoverview block. + + Example context (json): + { + "events": [ + { + "enddate": "Nov 4th, 10am", + "name": "Assignment due 1", + "url": "https://www.google.com", + "course": { + "fullname": "Course 1" + }, + "action": { + "name": "Submit assignment", + "url": "https://www.google.com", + "itemcount": 1 + }, + "icon": { + "key": "icon", + "component": "mod_assign", + "alttext": "Assignment icon" + } + }, + { + "enddate": "Nov 4th, 10am", + "name": "Assignment due 2", + "url": "https://www.google.com", + "course": { + "fullname": "Course 1" + }, + "action": { + "name": "Submit assignment", + "url": "https://www.google.com", + "itemcount": 1 + }, + "icon": { + "key": "icon", + "component": "mod_assign", + "alttext": "Assignment icon" + } + } + ] + } +}} + diff --git a/blocks/myoverview/templates/event-list-item.mustache b/blocks/myoverview/templates/event-list-item.mustache new file mode 100644 index 00000000000..139152f23b6 --- /dev/null +++ b/blocks/myoverview/templates/event-list-item.mustache @@ -0,0 +1,76 @@ +{{! + 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 . +}} +{{! + @template block_myoverview/event-list-item + + This template renders an event list item for the myoverview block. + + Example context (json): + { + "name": "Assignment due 1", + "url": "https://www.google.com", + "timesort": 1490320388, + "course": { + "fullnamedisplay": "Course 1" + }, + "action": { + "name": "Submit assignment", + "url": "https://www.google.com", + "itemcount": 1, + "showitemcount": true, + "actionable": true + }, + "icon": { + "key": "icon", + "component": "mod_assign", + "alttext": "Assignment icon" + } + } +}} +
  • +
    +
    +
    + {{#icon}}{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}{{/icon}} +
    +
    + {{name}} +

    {{course.fullnamedisplay}}

    +
    +
    +
    +
    +
    + {{#userdate}} {{timesort}}, {{#str}} strftimerecent {{/str}} {{/userdate}} +
    +
    + {{#action.actionable}} + {{action.name}} + {{#action.itemcount}} + {{#action.showitemcount}} + {{.}} + {{/action.showitemcount}} + {{/action.itemcount}} + {{/action.actionable}} + {{^action.actionable}} +
    {{action.name}}
    + {{/action.actionable}} +
    +
    +
    +
    +
  • diff --git a/blocks/myoverview/templates/event-list-items.mustache b/blocks/myoverview/templates/event-list-items.mustache new file mode 100644 index 00000000000..2dc770bfc5e --- /dev/null +++ b/blocks/myoverview/templates/event-list-items.mustache @@ -0,0 +1,68 @@ +{{! + 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 . +}} +{{! + @template block_myoverview/event-list-items + + This template renders a group of event list items for the myoverview block. + + Example context (json): + { + "events": [ + { + "name": "Assignment due 1", + "url": "https://www.google.com", + "timesort": 1490320388, + "course": { + "fullnamedisplay": "Course 1" + }, + "action": { + "name": "Submit assignment", + "url": "https://www.google.com", + "itemcount": 1, + "actionable": true + }, + "icon": { + "key": "icon", + "component": "mod_assign", + "alttext": "Assignment icon" + } + }, + { + "name": "Assignment due 2", + "url": "https://www.google.com", + "timesort": 1490320388, + "course": { + "fullnamedisplay": "Course 1" + }, + "action": { + "name": "Submit assignment", + "url": "https://www.google.com", + "itemcount": 1, + "actionable": true + }, + "icon": { + "key": "icon", + "component": "mod_assign", + "alttext": "Assignment icon" + } + } + ] + } +}} +{{#events}} + {{> block_myoverview/event-list-item }} +{{/events}} diff --git a/blocks/myoverview/templates/event-list.mustache b/blocks/myoverview/templates/event-list.mustache new file mode 100644 index 00000000000..83f62347ac4 --- /dev/null +++ b/blocks/myoverview/templates/event-list.mustache @@ -0,0 +1,85 @@ +{{! + 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 . +}} +{{! + @template block_myoverview/event-list + + This template renders a list of events for the myoverview block. + + Example context (json): + { + } +}} +
    + +
    + {{< block_myoverview/event-list-group }} + {{$title}}{{#str}} recentlyoverdue, block_myoverview {{/str}}{{/title}} + {{$extratitleclasses}}text-danger{{/extratitleclasses}} + {{$startday}}-14{{/startday}} + {{$endday}}0{{/endday}} + {{/ block_myoverview/event-list-group }} + {{< block_myoverview/event-list-group }} + {{$title}}{{#str}} today {{/str}}{{/title}} + {{$extratitleclasses}}{{/extratitleclasses}} + {{$startday}}0{{/startday}} + {{$endday}}1{{/endday}} + {{/ block_myoverview/event-list-group }} + {{< block_myoverview/event-list-group }} + {{$title}}{{#str}} next7days, block_myoverview {{/str}}{{/title}} + {{$extratitleclasses}}{{/extratitleclasses}} + {{$startday}}1{{/startday}} + {{$endday}}7{{/endday}} + {{/ block_myoverview/event-list-group }} + {{< block_myoverview/event-list-group }} + {{$title}}{{#str}} next30days, block_myoverview {{/str}}{{/title}} + {{$extratitleclasses}}{{/extratitleclasses}} + {{$startday}}7{{/startday}} + {{$endday}}30{{/endday}} + {{/ block_myoverview/event-list-group }} + {{< block_myoverview/event-list-group }} + {{$title}}{{#str}} future, block_myoverview {{/str}}{{/title}} + {{$extratitleclasses}}{{/extratitleclasses}} + {{$startday}}30{{/startday}} + {{$endday}}{{/endday}} + {{/ block_myoverview/event-list-group }} + +
    + +
    +
    + +
    +{{#js}} +require(['jquery', 'block_myoverview/event_list'], function($, EventList) { + var root = $("#event-list-container-{{$courseid}}{{/courseid}}"); + EventList.registerEventListeners(root); +}); +{{/js}} diff --git a/blocks/myoverview/templates/main.mustache b/blocks/myoverview/templates/main.mustache new file mode 100644 index 00000000000..54fc37a634f --- /dev/null +++ b/blocks/myoverview/templates/main.mustache @@ -0,0 +1,49 @@ +{{! + 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 . +}} +{{! + @template block_myoverview/main + + This template renders the main content area for the myoverview block. + + Example context (json): + {} +}} + +
    + +
    +
    + {{> block_myoverview/timeline-view }} +
    +
    + {{#coursesview}} + {{> block_myoverview/courses-view }} + {{/coursesview}} +
    +
    +
    diff --git a/blocks/myoverview/templates/paging-bar-item.mustache b/blocks/myoverview/templates/paging-bar-item.mustache new file mode 100644 index 00000000000..955ff02d991 --- /dev/null +++ b/blocks/myoverview/templates/paging-bar-item.mustache @@ -0,0 +1,41 @@ +{{! + 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 . +}} +{{! + @template block_myoverview/paging-bar-item + + This template renders a single item in the paging bar. + + Example context (json): + { + "url": "#", + "number": 1, + "page": "1", + "active": true + } +}} +
  • + + + {{$item-content}} + {{{page}}} + {{/item-content}} + +
  • diff --git a/blocks/myoverview/templates/paging-bar.mustache b/blocks/myoverview/templates/paging-bar.mustache new file mode 100644 index 00000000000..71ffecffcd4 --- /dev/null +++ b/blocks/myoverview/templates/paging-bar.mustache @@ -0,0 +1,96 @@ +{{! + 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 . +}} +{{! + @template block_myoverview/paging-bar + + This template renders the bootstrap style paging bar. + + Example context (json): + { + "pagingbar": { + "pagecount": 2, + "previous": {}, + "next": {}, + "first": { + "url": "#", + "page": "first" + }, + "last": { + "url": "#", + "page": "last" + }, + "pages": [ + { + "url": "#", + "number": 1, + "page": "1", + "active": true + }, + { + "url": "#", + "number": 2, + "page": "2" + } + ] + } + } +}} +{{#pagingbar}} + +{{#js}} +require(['jquery', 'block_myoverview/paging_bar'], function($, PagingBar) { + var root = $('#{{$pagingbarid}}paging-bar-{{uniqid}}{{/pagingbarid}}'); + PagingBar.registerEventListeners(root); +}); +{{/js}} +{{/pagingbar}} diff --git a/blocks/myoverview/templates/paging-content-item.mustache b/blocks/myoverview/templates/paging-content-item.mustache new file mode 100644 index 00000000000..82e73e19bde --- /dev/null +++ b/blocks/myoverview/templates/paging-content-item.mustache @@ -0,0 +1,36 @@ +{{! + 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 . +}} +{{! + @template block_myoverview/paging-content-item + + This template renders the content of a page. It is to be used with + the paging bar to toggle visibility of the content items. + + Example context (json): + { + "active": true, + "page": 1, + "content": "

    Some page content

    " + } +}} +
    + {{$content}} + {{{content}}} + {{/content}} +
    diff --git a/blocks/myoverview/templates/paging-content.mustache b/blocks/myoverview/templates/paging-content.mustache new file mode 100644 index 00000000000..83a9cdd799b --- /dev/null +++ b/blocks/myoverview/templates/paging-content.mustache @@ -0,0 +1,44 @@ +{{! + 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 . +}} +{{! + @template block_myoverview/paging-content + + This template renders each of the content regions for a paginated + content section. + + Example context (json): + { + "pages": [ + { + "active": true, + "page": 1, + "content": "

    Some page content

    " + }, + { + "page": 2, + "content": "

    Some page content

    " + } + ] + } +}} +
    + {{#pages}} + {{$paging-content-item}} + {{> block_myoverview/paging-content-item }} + {{/paging-content-item}} + {{/pages}} +
    diff --git a/blocks/myoverview/templates/progress-chart.mustache b/blocks/myoverview/templates/progress-chart.mustache new file mode 100644 index 00000000000..8c592e7c472 --- /dev/null +++ b/blocks/myoverview/templates/progress-chart.mustache @@ -0,0 +1,50 @@ +{{! + 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 . +}} +{{! + @template block_myoverview/progress-chart + + This template renders a doughnut chart to show course progress. + + Example context (json): + { + "hasprogress": true, + "progress": "60" + } +}} +
    + {{#hasprogress}} +
    +
    {{progress}}%
    +
    + + + {{progress}}% + + + +
    +
    + {{/hasprogress}} + {{^hasprogress}} +
    + {{#pix}} i/course {{/pix}} +
    + {{/hasprogress}} +
    diff --git a/blocks/myoverview/templates/timeline-view-courses.mustache b/blocks/myoverview/templates/timeline-view-courses.mustache new file mode 100644 index 00000000000..39b41e1a6fc --- /dev/null +++ b/blocks/myoverview/templates/timeline-view-courses.mustache @@ -0,0 +1,41 @@ +{{! + 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 . +}} +{{! + @template block_myoverview/timeline-view-courses + + This template renders the timeline view by courses for the myoverview block. + + Example context (json): + {} +}} +
      + {{#courses}} {{> block_myoverview/course-item }} {{/courses}} + {{^courses}} +
      + {{#str}} nocoursesinprogress, block_myoverview {{/str}} +

      {{#str}} nocoursesinprogress, block_myoverview {{/str}}

      +
      + {{/courses}} +
    +{{#js}} + require(['jquery', 'block_myoverview/event_list_by_course'], function($, EventListByCourse) { + var root = $("#timeline-view-courses-{{uniqid}}"); + EventListByCourse.init(root); + }); +{{/js}} diff --git a/blocks/myoverview/templates/timeline-view-dates.mustache b/blocks/myoverview/templates/timeline-view-dates.mustache new file mode 100644 index 00000000000..66cb8ea7b27 --- /dev/null +++ b/blocks/myoverview/templates/timeline-view-dates.mustache @@ -0,0 +1,35 @@ +{{! + 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 . +}} +{{! + @template block_myoverview/timeline-view-dates + + This template renders the timeline view by dates for the myoverview block. + + Example context (json): + {} +}} +
    + {{< block_myoverview/event-list }} + {{$limit}}20{{/limit}} + {{/ block_myoverview/event-list }} +
    +{{#js}} + require(['jquery', 'block_myoverview/event_list'], function($, EventList) { + var root = $("#timeline-view-dates-{{uniqid}}").find('[data-region="event-list-container"]'); + EventList.load(root); + }); +{{/js}} diff --git a/blocks/myoverview/templates/timeline-view.mustache b/blocks/myoverview/templates/timeline-view.mustache new file mode 100644 index 00000000000..cba411eb20a --- /dev/null +++ b/blocks/myoverview/templates/timeline-view.mustache @@ -0,0 +1,54 @@ +{{! + 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 . +}} +{{! + @template block_myoverview/timeline-view + + This template renders the timeline view for the myoverview block. + + Example context (json): + {} +}} +
    + + +
    +
    + {{> block_myoverview/timeline-view-dates }} +
    +
    + {{> block_myoverview/timeline-view-courses }} +
    +
    +
    +{{#js}} +require(['jquery', 'core/custom_interaction_events'], function($, customEvents) { + var root = $('#timeline-view-{{uniqid}}'); + customEvents.define(root, [customEvents.events.activate]); + root.on(customEvents.events.activate, '[data-toggle="btns"] > .btn', function() { + root.find('.btn.active').removeClass('active'); + }); +}); +{{/js}} diff --git a/blocks/myoverview/tests/behat/block_myoverview_dashboard.feature b/blocks/myoverview/tests/behat/block_myoverview_dashboard.feature new file mode 100644 index 00000000000..7aec53c4f6a --- /dev/null +++ b/blocks/myoverview/tests/behat/block_myoverview_dashboard.feature @@ -0,0 +1,81 @@ +@block @block_myoverview @javascript +Feature: The my overview block allows users to easily access their courses and see upcoming activities + In order to enable the my overview block in a course + As a student + I can add the my overview block to my dashboard + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | student1 | Student | 1 | student1@example.com | S1 | + | student2 | Student | 2 | student2@example.com | S2 | + And the following "courses" exist: + | fullname | shortname | category | startdate | enddate | + | Course 1 | C1 | 0 | ##1 month ago## | ##15 days ago## | + | Course 2 | C2 | 0 | ##yesterday## | ##tomorrow## | + | Course 3 | C3 | 0 | ##first day of next month## | ##last day of next month## | + And the following "activities" exist: + | activity | course | idnumber | name | intro | timeopen | timeclose | + | choice | C2 | choice1 | Test choice 1 | Test choice description | ##yesterday## | ##tomorrow## | + | choice | C1 | choice2 | Test choice 2 | Test choice description | ##1 month ago## | ##15 days ago## | + | choice | C3 | choice3 | Test choice 3 | Test choice description | ##first day of next month## | ##last day of next month## | + | feedback | C2 | feedback1 | Test feedback 1 | Test feedback description | ##yesterday## | ##tomorrow## | + | feedback | C3 | feedback3 | Test feedback 3 | Test feedback description | ##first day of next month## | ##last day of next month## | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + | student1 | C2 | student | + | student1 | C3 | student | + + Scenario: View courses and upcoming activities on timeline view + Given I log in as "student1" + And I click on "Timeline" "link" in the "My overview" "block" + When I click on "Sort by dates" "link" in the "My overview" "block" + Then I should see "Next 7 days" in the "My overview" "block" + And I should see "Choice Test choice 1 closes" in the "My overview" "block" + And I should see "View choices" in the "My overview" "block" + And I should see "Feedback Test feedback 1 closes" in the "My overview" "block" + And I should see "Answer the questions" in the "My overview" "block" + And I should see "##tomorrow##j M, H:i##" in the "My overview" "block" + And I should see "Future" in the "My overview" "block" + And I should see "Choice Test choice 3 closes" in the "My overview" "block" + And I should see "Feedback Test feedback 3 closes" in the "My overview" "block" + And I should see "##last day of next month##j M, H:i##" in the "My overview" "block" + And I log out + + Scenario: Past activities should not be displayed on the timeline view + Given I log in as "student1" + And I click on "Timeline" "link" in the "My overview" "block" + When I click on "Sort by dates" "link" in the "My overview" "block" + And I should not see "Choice Test choice 2 closes" in the "My overview" "block" + And I should not see "##1 month ago##j M, H:i##" in the "My overview" "block" + And I log out + + Scenario: See the courses I am enrolled by their status on courses view + Given I log in as "student1" + And I click on "Courses" "link" in the "My overview" "block" + And I click on "In progress" "link" in the "My overview" "block" + And I should see "Course 2" in the "My overview" "block" + And I should see "##yesterday##j F Y##" in the "My overview" "block" + And I should see "##tomorrow##j F Y##" in the "My overview" "block" + And I should not see "Course 1" in the "My overview" "block" + And I click on "Future" "link" in the "My overview" "block" + And I should see "Course 3" in the "My overview" "block" + And I should see "##first day of next month##j F Y##" in the "My overview" "block" + And I should see "##last day of next month##j F Y##" in the "My overview" "block" + And I should not see "Course 1" in the "My overview" "block" + When I click on "Past" "link" in the "My overview" "block" + Then I should see "Course 1" in the "My overview" "block" + And I should not see "Course 2" in the "My overview" "block" + And I should not see "Course 3" in the "My overview" "block" + And I should see "##1 month ago##j F Y##" in the "My overview" "block" + And I should see "##15 days ago##j F Y##" in the "My overview" "block" + And I log out + + Scenario: No activities should be displayed if the user is not enrolled + Given I log in as "student2" + And I click on "Timeline" "link" in the "My overview" "block" + And I should see "No upcoming activities" in the "My overview" "block" + When I click on "Courses" "link" in the "My overview" "block" + Then I should see "No courses" in the "My overview" "block" + And I log out diff --git a/blocks/myoverview/tests/behat/block_myoverview_progress.feature b/blocks/myoverview/tests/behat/block_myoverview_progress.feature new file mode 100644 index 00000000000..90508bbc373 --- /dev/null +++ b/blocks/myoverview/tests/behat/block_myoverview_progress.feature @@ -0,0 +1,62 @@ +@block @block_myoverview @javascript +Feature: My overview block show users their progress on courses + In order to enable the my overview block in a course + As a student + I can see the progress percentage of the courses I am enrolled in + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | student1 | Student | 1 | student1@example.com | S1 | + And the following "courses" exist: + | fullname | shortname | category | enablecompletion | startdate | enddate | + | Course 1 | C1 | 0 | 1 | ##yesterday## | ##tomorrow## | + And the following "activities" exist: + | activity | course | idnumber | name | intro | timeopen | timeclose | + | choice | C1 | choice1 | Test choice 1 | Test choice description | ##yesterday## | ##tomorrow## | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + + Scenario: Course progress percentage should not be displayed if completion is not enabled + Given I log in as "student1" + And I click on "Timeline" "link" in the "My overview" "block" + When I click on "Sort by courses" "link" in the "My overview" "block" + Then I should see "Choice Test choice 1 closes" in the "My overview" "block" + And I should not see "0%" in the "My overview" "block" + And I click on "Courses" "link" in the "My overview" "block" + And I click on "In progress" "link" in the "My overview" "block" + And I should see "Course 1" in the "My overview" "block" + And I should not see "0%" in the "My overview" "block" + And I log out + + Scenario: User complete activity and verify his progress + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I follow "Test choice 1" + And I navigate to "Edit settings" in current page administration + And I set the following fields to these values: + | Completion tracking | Show activity as complete when conditions are met | + | id_completionview | 1 | + And I press "Save and return to course" + And I log out + And I log in as "student1" + And I click on "Sort by courses" "link" in the "My overview" "block" + And I should see "Choice Test choice 1 closes" in the "My overview" "block" + And I should see "0%" in the "My overview" "block" + And I click on "Courses" "link" in the "My overview" "block" + When I click on "In progress" "link" in the "My overview" "block" + Then I should see "Course 1" in the "My overview" "block" + And I should see "0%" in the "My overview" "block" + And I am on "Course 1" course homepage + And I follow "Test choice 1" + And I follow "Dashboard" in the user menu + And I click on "Sort by courses" "link" in the "My overview" "block" + And I should see "100%" in the "My overview" "block" + And I click on "Courses" "link" in the "My overview" "block" + And I click on "In progress" "link" in the "My overview" "block" + And I should see "Course 1" in the "My overview" "block" + And I should see "100%" in the "My overview" "block" + And I log out diff --git a/blocks/course_overview/version.php b/blocks/myoverview/version.php similarity index 68% rename from blocks/course_overview/version.php rename to blocks/myoverview/version.php index dec7d909c67..682aeed95dc 100644 --- a/blocks/course_overview/version.php +++ b/blocks/myoverview/version.php @@ -15,15 +15,15 @@ // along with Moodle. If not, see . /** - * Version details + * Version details for the My overview block. * - * @package block_course_overview - * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) + * @package block_myoverview + * @copyright Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2016120500; // The current plugin version (Date: YYYYMMDDXX) -$plugin->requires = 2016112900; // Requires this Moodle version -$plugin->component = 'block_course_overview'; // Full name of the plugin (used for diagnostics) +$plugin->version = 2016122000; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2016112900; // Requires this Moodle version. +$plugin->component = 'block_myoverview'; // Full name of the plugin (used for diagnostics). diff --git a/blocks/myprofile/tests/behat/block_myprofile_activity.feature b/blocks/myprofile/tests/behat/block_myprofile_activity.feature index fb7cfd2d084..bc0f704946e 100644 --- a/blocks/myprofile/tests/behat/block_myprofile_activity.feature +++ b/blocks/myprofile/tests/behat/block_myprofile_activity.feature @@ -18,8 +18,7 @@ Feature: The logged in user block allows users to view their profile information | activity | course | idnumber | name | intro | | page | C1 | page1 | Test page name | Test page description | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I follow "Test page name" When I add the "Logged in user" block Then I should see "Teacher One" in the "Logged in user" "block" diff --git a/blocks/myprofile/tests/behat/block_myprofile_course.feature b/blocks/myprofile/tests/behat/block_myprofile_course.feature index 81d22767a6c..c0aa2db0ccf 100644 --- a/blocks/myprofile/tests/behat/block_myprofile_course.feature +++ b/blocks/myprofile/tests/behat/block_myprofile_course.feature @@ -15,7 +15,6 @@ Feature: The logged in user block allows users to view their profile information | user | course | role | | teacher1 | C1 | editingteacher | When I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Logged in user" block Then I should see "Teacher One" in the "Logged in user" "block" diff --git a/blocks/navigation/tests/behat/expand_courses_node.feature b/blocks/navigation/tests/behat/expand_courses_node.feature index 56c85bf877d..683c34ad41a 100644 --- a/blocks/navigation/tests/behat/expand_courses_node.feature +++ b/blocks/navigation/tests/behat/expand_courses_node.feature @@ -45,7 +45,7 @@ Feature: Expand the courses nodes within the navigation block | Page contexts | Display throughout the entire site | And I press "Save changes" And I turn editing mode off - And I follow "Course 2" + And I am on "Course 2" course homepage And I navigate to "Enrolment methods" node in "Course administration > Users" And I click on "Edit" "link" in the "Guest access" "table_row" And I set the following fields to these values: diff --git a/blocks/news_items/tests/behat/display_news.feature b/blocks/news_items/tests/behat/display_news.feature index 2093c763d26..dfc75eeadc8 100644 --- a/blocks/news_items/tests/behat/display_news.feature +++ b/blocks/news_items/tests/behat/display_news.feature @@ -17,8 +17,7 @@ Feature: Latest announcements block displays the course latest news And I enrol "Teacher 1" user as "Teacher" And I log out And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Latest announcements" block And I turn editing mode off When I add a new topic to "Announcements" forum with: @@ -30,7 +29,7 @@ Feature: Latest announcements block displays the course latest news And I add a new topic to "Announcements" forum with: | Subject | Discussion Three | | Message | Not important | - And I follow "Course 1" + And I am on "Course 1" course homepage Then I should see "Discussion One" in the "Latest announcements" "block" And I should see "Discussion Two" in the "Latest announcements" "block" And I should see "Discussion Three" in the "Latest announcements" "block" diff --git a/blocks/online_users/tests/behat/block_online_users_course.feature b/blocks/online_users/tests/behat/block_online_users_course.feature index f2022354fc3..22591be631e 100644 --- a/blocks/online_users/tests/behat/block_online_users_course.feature +++ b/blocks/online_users/tests/behat/block_online_users_course.feature @@ -21,21 +21,19 @@ Feature: The online users block allow you to see who is currently online Scenario: Add the online users on course page and see myself Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I add the "Online users" block Then I should see "Teacher 1" in the "Online users" "block" Scenario: Add the online users on course page and see other logged in users Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Online users" block And I log out And I log in as "student2" And I log out When I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage Then I should see "Teacher 1" in the "Online users" "block" And I should see "Student 1" in the "Online users" "block" And I should not see "Student 2" in the "Online users" "block" diff --git a/blocks/participants/tests/behat/block_participants_course.feature b/blocks/participants/tests/behat/block_participants_course.feature index b50286e7734..aeb75be704c 100644 --- a/blocks/participants/tests/behat/block_participants_course.feature +++ b/blocks/participants/tests/behat/block_participants_course.feature @@ -15,21 +15,19 @@ Feature: People Block used in a course | user | course | role | | student1 | C101 | student | And I log in as "admin" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "People" block And I log out Scenario: Student can view participants link When I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage Then "People" "block" should exist And I should see "Participants" in the "People" "block" Scenario: Student can follow participants link and be directed to the correct page When I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I click on "Participants" "link" in the "People" "block" Then I should see "All participants" in the "#page-content" "css_element" And the "My courses" select box should contain "C101" @@ -39,5 +37,5 @@ Feature: People Block used in a course | capability | permission | role | contextlevel | reference | | moodle/course:viewparticipants | Prevent | student | Course | C101 | When I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage Then "People" "block" should not exist diff --git a/blocks/private_files/tests/behat/block_private_files_activity.feature b/blocks/private_files/tests/behat/block_private_files_activity.feature index d8578719882..fb8437712f1 100644 --- a/blocks/private_files/tests/behat/block_private_files_activity.feature +++ b/blocks/private_files/tests/behat/block_private_files_activity.feature @@ -18,8 +18,7 @@ Feature: The private files block allows users to store files privately in moodle | activity | course | idnumber | name | intro | | page | C1 | page1 | Test page name | Test page description | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I follow "Test page name" And I add the "Private files" block And I should see "No files available" in the "Private files" "block" diff --git a/blocks/private_files/tests/behat/block_private_files_course.feature b/blocks/private_files/tests/behat/block_private_files_course.feature index b56c15b522b..35a4e9629cc 100644 --- a/blocks/private_files/tests/behat/block_private_files_course.feature +++ b/blocks/private_files/tests/behat/block_private_files_course.feature @@ -15,8 +15,7 @@ Feature: The private files block allows users to store files privately in moodle | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Private files" block And I should see "No files available" in the "Private files" "block" When I follow "Manage private files..." diff --git a/blocks/recent_activity/tests/behat/structural_changes.feature b/blocks/recent_activity/tests/behat/structural_changes.feature index 973b10c1dfb..62dc4904017 100644 --- a/blocks/recent_activity/tests/behat/structural_changes.feature +++ b/blocks/recent_activity/tests/behat/structural_changes.feature @@ -47,8 +47,7 @@ Feature: View structural changes in recent activity block Scenario: Check that Added module information is displayed respecting view capability Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Recent activity" block When I add a "Forum" to section "1" and I fill the form with: | name | ForumVisibleGroups | @@ -100,7 +99,7 @@ Feature: View structural changes in recent activity block And I should see "ForumSeparateGroupsG2" in the "Recent activity" "block" And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "ForumVisibleGroups" in the "Recent activity" "block" And I should see "ForumSeparateGroups" in the "Recent activity" "block" And I should see "ForumNoGroups" in the "Recent activity" "block" @@ -111,7 +110,7 @@ Feature: View structural changes in recent activity block And I should not see "ForumSeparateGroupsG2" in the "Recent activity" "block" And I log out And I log in as "student2" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "ForumVisibleGroups" in the "Recent activity" "block" And I should see "ForumSeparateGroups" in the "Recent activity" "block" And I should see "ForumNoGroups" in the "Recent activity" "block" @@ -122,7 +121,7 @@ Feature: View structural changes in recent activity block And I should see "ForumSeparateGroupsG2" in the "Recent activity" "block" And I log out And I log in as "student3" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "ForumVisibleGroups" in the "Recent activity" "block" And I should see "ForumSeparateGroups" in the "Recent activity" "block" And I should see "ForumNoGroups" in the "Recent activity" "block" @@ -134,7 +133,7 @@ Feature: View structural changes in recent activity block And I log out # Teachers have capability to see all groups and hidden activities And I log in as "assistant1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "ForumHidden" in the "Recent activity" "block" And I should see "ForumVisibleGroupsG1" in the "Recent activity" "block" And I should see "ForumSeparateGroupsG1" in the "Recent activity" "block" @@ -144,8 +143,7 @@ Feature: View structural changes in recent activity block Scenario: Updates and deletes in recent activity block When I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Recent activity" block And I add a "Forum" to section "1" and I fill the form with: | name | ForumNew | @@ -155,7 +153,7 @@ Feature: View structural changes in recent activity block And I log out And I wait "1" seconds And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Added Forum" in the "Recent activity" "block" And I should see "ForumNew" in the "Recent activity" "block" And I log out @@ -163,7 +161,7 @@ Feature: View structural changes in recent activity block And I wait "1" seconds # Update forum as a teacher And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "ForumNew" And I navigate to "Edit settings" in current page administration And I set the following fields to these values: @@ -173,7 +171,7 @@ Feature: View structural changes in recent activity block And I wait "1" seconds # Student 1 already saw that forum was created, now he can see that forum was updated And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should not see "Added Forum" in the "Recent activity" "block" And I should not see "ForumNew" in the "Recent activity" "block" And I should see "Updated Forum" in the "Recent activity" "block" @@ -182,7 +180,7 @@ Feature: View structural changes in recent activity block And I wait "1" seconds # Student 2 has bigger interval and he can see one entry that forum was created but with the new name And I log in as "student2" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Added Forum" in the "Recent activity" "block" And I should not see "ForumNew" in the "Recent activity" "block" And I should not see "Updated Forum" in the "Recent activity" "block" @@ -191,15 +189,14 @@ Feature: View structural changes in recent activity block And I wait "1" seconds # Delete forum as a teacher And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I delete "ForumUpdated" activity And I run all adhoc tasks And I log out And I wait "1" seconds # Students 1 and 2 see that forum was deleted And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should not see "Added Forum" in the "Recent activity" "block" And I should not see "ForumNew" in the "Recent activity" "block" And I should not see "Updated Forum" in the "Recent activity" "block" @@ -209,7 +206,7 @@ Feature: View structural changes in recent activity block And I wait "1" seconds # Student 3 never knew that forum was created, so he does not see anything And I log in as "student3" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should not see "Added Forum" in the "Recent activity" "block" And I should not see "ForumNew" in the "Recent activity" "block" And I should not see "Updated Forum" in the "Recent activity" "block" diff --git a/blocks/search_forums/tests/behat/block_search_forums_course.feature b/blocks/search_forums/tests/behat/block_search_forums_course.feature index dedc8bcd1b4..e2e37d27dc6 100644 --- a/blocks/search_forums/tests/behat/block_search_forums_course.feature +++ b/blocks/search_forums/tests/behat/block_search_forums_course.feature @@ -17,7 +17,7 @@ Feature: The search forums block allows users to search for forum posts on cours | teacher1 | C1 | editingteacher | | student1 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Edit settings" node in "Course administration" And I set the field "id_newsitems" to "1" And I press "Save and display" @@ -28,7 +28,7 @@ Feature: The search forums block allows users to search for forum posts on cours Scenario: Use the search forum block in a course without any forum posts Given I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage When I set the following fields to these values: | searchform_search | Moodle | And I press "Go" @@ -36,12 +36,11 @@ Feature: The search forums block allows users to search for forum posts on cours Scenario: Use the search forum block in a course with a hidden forum and search for posts Given I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I add a new topic to "Announcements" forum with: | Subject | My subject | | Message | My message | - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I follow "Announcements" And I navigate to "Edit settings" in current page administration And I expand all fieldsets @@ -49,7 +48,7 @@ Feature: The search forums block allows users to search for forum posts on cours And I press "Save and return to course" And I log out When I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And "Search forums" "block" should exist And I set the following fields to these values: | searchform_search | message | @@ -58,13 +57,13 @@ Feature: The search forums block allows users to search for forum posts on cours Scenario: Use the search forum block in a course and search for posts Given I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I add a new topic to "Announcements" forum with: | Subject | My subject | | Message | My message | And I log out When I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And "Search forums" "block" should exist And I set the following fields to these values: | searchform_search | message | diff --git a/blocks/section_links/tests/behat/block_section_links_course.feature b/blocks/section_links/tests/behat/block_section_links_course.feature index f43f6fd9287..140a11d0509 100644 --- a/blocks/section_links/tests/behat/block_section_links_course.feature +++ b/blocks/section_links/tests/behat/block_section_links_course.feature @@ -15,8 +15,7 @@ Feature: The section links block allows users to quickly navigate around a moodl | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "5" and I fill the form with: | Assignment name | Test assignment 1 | | Description | Offline text | diff --git a/blocks/social_activities/tests/behat/edit_activities.feature b/blocks/social_activities/tests/behat/edit_activities.feature index a72fa872390..ad6763592dd 100644 --- a/blocks/social_activities/tests/behat/edit_activities.feature +++ b/blocks/social_activities/tests/behat/edit_activities.feature @@ -20,8 +20,7 @@ Feature: Edit activities in social activities block @javascript Scenario: Edit name of acitivity in-place in social activities block Given I log in as "user1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I set the field "Add an activity to section 'section 0'" to "Forum" And I set the field "Forum name" to "My forum name" And I press "Save and return to course" @@ -41,8 +40,7 @@ Feature: Edit activities in social activities block | allowstealth | 1 | And I log out And I log in as "user1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Recent activity" block And I set the field "Add an activity to section 'section 0'" to "Forum" And I set the field "Forum name" to "My forum name" @@ -80,7 +78,7 @@ Feature: Edit activities in social activities block And I log out # Student will not see the module on the course page but can access it from other reports and blocks: And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should not see "My forum name" in the "Social activities" "block" And I click on "My forum name" "link" in the "Recent activity" "block" And I should see "My forum name" in the ".breadcrumb" "css_element" diff --git a/blocks/tags/tests/behat/tagcloud.feature b/blocks/tags/tests/behat/tagcloud.feature index 9c7ee6b5633..6a8bd414895 100644 --- a/blocks/tags/tests/behat/tagcloud.feature +++ b/blocks/tags/tests/behat/tagcloud.feature @@ -35,12 +35,11 @@ Feature: Block tags displaying tag cloud Scenario: Add Tags block in a course When I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Tags" block And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage Then I should see "Dogs" in the "Tags" "block" And I should see "Cats" in the "Tags" "block" And I should not see "Neverusedtag" in the "Tags" "block" diff --git a/blocks/tests/behat/add_blocks.feature b/blocks/tests/behat/add_blocks.feature index 5d17e01ade6..4fadd478b79 100644 --- a/blocks/tests/behat/add_blocks.feature +++ b/blocks/tests/behat/add_blocks.feature @@ -17,9 +17,7 @@ Feature: Add blocks | student1 | C1 | student | | student2 | C1 | student | And I log in as "admin" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I add the "Blog menu" block Then I should see "View my entries about this course" diff --git a/blocks/tests/behat/configure_block_throughout_site.feature b/blocks/tests/behat/configure_block_throughout_site.feature index a9d31d7af1b..40ae8db05ec 100644 --- a/blocks/tests/behat/configure_block_throughout_site.feature +++ b/blocks/tests/behat/configure_block_throughout_site.feature @@ -36,7 +36,7 @@ Feature: Add and configure blocks throughout the site And I set the following fields to these values: | Page contexts | Display throughout the entire site | And I press "Save changes" - When I follow "Course 1" + When I am on "Course 1" course homepage Then I should see "Comments" in the "Comments" "block" And I should see "Save comment" in the "Comments" "block" And I am on site homepage @@ -44,7 +44,7 @@ Feature: Add and configure blocks throughout the site And I set the following fields to these values: | Default weight | -10 (first) | And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage # The first block matching the pattern should be top-left block And I should see "Comments" in the "//*[@id='region-pre' or @id='block-region-side-pre']/descendant::*[contains(concat(' ', normalize-space(@class), ' '), ' block ')]" "xpath_element" @@ -55,8 +55,7 @@ Feature: Add and configure blocks throughout the site Scenario: Blocks on courses can have roles assigned to them Given I log in as "teacher1" - And I follow "Course 1" - And I follow "Turn editing on" + And I am on "Course 1" course homepage with editing mode on And I add the "Search forums" block Then I should see "Assign roles in Search forums block" @@ -71,4 +70,4 @@ Feature: Add and configure blocks throughout the site | Block title | Foo " onload="document.getElementsByTagName('body')[0].remove()" alt=" | | Content | Example | When I press "Save changes" - Then I should see "Course overview" + Then I should see "My overview" diff --git a/blocks/tests/behat/hidden_block_region.feature b/blocks/tests/behat/hidden_block_region.feature index 6abadca1865..e41f9f26917 100644 --- a/blocks/tests/behat/hidden_block_region.feature +++ b/blocks/tests/behat/hidden_block_region.feature @@ -12,8 +12,7 @@ Feature: Show hidden blocks in a docked block region when editing | user | course | role | | admin | C1 | editingteacher | And I log in as "admin" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Search forums" block And I add the "Latest announcements" block And I add the "Upcoming events" block diff --git a/blocks/tests/behat/hide_blocks.feature b/blocks/tests/behat/hide_blocks.feature index c83bd02c4f3..db11e3f529d 100644 --- a/blocks/tests/behat/hide_blocks.feature +++ b/blocks/tests/behat/hide_blocks.feature @@ -9,9 +9,7 @@ Feature: Block visibility | fullname | shortname | category | | Course 1 | C1 | 0 | And I log in as "admin" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on @javascript Scenario: Hiding all blocks on the page should remove the column they're in diff --git a/blocks/tests/behat/manage_blocks.feature b/blocks/tests/behat/manage_blocks.feature index 8cca1cf1b9a..19b0fc6bcc8 100644 --- a/blocks/tests/behat/manage_blocks.feature +++ b/blocks/tests/behat/manage_blocks.feature @@ -15,9 +15,7 @@ Feature: Block appearances | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "admin" - And I am on site homepage - And I follow "Course 1" - And I follow "Turn editing on" + And I am on "Course 1" course homepage with editing mode on And I add a "Survey" to section "1" and I fill the form with: | Name | Test survey name | | Survey type | ATTLS (20 item version) | @@ -32,8 +30,7 @@ Feature: Block appearances And I press "Save changes" And I log out And I log in as "teacher1" - And I follow "Course 1" - And I follow "Turn editing on" + And I am on "Course 1" course homepage with editing mode on And I add the "Comments" block And I configure the "Comments" block And I set the following fields to these values: @@ -43,7 +40,7 @@ Feature: Block appearances Scenario: Block settings can be modified so that a block apprears on any page When I follow "Test survey name" Then I should see "Comments" in the "Comments" "block" - And I follow "Course 1" + And I am on "Course 1" course homepage And I configure the "Comments" block And I set the following fields to these values: | Display on page types | Any course page | diff --git a/blocks/tests/behat/move_blocks.feature b/blocks/tests/behat/move_blocks.feature index 8771a9fc787..6a312cec5c2 100644 --- a/blocks/tests/behat/move_blocks.feature +++ b/blocks/tests/behat/move_blocks.feature @@ -15,9 +15,7 @@ Feature: Block region moving | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "admin" - And I am on site homepage - And I follow "Course 1" - And I follow "Turn editing on" + And I am on "Course 1" course homepage with editing mode on And I add a "Survey" to section "1" and I fill the form with: | Name | Test survey name | | Survey type | ATTLS (20 item version) | @@ -32,8 +30,7 @@ Feature: Block region moving And I press "Save changes" And I log out And I log in as "teacher1" - And I follow "Course 1" - And I follow "Turn editing on" + And I am on "Course 1" course homepage with editing mode on And I add the "Comments" block And I configure the "Comments" block And I set the following fields to these values: diff --git a/blocks/tests/behat/restrict_available_blocks.feature b/blocks/tests/behat/restrict_available_blocks.feature index 6a5c2a11ff2..581d0f84f77 100644 --- a/blocks/tests/behat/restrict_available_blocks.feature +++ b/blocks/tests/behat/restrict_available_blocks.feature @@ -17,8 +17,7 @@ Feature: Allowed blocks controls Scenario: Blocks can be added with the default permissions Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I add the "Course completion status" block And I add the "Activities" block Then I should see "Activities" in the "Activities" "block" @@ -28,14 +27,12 @@ Feature: Allowed blocks controls Given I log in as "admin" And I set the following system permissions of "Teacher" role: | block/activity_modules:addinstance | Prohibit | - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Users > Permissions" in current page administration And I override the system permissions of "Teacher" role with: | block/completionstatus:addinstance | Prohibit | And I log out When I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on Then the add block selector should not contain "Activities" block And the add block selector should not contain "Course completion status" block diff --git a/blocks/tests/behat/return_block_original_state.feature b/blocks/tests/behat/return_block_original_state.feature index 98226cdffaa..79c4b97a904 100644 --- a/blocks/tests/behat/return_block_original_state.feature +++ b/blocks/tests/behat/return_block_original_state.feature @@ -9,9 +9,7 @@ Feature: The context of a block can always be returned to it's original state. | fullname | shortname | category | | Course 1 | C1 | 0 | And I log in as "admin" - And I am on site homepage - When I follow "Course 1" - And I follow "Turn editing on" + When I am on "Course 1" course homepage with editing mode on And I add the "Tags" block Then I should see "Tags" in the "Tags" "block" And I navigate to course participants @@ -19,7 +17,7 @@ Feature: The context of a block can always be returned to it's original state. And I set the following fields to these values: | Display on page types | Any page | And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Assignment1 | | Description | Description | @@ -29,11 +27,11 @@ Feature: The context of a block can always be returned to it's original state. | Display on page types | Any assignment module page | And I press "Save changes" And I should see "Tags" in the "Tags" "block" - And I follow "Course 1" + And I am on "Course 1" course homepage And "Tags" "block" should not exist And I navigate to course participants And "Tags" "block" should not exist - And I follow "Course 1" + And I am on "Course 1" course homepage And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Assignment2 | | Description | Description | @@ -43,7 +41,7 @@ Feature: The context of a block can always be returned to it's original state. And I set the following fields to these values: | Display on page types | Any page | And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Tags" in the "Tags" "block" And I navigate to course participants And I should see "Tags" in the "Tags" "block" diff --git a/blocks/upgrade.txt b/blocks/upgrade.txt index e0de186a947..6add035366f 100644 --- a/blocks/upgrade.txt +++ b/blocks/upgrade.txt @@ -4,6 +4,10 @@ information provided here is intended especially for developers. === 3.3 === * block_manager::get_required_by_theme_block_types() is no longer static. +* The 'Course overview' block has been removed from core as it is being replaced by the 'My overview' block. + During the upgrade process the 'Course overview' block will be uninstalled and all its settings will be deleted. + If you wish to keep the 'Course overview' block and its settings, download it from moodle.org and put it back in + the blocks/ directory BEFORE UPGRADING. === 3.1 === diff --git a/blog/tests/behat/blog_visibility.feature b/blog/tests/behat/blog_visibility.feature index e1356810731..f4bae300fc0 100644 --- a/blog/tests/behat/blog_visibility.feature +++ b/blog/tests/behat/blog_visibility.feature @@ -26,7 +26,7 @@ Feature: Blogs can be set to be only visible by the author. Scenario: A student can not see another student's blog entries. Given I log in as "testuser" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to course participants And I follow "Test2 User2" And I should see "Miscellaneous" diff --git a/calendar/classes/action_factory.php b/calendar/classes/action_factory.php new file mode 100644 index 00000000000..118e3a22632 --- /dev/null +++ b/calendar/classes/action_factory.php @@ -0,0 +1,43 @@ +. + +/** + * Action factory. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar; + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\local\event\factories\action_factory_interface; +use core_calendar\local\event\value_objects\action; + +/** + * Action factory class. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class action_factory implements action_factory_interface { + + public function create_instance($name, \moodle_url $url, $itemcount, $actionable) { + return new action ($name, $url, $itemcount, $actionable); + } +} diff --git a/calendar/classes/external/event_action_exporter.php b/calendar/classes/external/event_action_exporter.php new file mode 100644 index 00000000000..2002cc3a817 --- /dev/null +++ b/calendar/classes/external/event_action_exporter.php @@ -0,0 +1,119 @@ +. + +/** + * Contains event class for displaying a calendar event's action. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\external; + +defined('MOODLE_INTERNAL') || die(); + +use core\external\exporter; +use core_calendar\local\event\entities\action_interface; +use core_calendar\local\event\container; +use renderer_base; + +/** + * Class for displaying a calendar event's action. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class event_action_exporter extends exporter { + + /** + * Constructor. + * + * @param action_interface $action The action object. + * @param array $related Related data. + */ + public function __construct(action_interface $action, $related = []) { + $data = new \stdClass(); + $data->name = $action->get_name(); + $data->url = $action->get_url()->out(true); + $data->itemcount = $action->get_item_count(); + $data->actionable = $action->is_actionable(); + + parent::__construct($data, $related); + } + + /** + * Return the list of properties. + * + * @return array + */ + protected static function define_properties() { + return [ + 'name' => ['type' => PARAM_TEXT], + 'url' => ['type' => PARAM_URL], + 'itemcount' => ['type' => PARAM_INT], + 'actionable' => ['type' => PARAM_BOOL] + ]; + } + + /** + * Return the list of additional properties. + * + * @return array + */ + protected static function define_other_properties() { + return [ + 'showitemcount' => ['type' => PARAM_BOOL, 'default' => false] + ]; + } + + /** + * Get the additional values to inject while exporting. + * + * @param renderer_base $output The renderer. + * @return array Keys are the property names, values are their values. + */ + protected function get_other_values(renderer_base $output) { + $event = $this->related['event']; + + $modulename = $event->get_course_module()->get('modname'); + $component = 'mod_' . $modulename; + $showitemcountcallback = 'core_calendar_event_action_shows_item_count'; + $mapper = container::get_event_mapper(); + $calevent = $mapper->from_event_to_legacy_event($event); + $params = [$calevent, $this->data->itemcount]; + $showitemcount = component_callback($component, $showitemcountcallback, $params, false); + + // Prepare other values data. + $data = [ + 'showitemcount' => $showitemcount + ]; + return $data; + } + + /** + * Returns a list of objects that are related. + * + * @return array + */ + protected static function define_related() { + return [ + 'context' => 'context', + 'event' => '\\core_calendar\\local\\event\\entities\\event_interface' + ]; + } +} diff --git a/calendar/classes/external/event_exporter.php b/calendar/classes/external/event_exporter.php new file mode 100644 index 00000000000..ade78fa0423 --- /dev/null +++ b/calendar/classes/external/event_exporter.php @@ -0,0 +1,218 @@ +. + +/** + * Contains event class for displaying a calendar event. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\external; + +defined('MOODLE_INTERNAL') || die(); + +use \core\external\exporter; +use \core_calendar\local\event\entities\event_interface; +use \core_calendar\local\event\entities\action_event_interface; +use \core_course\external\course_summary_exporter; +use \renderer_base; + +/** + * Class for displaying a calendar event. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class event_exporter extends exporter { + + /** + * @var event_interface $event + */ + protected $event; + + /** + * Constructor. + * + * @param event_interface $event + * @param array $related The related data. + */ + public function __construct(event_interface $event, $related = []) { + $this->event = $event; + + $starttimestamp = $event->get_times()->get_start_time()->getTimestamp(); + $endtimestamp = $event->get_times()->get_end_time()->getTimestamp(); + $groupid = $event->get_group() ? $event->get_group()->get('id') : null; + $userid = $event->get_user() ? $event->get_user()->get('id') : null; + + $data = new \stdClass(); + $data->id = $event->get_id(); + $data->name = $event->get_name(); + $data->description = $event->get_description()->get_value(); + $data->descriptionformat = $event->get_description()->get_format(); + $data->groupid = $groupid; + $data->userid = $userid; + $data->eventtype = $event->get_type(); + $data->timestart = $starttimestamp; + $data->timeduration = $endtimestamp - $starttimestamp; + $data->timesort = $event->get_times()->get_sort_time()->getTimestamp(); + $data->visible = $event->is_visible(); + $data->timemodified = $event->get_times()->get_modified_time()->getTimestamp(); + + if ($repeats = $event->get_repeats()) { + $data->repeatid = $repeats->get_id(); + } + + if ($cm = $event->get_course_module()) { + $data->modulename = $cm->get('modname'); + $data->instance = $cm->get('id'); + } + + parent::__construct($data, $related); + } + + /** + * Return the list of properties. + * + * @return array + */ + protected static function define_properties() { + return [ + 'id' => ['type' => PARAM_INT], + 'name' => ['type' => PARAM_TEXT], + 'description' => [ + 'type' => PARAM_RAW, + 'optional' => true, + 'default' => null, + 'null' => NULL_ALLOWED + ], + 'descriptionformat' => [ + 'type' => PARAM_INT, + 'optional' => true, + 'default' => null, + 'null' => NULL_ALLOWED + ], + 'groupid' => [ + 'type' => PARAM_INT, + 'optional' => true, + 'default' => null, + 'null' => NULL_ALLOWED + ], + 'userid' => [ + 'type' => PARAM_INT, + 'optional' => true, + 'default' => null, + 'null' => NULL_ALLOWED + ], + 'repeatid' => [ + 'type' => PARAM_INT, + 'optional' => true, + 'default' => null, + 'null' => NULL_ALLOWED + ], + 'modulename' => [ + 'type' => PARAM_TEXT, + 'optional' => true, + 'default' => null, + 'null' => NULL_ALLOWED + ], + 'instance' => [ + 'type' => PARAM_INT, + 'optional' => true, + 'default' => null, + 'null' => NULL_ALLOWED + ], + 'eventtype' => ['type' => PARAM_TEXT], + 'timestart' => ['type' => PARAM_INT], + 'timeduration' => ['type' => PARAM_INT], + 'timesort' => ['type' => PARAM_INT], + 'visible' => ['type' => PARAM_INT], + 'timemodified' => ['type' => PARAM_INT], + ]; + } + + /** + * Return the list of additional properties. + * + * @return array + */ + protected static function define_other_properties() { + return [ + 'url' => ['type' => PARAM_URL], + 'icon' => [ + 'type' => event_icon_exporter::read_properties_definition(), + ], + 'action' => [ + 'type' => event_action_exporter::read_properties_definition(), + 'optional' => true, + ], + 'course' => [ + 'type' => course_summary_exporter::read_properties_definition(), + 'optional' => true, + ] + ]; + } + + /** + * Get the additional values to inject while exporting. + * + * @param renderer_base $output The renderer. + * @return array Keys are the property names, values are their values. + */ + protected function get_other_values(renderer_base $output) { + $values = []; + $event = $this->event; + $context = $this->related['context']; + $modulename = $event->get_course_module()->get('modname'); + $moduleid = $event->get_course_module()->get('id'); + $timesort = $event->get_times()->get_sort_time()->getTimestamp(); + $url = new \moodle_url(sprintf('/mod/%s/view.php', $modulename), ['id' => $moduleid]); + $iconexporter = new event_icon_exporter($event, ['context' => $context]); + + $values['url'] = $url->out(false); + $values['icon'] = $iconexporter->export($output); + + if ($event instanceof action_event_interface) { + $actionrelated = [ + 'context' => $context, + 'event' => $event + ]; + $actionexporter = new event_action_exporter($event->get_action(), $actionrelated); + $values['action'] = $actionexporter->export($output); + } + + if ($course = $this->related['course']) { + $coursesummaryexporter = new course_summary_exporter($course, ['context' => $context]); + $values['course'] = $coursesummaryexporter->export($output); + } + + return $values; + } + + /** + * Returns a list of objects that are related. + * + * @return array + */ + protected static function define_related() { + return [ + 'context' => 'context', + 'course' => 'stdClass?', + ]; + } +} diff --git a/calendar/classes/external/event_icon_exporter.php b/calendar/classes/external/event_icon_exporter.php new file mode 100644 index 00000000000..2895a409b85 --- /dev/null +++ b/calendar/classes/external/event_icon_exporter.php @@ -0,0 +1,124 @@ +. + +/** + * Contains event class for displaying a calendar event's icon. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\external; + +defined('MOODLE_INTERNAL') || die(); + +use \core\external\exporter; +use \core_calendar\local\event\entities\event_interface; + +/** + * Class for displaying a calendar event's icon. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class event_icon_exporter extends exporter { + + /** + * Constructor. + * + * @param event_interface $event + * @param array $related The related data. + */ + public function __construct(event_interface $event, $related = []) { + $coursemodule = $event->get_course_module(); + $course = $event->get_course(); + $courseid = $course ? $course->get('id') : null; + $group = $event->get_group(); + $groupid = $group ? $group->get('id') : null; + $user = $event->get_user(); + $userid = $user ? $user->get('id') : null; + $isactivityevent = !empty($coursemodule); + $isglobalevent = ($course && $courseid == SITEID); + $iscourseevent = ($course && !empty($courseid) && $courseid != SITEID && $group && empty($groupid)); + $isgroupevent = ($group && !empty($groupid)); + $isuserevent = ($user && !empty($userid)); + + if ($isactivityevent) { + $key = 'icon'; + $component = $coursemodule->get('modname'); + + if (get_string_manager()->string_exists($event->get_type(), $component)) { + $alttext = get_string($event->get_type(), $component); + } else { + $alttext = get_string('activityevent', 'calendar'); + } + } else if ($isglobalevent) { + $key = 'i/siteevent'; + $component = 'core'; + $alttext = get_string('globalevent', 'calendar'); + } else if ($iscourseevent) { + $key = 'i/courseevent'; + $component = 'core'; + $alttext = get_string('courseevent', 'calendar'); + } else if ($isgroupevent) { + $key = 'i/groupevent'; + $component = 'core'; + $alttext = get_string('groupevent', 'calendar'); + } else if ($isuserevent) { + $key = 'i/userevent'; + $component = 'core'; + $alttext = get_string('userevent', 'calendar'); + } else { + // Default to site event icon? + $key = 'i/siteevent'; + $component = 'core'; + $alttext = get_string('globalevent', 'calendar'); + } + + $data = new \stdClass(); + $data->key = $key; + $data->component = $component; + $data->alttext = $alttext; + + parent::__construct($data, $related); + } + + /** + * Return the list of properties. + * + * @return array + */ + protected static function define_properties() { + return [ + 'key' => ['type' => PARAM_TEXT], + 'component' => ['type' => PARAM_TEXT], + 'alttext' => ['type' => PARAM_TEXT], + ]; + } + + /** + * Returns a list of objects that are related. + * + * @return array + */ + protected static function define_related() { + return [ + 'context' => 'context', + ]; + } +} diff --git a/calendar/classes/external/events_exporter.php b/calendar/classes/external/events_exporter.php new file mode 100644 index 00000000000..9198c4c9c98 --- /dev/null +++ b/calendar/classes/external/events_exporter.php @@ -0,0 +1,121 @@ +. + +/** + * Contains event class for displaying a list of calendar events. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\external; + +defined('MOODLE_INTERNAL') || die(); + +use \core\external\exporter; +use \renderer_base; + +/** + * Class for displaying a list of calendar events. + * + * This class uses the events relateds cache in order to get the related + * data for exporting an event without having to naively hit the database + * for each event. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class events_exporter extends exporter { + + /** + * @var array $events An array of event_interface objects. + */ + protected $events; + + /** + * Constructor. + * + * @param array $events An array of event_interface objects + * @param array $related An array of related objects + */ + public function __construct(array $events, $related = []) { + $this->events = $events; + parent::__construct([], $related); + } + + /** + * Return the list of additional properties. + * + * @return array + */ + protected static function define_other_properties() { + return [ + 'events' => [ + 'type' => event_exporter::read_properties_definition(), + 'multiple' => true, + ], + 'firstid' => [ + 'type' => PARAM_INT, + 'null' => NULL_ALLOWED, + 'default' => null, + ], + 'lastid' => [ + 'type' => PARAM_INT, + 'null' => NULL_ALLOWED, + 'default' => null, + ], + ]; + } + + /** + * Get the additional values to inject while exporting. + * + * @param renderer_base $output The renderer. + * @return array Keys are the property names, values are their values. + */ + protected function get_other_values(renderer_base $output) { + $return = []; + $cache = $this->related['cache']; + + $return['events'] = array_map(function($event) use ($cache, $output) { + $context = $cache->get_context($event); + $course = $cache->get_course($event); + $exporter = new event_exporter($event, ['context' => $context, 'course' => $course]); + + return $exporter->export($output); + }, $this->events); + + if ($count = count($return['events'])) { + $return['firstid'] = $return['events'][0]->id; + $return['lastid'] = $return['events'][$count - 1]->id; + } + + return $return; + } + + /** + * Returns a list of objects that are related. + * + * @return array + */ + protected static function define_related() { + return [ + 'cache' => 'core_calendar\external\events_related_objects_cache', + ]; + } +} diff --git a/calendar/classes/external/events_grouped_by_course_exporter.php b/calendar/classes/external/events_grouped_by_course_exporter.php new file mode 100644 index 00000000000..349ff6094ac --- /dev/null +++ b/calendar/classes/external/events_grouped_by_course_exporter.php @@ -0,0 +1,106 @@ +. + +/** + * Contains event class for displaying a list of calendar events grouped by course id. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\external; + +defined('MOODLE_INTERNAL') || die(); + +use \core\external\exporter; +use \renderer_base; + +/** + * Class for displaying a list of calendar events grouped by course id. + * + * This class uses the events relateds cache in order to get the related + * data for exporting an event without having to naively hit the database + * for each event. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class events_grouped_by_course_exporter extends exporter { + + /** + * @var array $events An array of event_interface objects + * grouped and index by course id. + */ + protected $eventsbycourse; + + /** + * Constructor. + * + * @param array $eventsbycourse An array of event_interface objects + * @param array $related An array of related objects + */ + public function __construct(array $eventsbycourse, $related = []) { + $this->eventsbycourse = $eventsbycourse; + parent::__construct([], $related); + } + + /** + * Return the list of additional properties. + * + * @return array + */ + protected static function define_other_properties() { + return [ + 'groupedbycourse' => [ + 'type' => events_same_course_exporter::read_properties_definition(), + 'multiple' => true, + 'default' => [], + ], + ]; + } + + /** + * Get the additional values to inject while exporting. + * + * @param renderer_base $output The renderer. + * @return array Keys are the property names, values are their values. + */ + protected function get_other_values(renderer_base $output) { + $return = []; + $cache = $this->related['cache']; + + foreach ($this->eventsbycourse as $courseid => $events) { + $eventsexporter = new events_same_course_exporter( + $courseid, $events, ['cache' => $cache]); + $return['groupedbycourse'][] = $eventsexporter->export($output); + } + + return $return; + } + + /** + * Returns a list of objects that are related. + * + * @return array + */ + protected static function define_related() { + return [ + 'cache' => 'core_calendar\external\events_related_objects_cache', + ]; + } +} diff --git a/calendar/classes/external/events_related_objects_cache.php b/calendar/classes/external/events_related_objects_cache.php new file mode 100644 index 00000000000..b19fd60cc0a --- /dev/null +++ b/calendar/classes/external/events_related_objects_cache.php @@ -0,0 +1,224 @@ +. + +/** + * Contains event class for providing the related objects when exporting a list of calendar events. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\external; + +defined('MOODLE_INTERNAL') || die(); + +use context; +use \core_calendar\local\event\entities\event_interface; +use stdClass; + +/** + * Class to providing the related objects when exporting a list of calendar events. + * + * This class is only meant for use with exporters. It attempts to bulk load + * the related objects for a list of events and cache them to avoid having + * to query the database when exporting each individual event. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class events_related_objects_cache { + + /** + * @var array $events The events for which we need related objects. + */ + protected $events; + + /** + * @var array $courses The related courses. + */ + protected $courses = null; + + /** + * @var array $events The related groups. + */ + protected $groups = null; + + /** + * @var array $events The related course modules. + */ + protected $coursemodules = []; + + /** + * Constructor. + * + * @param array $events Array of event_interface events + * @param array $courses Array of courses to populate the cache with + */ + public function __construct(array $events, array $courses = null) { + $this->events = $events; + + if (!is_null($courses)) { + $this->courses = []; + + foreach ($courses as $course) { + $this->courses[$course->id] = $course; + } + } + } + + /** + * Get the related course object for a given event. + * + * @param event_interface $event The event object. + * @return stdClass|null + */ + public function get_course(event_interface $event) { + if (is_null($this->courses)) { + $this->load_courses(); + } + + if ($course = $event->get_course()) { + $courseid = $course->get('id'); + return isset($this->courses[$courseid]) ? $this->courses[$courseid] : null; + } else { + return null; + } + } + + /** + * Get the related context for a given event. + * + * @param event_interface $event The event object. + * @return context|null + */ + public function get_context(event_interface $event) { + global $USER; + + $courseid = $event->get_course() ? $event->get_course()->get('id') : null; + $groupid = $event->get_group() ? $event->get_group()->get('id') : null; + $userid = $event->get_user() ? $event->get_user()->get('id') : null; + $moduleid = $event->get_course_module() ? $event->get_course_module()->get('id') : null; + + if (!empty($courseid)) { + return \context_course::instance($event->get_course()->get('id')); + } else if (!empty($groupid)) { + $group = $this->get_group($event); + return \context_course::instance($group->courseid); + } else if (!empty($userid) && $userid == $USER->id) { + return \context_user::instance($userid); + } else if (!empty($userid) && $userid != $USER->id && $moduleid && $moduleid > 0) { + $cm = $this->get_course_module($event); + return \context_course::instance($cm->course); + } else { + return \context_user::instance($userid); + } + } + + /** + * Get the related group object for a given event. + * + * @param event_interface $event The event object. + * @return stdClass|null + */ + public function get_group(event_interface $event) { + if (is_null($this->groups)) { + $this->load_groups(); + } + + if ($group = $event->get_group()) { + $groupid = $group->get('id'); + return isset($this->groups[$groupid]) ? $this->groups[$groupid] : null; + } else { + return null; + } + } + + /** + * Get the related course module for a given event. + * + * @param event_interface $event The event object. + * @return stdClass|null + */ + public function get_course_module(event_interface $event) { + if (!$event->get_course_module()) { + return null; + } + + $id = $event->get_course_module()->get('id'); + $name = $event->get_course_module()->get('modname'); + $key = $name . '_' . $id; + + if (!isset($this->coursemodules[$key])) { + $this->coursemodules[$key] = get_coursemodule_from_instance($name, $id, 0, false, MUST_EXIST); + } + + return $this->coursemodules[$key]; + } + + /** + * Load the list of all of the distinct courses required for the + * list of provided events and save the result in memory. + */ + protected function load_courses() { + global $DB; + + $courseids = []; + foreach ($this->events as $event) { + if ($course = $event->get_course()) { + $id = $course->get('id'); + $courseids[$id] = true; + } + } + + if (empty($courseids)) { + $this->courses = []; + return; + } + + list($idsql, $params) = $DB->get_in_or_equal(array_keys($courseids)); + $sql = "SELECT * FROM {course} WHERE id {$idsql}"; + + $this->courses = $DB->get_records_sql($sql, $params); + } + + /** + * Load the list of all of the distinct groups required for the + * list of provided events and save the result in memory. + */ + protected function load_groups() { + global $DB; + + $groupids = []; + foreach ($this->events as $event) { + if ($group = $event->get_group()) { + $id = $group->get('id'); + $groupids[$id] = true; + } + } + + if (empty($groupids)) { + $this->groups = []; + return; + } + + list($idsql, $params) = $DB->get_in_or_equal(array_keys($groupids)); + $sql = "SELECT * FROM {groups} WHERE id {$idsql}"; + + $this->groups = $DB->get_records_sql($sql, $params); + } +} diff --git a/calendar/classes/external/events_same_course_exporter.php b/calendar/classes/external/events_same_course_exporter.php new file mode 100644 index 00000000000..39f24df0815 --- /dev/null +++ b/calendar/classes/external/events_same_course_exporter.php @@ -0,0 +1,83 @@ +. + +/** + * Contains event class for displaying a list of calendar events for a single course. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\external; + +defined('MOODLE_INTERNAL') || die(); + +use \renderer_base; + +/** + * Class for displaying a list of calendar events for a single course. + * + * This class uses the events relateds cache in order to get the related + * data for exporting an event without having to naively hit the database + * for each event. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class events_same_course_exporter extends events_exporter { + + /** + * @var array $courseid The id of the course for these events. + */ + protected $courseid; + + /** + * Constructor. + * + * @param int $courseid The course id for these events + * @param array $events An array of event_interface objects + * @param array $related An array of related objects + */ + public function __construct($courseid, array $events, $related = []) { + parent::__construct($events, $related); + $this->courseid = $courseid; + } + + /** + * Return the list of additional properties. + * + * @return array + */ + protected static function define_other_properties() { + $properties = parent::define_other_properties(); + $properties['courseid'] = ['type' => PARAM_INT]; + return $properties; + } + + /** + * Get the additional values to inject while exporting. + * + * @param renderer_base $output The renderer. + * @return array Keys are the property names, values are their values. + */ + protected function get_other_values(renderer_base $output) { + $values = parent::get_other_values($output); + $values['courseid'] = $this->courseid; + return $values; + } +} diff --git a/calendar/classes/local/api.php b/calendar/classes/local/api.php new file mode 100644 index 00000000000..18682ff1170 --- /dev/null +++ b/calendar/classes/local/api.php @@ -0,0 +1,271 @@ +. + +/** + * Contains class containing the internal calendar API. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local; + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\local\event\exceptions\limit_invalid_parameter_exception; + +/** + * Class containing the local calendar API. + * + * This should not be used outside of core_calendar. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class api { + /** + * Get all events restricted by various parameters, taking in to account user and group overrides. + * + * @param int|null $timestartfrom Events with timestart from this value (inclusive). + * @param int|null $timestartto Events with timestart until this value (inclusive). + * @param int|null $timesortfrom Events with timesort from this value (inclusive). + * @param int|null $timesortto Events with timesort until this value (inclusive). + * @param int|null $timestartaftereventid Restrict the events in the timestart range to ones after this ID. + * @param int|null $timesortaftereventid Restrict the events in the timesort range to ones after this ID. + * @param int $limitnum Return at most this number of events. + * @param int|null $type Return only events of this type. + * @param array|null $usersfilter Return only events for these users. + * @param array|null $groupsfilter Return only events for these groups. + * @param array|null $coursesfilter Return only events for these courses. + * @param bool $withduration If true return only events starting within specified + * timestart otherwise return in progress events as well. + * @param bool $ignorehidden If true don't return hidden events. + * @return \core_calendar\local\event\entities\event_interface[] Array of event_interfaces. + */ + public static function get_events( + $timestartfrom = null, + $timestartto = null, + $timesortfrom = null, + $timesortto = null, + $timestartaftereventid = null, + $timesortaftereventid = null, + $limitnum = 20, + $type = null, + array $usersfilter = null, + array $groupsfilter = null, + array $coursesfilter = null, + $withduration = true, + $ignorehidden = true + ) { + global $USER; + + $vault = \core_calendar\local\event\container::get_event_vault(); + + $timestartafterevent = null; + $timesortafterevent = null; + + if ($timestartaftereventid && $event = $vault->get_event_by_id($timestartaftereventid)) { + $timestartafterevent = $event; + } + + if ($timesortaftereventid && $event = $vault->get_event_by_id($timesortaftereventid)) { + $timesortafterevent = $event; + } + + return $vault->get_events( + $timestartfrom, + $timestartto, + $timesortfrom, + $timesortto, + $timestartafterevent, + $timesortafterevent, + $limitnum, + $type, + $usersfilter, + $groupsfilter, + $coursesfilter, + $withduration, + $ignorehidden + ); + } + + /** + * Get legacy calendar events + * + * @param int $tstart Start time of time range for events + * @param int $tend End time of time range for events + * @param array|int|boolean $users array of users, user id or boolean for all/no user events + * @param array|int|boolean $groups array of groups, group id or boolean for all/no group events + * @param array|int|boolean $courses array of courses, course id or boolean for all/no course events + * @param boolean $withduration whether only events starting within time range selected + * or events in progress/already started selected as well + * @param boolean $ignorehidden whether to select only visible events or all events + * @return array $events of selected events or an empty array if there aren't any (or there was an error) + */ + public static function get_legacy_events( + $tstart, + $tend, + $users, + $groups, + $courses, + $withduration = true, + $ignorehidden = true + ) { + $fixedparams = array_map(function($param) { + if ($param === true) { + return null; + } + + if (!is_array($param)) { + return [$param]; + } + + return $param; + }, [$users, $groups, $courses]); + + $mapper = \core_calendar\local\event\container::get_event_mapper(); + $events = self::get_events( + $tstart, + $tend, + null, + null, + null, + null, + 40, + null, + $fixedparams[0], + $fixedparams[1], + $fixedparams[2], + $withduration, + $ignorehidden + ); + + return array_reduce($events, function($carry, $event) use ($mapper) { + return $carry + [$event->get_id() => $mapper->from_event_to_stdclass($event)]; + }, []); + } + + /** + * Get a list of action events for the logged in user by the given + * timesort values. + * + * @param int|null $timesortfrom The start timesort value (inclusive) + * @param int|null $timesortto The end timesort value (inclusive) + * @param int|null $aftereventid Only return events after this one + * @param int $limitnum Limit results to this amount (between 1 and 50) + * @return array A list of action_event_interface objects + * @throws \moodle_exception + */ + public static function get_action_events_by_timesort( + $timesortfrom = null, + $timesortto = null, + $aftereventid = null, + $limitnum = 20 + ) { + global $USER; + + if (is_null($timesortfrom) && is_null($timesortto)) { + throw new \moodle_exception("Must provide a timesort to and/or from value"); + } + + if ($limitnum < 1 || $limitnum > 50) { + throw new \moodle_exception("Limit must be between 1 and 50 (inclusive)"); + } + + $vault = \core_calendar\local\event\container::get_event_vault(); + + $afterevent = null; + if ($aftereventid && $event = $vault->get_event_by_id($aftereventid)) { + $afterevent = $event; + } + + return $vault->get_action_events_by_timesort($USER, $timesortfrom, $timesortto, $afterevent, $limitnum); + } + + /** + * Get a list of action events for the logged in user by the given + * course and timesort values. + * + * @param \stdClass $course The course the events must belong to + * @param int|null $timesortfrom The start timesort value (inclusive) + * @param int|null $timesortto The end timesort value (inclusive) + * @param int|null $aftereventid Only return events after this one + * @param int $limitnum Limit results to this amount (between 1 and 50) + * @return array A list of action_event_interface objects + * @throws limit_invalid_parameter_exception + */ + public static function get_action_events_by_course( + $course, + $timesortfrom = null, + $timesortto = null, + $aftereventid = null, + $limitnum = 20 + ) { + global $USER; + + if ($limitnum < 1 || $limitnum > 50) { + throw new limit_invalid_parameter_exception( + "Limit must be between 1 and 50 (inclusive)"); + } + + $vault = \core_calendar\local\event\container::get_event_vault(); + + $afterevent = null; + if ($aftereventid && $event = $vault->get_event_by_id($aftereventid)) { + $afterevent = $event; + } + + return $vault->get_action_events_by_course( + $USER, $course, $timesortfrom, $timesortto, $afterevent, $limitnum); + } + + /** + * Get a list of action events for the logged in user by the given + * courses and timesort values. + * + * The limit number applies per course, not for the result set as a whole. + * E.g. Requesting 3 courses with a limit of 10 will result in up to 30 + * events being returned (up to 10 per course). + * + * @param array $courses The courses the events must belong to + * @param int|null $timesortfrom The start timesort value (inclusive) + * @param int|null $timesortto The end timesort value (inclusive) + * @param int $limitnum Limit results per course to this amount (between 1 and 50) + * @return array A list of action_event_interface objects indexed by course id + */ + public static function get_action_events_by_courses( + $courses = [], + $timesortfrom = null, + $timesortto = null, + $limitnum = 20 + ) { + $return = []; + + foreach ($courses as $course) { + $return[$course->id] = self::get_action_events_by_course( + $course, + $timesortfrom, + $timesortto, + null, + $limitnum + ); + } + + return $return; + } +} diff --git a/calendar/classes/local/event/container.php b/calendar/classes/local/event/container.php new file mode 100644 index 00000000000..bb88a5ca6dc --- /dev/null +++ b/calendar/classes/local/event/container.php @@ -0,0 +1,283 @@ +. + +/** + * Core container for calendar events. + * + * The purpose of this class is simply to wire together the various + * implementations of calendar event components to produce a solution + * to the problems Moodle core wants to solve. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event; + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\action_factory; +use core_calendar\local\event\data_access\event_vault; +use core_calendar\local\event\entities\action_event; +use core_calendar\local\event\entities\action_event_interface; +use core_calendar\local\event\entities\event_interface; +use core_calendar\local\event\factories\event_factory; +use core_calendar\local\event\mappers\event_mapper; +use core_calendar\local\event\strategies\raw_event_retrieval_strategy; + +/** + * Core container. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class container { + /** + * @var event_factory $eventfactory Event factory. + */ + protected static $eventfactory; + + /** + * @var event_mapper $eventmapper Event mapper. + */ + protected static $eventmapper; + + /** + * @var action_factory $actionfactory Action factory. + */ + protected static $actionfactory; + + /** + * @var event_vault $eventvault Event vault. + */ + protected static $eventvault; + + /** + * @var raw_event_retrieval_strategy $eventretrievalstrategy Event retrieval strategy. + */ + protected static $eventretrievalstrategy; + + /** + * @var array A list of callbacks to use. + */ + protected static $callbacks = array(); + + /** + * @var \stdClass[] An array of cached courses to use with the event factory. + */ + protected static $coursecache = array(); + + /** + * @var \stdClass[] An array of cached modules to use with the event factory. + */ + protected static $modulecache = array(); + + /** + * Initialises the dependency graph if it hasn't yet been. + */ + private static function init() { + if (empty(self::$eventfactory)) { + // When testing the container's components, we need to make sure + // the callback implementations in modules are not executed, since + // we cannot control their output from PHPUnit. To do this we have + // a set of 'testing' callbacks that the factory can use. This way + // we know exactly how the factory behaves when being tested. + $getcallback = function($which) { + return self::$callbacks[PHPUNIT_TEST ? 'testing' : 'production'][$which]; + }; + + self::initcallbacks(); + self::$actionfactory = new action_factory(); + self::$eventmapper = new event_mapper( + // The event mapper we return from here needs to know how to + // make events, so it needs an event factory. However we can't + // give it the same one as we store and return in the container + // as that one uses all our plumbing to control event visibility. + // + // So we make a new even factory that doesn't do anyting other than + // return the instance. + new event_factory( + // Never apply actions, simply return. + function(event_interface $event) { + return $event; + }, + // Never hide an event. + function() { + return true; + }, + // Never bail out early when instantiating an event. + function() { + return false; + }, + self::$coursecache, + self::$modulecache + ) + ); + + self::$eventfactory = new event_factory( + $getcallback('action'), + $getcallback('visibility'), + function ($dbrow) { + // At present we only handle callbacks in course modules. + if (empty($dbrow->modulename)) { + return false; + } + + $instances = get_fast_modinfo($dbrow->courseid)->instances; + + // If modinfo doesn't know about the module, we should ignore it. + if (!isset($instances[$dbrow->modulename]) || !isset($instances[$dbrow->modulename][$dbrow->instance])) { + return true; + } + + $cm = $instances[$dbrow->modulename][$dbrow->instance]; + + // If the module is not visible to the current user, we should ignore it. + if (!$cm->uservisible) { + return true; + } + + // Ok, now check if we are looking at a completion event. + if ($dbrow->eventtype === \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED) { + // Need to have completion enabled before displaying these events. + $course = new \stdClass(); + $course->id = $dbrow->courseid; + $completion = new \completion_info($course); + + return (bool) !$completion->is_enabled($cm); + } + + return false; + }, + self::$coursecache, + self::$modulecache + ); + } + + if (empty(self::$eventvault)) { + self::$eventretrievalstrategy = new raw_event_retrieval_strategy(); + self::$eventvault = new event_vault(self::$eventfactory, self::$eventretrievalstrategy); + } + } + + /** + * Gets the event factory. + * + * @return event_factory + */ + public static function get_event_factory() { + self::init(); + return self::$eventfactory; + } + + /** + * Gets the event mapper. + * + * @return event_mapper + */ + public static function get_event_mapper() { + self::init(); + return self::$eventmapper; + } + + /** + * Return an event vault. + * + * @return event_vault + */ + public static function get_event_vault() { + self::init(); + return self::$eventvault; + } + + /** + * Initialises the callbacks. + * + * There are two sets here, one is used during PHPUnit runs. + * See the comment at the start of the init method for more + * detail. + */ + private static function initcallbacks() { + self::$callbacks = array( + 'testing' => array( + // Always return an action event. + 'action' => function (event_interface $event) { + return new action_event( + $event, + new \core_calendar\local\event\value_objects\action( + 'test', + new \moodle_url('http://example.com'), + 420, + true + )); + }, + // Always be visible. + 'visibility' => function (event_interface $event) { + return true; + } + ), + 'production' => array( + // This function has type event_interface -> event_interface. + // This is enforced by the event_factory. + 'action' => function (event_interface $event) { + // Callbacks will get supplied a "legacy" version + // of the event class. + $mapper = self::$eventmapper; + $action = component_callback( + 'mod_' . $event->get_course_module()->get('modname'), + 'core_calendar_provide_event_action', + [ + $mapper->from_event_to_legacy_event($event), + self::$actionfactory + ] + ); + + // If we get an action back, return an action event, otherwise + // continue piping through the original event. + // + // If a module does not implement the callback, component_callback + // returns null. + return $action ? new action_event($event, $action) : $event; + }, + // This function has type event_interface -> bool. + // This is enforced by the event_factory. + 'visibility' => function (event_interface $event) { + $mapper = self::$eventmapper; + $eventvisible = component_callback( + 'mod_' . $event->get_course_module()->get('modname'), + 'core_calendar_is_event_visible', + [ + $mapper->from_event_to_legacy_event($event) + ] + ); + + // Do not display the event if there is nothing to action. + if ($event instanceof action_event_interface && $event->get_action()->get_item_count() === 0) { + return false; + } + + // Module does not implement the callback, event should be visible. + if (is_null($eventvisible)) { + return true; + } + + return $eventvisible ? true : false; + } + ), + ); + } +} diff --git a/calendar/classes/local/event/data_access/event_vault.php b/calendar/classes/local/event/data_access/event_vault.php new file mode 100644 index 00000000000..8517e268f5d --- /dev/null +++ b/calendar/classes/local/event/data_access/event_vault.php @@ -0,0 +1,380 @@ +. + +/** + * Event vault class + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\data_access; + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\local\event\entities\action_event_interface; +use core_calendar\local\event\entities\event_interface; +use core_calendar\local\event\exceptions\limit_invalid_parameter_exception; +use core_calendar\local\event\factories\action_factory_interface; +use core_calendar\local\event\factories\event_factory_interface; +use core_calendar\local\event\strategies\raw_event_retrieval_strategy_interface; + +/** + * Event vault class. + * + * This class will handle interacting with the database layer to retrieve + * the records. This is required to house the complex logic required for + * pagination because it's not a one-to-one mapping between database records + * and users. + * + * This is a repository. It's called a vault to reduce confusion because + * Moodle has already taken the name repository. Vault is cooler anyway. + * + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class event_vault implements event_vault_interface { + + /** + * @var event_factory_interface $factory Factory for creating events. + */ + protected $factory; + + /** + * @var raw_event_retrieval_strategy_interface $retrievalstrategy Strategy for getting events from the DB. + */ + protected $retrievalstrategy; + + /** + * Create an event vault. + * + * @param event_factory_interface $factory An event factory + * @param raw_event_retrieval_strategy_interface $retrievalstrategy + */ + public function __construct( + event_factory_interface $factory, + raw_event_retrieval_strategy_interface $retrievalstrategy + ) { + $this->factory = $factory; + $this->retrievalstrategy = $retrievalstrategy; + } + + public function get_event_by_id($id) { + global $DB; + + if ($record = $DB->get_record('event', ['id' => $id])) { + return $this->transform_from_database_record($record); + } else { + return false; + } + } + + public function get_events( + $timestartfrom = null, + $timestartto = null, + $timesortfrom = null, + $timesortto = null, + event_interface $timestartafterevent = null, + event_interface $timesortafterevent = null, + $limitnum = 20, + $type = null, + array $usersfilter = null, + array $groupsfilter = null, + array $coursesfilter = null, + $withduration = true, + $ignorehidden = true, + callable $filter = null + ) { + if ($limitnum < 1 || $limitnum > 50) { + throw new limit_invalid_parameter_exception("Limit must be between 1 and 50 (inclusive)"); + } + + $fromquery = function($field, $timefrom, $lastseenmethod, $afterevent, $withduration) { + if (!$timefrom) { + return false; + } + + return $this->timefield_pagination_from( + $field, + $timefrom, + $afterevent ? $afterevent->get_times()->{$lastseenmethod}()->getTimestamp() : null, + $afterevent ? $afterevent->get_id() : null, + $withduration + ); + }; + + $toquery = function($field, $timeto, $lastseenmethod, $afterevent) { + if (!$timeto) { + return false; + } + + return $this->timefield_pagination_to( + $field, + $timeto, + $afterevent ? $afterevent->get_times()->{$lastseenmethod}()->getTimestamp() : null, + $afterevent ? $afterevent->get_id() : null + ); + }; + + $timesortfromquery = $fromquery('timesort', $timesortfrom, 'get_sort_time', $timesortafterevent, $withduration); + $timesorttoquery = $toquery('timesort', $timesortto, 'get_sort_time', $timesortafterevent); + $timestartfromquery = $fromquery('timestart', $timestartfrom, 'get_start_time', $timestartafterevent, $withduration); + $timestarttoquery = $toquery('timestart', $timestartto, 'get_start_time', $timestartafterevent); + + if (($timesortto && !$timesorttoquery) || ($timestartto && !$timestarttoquery)) { + return []; + } + + $params = array_merge( + $type ? ['type' => $type] : [], + $timesortfromquery ? $timesortfromquery['params'] : [], + $timesorttoquery ? $timesorttoquery['params'] : [], + $timestartfromquery ? $timestartfromquery['params'] : [], + $timestarttoquery ? $timestarttoquery['params'] : [] + ); + + $where = array_merge( + $type ? ['type = :type'] : [], + $timesortfromquery ? $timesortfromquery['where'] : [], + $timesorttoquery ? $timesorttoquery['where'] : [], + $timestartfromquery ? $timestartfromquery['where'] : [], + $timestarttoquery ? $timestarttoquery['where'] : [] + ); + + $offset = 0; + $events = []; + + while ($records = array_values($this->retrievalstrategy->get_raw_events( + $usersfilter, + $groupsfilter, + $coursesfilter, + $where, + $params, + "timesort ASC, id ASC", + $offset, + $limitnum, + $ignorehidden + ))) { + foreach ($records as $record) { + if ($event = $this->transform_from_database_record($record)) { + $filtertest = $filter ? $filter($event) : true; + + if ($event && $filtertest) { + $events[] = $event; + } + + if (count($events) == $limitnum) { + // We've got all of the events so break both loops. + break 2; + } + } + } + + $offset += $limitnum; + } + + return $events; + } + + public function get_action_events_by_timesort( + \stdClass $user, + $timesortfrom = null, + $timesortto = null, + event_interface $afterevent = null, + $limitnum = 20 + ) { + return $this->get_events( + null, + null, + $timesortfrom, + $timesortto, + null, + $afterevent, + $limitnum, + CALENDAR_EVENT_TYPE_ACTION, + [$user->id], + null, + null, + true, + true, + function ($event) { + return $event instanceof action_event_interface; + } + ); + } + + public function get_action_events_by_course( + \stdClass $user, + \stdClass $course, + $timesortfrom = null, + $timesortto = null, + event_interface $afterevent = null, + $limitnum = 20 + ) { + return array_values( + $this->get_events( + null, + null, + $timesortfrom, + $timesortto, + null, + $afterevent, + $limitnum, + CALENDAR_EVENT_TYPE_ACTION, + [$user->id], + null, + [$course->id], + true, + true, + function ($event) use ($course) { + return $event instanceof action_event_interface && $event->get_course()->get_id() == $course->id; + } + ) + ); + } + + /** + * Generates SQL subquery and parameters for 'from' pagination. + * + * @param string $field + * @param int $timefrom + * @param int|null $lastseentime + * @param int|null $lastseenid + * @param bool $withduration + * @return array + */ + protected function timefield_pagination_from( + $field, + $timefrom, + $lastseentime = null, + $lastseenid = null, + $withduration = true + ) { + $where = ''; + $params = []; + + if ($lastseentime && $lastseentime >= $timefrom) { + $where = '((timesort = :timefrom1 AND e.id > :timefromid) OR timesort > :timefrom2)'; + if ($field === 'timestart') { + $where = '((timestart = :timefrom1 AND e.id > :timefromid) OR timestart > :timefrom2' . + ($withduration ? ' OR timestart + timeduration > :timefrom3' : '') . ')'; + } + $params['timefromid'] = $lastseenid; + $params['timefrom1'] = $lastseentime; + $params['timefrom2'] = $lastseentime; + $params['timefrom3'] = $lastseentime; + } else { + $where = 'timesort >= :timefrom'; + if ($field === 'timestart') { + $where = '(timestart >= :timefrom' . + ($withduration ? ' OR timestart + timeduration > :timefrom2' : '') . ')'; + } + + $params['timefrom'] = $timefrom; + $params['timefrom2'] = $timefrom; + } + + return ['where' => [$where], 'params' => $params]; + } + + /** + * Generates SQL subquery and parameters for 'to' pagination. + * + * @param string $field + * @param int $timeto + * @param int|null $lastseentime + * @param int|null $lastseenid + * @return array|bool + */ + protected function timefield_pagination_to( + $field, + $timeto, + $lastseentime = null, + $lastseenid = null + ) { + $where = []; + $params = []; + + if ($lastseentime && $lastseentime > $timeto) { + // The last seen event from this set is after the time sort range which + // means all events in this range have been seen, so we can just return + // early here. + return false; + } else if ($lastseentime && $lastseentime == $timeto) { + $where[] = '((timesort = :timeto1 AND e.id > :timetoid) OR timesort < :timeto2)'; + if ($field === 'timestart') { + $where[] = '((timestart = :timeto1 AND e.id > :timetoid) OR timestart < :timeto2)'; + } + $params['timetoid'] = $lastseenid; + $params['timeto1'] = $timeto; + $params['timeto2'] = $timeto; + } else { + $where[] = ($field === 'timestart' ? 'timestart' : 'timesort') . ' <= :timeto'; + $params['timeto'] = $timeto; + } + + return ['where' => $where, 'params' => $params]; + } + + /** + * Create an event from a database record. + * + * @param \stdClass $record The database record + * @return event_interface|null + */ + protected function transform_from_database_record(\stdClass $record) { + if ($record->courseid == 0 && $record->instance && $record->modulename) { + list($course, $cm) = get_course_and_cm_from_instance($record->instance, $record->modulename); + $record->courseid = $course->id; + } + + return $this->factory->create_instance($record); + } + + /** + * Fetches records from DB. + * + * @param int $userid + * @param string $whereconditions + * @param array $whereparams + * @param string $ordersql + * @param int $offset + * @param int $limitnum + * @return array + */ + protected function get_from_db( + $userid, + $whereconditions, + $whereparams, + $ordersql, + $offset, + $limitnum + ) { + return array_values( + $this->retrievalstrategy->get_raw_events( + [$userid], + null, + null, + $whereconditions, + $whereparams, + $ordersql, + $offset, + $limitnum + ) + ); + } +} diff --git a/calendar/classes/local/event/data_access/event_vault_interface.php b/calendar/classes/local/event/data_access/event_vault_interface.php new file mode 100644 index 00000000000..705b8327de0 --- /dev/null +++ b/calendar/classes/local/event/data_access/event_vault_interface.php @@ -0,0 +1,128 @@ +. + +/** + * Event vault interface + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\data_access; + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\local\event\entities\event_interface; + +/** + * Interface for an event vault class + * + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface event_vault_interface { + /** + * Retrieve an event for the given id. + * + * @param int $id The event id + * @return event_interface|false + */ + public function get_event_by_id($id); + + /** + * Get all events restricted by various parameters, taking in to account user and group overrides. + * + * @param int|null $timestartfrom Events with timestart from this value (inclusive). + * @param int|null $timestartto Events with timestart until this value (inclusive). + * @param int|null $timesortfrom Events with timesort from this value (inclusive). + * @param int|null $timesortto Events with timesort until this value (inclusive). + * @param event_interface|null $timestartafterevent Restrict the events in the timestart range to ones after this one. + * @param event_interface|null $timesortafterevent Restrict the events in the timesort range to ones after this one. + * @param int $limitnum Return at most this number of events. + * @param int|null $type Return only events of this type. + * @param array|null $usersfilter Return only events for these users. + * @param array|null $groupsfilter Return only events for these groups. + * @param array|null $coursesfilter Return only events for these courses. + * @param bool $withduration If true return only events starting within specified + * timestart otherwise return in progress events as well. + * @param bool $ignorehidden If true don't return hidden events. + * @param callable|null $filter Additional logic to filter out unwanted events. + * Must return true to keep the event, false to discard it. + * @return event_interface[] Array of event_interfaces. + */ + public function get_events( + $timestartfrom = null, + $timestartto = null, + $timesortfrom = null, + $timesortto = null, + event_interface $timestartafterevent = null, + event_interface $timesortafterevent = null, + $limitnum = 20, + $type = null, + array $usersfilter = null, + array $groupsfilter = null, + array $coursesfilter = null, + $withduration = true, + $ignorehidden = true, + callable $filter = null + ); + + /** + * Retrieve an array of events for the given user and time constraints. + * + * If using this function for pagination then you can provide the last event that you've seen + * ($afterevent) and it will be used to appropriately offset the result set so that you don't + * receive the same events again. + * @param \stdClass $user The user for whom the events belong + * @param int $timesortfrom Events with timesort from this value (inclusive) + * @param int $timesortto Events with timesort until this value (inclusive) + * @param event_interface $afterevent Only return events after this one + * @param int $limitnum Return at most this number of events + * @return event_interface + */ + public function get_action_events_by_timesort( + \stdClass $user, + $timesortfrom, + $timesortto, + event_interface $afterevent, + $limitnum + ); + + /** + * Retrieve an array of events for the given user filtered by the course and time constraints. + * + * If using this function for pagination then you can provide the last event that you've seen + * ($afterevent) and it will be used to appropriately offset the result set so that you don't + * receive the same events again. + * + * @param \stdClass $user The user for whom the events belong + * @param \stdClass $course The course to filter by + * @param int $timesortfrom Events with timesort from this value (inclusive) + * @param int $timesortto Events with timesort until this value (inclusive) + * @param event_interface $afterevent Only return events after this one + * @param int $limitnum Return at most this number of events + * @return action_event_interface + */ + public function get_action_events_by_course( + \stdClass $user, + \stdClass $course, + $timesortfrom, + $timesortto, + event_interface $afterevent, + $limitnum + ); +} diff --git a/calendar/classes/local/event/entities/action_event.php b/calendar/classes/local/event/entities/action_event.php new file mode 100644 index 00000000000..1c76999e97a --- /dev/null +++ b/calendar/classes/local/event/entities/action_event.php @@ -0,0 +1,115 @@ +. + +/** + * Calendar action event class. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\entities; + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\local\event\factories\action_factory_interface; + +/** + * Class representing an actionable event. + * + * An actionable event can be thought of as an embellished event. That is, + * it does everything a regular event does, but has some extra information + * attached to it. For example, the URL a user needs to visit to complete + * an action, the number of actionable items, etc. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class action_event implements action_event_interface { + /** + * @var event_interface $event The event to delegate to. + */ + protected $event; + + /** + * @var action_interface $action The action associated with this event. + */ + protected $action; + + /** + * Constructor. + * + * @param event_interface $event The event to delegate to. + * @param action_interface $action The action associated with this event. + */ + public function __construct(event_interface $event, action_interface $action) { + $this->event = $event; + $this->action = $action; + } + + public function get_id() { + return $this->event->get_id(); + } + + public function get_name() { + return $this->event->get_name(); + } + + public function get_description() { + return $this->event->get_description(); + } + + public function get_course() { + return $this->event->get_course(); + } + + public function get_course_module() { + return $this->event->get_course_module(); + } + + public function get_group() { + return $this->event->get_group(); + } + + public function get_user() { + return $this->event->get_user(); + } + + public function get_type() { + return $this->event->get_type(); + } + + public function get_times() { + return $this->event->get_times(); + } + + public function get_repeats() { + return $this->event->get_repeats(); + } + + public function get_subscription() { + return $this->event->get_subscription(); + } + + public function is_visible() { + return $this->event->is_visible(); + } + + public function get_action() { + return $this->action; + } +} diff --git a/calendar/classes/local/event/entities/action_event_interface.php b/calendar/classes/local/event/entities/action_event_interface.php new file mode 100644 index 00000000000..41be943d656 --- /dev/null +++ b/calendar/classes/local/event/entities/action_event_interface.php @@ -0,0 +1,42 @@ +. + +/** + * Calendar action event interface. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\entities; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Interface for an action event class. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface action_event_interface extends event_interface { + /** + * Get the action event's action. + * + * @return action_interface + */ + public function get_action(); +} diff --git a/calendar/classes/local/event/entities/action_interface.php b/calendar/classes/local/event/entities/action_interface.php new file mode 100644 index 00000000000..70c620227ee --- /dev/null +++ b/calendar/classes/local/event/entities/action_interface.php @@ -0,0 +1,63 @@ +. + +/** + * Action interface. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\entities; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Interface for a action class. + * + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface action_interface { + /** + * Get the name of the action. + * + * @return string + */ + public function get_name(); + + /** + * Get the URL of the action. + * + * @return \moodle_url + */ + public function get_url(); + + /** + * Get the number of items that need actioning. + * + * @return int + */ + public function get_item_count(); + + /** + * Get the actions actionability. + * + * @return bool + */ + public function is_actionable(); +} diff --git a/calendar/classes/local/event/entities/event.php b/calendar/classes/local/event/entities/event.php new file mode 100644 index 00000000000..cd5a1dff68b --- /dev/null +++ b/calendar/classes/local/event/entities/event.php @@ -0,0 +1,191 @@ +. + +/** + * Calendar event class. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\entities; + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\local\event\proxies\proxy_interface; +use core_calendar\local\event\value_objects\description_interface; +use core_calendar\local\event\value_objects\times_interface; + +/** + * Class representing a calendar event. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class event implements event_interface { + /** + * @var int $id The event's id in the database. + */ + protected $id; + + /** + * @var string $name The name of this event. + */ + protected $name; + + /** + * @var description_interface $description Description for this event. + */ + protected $description; + + /** + * @var proxy_interface $course Course for this event. + */ + protected $course; + + /** + * @var proxy_interface $group Group for this event. + */ + protected $group; + + /** + * @var proxy_interface $user User for this event. + */ + protected $user; + + /** + * @var event_collection_interface $repeats Collection of repeat events. + */ + protected $repeats; + + /** + * @var proxy_interface $coursemodule The course module that created this event. + */ + protected $coursemodule; + + /** + * @var string type The type of this event. + */ + protected $type; + + /** + * @var times_interface $times The times for this event. + */ + protected $times; + + /** + * @var bool $visible The visibility of this event. + */ + protected $visible; + + /** + * @var proxy_interface $subscription Subscription for this event. + */ + protected $subscription; + + /** + * Constructor. + * + * @param int $id The event's ID in the database. + * @param string $name The event's name. + * @param description_interface $description The event's description. + * @param proxy_interface $course The course associated with the event. + * @param proxy_interface $group The group associated with the event. + * @param proxy_interface $user The user associated with the event. + * @param event_collection_interface $repeats Collection of repeat events. + * @param proxy_interface $coursemodule The course module that created the event. + * @param string $type The event's type. + * @param times_interface $times The times associated with the event. + * @param bool $visible The event's visibility. True for visible, false for invisible. + * @param proxy_interface $subscription The event's subscription. + */ + public function __construct( + $id, + $name, + description_interface $description, + proxy_interface $course = null, + proxy_interface $group = null, + proxy_interface $user = null, + event_collection_interface $repeats, + proxy_interface $coursemodule = null, + $type, + times_interface $times, + $visible, + proxy_interface $subscription = null + ) { + $this->id = $id; + $this->name = $name; + $this->description = $description; + $this->course = $course; + $this->group = $group; + $this->user = $user; + $this->repeats = $repeats; + $this->coursemodule = $coursemodule; + $this->type = $type; + $this->times = $times; + $this->visible = $visible; + $this->subscription = $subscription; + } + + public function get_id() { + return $this->id; + } + + public function get_name() { + return $this->name; + } + + public function get_description() { + return $this->description; + } + + public function get_course() { + return $this->course; + } + + public function get_course_module() { + return $this->coursemodule; + } + + public function get_group() { + return $this->group; + } + + public function get_user() { + return $this->user; + } + + public function get_type() { + return $this->type; + } + + public function get_times() { + return $this->times; + } + + public function get_repeats() { + return $this->repeats; + } + + public function get_subscription() { + return $this->subscription; + } + + public function is_visible() { + return $this->visible; + } +} diff --git a/calendar/classes/local/event/entities/event_collection_interface.php b/calendar/classes/local/event/entities/event_collection_interface.php new file mode 100644 index 00000000000..d1bccbcd453 --- /dev/null +++ b/calendar/classes/local/event/entities/event_collection_interface.php @@ -0,0 +1,49 @@ +. + +/** + * Interface for an event collection class. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\entities; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Interface for an event collection class. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface event_collection_interface extends \IteratorAggregate { + /** + * Get the event collection's ID. + * + * @return int + */ + public function get_id(); + + /** + * Get the total number of repeats in the collection. + * + * @return int + */ + public function get_num(); +} diff --git a/calendar/classes/local/event/entities/event_interface.php b/calendar/classes/local/event/entities/event_interface.php new file mode 100644 index 00000000000..a277901c3db --- /dev/null +++ b/calendar/classes/local/event/entities/event_interface.php @@ -0,0 +1,119 @@ +. + +/** + * Calendar event interface. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\entities; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Interface for an event class. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface event_interface { + /** + * Get the event's ID. + * + * @return integer + */ + public function get_id(); + + /** + * Get the event's name. + * + * @return string + */ + public function get_name(); + + /** + * Get the event's description. + * + * @return description_interface + */ + public function get_description(); + + /** + * Get the course object associated with the event. + * + * @return proxy_interface + */ + public function get_course(); + + /** + * Get the course module object that created the event. + * + * @return proxy_interface + */ + public function get_course_module(); + + /** + * Get the group object associated with the event. + * + * @return proxy_interface + */ + public function get_group(); + + /** + * Get the user object associated with the event. + * + * @return proxy_interface + */ + public function get_user(); + + /** + * Get the event's type. + * + * @return string + */ + public function get_type(); + + /** + * Get the times associated with the event. + * + * @return times_interface + */ + public function get_times(); + + /** + * Get repeats of this event. + * + * @return event_collection_interface + */ + public function get_repeats(); + + /** + * Get the event's subscription. + * + * @return proxy_interface + */ + public function get_subscription(); + + /** + * Get the event's visibility. + * + * @return bool true if the event is visible, false otherwise + */ + public function is_visible(); +} diff --git a/calendar/classes/local/event/entities/repeat_event_collection.php b/calendar/classes/local/event/entities/repeat_event_collection.php new file mode 100644 index 00000000000..4289cb3ddcd --- /dev/null +++ b/calendar/classes/local/event/entities/repeat_event_collection.php @@ -0,0 +1,139 @@ +. + +/** + * Event collection class. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\entities; + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\local\event\factories\event_factory_interface; +use core_calendar\local\event\exceptions\no_repeat_parent_exception; + +/** + * Class representing a collection of repeat events. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class repeat_event_collection implements event_collection_interface { + /** + * @var int DB_QUERY_LIMIT How many records to pull from the DB at once. + */ + const DB_QUERY_LIMIT = 100; + + /** + * @var int $parentid The ID of the event which the events in this collection are repeats of. + */ + protected $parentid; + + /** + * @var \stdClass $parentrecord The parent event record from the database. + */ + protected $parentrecord; + + /** + * @var event_factory_interface $factory Event factory. + */ + protected $factory; + + /** + * @var int $num Total number of events that could be retrieved by this collection. + */ + protected $num; + + /** + * Constructor. + * + * @param int $parentid ID of the parent event. + * @param int $repeatid If non-zero this will be used as the parent id. + * @param event_factory_interface $factory Event factory. + * @throws no_repeat_parent_exception If the parent record can't be loaded. + */ + public function __construct($parentid, $repeatid, event_factory_interface $factory) { + $this->parentid = $repeatid ? $repeatid : $parentid; + $this->factory = $factory; + + if (!$this->get_parent_record()) { + throw new no_repeat_parent_exception(sprintf('No record found for id %d', $parentid)); + } + } + + public function get_id() { + return $this->parentid; + } + + public function get_num() { + global $DB; + // Subtract one because the original event has repeatid = its own id. + return $this->num = max( + isset($this->num) ? $this->num : ($DB->count_records('event', ['repeatid' => $this->parentid]) - 1), + 0 + ); + } + + public function getIterator() { + $parentrecord = $this->get_parent_record(); + foreach ($this->load_event_records() as $eventrecords) { + foreach ($eventrecords as $eventrecord) { + // In the case of the repeat event having unset information, fallback on the parent. + yield $this->factory->create_instance((object)array_merge((array)$parentrecord, (array)$eventrecord)); + } + } + } + + /** + * Return the parent DB record. + * + * @return \stdClass + */ + protected function get_parent_record() { + global $DB; + + if (isset($this->parentrecord)) { + return $this->parentrecord; + } + + return $DB->get_record('event', ['id' => $this->parentid]); + } + + /** + * Generate more event records. + * + * @param int $start Start offset. + * @return \stdClass[] + */ + protected function load_event_records($start = 1) { + global $DB; + while ($records = $DB->get_records( + 'event', + ['repeatid' => $this->parentid], + '', + '*', + $start, + self::DB_QUERY_LIMIT + )) { + yield $records; + $start += self::DB_QUERY_LIMIT; + } + } +} diff --git a/calendar/classes/local/event/exceptions/invalid_callback_exception.php b/calendar/classes/local/event/exceptions/invalid_callback_exception.php new file mode 100644 index 00000000000..257a4c226a0 --- /dev/null +++ b/calendar/classes/local/event/exceptions/invalid_callback_exception.php @@ -0,0 +1,36 @@ +. + +/** + * Invalid callback exception. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\exceptions; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Invalid callback exception. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class invalid_callback_exception extends \moodle_exception { +} diff --git a/calendar/classes/local/event/exceptions/invalid_parameter_exception.php b/calendar/classes/local/event/exceptions/invalid_parameter_exception.php new file mode 100644 index 00000000000..93b831f357f --- /dev/null +++ b/calendar/classes/local/event/exceptions/invalid_parameter_exception.php @@ -0,0 +1,36 @@ +. + +/** + * General invalid parameter exception. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\exceptions; + +defined('MOODLE_INTERNAL') || die(); + +/** + * General invalid parameter exception. + * + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class invalid_parameter_exception extends \moodle_exception { +} diff --git a/calendar/classes/local/event/exceptions/limit_invalid_parameter_exception.php b/calendar/classes/local/event/exceptions/limit_invalid_parameter_exception.php new file mode 100644 index 00000000000..9f2b92c7e58 --- /dev/null +++ b/calendar/classes/local/event/exceptions/limit_invalid_parameter_exception.php @@ -0,0 +1,36 @@ +. + +/** + * Invalid limit parameter exception. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\exceptions; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Invalid limit parameter exception. + * + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class limit_invalid_parameter_exception extends invalid_parameter_exception { +} diff --git a/calendar/classes/local/event/exceptions/member_does_not_exist_exception.php b/calendar/classes/local/event/exceptions/member_does_not_exist_exception.php new file mode 100644 index 00000000000..4efdb38d437 --- /dev/null +++ b/calendar/classes/local/event/exceptions/member_does_not_exist_exception.php @@ -0,0 +1,36 @@ +. + +/** + * Member does not exist exception. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\exceptions; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Member does not exist exception. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class member_does_not_exist_exception extends \moodle_exception { +} diff --git a/blocks/course_overview/save.php b/calendar/classes/local/event/exceptions/no_repeat_parent_exception.php similarity index 64% rename from blocks/course_overview/save.php rename to calendar/classes/local/event/exceptions/no_repeat_parent_exception.php index b4bb175bfa5..8acca90a051 100644 --- a/blocks/course_overview/save.php +++ b/calendar/classes/local/event/exceptions/no_repeat_parent_exception.php @@ -15,20 +15,22 @@ // along with Moodle. If not, see . /** - * Save course order in course_overview block + * No repeat parent exception. * - * @package block_course_overview - * @copyright 2012 Adam Olley + * @package core_calendar + * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define('AJAX_SCRIPT', true); -require_once(__DIR__ . '/../../config.php'); -require_once(__DIR__ . '/locallib.php'); +namespace core_calendar\local\event\exceptions; -require_sesskey(); -require_login(); +defined('MOODLE_INTERNAL') || die(); -$sortorder = required_param_array('sortorder', PARAM_INT); - -block_course_overview_update_myorder($sortorder); +/** + * No repeat parent exception. + * + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class no_repeat_parent_exception extends \moodle_exception { +} diff --git a/calendar/classes/local/event/exceptions/timesort_invalid_parameter_exception.php b/calendar/classes/local/event/exceptions/timesort_invalid_parameter_exception.php new file mode 100644 index 00000000000..ee034bbb86b --- /dev/null +++ b/calendar/classes/local/event/exceptions/timesort_invalid_parameter_exception.php @@ -0,0 +1,36 @@ +. + +/** + * Invalid timesort parameter exception. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\exceptions; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Invalid timesort parameter exception. + * + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class timesort_invalid_parameter_exception extends invalid_parameter_exception { +} diff --git a/calendar/classes/local/event/factories/action_factory_interface.php b/calendar/classes/local/event/factories/action_factory_interface.php new file mode 100644 index 00000000000..89975d563c5 --- /dev/null +++ b/calendar/classes/local/event/factories/action_factory_interface.php @@ -0,0 +1,40 @@ +. + +/** + * Action factory interface. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\factories; + +defined('MOODLE_INTERNAL') || die(); + +interface action_factory_interface { + /** + * Creates an instance of an action. + * + * @param string $name The action's name. + * @param \moodle_url $url The action's URL. + * @param int $itemcount The number of items needing action. + * @param bool $actionable The action's actionability. + * @return \core_calendar\local\event\entities\action_interface The action. + */ + public function create_instance($name, \moodle_url $url, $itemcount, $actionable); +} diff --git a/calendar/classes/local/event/factories/event_abstract_factory.php b/calendar/classes/local/event/factories/event_abstract_factory.php new file mode 100644 index 00000000000..c3520f42b9b --- /dev/null +++ b/calendar/classes/local/event/factories/event_abstract_factory.php @@ -0,0 +1,195 @@ +. + +/** + * Abstract event factory. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\factories; + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\local\event\entities\event; +use core_calendar\local\event\entities\repeat_event_collection; +use core_calendar\local\event\exceptions\invalid_callback_exception; +use core_calendar\local\event\proxies\module_std_proxy; +use core_calendar\local\event\proxies\std_proxy; +use core_calendar\local\event\value_objects\event_description; +use core_calendar\local\event\value_objects\event_times; +use core_calendar\local\event\entities\event_interface; + +/** + * Abstract factory for creating calendar events. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class event_abstract_factory implements event_factory_interface { + /** + * @var callable $actioncallbackapplier Function to apply component action callbacks. + */ + protected $actioncallbackapplier; + + /** + * @var callable $visibilitycallbackapplier Function to apply component visibility callbacks. + */ + protected $visibilitycallbackapplier; + + /** + * @var array Course cache for use with get_course_cached. + */ + protected $coursecachereference; + + /** + * @var array Module cache reference for use with get_module_cached. + */ + protected $modulecachereference; + + /** + * @var callable Bail out check for create_instance. + */ + protected $bailoutcheck; + + /** + * Applies component actions to the event. + * + * @param event_interface $event The event to be updated. + * @return event_interface The potentially modified event. + */ + protected abstract function apply_component_action(event_interface $event); + + /** + * Exposes the event (or not). + * + * @param event_interface $event The event to potentially expose. + * @return event_interface|null The exposed event or null. + */ + protected abstract function expose_event(event_interface $event); + + /** + * Constructor. + * + * @param callable $actioncallbackapplier Function to apply component action callbacks. + * @param callable $visibilitycallbackapplier Function to apply component visibility callbacks. + * @param callable $bailoutcheck Function to test if we can return null early. + * @param array $coursecachereference Cache to use with get_course_cached. + * @param array $modulecachereference Cache to use with get_module_cached. + */ + public function __construct( + callable $actioncallbackapplier, + callable $visibilitycallbackapplier, + callable $bailoutcheck, + array &$coursecachereference, + array &$modulecachereference + ) { + $this->actioncallbackapplier = $actioncallbackapplier; + $this->visibilitycallbackapplier = $visibilitycallbackapplier; + $this->bailoutcheck = $bailoutcheck; + $this->coursecachereference = &$coursecachereference; + $this->modulecachereference = &$modulecachereference; + } + + public function create_instance(\stdClass $dbrow) { + $bailcheck = $this->bailoutcheck; + $bail = $bailcheck($dbrow); + + if (!is_bool($bail)) { + throw new invalid_callback_exception( + 'Bail check must return true or false' + ); + } + + if ($bail) { + return null; + } + + $course = null; + $group = null; + $user = null; + $module = null; + $subscription = null; + + if ($dbrow->courseid == 0 && !empty($dbrow->modulename)) { + $cm = get_coursemodule_from_instance($dbrow->modulename, $dbrow->instance); + $dbrow->courseid = get_course($cm->course)->id; + } + + $course = new std_proxy($dbrow->courseid, function($id) { + return calendar_get_course_cached($this->coursecachereference, $id); + }); + + if ($dbrow->groupid) { + $group = new std_proxy($dbrow->groupid, function($id) { + return calendar_get_group_cached($id); + }); + } + + if ($dbrow->userid) { + $user = new std_proxy($dbrow->userid, function($id) { + global $DB; + return $DB->get_record('user', ['id' => $id]); + }); + } + + if ($dbrow->instance && !empty($dbrow->modulename)) { + $module = new module_std_proxy( + $dbrow->modulename, + $dbrow->instance, + function($modulename, $instance) { + return calendar_get_module_cached( + $this->modulecachereference, + $modulename, + $instance + ); + } + ); + } + + if ($dbrow->subscriptionid) { + $subscription = new std_proxy($dbrow->subscriptionid, function($id) { + return calendar_get_subscription($id); + }); + } + + $event = new event( + $dbrow->id, + $dbrow->name, + new event_description($dbrow->description, $dbrow->format), + $course, + $group, + $user, + new repeat_event_collection($dbrow->id, $dbrow->repeatid, $this), + $module, + $dbrow->eventtype, + new event_times( + (new \DateTimeImmutable())->setTimestamp($dbrow->timestart), + (new \DateTimeImmutable())->setTimestamp($dbrow->timestart + $dbrow->timeduration), + (new \DateTimeImmutable())->setTimestamp($dbrow->timesort ? $dbrow->timesort : $dbrow->timestart), + (new \DateTimeImmutable())->setTimestamp($dbrow->timemodified) + ), + !empty($dbrow->visible), + $subscription + ); + + $isactionevent = !empty($dbrow->type) && $dbrow->type == CALENDAR_EVENT_TYPE_ACTION; + + return $isactionevent ? $this->expose_event($this->apply_component_action($event)) : $event; + } +} diff --git a/calendar/classes/local/event/factories/event_factory.php b/calendar/classes/local/event/factories/event_factory.php new file mode 100644 index 00000000000..9434f2a6c9c --- /dev/null +++ b/calendar/classes/local/event/factories/event_factory.php @@ -0,0 +1,62 @@ +. + +/** + * Event factory class. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\factories; + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\local\event\exceptions\invalid_callback_exception; +use core_calendar\local\event\entities\event_interface; + +/** + * Event factory class. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class event_factory extends event_abstract_factory { + + protected function apply_component_action(event_interface $event) { + $callbackapplier = $this->actioncallbackapplier; + $callbackresult = $callbackapplier($event); + + if (!$callbackresult instanceof event_interface) { + throw new invalid_callback_exception( + 'Event factory action callback applier must return an instance of event_interface'); + } + + return $callbackresult; + } + + protected function expose_event(event_interface $event) { + $callbackapplier = $this->visibilitycallbackapplier; + $callbackresult = $callbackapplier($event); + + if (!is_bool($callbackresult)) { + throw new invalid_callback_exception('Event factory visibility callback applier must return true or false'); + } + + return $callbackresult === true ? $event : null; + } +} diff --git a/calendar/classes/local/event/factories/event_factory_interface.php b/calendar/classes/local/event/factories/event_factory_interface.php new file mode 100644 index 00000000000..74afeeaf2ee --- /dev/null +++ b/calendar/classes/local/event/factories/event_factory_interface.php @@ -0,0 +1,43 @@ +. + +/** + * Event factory interface. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\factories; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Interface for an event factory class. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface event_factory_interface { + /** + * Creates an instance of an event. + * + * @param \stdClass $dbrow The event row from the database. + * @return \core_calendar\local\event\entities\event_interface + */ + public function create_instance(\stdClass $dbrow); +} diff --git a/calendar/classes/local/event/mappers/event_mapper.php b/calendar/classes/local/event/mappers/event_mapper.php new file mode 100644 index 00000000000..ae50e7d3615 --- /dev/null +++ b/calendar/classes/local/event/mappers/event_mapper.php @@ -0,0 +1,126 @@ +. + +/** + * Event mapper. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\mappers; + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\event; +use core_calendar\local\event\entities\action_event_interface; +use core_calendar\local\event\entities\event_interface; +use core_calendar\local\event\factories\event_factory_interface; + +/** + * Event mapper class. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class event_mapper implements event_mapper_interface { + /** + * @var event_factory_interface $factory Event factory. + */ + protected $factory; + + /** + * Constructor. + * + * @param event_factory_interface $factory Event factory. + */ + public function __construct(event_factory_interface $factory) { + $this->factory = $factory; + } + + public function from_legacy_event_to_event(\calendar_event $legacyevent) { + $coalesce = function($property) use ($legacyevent) { + return property_exists($legacyevent, $property) ? $legacyevent->{$property} : null; + }; + + return $this->factory->create_instance( + (object)[ + $coalesce('id'), + $coalesce('name'), + $coalesce('description'), + $coalesce('format'), + $coalesce('courseid'), + $coalesce('groupid'), + $coalesce('userid'), + $coalesce('repeatid'), + $coalesce('modulename'), + $coalesce('instance'), + $coalesce('type'), + $coalesce('timestart'), + $coalesce('timeduration'), + $coalesce('timemodified'), + $coalesce('timesort'), + $coalesce('visible'), + $coalesce('subscription') + ] + ); + } + + public function from_event_to_legacy_event(event_interface $event) { + $action = ($event instanceof action_event_interface) ? $event->get_action() : null; + $timeduration = $event->get_times()->get_end_time()->getTimestamp() - $event->get_times()->get_start_time()->getTimestamp(); + + return new \calendar_event($this->from_event_to_stdclass($event)); + } + + public function from_event_to_stdclass(event_interface $event) { + $action = ($event instanceof action_event_interface) ? $event->get_action() : null; + $timeduration = $event->get_times()->get_end_time()->getTimestamp() - $event->get_times()->get_start_time()->getTimestamp(); + + return (object)$this->from_event_to_assoc_array($event); + } + + public function from_event_to_assoc_array(event_interface $event) { + $action = ($event instanceof action_event_interface) ? $event->get_action() : null; + $timeduration = $event->get_times()->get_end_time()->getTimestamp() - $event->get_times()->get_start_time()->getTimestamp(); + + return [ + 'id' => $event->get_id(), + 'name' => $event->get_name(), + 'description' => $event->get_description()->get_value(), + 'format' => $event->get_description()->get_format(), + 'courseid' => $event->get_course() ? $event->get_course()->get_id() : null, + 'groupid' => $event->get_group() ? $event->get_group()->get_id() : null, + 'userid' => $event->get_user() ? $event->get_user()->get_id() : null, + 'repeatid' => $event->get_repeats()->get_id(), + 'modulename' => $event->get_course_module() ? $event->get_course_module()->get('modname') : null, + 'instance' => $event->get_course_module() ? $event->get_course_module()->get('instance') : null, + 'eventtype' => $event->get_type(), + 'timestart' => $event->get_times()->get_start_time()->getTimestamp(), + 'timeduration' => $timeduration, + 'timesort' => $event->get_times()->get_sort_time()->getTimestamp(), + 'visible' => $event->is_visible() ? 1 : 0, + 'timemodified' => $event->get_times()->get_modified_time()->getTimestamp(), + 'subscriptionid' => $event->get_subscription() ? $event->get_subscription()->get_id() : null, + 'actionname' => $action ? $action->get_name() : null, + 'actionurl' => $action ? $action->get_url() : null, + 'actionnum' => $action ? $action->get_item_count() : null, + 'actionactionable' => $action ? $action->is_actionable() : null, + 'sequence' => 1 + ]; + } +} diff --git a/calendar/classes/local/event/mappers/event_mapper_interface.php b/calendar/classes/local/event/mappers/event_mapper_interface.php new file mode 100644 index 00000000000..6cf61bfad13 --- /dev/null +++ b/calendar/classes/local/event/mappers/event_mapper_interface.php @@ -0,0 +1,70 @@ +. + +/** + * Event mapper interface. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\mappers; + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\event; +use core_calendar\local\event\entities\event_interface; + +/** + * Interface for an event mapper class + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface event_mapper_interface { + /** + * Map a legacy event to an event. + * + * @param \calendar_event $event The legacy event. + * @return event_interface The mapped event. + */ + public function from_legacy_event_to_event(\calendar_event $event); + + /** + * Map an event to a legacy event. + * + * @param event_interface $event The legacy event. + * @return \calendar_event The mapped legacy event. + */ + public function from_event_to_legacy_event(event_interface $event); + + /** + * Map an event to a stdClass + * + * @param event_interface $event The legacy event. + * @return \stdClass The mapped stdClass. + */ + public function from_event_to_stdclass(event_interface $event); + + /** + * Map an event to an associative array. + * + * @param event_interface $event The legacy event. + * @return array The mapped legacy event array. + */ + public function from_event_to_assoc_array(event_interface $event); +} diff --git a/calendar/classes/local/event/proxies/module_std_proxy.php b/calendar/classes/local/event/proxies/module_std_proxy.php new file mode 100644 index 00000000000..1ad48fbf174 --- /dev/null +++ b/calendar/classes/local/event/proxies/module_std_proxy.php @@ -0,0 +1,64 @@ +. + +/** + * Course module stdClass proxy. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\proxies; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Course module stdClass proxy. + * + * This implementation differs from the regular std_proxy in that it takes + * a module name and instance instead of an id to construct the proxied class. + * + * This is needed as the event table does not store the id of course modules + * instead it stores the module name and instance. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class module_std_proxy extends std_proxy implements proxy_interface { + + /** + * module_std_proxy constructor. + * + * @param int $modulename The module name. + * @param callable $instance The module instance. + * @param callable $callback Callback to load the class. + * @param \stdClass $base Class containing base values. + */ + public function __construct($modulename, $instance, callable $callback, \stdClass $base = null) { + $this->modulename = $modulename; + $this->instance = $instance; + $this->callbackargs = [$modulename, $instance]; + $this->callback = $callback; + $this->base = $base = is_null($base) ? new \stdClass() : $base; + $this->base->modulename = $modulename; + $this->base->instance = $instance; + } + + public function get_id() { + return $this->get_proxied_instance()->id; + } +} diff --git a/calendar/classes/local/event/proxies/proxy_interface.php b/calendar/classes/local/event/proxies/proxy_interface.php new file mode 100644 index 00000000000..6869fd85966 --- /dev/null +++ b/calendar/classes/local/event/proxies/proxy_interface.php @@ -0,0 +1,69 @@ +. + +/** + * Proxy interface. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\proxies; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Interface for a proxy class. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface proxy_interface { + /** + * Retrieve a member of the proxied class. + * + * @param string $member The name of the member to retrieve + * @throws \core_calendar\local\event\exceptions\member_does_not_exist_exception If the proxied class does not have the + * requested member. + * @return mixed The member. + */ + public function get($member); + + /** + * Retrieve the ID of the proxied class. + * @return int The proxied class' ID. + */ + public function get_id(); + + /** + * Set a member of the proxied class. + * + * @param string $member The name of the member to set + * @param mixed $value The value to set the member to + * @throws \core_calendar\local\event\exceptions\member_does_not_exist_exception If the proxied class does not have the + * requested member. + * @return void + */ + public function set($member, $value); + + /** + * Get the full instance of the proxied class. + * + * @return \stdClass + */ + public function get_proxied_instance(); +} diff --git a/calendar/classes/local/event/proxies/std_proxy.php b/calendar/classes/local/event/proxies/std_proxy.php new file mode 100644 index 00000000000..e94746d65a2 --- /dev/null +++ b/calendar/classes/local/event/proxies/std_proxy.php @@ -0,0 +1,113 @@ +. + +/** + * std_proxy class. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\proxies; + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\local\event\exceptions\member_does_not_exist_exception; + +/** + * stdClass proxy. + * + * This class is intended to proxy things like user, group, etc 'classes' + * It will only run the callback to load the object from the DB when necessary. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class std_proxy implements proxy_interface { + /** + * @var int $id The ID of the database record. + */ + protected $id; + + /** + * @var \stdClass $class The class we are proxying. + */ + protected $class; + + /** + * @var callable $callback Callback to run which will load the class to proxy. + */ + protected $callback; + + /** + * @var array $callbackargs Array of arguments to pass to the callback. + */ + protected $callbackargs; + + /** + * @var \stdClass $base Base class to get members from. + */ + protected $base; + + + /** + * Constructor. + * + * @param int $id The ID of the record in the database. + * @param callable $callback Callback to load the class. + * @param \stdClass $base Class containing base values. + */ + public function __construct($id, callable $callback, \stdClass $base = null) { + $this->id = $id; + $this->callbackargs = [$id]; + $this->callback = $callback; + $this->base = $base; + } + + public function get_id() { + return $this->id; + } + + public function get($member) { + if ($member === 'id') { + return $this->get_id(); + } + + if ($this->base && property_exists($this->base, $member)) { + return $this->base->{$member}; + } + + if (!property_exists($this->get_proxied_instance(), $member)) { + throw new member_does_not_exist_exception(sprintf('Member %s does not exist', $member)); + } + + return $this->get_proxied_instance()->{$member}; + } + + public function set($member, $value) { + if (!property_exists($this->get_proxied_instance(), $member)) { + throw new member_does_not_exist_exception(sprintf('Member %s does not exist', $member)); + } + + $this->get_proxied_instance()->{$member} = $value; + } + + public function get_proxied_instance() { + $callback = $this->callback; + return $this->class = $this->class ? $this->class : $callback(...$this->callbackargs); + } +} diff --git a/calendar/classes/local/event/strategies/raw_event_retrieval_strategy.php b/calendar/classes/local/event/strategies/raw_event_retrieval_strategy.php new file mode 100644 index 00000000000..7d1836149e0 --- /dev/null +++ b/calendar/classes/local/event/strategies/raw_event_retrieval_strategy.php @@ -0,0 +1,257 @@ +. + +/** + * Raw event retrieval strategy. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\strategies; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Raw event retrieval strategy. + * + * This strategy is based on what used to be the calendar API's get_events function. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_interface { + + public function get_raw_events( + array $usersfilter = null, + array $groupsfilter = null, + array $coursesfilter = null, + array $whereconditions = null, + array $whereparams = null, + $ordersql = null, + $offset = null, + $limitnum = null, + $ignorehidden = true + ) { + return $this->get_raw_events_legacy_implementation( + !is_null($usersfilter) ? $usersfilter : true, // True means no filter in old implementation. + !is_null($groupsfilter) ? $groupsfilter : true, + !is_null($coursesfilter) ? $coursesfilter : true, + $whereconditions, + $whereparams, + $ordersql, + $offset, + $limitnum, + $ignorehidden + ); + } + + /** + * The legacy implementation with minor tweaks. + * + * @param array|int|boolean $users array of users, user id or boolean for all/no user events + * @param array|int|boolean $groups array of groups, group id or boolean for all/no group events + * @param array|int|boolean $courses array of courses, course id or boolean for all/no course events + * @param string $whereconditions The conditions in the WHERE clause. + * @param array $whereparams The parameters for the WHERE clause. + * @param string $ordersql The ORDER BY clause. + * @param int $offset Offset. + * @param int $limitnum Limit. + * @param boolean $ignorehidden whether to select only visible events or all events + * @return array $events of selected events or an empty array if there aren't any (or there was an error) + */ + protected function get_raw_events_legacy_implementation( + $users, + $groups, + $courses, + $whereconditions, + $whereparams, + $ordersql, + $offset, + $limitnum, + $ignorehidden + ) { + global $DB; + + $params = array(); + // Quick test. + if (empty($users) && empty($groups) && empty($courses)) { + return array(); + } + + // Array of filter conditions. To be concatenated by the OR operator. + $filters = []; + + // User filter. + if ((is_array($users) && !empty($users)) or is_numeric($users)) { + // Events from a number of users. + list($insqlusers, $inparamsusers) = $DB->get_in_or_equal($users, SQL_PARAMS_NAMED); + $filters[] = "(e.userid $insqlusers AND e.courseid = 0 AND e.groupid = 0)"; + $params = array_merge($params, $inparamsusers); + } else if ($users === true) { + // Events from ALL users. + $filters[] = "(e.userid != 0 AND e.courseid = 0 AND e.groupid = 0)"; + } + // Boolean false (no users at all): We don't need to do anything. + + // Group filter. + if ((is_array($groups) && !empty($groups)) or is_numeric($groups)) { + // Events from a number of groups. + list($insqlgroups, $inparamsgroups) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED); + $filters[] = "e.groupid $insqlgroups"; + $params = array_merge($params, $inparamsgroups); + } else if ($groups === true) { + // Events from ALL groups. + $filters[] = "e.groupid != 0"; + } + // Boolean false (no groups at all): We don't need to do anything. + + // Course filter. + if ((is_array($courses) && !empty($courses)) or is_numeric($courses)) { + list($insqlcourses, $inparamscourses) = $DB->get_in_or_equal($courses, SQL_PARAMS_NAMED); + $filters[] = "(e.groupid = 0 AND e.courseid $insqlcourses)"; + $params = array_merge($params, $inparamscourses); + } else if ($courses === true) { + // Events from ALL courses. + $filters[] = "(e.groupid = 0 AND e.courseid != 0)"; + } + + // Security check: if, by now, we have NOTHING in $whereclause, then it means + // that NO event-selecting clauses were defined. Thus, we won't be returning ANY + // events no matter what. Allowing the code to proceed might return a completely + // valid query with only time constraints, thus selecting ALL events in that time frame! + if (empty($filters)) { + return array(); + } + + // Build our clause for the filters. + $filterclause = implode(' OR ', $filters); + + // Array of where conditions for our query. To be concatenated by the AND operator. + $whereconditions[] = "($filterclause)"; + + // Show visible only. + if ($ignorehidden) { + $whereconditions[] = "(e.visible = 1)"; + } + + // Build the main query's WHERE clause. + $whereclause = implode(' AND ', $whereconditions); + + // Build SQL subquery and conditions for filtered events based on priorities. + $subquerywhere = ''; + $subqueryconditions = []; + + // Get the user's courses. Otherwise, get the default courses being shown by the calendar. + $usercourses = calendar_get_default_courses(); + + // Set calendar filters. + list($usercourses, $usergroups, $user) = calendar_set_filters($usercourses, true); + $subqueryparams = []; + + // Flag to indicate whether the query needs to exclude group overrides. + $viewgroupsonly = false; + + if ($user) { + // Set filter condition for the user's events. + $subqueryconditions[] = "(ev.userid = :user AND ev.courseid = 0 AND ev.groupid = 0)"; + $subqueryparams['user'] = $user; + + foreach ($usercourses as $courseid) { + if (has_capability('moodle/site:accessallgroups', \context_course::instance($courseid))) { + $usergroupmembership = groups_get_all_groups($courseid, $user, 0, 'g.id'); + if (count($usergroupmembership) == 0) { + $viewgroupsonly = true; + break; + } + } + } + } + + // Set filter condition for the user's group events. + if ($usergroups === true || $viewgroupsonly) { + // Fetch group events, but not group overrides. + $subqueryconditions[] = "(ev.groupid != 0 AND ev.eventtype = 'group')"; + } else if (!empty($usergroups)) { + // Fetch group events and group overrides. + list($inusergroups, $inusergroupparams) = $DB->get_in_or_equal($usergroups, SQL_PARAMS_NAMED); + $subqueryconditions[] = "(ev.groupid $inusergroups)"; + $subqueryparams = array_merge($subqueryparams, $inusergroupparams); + } + + // Get courses to be used for the subquery. + $subquerycourses = []; + if (is_array($courses)) { + $subquerycourses = $courses; + } else if (is_numeric($courses)) { + $subquerycourses[] = $courses; + } + // Merge with user courses, if necessary. + if (!empty($usercourses)) { + $subquerycourses = array_merge($subquerycourses, $usercourses); + // Make sure we remove duplicate values. + $subquerycourses = array_unique($subquerycourses); + } + + // Set subquery filter condition for the courses. + if (!empty($subquerycourses)) { + list($incourses, $incoursesparams) = $DB->get_in_or_equal($subquerycourses, SQL_PARAMS_NAMED); + $subqueryconditions[] = "(ev.groupid = 0 AND ev.courseid $incourses)"; + $subqueryparams = array_merge($subqueryparams, $incoursesparams); + } + + // Build the WHERE condition for the sub-query. + if (!empty($subqueryconditions)) { + $subquerywhere = 'WHERE ' . implode(" OR ", $subqueryconditions); + } + + // Merge subquery parameters to the parameters of the main query. + if (!empty($subqueryparams)) { + $params = array_merge($params, $subqueryparams); + } + + // Sub-query that fetches the list of unique events that were filtered based on priority. + $subquery = "SELECT ev.modulename, + ev.instance, + ev.eventtype, + MAX(ev.priority) as priority + FROM {event} ev + $subquerywhere + GROUP BY ev.modulename, ev.instance, ev.eventtype"; + + // Build the main query. + $sql = "SELECT e.* + FROM {event} e + INNER JOIN ($subquery) fe + ON e.modulename = fe.modulename + AND e.instance = fe.instance + AND e.eventtype = fe.eventtype + AND (e.priority = fe.priority OR (e.priority IS NULL AND fe.priority IS NULL)) + LEFT JOIN {modules} m + ON e.modulename = m.name + WHERE (m.visible = 1 OR m.visible IS NULL) AND $whereclause + ORDER BY " . ($ordersql ? $ordersql : "e.timestart"); + + if (!empty($whereparams)) { + $params = array_merge($params, $whereparams); + } + + $events = $DB->get_records_sql($sql, $params, $offset, $limitnum); + + return $events === false ? [] : $events; + } +} diff --git a/calendar/classes/local/event/strategies/raw_event_retrieval_strategy_interface.php b/calendar/classes/local/event/strategies/raw_event_retrieval_strategy_interface.php new file mode 100644 index 00000000000..cc670d3b05c --- /dev/null +++ b/calendar/classes/local/event/strategies/raw_event_retrieval_strategy_interface.php @@ -0,0 +1,61 @@ +. + +/** + * Raw event strategy retrieval interface. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\strategies; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Interface for an raw event retrival strategy class. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface raw_event_retrieval_strategy_interface { + /** + * Retrieve raw calendar event records from the DB. + * + * @param array|null $usersfilter Array of users to retrieve events for. + * @param array|null $groupsfilter Array of groups to retrieve events for. + * @param array|null $coursesfilter Array of courses to retrieve events for. + * @param array|null $whereconditions Array of where conditions to restrict results. + * @param array|null $whereparams Array of parameters for $whereconditions. + * @param string|null $ordersql SQL to order results. + * @param int|null $offset Amount to offset results by. + * @param int $limitnum Return at most this many results. + * @param bool $ignorehidden True to ignore hidden events. False to include them. + * @return \stdClass[] Array of event records. + */ + public function get_raw_events( + array $usersfilter = null, + array $groupsfilter = null, + array $coursesfilter = null, + array $whereconditions = null, + array $whereparams = null, + $ordersql = null, + $offset = null, + $limitnum = 40, + $ignorehidden = true + ); +} diff --git a/calendar/classes/local/event/value_objects/action.php b/calendar/classes/local/event/value_objects/action.php new file mode 100644 index 00000000000..f7aa538b317 --- /dev/null +++ b/calendar/classes/local/event/value_objects/action.php @@ -0,0 +1,93 @@ +. + +/** + * Class representing an action a user should take. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\value_objects; + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\local\event\entities\action_interface; + +/** + * Class representing an action a user should take + * + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class action implements action_interface { + /** + * @var string $name The action's name. + */ + protected $name; + + /** + * @var \moodle_url $url The action's URL. + */ + protected $url; + + /** + * @var int $itemcount How many items there are to action. + */ + protected $itemcount; + + /** + * @var bool $actionable Whether or not the event is currently actionable. + */ + protected $actionable; + + /** + * Constructor. + * + * @param string $name The action's name. + * @param \moodle_url $url The action's URL. + * @param int $itemcount How many items there are to action. + * @param bool $actionable Whether or not the event is currently actionable. + */ + public function __construct( + $name, + \moodle_url $url, + $itemcount, + $actionable + ) { + $this->name = $name; + $this->url = $url; + $this->itemcount = $itemcount; + $this->actionable = $actionable; + } + + public function get_name() { + return $this->name; + } + + public function get_url() { + return $this->url; + } + + public function get_item_count() { + return $this->itemcount; + } + + public function is_actionable() { + return $this->actionable; + } +} diff --git a/calendar/classes/local/event/value_objects/description_interface.php b/calendar/classes/local/event/value_objects/description_interface.php new file mode 100644 index 00000000000..483eed3aa0c --- /dev/null +++ b/calendar/classes/local/event/value_objects/description_interface.php @@ -0,0 +1,49 @@ +. + +/** + * Description value object interface. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\value_objects; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Interface for a description value object. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface description_interface { + /** + * Get the description's text. + * + * @return string The description's text. + */ + public function get_value(); + + /** + * Get the description's format. + * + * @return int The description's format. + */ + public function get_format(); +} diff --git a/calendar/classes/local/event/value_objects/event_description.php b/calendar/classes/local/event/value_objects/event_description.php new file mode 100644 index 00000000000..3d4db691a3e --- /dev/null +++ b/calendar/classes/local/event/value_objects/event_description.php @@ -0,0 +1,64 @@ +. + +/** + * Description value object. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\value_objects; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class representing a description value object. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class event_description implements description_interface { + /** + * @var string $value The description's text. + */ + protected $value; + + /** + * @var int $format The description's format. + */ + protected $format; + + /** + * Constructor. + * + * @param string $value The description's value. + * @param int $format The description's format. + */ + public function __construct($value, $format) { + $this->value = $value; + $this->format = $format; + } + + public function get_value() { + return $this->value; + } + + public function get_format() { + return $this->format; + } +} diff --git a/calendar/classes/local/event/value_objects/event_times.php b/calendar/classes/local/event/value_objects/event_times.php new file mode 100644 index 00000000000..511b59c9f49 --- /dev/null +++ b/calendar/classes/local/event/value_objects/event_times.php @@ -0,0 +1,95 @@ +. + +/** + * Event times class. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\value_objects; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class representing event times. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class event_times implements times_interface { + /** + * @var \DateTimeImmutable $start Event start time. + */ + protected $start; + + /** + * @var \DateTimeImmutable $end Event end time. + */ + protected $end; + + /** + * @var \DateTimeImmutable $sort Time used to sort events. + */ + protected $sort; + + /** + * @var \DateTimeImmutable $modified Time event was last modified. + */ + protected $modified; + + /** + * Constructor. + * + * @param \DateTimeImmutable $start Event start time. + * @param \DateTimeImmutable $end Event end time. + * @param \DateTimeImmutable $sort Date used to sort events. + * @param \DateTimeImmutable $modified Time event was last updated. + */ + public function __construct( + \DateTimeImmutable $start, + \DateTimeImmutable $end, + \DateTimeImmutable $sort, + \DateTimeImmutable $modified + ) { + $this->start = $start; + $this->end = $end; + $this->sort = $sort; + $this->modified = $modified; + } + + public function get_start_time() { + return $this->start; + } + + public function get_end_time() { + return $this->end; + } + + public function get_duration() { + return $this->end->diff($this->start); + } + + public function get_modified_time() { + return $this->modified; + } + + public function get_sort_time() { + return $this->sort; + } +} diff --git a/calendar/classes/local/event/value_objects/times_interface.php b/calendar/classes/local/event/value_objects/times_interface.php new file mode 100644 index 00000000000..22cc8217e90 --- /dev/null +++ b/calendar/classes/local/event/value_objects/times_interface.php @@ -0,0 +1,70 @@ +. + +/** + * Times interface. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_calendar\local\event\value_objects; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Interface for various times. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface times_interface { + /** + * Get the start time. + * + * @return \DateTimeImmutable + */ + public function get_start_time(); + + /** + * Get the end time. + * + * @return \DateTimeImmutable + */ + public function get_end_time(); + + /** + * Get the duration (the time between start and end). + * + * @return \DateInterval + */ + public function get_duration(); + + /** + * Get the sort time. + * + * @return \DateTimeImmutable + */ + public function get_sort_time(); + + /** + * Get the modified time. + * + * @return \DateTimeImmutable + */ + public function get_modified_time(); +} diff --git a/calendar/classes/rrule_manager.php b/calendar/classes/rrule_manager.php index f11d89aaf70..78cee59681d 100644 --- a/calendar/classes/rrule_manager.php +++ b/calendar/classes/rrule_manager.php @@ -24,7 +24,6 @@ namespace core_calendar; -use calendar_event; use DateInterval; use DateTime; use moodle_exception; @@ -224,7 +223,7 @@ class rrule_manager { /** * Create events for specified rrule. * - * @param calendar_event $passedevent Properties of event to create. + * @param \calendar_event $passedevent Properties of event to create. * @throws moodle_exception */ public function create_events($passedevent) { @@ -246,7 +245,7 @@ class rrule_manager { // Adjust the parent event's timestart, if necessary. if (count($eventtimes) > 0 && !in_array($eventrec->timestart, $eventtimes)) { - $calevent = new calendar_event($eventrec); + $calevent = new \calendar_event($eventrec); $updatedata = (object)['timestart' => $eventtimes[0], 'repeatid' => $eventrec->id]; $calevent->update($updatedata, false); $eventrec->timestart = $calevent->timestart; @@ -720,7 +719,7 @@ class rrule_manager { $cloneevent->repeatid = $event->id; $cloneevent->timestart = $time; unset($cloneevent->id); - calendar_event::create($cloneevent, false); + \calendar_event::create($cloneevent, false); } } diff --git a/calendar/export_execute.php b/calendar/export_execute.php index da5f078e202..0d0d75a28e6 100644 --- a/calendar/export_execute.php +++ b/calendar/export_execute.php @@ -60,7 +60,8 @@ if (!empty($generateurl)) { if(!empty($what) && !empty($time)) { if(in_array($what, $allowed_what) && in_array($time, $allowed_time)) { $courses = enrol_get_users_courses($user->id, true, 'id, visible, shortname'); - // Array of courses that we will pass to calendar_get_events() which is initially set to the list of the user's courses. + // Array of courses that we will pass to \core_calendar\local\api::get_legacy_events() which + // is initially set to the list of the user's courses. $paramcourses = $courses; if ($what == 'all' || $what == 'groups') { $groups = array(); @@ -98,7 +99,7 @@ if(!empty($what) && !empty($time)) { $startmonthday = find_day_in_month($now['mday'] - ($numberofdaysinweek - 1), $startweekday, $now['mon'], $now['year']); $startmonth = $now['mon']; $startyear = $now['year']; - if($startmonthday > calendar_days_in_month($startmonth, $startyear)) { + if ($startmonthday > calendar_days_in_month($startmonth, $startyear)) { list($startmonth, $startyear) = calendar_add_month($startmonth, $startyear); $startmonthday = find_day_in_month(1, $startweekday, $startmonth, $startyear); } @@ -109,7 +110,7 @@ if(!empty($what) && !empty($time)) { $endmonthday = $startmonthday + $numberofdaysinweek; $endmonth = $startmonth; $endyear = $startyear; - if($endmonthday > calendar_days_in_month($endmonth, $endyear)) { + if ($endmonthday > calendar_days_in_month($endmonth, $endyear)) { list($endmonth, $endyear) = calendar_add_month($endmonth, $endyear); $endmonthday = find_day_in_month(1, $startweekday, $endmonth, $endyear); } @@ -122,7 +123,7 @@ if(!empty($what) && !empty($time)) { $startmonthday = find_day_in_month($now['mday'] + 1, $startweekday, $now['mon'], $now['year']); $startmonth = $now['mon']; $startyear = $now['year']; - if($startmonthday > calendar_days_in_month($startmonth, $startyear)) { + if ($startmonthday > calendar_days_in_month($startmonth, $startyear)) { list($startmonth, $startyear) = calendar_add_month($startmonth, $startyear); $startmonthday = find_day_in_month(1, $startweekday, $startmonth, $startyear); } @@ -133,7 +134,7 @@ if(!empty($what) && !empty($time)) { $endmonthday = $startmonthday + $numberofdaysinweek; $endmonth = $startmonth; $endyear = $startyear; - if($endmonthday > calendar_days_in_month($endmonth, $endyear)) { + if ($endmonthday > calendar_days_in_month($endmonth, $endyear)) { list($endmonth, $endyear) = calendar_add_month($endmonth, $endyear); $endmonthday = find_day_in_month(1, $startweekday, $endmonth, $endyear); } @@ -179,7 +180,7 @@ if(!empty($what) && !empty($time)) { die(); } } -$events = calendar_get_events($timestart, $timeend, $users, $groups, array_keys($paramcourses), false); +$events = \core_calendar\local\api::get_legacy_events($timestart, $timeend, $users, $groups, array_keys($paramcourses), false); $ical = new iCalendar; $ical->add_property('method', 'PUBLISH'); diff --git a/calendar/externallib.php b/calendar/externallib.php index 2383693de14..418731e6f57 100644 --- a/calendar/externallib.php +++ b/calendar/externallib.php @@ -29,6 +29,11 @@ defined('MOODLE_INTERNAL') || die; require_once("$CFG->libdir/externallib.php"); +use \core_calendar\local\api as local_api; +use \core_calendar\external\events_exporter; +use \core_calendar\external\events_grouped_by_course_exporter; +use \core_calendar\external\events_related_objects_cache; + /** * Calendar external functions * @@ -228,8 +233,8 @@ class core_calendar_external extends external_api { } // Event list does not check visibility and permissions, we'll check that later. - $eventlist = calendar_get_events($params['options']['timestart'], $params['options']['timeend'], $funcparam['users'], $funcparam['groups'], - $funcparam['courses'], true, $params['options']['ignorehidden']); + $eventlist = \core_calendar\local\api::get_legacy_events($params['options']['timestart'], $params['options']['timeend'], + $funcparam['users'], $funcparam['groups'], $funcparam['courses'], true, $params['options']['ignorehidden']); // WS expects arrays. $events = array(); @@ -305,6 +310,251 @@ class core_calendar_external extends external_api { ); } + /** + * Returns description of method parameters. + * + * @since Moodle 3.3 + * @return external_function_parameters + */ + public static function get_calendar_action_events_by_timesort_parameters() { + return new external_function_parameters( + array( + 'timesortfrom' => new external_value(PARAM_INT, 'Time sort from', VALUE_DEFAULT, 0), + 'timesortto' => new external_value(PARAM_INT, 'Time sort to', VALUE_DEFAULT, null), + 'aftereventid' => new external_value(PARAM_INT, 'The last seen event id', VALUE_DEFAULT, 0), + 'limitnum' => new external_value(PARAM_INT, 'Limit number', VALUE_DEFAULT, 20) + ) + ); + } + + /** + * Get calendar action events based on the timesort value. + * + * @since Moodle 3.3 + * @param null|int $timesortfrom Events after this time (inclusive) + * @param null|int $timesortto Events before this time (inclusive) + * @param null|int $aftereventid Get events with ids greater than this one + * @param int $limitnum Limit the number of results to this value + * @return array + */ + public static function get_calendar_action_events_by_timesort($timesortfrom = 0, $timesortto = null, + $aftereventid = 0, $limitnum = 20) { + global $CFG, $PAGE, $USER; + + require_once($CFG->dirroot . '/calendar/lib.php'); + + $user = null; + $params = self::validate_parameters( + self::get_calendar_action_events_by_timesort_parameters(), + [ + 'timesortfrom' => $timesortfrom, + 'timesortto' => $timesortto, + 'aftereventid' => $aftereventid, + 'limitnum' => $limitnum, + ] + ); + $context = \context_user::instance($USER->id); + self::validate_context($context); + + if (empty($params['aftereventid'])) { + $params['aftereventid'] = null; + } + + $renderer = $PAGE->get_renderer('core_calendar'); + $events = local_api::get_action_events_by_timesort( + $params['timesortfrom'], + $params['timesortto'], + $params['aftereventid'], + $params['limitnum'] + ); + + $exportercache = new events_related_objects_cache($events); + $exporter = new events_exporter($events, ['cache' => $exportercache]); + + return $exporter->export($renderer); + } + + /** + * Returns description of method result value. + * + * @since Moodle 3.3 + * @return external_description + */ + public static function get_calendar_action_events_by_timesort_returns() { + return events_exporter::get_read_structure(); + } + + /** + * Returns description of method parameters. + * + * @return external_function_parameters + */ + public static function get_calendar_action_events_by_course_parameters() { + return new external_function_parameters( + array( + 'courseid' => new external_value(PARAM_INT, 'Course id'), + 'timesortfrom' => new external_value(PARAM_INT, 'Time sort from', VALUE_DEFAULT, null), + 'timesortto' => new external_value(PARAM_INT, 'Time sort to', VALUE_DEFAULT, null), + 'aftereventid' => new external_value(PARAM_INT, 'The last seen event id', VALUE_DEFAULT, 0), + 'limitnum' => new external_value(PARAM_INT, 'Limit number', VALUE_DEFAULT, 20) + ) + ); + } + + /** + * Get calendar action events for the given course. + * + * @since Moodle 3.3 + * @param int $courseid Only events in this course + * @param null|int $timesortfrom Events after this time (inclusive) + * @param null|int $timesortto Events before this time (inclusive) + * @param null|int $aftereventid Get events with ids greater than this one + * @param int $limitnum Limit the number of results to this value + * @return array + */ + public static function get_calendar_action_events_by_course( + $courseid, $timesortfrom = null, $timesortto = null, $aftereventid = 0, $limitnum = 20) { + + global $CFG, $PAGE, $USER; + + require_once($CFG->dirroot . '/calendar/lib.php'); + + $user = null; + $params = self::validate_parameters( + self::get_calendar_action_events_by_course_parameters(), + [ + 'courseid' => $courseid, + 'timesortfrom' => $timesortfrom, + 'timesortto' => $timesortto, + 'aftereventid' => $aftereventid, + 'limitnum' => $limitnum, + ] + ); + $context = \context_user::instance($USER->id); + self::validate_context($context); + + if (empty($params['aftereventid'])) { + $params['aftereventid'] = null; + } + + $courses = enrol_get_my_courses('*', 'visible DESC,sortorder ASC', 0, [$courseid]); + $courses = array_values($courses); + + if (empty($courses)) { + return []; + } + + $course = $courses[0]; + $renderer = $PAGE->get_renderer('core_calendar'); + $events = local_api::get_action_events_by_course( + $course, + $params['timesortfrom'], + $params['timesortto'], + $params['aftereventid'], + $params['limitnum'] + ); + + $exportercache = new events_related_objects_cache($events, $courses); + $exporter = new events_exporter($events, ['cache' => $exportercache]); + + return $exporter->export($renderer); + } + + /** + * Returns description of method result value. + * + * @return external_description + */ + public static function get_calendar_action_events_by_course_returns() { + return events_exporter::get_read_structure(); + } + + /** + * Returns description of method parameters. + * + * @return external_function_parameters + */ + public static function get_calendar_action_events_by_courses_parameters() { + return new external_function_parameters( + array( + 'courseids' => new external_multiple_structure( + new external_value(PARAM_INT, 'Course id') + ), + 'timesortfrom' => new external_value(PARAM_INT, 'Time sort from', VALUE_DEFAULT, null), + 'timesortto' => new external_value(PARAM_INT, 'Time sort to', VALUE_DEFAULT, null), + 'limitnum' => new external_value(PARAM_INT, 'Limit number', VALUE_DEFAULT, 10) + ) + ); + } + + /** + * Get calendar action events for a given list of courses. + * + * @since Moodle 3.3 + * @param array $courseids Only include events for these courses + * @param null|int $timesortfrom Events after this time (inclusive) + * @param null|int $timesortto Events before this time (inclusive) + * @param int $limitnum Limit the number of results per course to this value + * @return array + */ + public static function get_calendar_action_events_by_courses( + array $courseids, $timesortfrom = null, $timesortto = null, $limitnum = 10) { + + global $CFG, $PAGE, $USER; + + require_once($CFG->dirroot . '/calendar/lib.php'); + + $user = null; + $params = self::validate_parameters( + self::get_calendar_action_events_by_courses_parameters(), + [ + 'courseids' => $courseids, + 'timesortfrom' => $timesortfrom, + 'timesortto' => $timesortto, + 'limitnum' => $limitnum, + ] + ); + $context = \context_user::instance($USER->id); + self::validate_context($context); + + if (empty($params['courseids'])) { + return ['groupedbycourse' => []]; + } + + $renderer = $PAGE->get_renderer('core_calendar'); + $courses = enrol_get_my_courses('*', 'visible DESC,sortorder ASC', 0, $params['courseids']); + $courses = array_values($courses); + + if (empty($courses)) { + return ['groupedbycourse' => []]; + } + + $events = local_api::get_action_events_by_courses( + $courses, + $params['timesortfrom'], + $params['timesortto'], + $params['limitnum'] + ); + + if (empty($events)) { + return ['groupedbycourse' => []]; + } + + $exportercache = new events_related_objects_cache($events, $courses); + $exporter = new events_grouped_by_course_exporter($events, ['cache' => $exportercache]); + + return $exporter->export($renderer); + } + + /** + * Returns description of method result value. + * + * @return external_description + */ + public static function get_calendar_action_events_by_courses_returns() { + return events_grouped_by_course_exporter::get_read_structure(); + } + /** * Returns description of method parameters. * diff --git a/calendar/lib.php b/calendar/lib.php index 199cd65aa48..01abe8d38b7 100644 --- a/calendar/lib.php +++ b/calendar/lib.php @@ -126,1925 +126,22 @@ define('CALENDAR_SUBSCRIPTION_REMOVE', 2); define('CALENDAR_EVENT_USER_OVERRIDE_PRIORITY', 9999999); /** - * Return the days of the week - * - * @return array array of days + * CALENDAR_EVENT_TYPE_STANDARD - Standard events. */ -function calendar_get_days() { - $calendartype = \core_calendar\type_factory::get_calendar_instance(); - return $calendartype->get_weekdays(); -} +define('CALENDAR_EVENT_TYPE_STANDARD', 0); /** - * Get the subscription from a given id - * - * @since Moodle 2.5 - * @param int $id id of the subscription - * @return stdClass Subscription record from DB - * @throws moodle_exception for an invalid id + * CALENDAR_EVENT_TYPE_ACTION - Action events. */ -function calendar_get_subscription($id) { - global $DB; - - $cache = cache::make('core', 'calendar_subscriptions'); - $subscription = $cache->get($id); - if (empty($subscription)) { - $subscription = $DB->get_record('event_subscriptions', array('id' => $id), '*', MUST_EXIST); - // cache the data. - $cache->set($id, $subscription); - } - return $subscription; -} +define('CALENDAR_EVENT_TYPE_ACTION', 1); /** - * Gets the first day of the week - * - * Used to be define('CALENDAR_STARTING_WEEKDAY', blah); - * - * @return int - */ -function calendar_get_starting_weekday() { - $calendartype = \core_calendar\type_factory::get_calendar_instance(); - return $calendartype->get_starting_weekday(); -} - -/** - * Generates the HTML for a miniature calendar - * - * @param array $courses list of course to list events from - * @param array $groups list of group - * @param array $users user's info - * @param int|bool $calmonth calendar month in numeric, default is set to false - * @param int|bool $calyear calendar month in numeric, default is set to false - * @param string|bool $placement the place/page the calendar is set to appear - passed on the the controls function - * @param int|bool $courseid id of the course the calendar is displayed on - passed on the the controls function - * @param int $time the unixtimestamp representing the date we want to view, this is used instead of $calmonth - * and $calyear to support multiple calendars - * @return string $content return html table for mini calendar - */ -function calendar_get_mini($courses, $groups, $users, $calmonth = false, $calyear = false, $placement = false, - $courseid = false, $time = 0) { - global $CFG, $OUTPUT, $PAGE; - - // Get the calendar type we are using. - $calendartype = \core_calendar\type_factory::get_calendar_instance(); - - $display = new stdClass; - - // Assume we are not displaying this month for now. - $display->thismonth = false; - - $content = ''; - - // Do this check for backwards compatibility. The core should be passing a timestamp rather than month and year. - // If a month and year are passed they will be in Gregorian. - if (!empty($calmonth) && !empty($calyear)) { - // Ensure it is a valid date, else we will just set it to the current timestamp. - if (checkdate($calmonth, 1, $calyear)) { - $time = make_timestamp($calyear, $calmonth, 1); - } else { - $time = time(); - } - $date = usergetdate($time); - if ($calmonth == $date['mon'] && $calyear == $date['year']) { - $display->thismonth = true; - } - // We can overwrite date now with the date used by the calendar type, if it is not Gregorian, otherwise - // there is no need as it is already in Gregorian. - if ($calendartype->get_name() != 'gregorian') { - $date = $calendartype->timestamp_to_date_array($time); - } - } else if (!empty($time)) { - // Get the specified date in the calendar type being used. - $date = $calendartype->timestamp_to_date_array($time); - $thisdate = $calendartype->timestamp_to_date_array(time()); - if ($date['month'] == $thisdate['month'] && $date['year'] == $thisdate['year']) { - $display->thismonth = true; - // If we are the current month we want to set the date to the current date, not the start of the month. - $date = $thisdate; - } - } else { - // Get the current date in the calendar type being used. - $time = time(); - $date = $calendartype->timestamp_to_date_array($time); - $display->thismonth = true; - } - - list($d, $m, $y) = array($date['mday'], $date['mon'], $date['year']); // This is what we want to display. - - // Get Gregorian date for the start of the month. - $gregoriandate = $calendartype->convert_to_gregorian($date['year'], $date['mon'], 1); - - // Store the gregorian date values to be used later. - list($gy, $gm, $gd, $gh, $gmin) = array($gregoriandate['year'], $gregoriandate['month'], $gregoriandate['day'], - $gregoriandate['hour'], $gregoriandate['minute']); - - // Get the max number of days in this month for this calendar type. - $display->maxdays = calendar_days_in_month($m, $y); - // Get the starting week day for this month. - $startwday = dayofweek(1, $m, $y); - // Get the days in a week. - $daynames = calendar_get_days(); - // Store the number of days in a week. - $numberofdaysinweek = $calendartype->get_num_weekdays(); - - // Set the min and max weekday. - $display->minwday = calendar_get_starting_weekday(); - $display->maxwday = $display->minwday + ($numberofdaysinweek - 1); - - // These are used for DB queries, so we want unixtime, so we need to use Gregorian dates. - $display->tstart = make_timestamp($gy, $gm, $gd, $gh, $gmin, 0); - $display->tend = $display->tstart + ($display->maxdays * DAYSECS) - 1; - - // Align the starting weekday to fall in our display range - // This is simple, not foolproof. - if ($startwday < $display->minwday) { - $startwday += $numberofdaysinweek; - } - - // Get the events matching our criteria. Don't forget to offset the timestamps for the user's TZ! - $events = calendar_get_events($display->tstart, $display->tend, $users, $groups, $courses); - - // Set event course class for course events - if (!empty($events)) { - foreach ($events as $eventid => $event) { - if (!empty($event->modulename)) { - $cm = get_coursemodule_from_instance($event->modulename, $event->instance); - if (!\core_availability\info_module::is_user_visible($cm, 0, false)) { - unset($events[$eventid]); - } - } - } - } - - // This is either a genius idea or an idiot idea: in order to not complicate things, we use this rule: if, after - // possibly removing SITEID from $courses, there is only one course left, then clicking on a day in the month - // will also set the $SESSION->cal_courses_shown variable to that one course. Otherwise, we 'd need to add extra - // arguments to this function. - $hrefparams = array(); - if(!empty($courses)) { - $courses = array_diff($courses, array(SITEID)); - if(count($courses) == 1) { - $hrefparams['course'] = reset($courses); - } - } - - // We want to have easy access by day, since the display is on a per-day basis. - calendar_events_by_day($events, $m, $y, $eventsbyday, $durationbyday, $typesbyday, $courses); - - // Accessibility: added summary and elements. - $summary = get_string('calendarheading', 'calendar', userdate($display->tstart, get_string('strftimemonthyear'))); - // Begin table. - $content .= ''; - if (($placement !== false) && ($courseid !== false)) { - $content .= ''; - } - $content .= ''; // Header row: day names - - // Print out the names of the weekdays. - for ($i = $display->minwday; $i <= $display->maxwday; ++$i) { - $pos = $i % $numberofdaysinweek; - $content .= '\n"; - } - - $content .= ''; // End of day names; prepare for day numbers - - // For the table display. $week is the row; $dayweek is the column. - $dayweek = $startwday; - - // Paddding (the first week may have blank days in the beginning) - for($i = $display->minwday; $i < $startwday; ++$i) { - $content .= ''."\n"; - } - - $weekend = CALENDAR_DEFAULT_WEEKEND; - if (isset($CFG->calendar_weekend)) { - $weekend = intval($CFG->calendar_weekend); - } - - // Now display all the calendar - $daytime = strtotime('-1 day', $display->tstart); - for($day = 1; $day <= $display->maxdays; ++$day, ++$dayweek) { - $cellattributes = array(); - $daytime = strtotime('+1 day', $daytime); - if($dayweek > $display->maxwday) { - // We need to change week (table row) - $content .= ''; - $dayweek = $display->minwday; - } - - // Reset vars. - if ($weekend & (1 << ($dayweek % $numberofdaysinweek))) { - // Weekend. This is true no matter what the exact range is. - $class = 'weekend day'; - } else { - // Normal working day. - $class = 'day'; - } - - $eventids = array(); - if (!empty($eventsbyday[$day])) { - $eventids = $eventsbyday[$day]; - } - - if (!empty($durationbyday[$day])) { - $eventids = array_unique(array_merge($eventids, $durationbyday[$day])); - } - - $finishclass = false; - - if (!empty($eventids)) { - // There is at least one event on this day. - - $class .= ' hasevent'; - $hrefparams['view'] = 'day'; - $dayhref = calendar_get_link_href(new moodle_url(CALENDAR_URL . 'view.php', $hrefparams), 0, 0, 0, $daytime); - - $popupcontent = ''; - foreach ($eventids as $eventid) { - if (!isset($events[$eventid])) { - continue; - } - $event = new calendar_event($events[$eventid]); - $popupalt = ''; - $component = 'moodle'; - if (!empty($event->modulename)) { - $popupicon = 'icon'; - $popupalt = $event->modulename; - $component = $event->modulename; - } else if ($event->courseid == SITEID) { // Site event. - $popupicon = 'i/siteevent'; - } else if ($event->courseid != 0 && $event->courseid != SITEID && $event->groupid == 0) { // Course event. - $popupicon = 'i/courseevent'; - } else if ($event->groupid) { // Group event. - $popupicon = 'i/groupevent'; - } else { // Must be a user event. - $popupicon = 'i/userevent'; - } - - if ($event->timeduration) { - $startdate = $calendartype->timestamp_to_date_array($event->timestart); - $enddate = $calendartype->timestamp_to_date_array($event->timestart + $event->timeduration - 1); - if ($enddate['mon'] == $m && $enddate['year'] == $y && $enddate['mday'] == $day) { - $finishclass = true; - } - } - - $dayhref->set_anchor('event_'.$event->id); - - $popupcontent .= html_writer::start_tag('div'); - if ($component == 'moodle') { - $popupcontent .= $OUTPUT->pix_icon($popupicon, $popupalt, $component); - } else { - $popupcontent .= $OUTPUT->image_icon($popupicon, $popupalt, $component); - } - // Show ical source if needed. - if (!empty($event->subscription) && $CFG->calendar_showicalsource) { - $a = new stdClass(); - $a->name = format_string($event->name, true); - $a->source = $event->subscription->name; - $name = get_string('namewithsource', 'calendar', $a); - } else { - if ($finishclass) { - $samedate = $startdate['mon'] == $enddate['mon'] && - $startdate['year'] == $enddate['year'] && - $startdate['mday'] == $enddate['mday']; - - if ($samedate) { - $name = format_string($event->name, true); - } else { - $name = format_string($event->name, true) . ' (' . get_string('eventendtime', 'calendar') . ')'; - } - } else { - $name = format_string($event->name, true); - } - } - $popupcontent .= html_writer::link($dayhref, $name); - $popupcontent .= html_writer::end_tag('div'); - } - - if ($display->thismonth && $day == $d) { - $popupdata = calendar_get_popup(true, $daytime, $popupcontent); - } else { - $popupdata = calendar_get_popup(false, $daytime, $popupcontent); - } - - // Class and cell content - if(isset($typesbyday[$day]['startglobal'])) { - $class .= ' calendar_event_global'; - } else if(isset($typesbyday[$day]['startcourse'])) { - $class .= ' calendar_event_course'; - } else if(isset($typesbyday[$day]['startgroup'])) { - $class .= ' calendar_event_group'; - } else if(isset($typesbyday[$day]['startuser'])) { - $class .= ' calendar_event_user'; - } - if ($finishclass) { - $class .= ' duration_finish'; - } - - $data = array( - 'url' => $dayhref, - 'day' => $day, - 'content' => $popupdata['data-core_calendar-popupcontent'], - 'title' => $popupdata['data-core_calendar-title'] - ); - $cell = $OUTPUT->render_from_template('core_calendar/minicalendar_day_link', $data); - } else { - $cell = $day; - } - - $durationclass = false; - if (isset($typesbyday[$day]['durationglobal'])) { - $durationclass = ' duration_global'; - } else if(isset($typesbyday[$day]['durationcourse'])) { - $durationclass = ' duration_course'; - } else if(isset($typesbyday[$day]['durationgroup'])) { - $durationclass = ' duration_group'; - } else if(isset($typesbyday[$day]['durationuser'])) { - $durationclass = ' duration_user'; - } - if ($durationclass) { - $class .= ' duration '.$durationclass; - } - - // If event has a class set then add it to the table day '; - } - $content .= ''; // Last row ends - - $content .= '
    '. calendar_top_controls($placement, array('id' => $courseid, 'time' => $time)) .'
    '. - $daynames[$pos]['shortname'] ."
     
    tag - // Note: only one colour for minicalendar - if(isset($eventsbyday[$day])) { - foreach($eventsbyday[$day] as $eventid) { - if (!isset($events[$eventid])) { - continue; - } - $event = $events[$eventid]; - if (!empty($event->class)) { - $class .= ' '.$event->class; - } - break; - } - } - - if ($display->thismonth && $day == $d) { - // The current cell is for today - add appropriate classes and additional information for styling. - $class .= ' today'; - $today = get_string('today', 'calendar').' '.userdate(time(), get_string('strftimedayshort')); - - if (!isset($eventsbyday[$day]) && !isset($durationbyday[$day])) { - $class .= ' eventnone'; - $popupdata = calendar_get_popup(true, false); - $data = array( - 'url' => '#', - 'day' => $day, - 'content' => $popupdata['data-core_calendar-popupcontent'], - 'title' => $popupdata['data-core_calendar-title'] - ); - $cell = $OUTPUT->render_from_template('core_calendar/minicalendar_day_link', $data); - } - $cell = get_accesshide($today . ' ') . $cell; - } - - // Just display it - $cellattributes['class'] = $class; - $content .= html_writer::tag('td', $cell, $cellattributes); - } - - // Paddding (the last week may have blank days at the end) - for($i = $dayweek; $i <= $display->maxwday; ++$i) { - $content .= ' 
    '; // Tabular display of days ends - return $content; -} - -/** - * Gets the calendar popup - * - * It called at multiple points in from calendar_get_mini. - * Copied and modified from calendar_get_mini. - * - * @param bool $is_today false except when called on the current day. - * @param mixed $event_timestart $events[$eventid]->timestart, OR false if there are no events. - * @param string $popupcontent content for the popup window/layout. - * @return string eventid for the calendar_tooltip popup window/layout. - */ -function calendar_get_popup($today = false, $timestart, $popupcontent = '') { - global $PAGE; - - $popupcaption = ''; - if ($today) { - $popupcaption = get_string('today', 'calendar') . ' '; - } - - if (false === $timestart) { - $popupcaption .= userdate(time(), get_string('strftimedayshort')); - $popupcontent = get_string('eventnone', 'calendar'); - - } else { - $popupcaption .= get_string('eventsfor', 'calendar', userdate($timestart, get_string('strftimedayshort'))); - } - - return array( - 'data-core_calendar-title' => $popupcaption, - 'data-core_calendar-popupcontent' => $popupcontent, - ); -} - -/** - * Gets the calendar upcoming event - * - * @param array $courses array of courses - * @param array|int|bool $groups array of groups, group id or boolean for all/no group events - * @param array|int|bool $users array of users, user id or boolean for all/no user events - * @param int $daysinfuture number of days in the future we 'll look - * @param int $maxevents maximum number of events - * @param int $fromtime start time - * @return array $output array of upcoming events - */ -function calendar_get_upcoming($courses, $groups, $users, $daysinfuture, $maxevents, $fromtime=0) { - global $CFG, $COURSE, $DB; - - $display = new stdClass; - $display->range = $daysinfuture; // How many days in the future we 'll look - $display->maxevents = $maxevents; - - $output = array(); - - // Prepare "course caching", since it may save us a lot of queries - $coursecache = array(); - - $processed = 0; - $now = time(); // We 'll need this later - $usermidnighttoday = usergetmidnight($now); - - if ($fromtime) { - $display->tstart = $fromtime; - } else { - $display->tstart = $usermidnighttoday; - } - - // This works correctly with respect to the user's DST, but it is accurate - // only because $fromtime is always the exact midnight of some day! - $display->tend = usergetmidnight($display->tstart + DAYSECS * $display->range + 3 * HOURSECS) - 1; - - // Get the events matching our criteria - $events = calendar_get_events($display->tstart, $display->tend, $users, $groups, $courses); - - // This is either a genius idea or an idiot idea: in order to not complicate things, we use this rule: if, after - // possibly removing SITEID from $courses, there is only one course left, then clicking on a day in the month - // will also set the $SESSION->cal_courses_shown variable to that one course. Otherwise, we 'd need to add extra - // arguments to this function. - - $hrefparams = array(); - if(!empty($courses)) { - $courses = array_diff($courses, array(SITEID)); - if(count($courses) == 1) { - $hrefparams['course'] = reset($courses); - } - } - - if ($events !== false) { - - $modinfo = get_fast_modinfo($COURSE); - - foreach($events as $event) { - - - if (!empty($event->modulename)) { - if ($event->courseid == $COURSE->id) { - if (isset($modinfo->instances[$event->modulename][$event->instance])) { - $cm = $modinfo->instances[$event->modulename][$event->instance]; - if (!$cm->uservisible) { - continue; - } - } - } else { - if (!$cm = get_coursemodule_from_instance($event->modulename, $event->instance)) { - continue; - } - if (!\core_availability\info_module::is_user_visible($cm, 0, false)) { - continue; - } - } - } - - if ($processed >= $display->maxevents) { - break; - } - - $event->time = calendar_format_event_time($event, $now, $hrefparams); - $output[] = $event; - ++$processed; - } - } - return $output; -} - - -/** - * Get a HTML link to a course. - * - * @param int $courseid the course id - * @return string a link to the course (as HTML); empty if the course id is invalid - */ -function calendar_get_courselink($courseid) { - - if (!$courseid) { - return ''; - } - - calendar_get_course_cached($coursecache, $courseid); - $context = context_course::instance($courseid); - $fullname = format_string($coursecache[$courseid]->fullname, true, array('context' => $context)); - $url = new moodle_url('/course/view.php', array('id' => $courseid)); - $link = html_writer::link($url, $fullname); - - return $link; -} - - -/** - * Add calendar event metadata - * - * @param stdClass $event event info - * @return stdClass $event metadata - */ -function calendar_add_event_metadata($event) { - global $CFG, $OUTPUT; - - //Support multilang in event->name - $event->name = format_string($event->name,true); - - if(!empty($event->modulename)) { // Activity event - // The module name is set. I will assume that it has to be displayed, and - // also that it is an automatically-generated event. And of course that the - // fields for get_coursemodule_from_instance are set correctly. - $module = calendar_get_module_cached($coursecache, $event->modulename, $event->instance); - - if ($module === false) { - return; - } - - $modulename = get_string('modulename', $event->modulename); - if (get_string_manager()->string_exists($event->eventtype, $event->modulename)) { - // will be used as alt text if the event icon - $eventtype = get_string($event->eventtype, $event->modulename); - } else { - $eventtype = ''; - } - $event->icon = $OUTPUT->image_icon('icon', $event->modulename, $eventtype); - - $event->referer = ''.$event->name.''; - $event->courselink = calendar_get_courselink($module->course); - $event->cmid = $module->id; - - } else if($event->courseid == SITEID) { // Site event - $event->icon = $OUTPUT->pix_icon('i/siteevent', get_string('globalevent', 'calendar')); - $event->cssclass = 'calendar_event_global'; - } else if($event->courseid != 0 && $event->courseid != SITEID && $event->groupid == 0) { // Course event - $event->icon = $OUTPUT->pix_icon('i/courseevent', get_string('courseevent', 'calendar')); - $event->courselink = calendar_get_courselink($event->courseid); - $event->cssclass = 'calendar_event_course'; - } else if ($event->groupid) { // Group event - if ($group = calendar_get_group_cached($event->groupid)) { - $groupname = format_string($group->name, true, context_course::instance($group->courseid)); - } else { - $groupname = ''; - } - $event->icon = $OUTPUT->pix_icon('i/groupevent', get_string('groupevent', 'calendar')); - $event->courselink = calendar_get_courselink($event->courseid) . ', ' . $groupname; - $event->cssclass = 'calendar_event_group'; - } else if($event->userid) { // User event - $event->icon = $OUTPUT->pix_icon('i/userevent', get_string('userevent', 'calendar')); - $event->cssclass = 'calendar_event_user'; - } - return $event; -} - -/** - * Get calendar events - * - * @param int $tstart Start time of time range for events - * @param int $tend End time of time range for events - * @param array|int|boolean $users array of users, user id or boolean for all/no user events - * @param array|int|boolean $groups array of groups, group id or boolean for all/no group events - * @param array|int|boolean $courses array of courses, course id or boolean for all/no course events - * @param boolean $withduration whether only events starting within time range selected - * or events in progress/already started selected as well - * @param boolean $ignorehidden whether to select only visible events or all events - * @return array $events of selected events or an empty array if there aren't any (or there was an error) - */ -function calendar_get_events($tstart, $tend, $users, $groups, $courses, $withduration = true, $ignorehidden = true) { - global $DB; - - $params = array(); - // Quick test. - if (empty($users) && empty($groups) && empty($courses)) { - return array(); - } - - // Array of filter conditions. To be concatenated by the OR operator. - $filters = []; - - // User filter. - if ((is_array($users) && !empty($users)) or is_numeric($users)) { - // Events from a number of users - list($insqlusers, $inparamsusers) = $DB->get_in_or_equal($users, SQL_PARAMS_NAMED); - $filters[] = "(e.userid $insqlusers AND e.courseid = 0 AND e.groupid = 0)"; - $params = array_merge($params, $inparamsusers); - } else if ($users === true) { - // Events from ALL users - $filters[] = "(e.userid != 0 AND e.courseid = 0 AND e.groupid = 0)"; - } - // Boolean false (no users at all): We don't need to do anything. - - // Group filter. - if ((is_array($groups) && !empty($groups)) or is_numeric($groups)) { - // Events from a number of groups - list($insqlgroups, $inparamsgroups) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED); - $filters[] = "e.groupid $insqlgroups"; - $params = array_merge($params, $inparamsgroups); - } else if ($groups === true) { - // Events from ALL groups - $filters[] = "e.groupid != 0"; - } - // Boolean false (no groups at all): We don't need to do anything. - - // Course filter. - if ((is_array($courses) && !empty($courses)) or is_numeric($courses)) { - list($insqlcourses, $inparamscourses) = $DB->get_in_or_equal($courses, SQL_PARAMS_NAMED); - $filters[] = "(e.groupid = 0 AND e.courseid $insqlcourses)"; - $params = array_merge($params, $inparamscourses); - } else if ($courses === true) { - // Events from ALL courses - $filters[] = "(e.groupid = 0 AND e.courseid != 0)"; - } - - // Security check: if, by now, we have NOTHING in $whereclause, then it means - // that NO event-selecting clauses were defined. Thus, we won't be returning ANY - // events no matter what. Allowing the code to proceed might return a completely - // valid query with only time constraints, thus selecting ALL events in that time frame! - if (empty($filters)) { - return array(); - } - - // Build our clause for the filters. - $filterclause = implode(' OR ', $filters); - - // Array of where conditions for our query. To be concatenated by the AND operator. - $whereconditions = ["($filterclause)"]; - - // Time clause. - if ($withduration) { - $timeclause = "((e.timestart >= :tstart1 OR e.timestart + e.timeduration > :tstart2) AND e.timestart <= :tend)"; - $params['tstart1'] = $tstart; - $params['tstart2'] = $tstart; - $params['tend'] = $tend; - } else { - $timeclause = "(e.timestart >= :tstart AND e.timestart <= :tend)"; - $params['tstart'] = $tstart; - $params['tend'] = $tend; - } - $whereconditions[] = $timeclause; - - // Show visible only. - if ($ignorehidden) { - $whereconditions[] = "(e.visible = 1)"; - } - - // Build the main query's WHERE clause. - $whereclause = implode(' AND ', $whereconditions); - - // Build SQL subquery and conditions for filtered events based on priorities. - $subquerywhere = ''; - $subqueryconditions = []; - - // Get the user's courses. Otherwise, get the default courses being shown by the calendar. - $usercourses = calendar_get_default_courses(); - - // Set calendar filters. - list($usercourses, $usergroups, $user) = calendar_set_filters($usercourses, true); - $subqueryparams = []; - - // Flag to indicate whether the query needs to exclude group overrides. - $viewgroupsonly = false; - - if ($user) { - // Set filter condition for the user's events. - $subqueryconditions[] = "(ev.userid = :user AND ev.courseid = 0 AND ev.groupid = 0)"; - $subqueryparams['user'] = $user; - - foreach ($usercourses as $courseid) { - if (has_capability('moodle/site:accessallgroups', context_course::instance($courseid))) { - $usergroupmembership = groups_get_all_groups($courseid, $user, 0, 'g.id'); - if (count($usergroupmembership) == 0) { - $viewgroupsonly = true; - break; - } - } - } - } - - // Set filter condition for the user's group events. - if ($usergroups === true || $viewgroupsonly) { - // Fetch group events, but not group overrides. - $subqueryconditions[] = "(ev.groupid != 0 AND ev.eventtype = 'group')"; - } else if (!empty($usergroups)) { - // Fetch group events and group overrides. - list($inusergroups, $inusergroupparams) = $DB->get_in_or_equal($usergroups, SQL_PARAMS_NAMED); - $subqueryconditions[] = "(ev.groupid $inusergroups)"; - $subqueryparams = array_merge($subqueryparams, $inusergroupparams); - } - - // Get courses to be used for the subquery. - $subquerycourses = []; - if (is_array($courses)) { - $subquerycourses = $courses; - } else if (is_numeric($courses)) { - $subquerycourses[] = $courses; - } - // Merge with user courses, if necessary. - if (!empty($usercourses)) { - $subquerycourses = array_merge($subquerycourses, $usercourses); - // Make sure we remove duplicate values. - $subquerycourses = array_unique($subquerycourses); - } - - // Set subquery filter condition for the courses. - if (!empty($subquerycourses)) { - list($incourses, $incoursesparams) = $DB->get_in_or_equal($subquerycourses, SQL_PARAMS_NAMED); - $subqueryconditions[] = "(ev.groupid = 0 AND ev.courseid $incourses)"; - $subqueryparams = array_merge($subqueryparams, $incoursesparams); - } - - // Build the WHERE condition for the sub-query. - if (!empty($subqueryconditions)) { - $subquerywhere = 'WHERE ' . implode(" OR ", $subqueryconditions); - } - - // Merge subquery parameters to the parameters of the main query. - if (!empty($subqueryparams)) { - $params = array_merge($params, $subqueryparams); - } - - // Sub-query that fetches the list of unique events that were filtered based on priority. - $subquery = "SELECT ev.modulename, - ev.instance, - ev.eventtype, - MAX(ev.priority) as priority - FROM {event} ev - $subquerywhere - GROUP BY ev.modulename, ev.instance, ev.eventtype"; - - // Build the main query. - $sql = "SELECT e.* - FROM {event} e - INNER JOIN ($subquery) fe - ON e.modulename = fe.modulename - AND e.instance = fe.instance - AND e.eventtype = fe.eventtype - AND (e.priority = fe.priority OR (e.priority IS NULL AND fe.priority IS NULL)) - LEFT JOIN {modules} m - ON e.modulename = m.name - WHERE (m.visible = 1 OR m.visible IS NULL) AND $whereclause - ORDER BY e.timestart"; - $events = $DB->get_records_sql($sql, $params); - - if ($events === false) { - $events = array(); - } - return $events; -} - -/** Get calendar events by id - * - * @since Moodle 2.5 - * @param array $eventids list of event ids - * @return array Array of event entries, empty array if nothing found - */ - -function calendar_get_events_by_id($eventids) { - global $DB; - - if (!is_array($eventids) || empty($eventids)) { - return array(); - } - list($wheresql, $params) = $DB->get_in_or_equal($eventids); - $wheresql = "id $wheresql"; - - return $DB->get_records_select('event', $wheresql, $params); -} - -/** - * Get control options for Calendar - * - * @param string $type of calendar - * @param array $data calendar information - * @return string $content return available control for the calender in html - */ -function calendar_top_controls($type, $data) { - global $PAGE, $OUTPUT; - - // Get the calendar type we are using. - $calendartype = \core_calendar\type_factory::get_calendar_instance(); - - $content = ''; - - // Ensure course id passed if relevant. - $courseid = ''; - if (!empty($data['id'])) { - $courseid = '&course='.$data['id']; - } - - // If we are passing a month and year then we need to convert this to a timestamp to - // support multiple calendars. No where in core should these be passed, this logic - // here is for third party plugins that may use this function. - if (!empty($data['m']) && !empty($date['y'])) { - if (!isset($data['d'])) { - $data['d'] = 1; - } - if (!checkdate($data['m'], $data['d'], $data['y'])) { - $time = time(); - } else { - $time = make_timestamp($data['y'], $data['m'], $data['d']); - } - } else if (!empty($data['time'])) { - $time = $data['time']; - } else { - $time = time(); - } - - // Get the date for the calendar type. - $date = $calendartype->timestamp_to_date_array($time); - - $urlbase = $PAGE->url; - - // We need to get the previous and next months in certain cases. - if ($type == 'frontpage' || $type == 'course' || $type == 'month') { - $prevmonth = calendar_sub_month($date['mon'], $date['year']); - $prevmonthtime = $calendartype->convert_to_gregorian($prevmonth[1], $prevmonth[0], 1); - $prevmonthtime = make_timestamp($prevmonthtime['year'], $prevmonthtime['month'], $prevmonthtime['day'], - $prevmonthtime['hour'], $prevmonthtime['minute']); - - $nextmonth = calendar_add_month($date['mon'], $date['year']); - $nextmonthtime = $calendartype->convert_to_gregorian($nextmonth[1], $nextmonth[0], 1); - $nextmonthtime = make_timestamp($nextmonthtime['year'], $nextmonthtime['month'], $nextmonthtime['day'], - $nextmonthtime['hour'], $nextmonthtime['minute']); - } - - switch ($type) { - case 'frontpage': - $prevlink = calendar_get_link_previous(get_string('monthprev', 'access'), $urlbase, false, false, false, true, $prevmonthtime); - $nextlink = calendar_get_link_next(get_string('monthnext', 'access'), $urlbase, false, false, false, true, $nextmonthtime); - $calendarlink = calendar_get_link_href(new moodle_url(CALENDAR_URL.'view.php', array('view' => 'month')), false, false, false, $time); - - if (!empty($data['id'])) { - $calendarlink->param('course', $data['id']); - } - - $prevlink = $prevlink; - $right = $nextlink; - - $content .= html_writer::start_tag('div', array('class'=>'calendar-controls')); - $content .= $prevlink.' | '; - $content .= html_writer::tag('span', html_writer::link($calendarlink, userdate($time, get_string('strftimemonthyear')), array('title'=>get_string('monththis','calendar'))), array('class'=>'current')); - $content .= ' | '. $right; - $content .= "\n"; - $content .= html_writer::end_tag('div'); - - break; - case 'course': - $prevlink = calendar_get_link_previous(get_string('monthprev', 'access'), $urlbase, false, false, false, true, $prevmonthtime); - $nextlink = calendar_get_link_next(get_string('monthnext', 'access'), $urlbase, false, false, false, true, $nextmonthtime); - $calendarlink = calendar_get_link_href(new moodle_url(CALENDAR_URL.'view.php', array('view' => 'month')), false, false, false, $time); - - if (!empty($data['id'])) { - $calendarlink->param('course', $data['id']); - } - - $content .= html_writer::start_tag('div', array('class'=>'calendar-controls')); - $content .= $prevlink.' | '; - $content .= html_writer::tag('span', html_writer::link($calendarlink, userdate($time, get_string('strftimemonthyear')), array('title'=>get_string('monththis','calendar'))), array('class'=>'current')); - $content .= ' | '. $nextlink; - $content .= ""; - $content .= html_writer::end_tag('div'); - break; - case 'upcoming': - $calendarlink = calendar_get_link_href(new moodle_url(CALENDAR_URL.'view.php', array('view' => 'upcoming')), false, false, false, $time); - if (!empty($data['id'])) { - $calendarlink->param('course', $data['id']); - } - $calendarlink = html_writer::link($calendarlink, userdate($time, get_string('strftimemonthyear'))); - $content .= html_writer::tag('div', $calendarlink, array('class'=>'centered')); - break; - case 'display': - $calendarlink = calendar_get_link_href(new moodle_url(CALENDAR_URL.'view.php', array('view' => 'month')), false, false, false, $time); - if (!empty($data['id'])) { - $calendarlink->param('course', $data['id']); - } - $calendarlink = html_writer::link($calendarlink, userdate($time, get_string('strftimemonthyear'))); - $content .= html_writer::tag('h3', $calendarlink); - break; - case 'month': - $prevlink = calendar_get_link_previous(userdate($prevmonthtime, get_string('strftimemonthyear')), 'view.php?view=month'.$courseid.'&', false, false, false, false, $prevmonthtime); - $nextlink = calendar_get_link_next(userdate($nextmonthtime, get_string('strftimemonthyear')), 'view.php?view=month'.$courseid.'&', false, false, false, false, $nextmonthtime); - - $content .= html_writer::start_tag('div', array('class'=>'calendar-controls')); - $content .= $prevlink . ' | '; - $content .= $OUTPUT->heading(userdate($time, get_string('strftimemonthyear')), 2, 'current'); - $content .= ' | ' . $nextlink; - $content .= ''; - $content .= html_writer::end_tag('div')."\n"; - break; - case 'day': - $days = calendar_get_days(); - - $prevtimestamp = strtotime('-1 day', $time); - $nexttimestamp = strtotime('+1 day', $time); - - $prevdate = $calendartype->timestamp_to_date_array($prevtimestamp); - $nextdate = $calendartype->timestamp_to_date_array($nexttimestamp); - - $prevname = $days[$prevdate['wday']]['fullname']; - $nextname = $days[$nextdate['wday']]['fullname']; - $prevlink = calendar_get_link_previous($prevname, 'view.php?view=day'.$courseid.'&', false, false, false, false, $prevtimestamp); - $nextlink = calendar_get_link_next($nextname, 'view.php?view=day'.$courseid.'&', false, false, false, false, $nexttimestamp); - - $content .= html_writer::start_tag('div', array('class'=>'calendar-controls')); - $content .= $prevlink; - $content .= ' | '.userdate($time, get_string('strftimedaydate')).''; - $content .= ' | '. $nextlink; - $content .= ""; - $content .= html_writer::end_tag('div')."\n"; - - break; - } - return $content; -} - -/** - * Formats a filter control element. - * - * @param moodle_url $url of the filter - * @param int $type constant defining the type filter - * @return string html content of the element - */ -function calendar_filter_controls_element(moodle_url $url, $type) { - global $OUTPUT; - switch ($type) { - case CALENDAR_EVENT_GLOBAL: - $typeforhumans = 'global'; - $class = 'calendar_event_global'; - break; - case CALENDAR_EVENT_COURSE: - $typeforhumans = 'course'; - $class = 'calendar_event_course'; - break; - case CALENDAR_EVENT_GROUP: - $typeforhumans = 'groups'; - $class = 'calendar_event_group'; - break; - case CALENDAR_EVENT_USER: - $typeforhumans = 'user'; - $class = 'calendar_event_user'; - break; - } - if (calendar_show_event_type($type)) { - $icon = $OUTPUT->pix_icon('t/hide', get_string('hide')); - $str = get_string('hide'.$typeforhumans.'events', 'calendar'); - } else { - $icon = $OUTPUT->pix_icon('t/show', get_string('show')); - $str = get_string('show'.$typeforhumans.'events', 'calendar'); - } - $content = html_writer::start_tag('li', array('class' => 'calendar_event')); - $content .= html_writer::start_tag('a', array('href' => $url, 'rel' => 'nofollow')); - $content .= html_writer::tag('span', $icon, array('class' => $class)); - $content .= html_writer::tag('span', $str, array('class' => 'eventname')); - $content .= html_writer::end_tag('a'); - $content .= html_writer::end_tag('li'); - return $content; -} - -/** - * Get the controls filter for calendar. - * - * Filter is used to hide calendar info from the display page - * - * @param moodle_url $returnurl return-url for filter controls - * @return string $content return filter controls in html - */ -function calendar_filter_controls(moodle_url $returnurl) { - global $CFG, $USER, $OUTPUT; - - $groupevents = true; - $id = optional_param( 'id',0,PARAM_INT ); - $seturl = new moodle_url('/calendar/set.php', array('return' => base64_encode($returnurl->out_as_local_url(false)), 'sesskey'=>sesskey())); - $content = html_writer::start_tag('ul'); - - $seturl->param('var', 'showglobal'); - $content .= calendar_filter_controls_element($seturl, CALENDAR_EVENT_GLOBAL); - - $seturl->param('var', 'showcourses'); - $content .= calendar_filter_controls_element($seturl, CALENDAR_EVENT_COURSE); - - if (isloggedin() && !isguestuser()) { - if ($groupevents) { - // This course MIGHT have group events defined, so show the filter - $seturl->param('var', 'showgroups'); - $content .= calendar_filter_controls_element($seturl, CALENDAR_EVENT_GROUP); - } else { - // This course CANNOT have group events, so lose the filter - } - $seturl->param('var', 'showuser'); - $content .= calendar_filter_controls_element($seturl, CALENDAR_EVENT_USER); - } - $content .= html_writer::end_tag('ul'); - - return $content; -} - -/** - * Return the representation day - * - * @param int $tstamp Timestamp in GMT - * @param int $now current Unix timestamp - * @param bool $usecommonwords - * @return string the formatted date/time - */ -function calendar_day_representation($tstamp, $now = false, $usecommonwords = true) { - - static $shortformat; - if(empty($shortformat)) { - $shortformat = get_string('strftimedayshort'); - } - - if($now === false) { - $now = time(); - } - - // To have it in one place, if a change is needed - $formal = userdate($tstamp, $shortformat); - - $datestamp = usergetdate($tstamp); - $datenow = usergetdate($now); - - if($usecommonwords == false) { - // We don't want words, just a date - return $formal; - } - else if($datestamp['year'] == $datenow['year'] && $datestamp['yday'] == $datenow['yday']) { - // Today - return get_string('today', 'calendar'); - } - else if( - ($datestamp['year'] == $datenow['year'] && $datestamp['yday'] == $datenow['yday'] - 1 ) || - ($datestamp['year'] == $datenow['year'] - 1 && $datestamp['mday'] == 31 && $datestamp['mon'] == 12 && $datenow['yday'] == 1) - ) { - // Yesterday - return get_string('yesterday', 'calendar'); - } - else if( - ($datestamp['year'] == $datenow['year'] && $datestamp['yday'] == $datenow['yday'] + 1 ) || - ($datestamp['year'] == $datenow['year'] + 1 && $datenow['mday'] == 31 && $datenow['mon'] == 12 && $datestamp['yday'] == 1) - ) { - // Tomorrow - return get_string('tomorrow', 'calendar'); - } - else { - return $formal; - } -} - -/** - * return the formatted representation time - * - * @param int $time the timestamp in UTC, as obtained from the database - * @return string the formatted date/time - */ -function calendar_time_representation($time) { - static $langtimeformat = NULL; - if($langtimeformat === NULL) { - $langtimeformat = get_string('strftimetime'); - } - $timeformat = get_user_preferences('calendar_timeformat'); - if(empty($timeformat)){ - $timeformat = get_config(NULL,'calendar_site_timeformat'); - } - // The ? is needed because the preference might be present, but empty - return userdate($time, empty($timeformat) ? $langtimeformat : $timeformat); -} - -/** - * Adds day, month, year arguments to a URL and returns a moodle_url object. - * - * @param string|moodle_url $linkbase - * @param int $d The number of the day. - * @param int $m The number of the month. - * @param int $y The number of the year. - * @param int $time the unixtime, used for multiple calendar support. The values $d, - * $m and $y are kept for backwards compatibility. - * @return moodle_url|null $linkbase - */ -function calendar_get_link_href($linkbase, $d, $m, $y, $time = 0) { - if (empty($linkbase)) { - return ''; - } - if (!($linkbase instanceof moodle_url)) { - $linkbase = new moodle_url($linkbase); - } - - // If a day, month and year were passed then convert it to a timestamp. If these were passed - // then we can assume the day, month and year are passed as Gregorian, as no where in core - // should we be passing these values rather than the time. - if (!empty($d) && !empty($m) && !empty($y)) { - if (checkdate($m, $d, $y)) { - $time = make_timestamp($y, $m, $d); - } else { - $time = time(); - } - } else if (empty($time)) { - $time = time(); - } - - $linkbase->param('time', $time); - - return $linkbase; -} - -/** - * Build and return a previous month HTML link, with an arrow. - * - * @param string $text The text label. - * @param string|moodle_url $linkbase The URL stub. - * @param int $d The number of the date. - * @param int $m The number of the month. - * @param int $y year The number of the year. - * @param bool $accesshide Default visible, or hide from all except screenreaders. - * @param int $time the unixtime, used for multiple calendar support. The values $d, - * $m and $y are kept for backwards compatibility. - * @return string HTML string. - */ -function calendar_get_link_previous($text, $linkbase, $d, $m, $y, $accesshide = false, $time = 0) { - $href = calendar_get_link_href(new moodle_url($linkbase), $d, $m, $y, $time); - if (empty($href)) { - return $text; - } - return link_arrow_left($text, (string)$href, $accesshide, 'previous'); -} - -/** - * Build and return a next month HTML link, with an arrow. - * - * @param string $text The text label. - * @param string|moodle_url $linkbase The URL stub. - * @param int $d the number of the Day - * @param int $m The number of the month. - * @param int $y The number of the year. - * @param bool $accesshide Default visible, or hide from all except screenreaders. - * @param int $time the unixtime, used for multiple calendar support. The values $d, - * $m and $y are kept for backwards compatibility. - * @return string HTML string. - */ -function calendar_get_link_next($text, $linkbase, $d, $m, $y, $accesshide = false, $time = 0) { - $href = calendar_get_link_href(new moodle_url($linkbase), $d, $m, $y, $time); - if (empty($href)) { - return $text; - } - return link_arrow_right($text, (string)$href, $accesshide, 'next'); -} - -/** - * Return the name of the weekday - * - * @param string $englishname - * @return string of the weekeday - */ -function calendar_wday_name($englishname) { - return get_string(strtolower($englishname), 'calendar'); -} - -/** - * Return the number of days in month - * - * @param int $month the number of the month. - * @param int $year the number of the year - * @return int - */ -function calendar_days_in_month($month, $year) { - $calendartype = \core_calendar\type_factory::get_calendar_instance(); - return $calendartype->get_num_days_in_month($year, $month); -} - -/** - * Get the upcoming event block - * - * @param array $events list of events - * @param moodle_url|string $linkhref link to event referer - * @param boolean $showcourselink whether links to courses should be shown - * @return string|null $content html block content - */ -function calendar_get_block_upcoming($events, $linkhref = NULL, $showcourselink = false) { - $content = ''; - $lines = count($events); - if (!$lines) { - return $content; - } - - for ($i = 0; $i < $lines; ++$i) { - if (!isset($events[$i]->time)) { // Just for robustness - continue; - } - $events[$i] = calendar_add_event_metadata($events[$i]); - $content .= '
    '.$events[$i]->icon.''; - if (!empty($events[$i]->referer)) { - // That's an activity event, so let's provide the hyperlink - $content .= $events[$i]->referer; - } else { - if(!empty($linkhref)) { - $href = calendar_get_link_href(new moodle_url(CALENDAR_URL . $linkhref), 0, 0, 0, $events[$i]->timestart); - $href->set_anchor('event_'.$events[$i]->id); - $content .= html_writer::link($href, $events[$i]->name); - } - else { - $content .= $events[$i]->name; - } - } - $events[$i]->time = str_replace('»', '
    »', $events[$i]->time); - if ($showcourselink && !empty($events[$i]->courselink)) { - $content .= html_writer::div($events[$i]->courselink, 'course'); - } - $content .= '
    '.$events[$i]->time.'
    '; - if ($i < $lines - 1) $content .= '
    '; - } - - return $content; -} - -/** - * Get the next following month - * - * @param int $month the number of the month. - * @param int $year the number of the year. - * @return array the following month - */ -function calendar_add_month($month, $year) { - // Get the calendar type we are using. - $calendartype = \core_calendar\type_factory::get_calendar_instance(); - return $calendartype->get_next_month($year, $month); -} - -/** - * Get the previous month. - * - * @param int $month the number of the month. - * @param int $year the number of the year. - * @return array previous month - */ -function calendar_sub_month($month, $year) { - // Get the calendar type we are using. - $calendartype = \core_calendar\type_factory::get_calendar_instance(); - return $calendartype->get_prev_month($year, $month); -} - -/** - * Get per-day basis events - * - * @param array $events list of events - * @param int $month the number of the month - * @param int $year the number of the year - * @param array $eventsbyday event on specific day - * @param array $durationbyday duration of the event in days - * @param array $typesbyday event type (eg: global, course, user, or group) - * @param array $courses list of courses - * @return void - */ -function calendar_events_by_day($events, $month, $year, &$eventsbyday, &$durationbyday, &$typesbyday, &$courses) { - // Get the calendar type we are using. - $calendartype = \core_calendar\type_factory::get_calendar_instance(); - - $eventsbyday = array(); - $typesbyday = array(); - $durationbyday = array(); - - if($events === false) { - return; - } - - foreach ($events as $event) { - $startdate = $calendartype->timestamp_to_date_array($event->timestart); - // Set end date = start date if no duration - if ($event->timeduration) { - $enddate = $calendartype->timestamp_to_date_array($event->timestart + $event->timeduration - 1); - } else { - $enddate = $startdate; - } - - // Simple arithmetic: $year * 13 + $month is a distinct integer for each distinct ($year, $month) pair - if(!($startdate['year'] * 13 + $startdate['mon'] <= $year * 13 + $month) && ($enddate['year'] * 13 + $enddate['mon'] >= $year * 13 + $month)) { - // Out of bounds - continue; - } - - $eventdaystart = intval($startdate['mday']); - - if($startdate['mon'] == $month && $startdate['year'] == $year) { - // Give the event to its day - $eventsbyday[$eventdaystart][] = $event->id; - - // Mark the day as having such an event - if($event->courseid == SITEID && $event->groupid == 0) { - $typesbyday[$eventdaystart]['startglobal'] = true; - // Set event class for global event - $events[$event->id]->class = 'calendar_event_global'; - } - else if($event->courseid != 0 && $event->courseid != SITEID && $event->groupid == 0) { - $typesbyday[$eventdaystart]['startcourse'] = true; - // Set event class for course event - $events[$event->id]->class = 'calendar_event_course'; - } - else if($event->groupid) { - $typesbyday[$eventdaystart]['startgroup'] = true; - // Set event class for group event - $events[$event->id]->class = 'calendar_event_group'; - } - else if($event->userid) { - $typesbyday[$eventdaystart]['startuser'] = true; - // Set event class for user event - $events[$event->id]->class = 'calendar_event_user'; - } - } - - if($event->timeduration == 0) { - // Proceed with the next - continue; - } - - // The event starts on $month $year or before. So... - $lowerbound = $startdate['mon'] == $month && $startdate['year'] == $year ? intval($startdate['mday']) : 0; - - // Also, it ends on $month $year or later... - $upperbound = $enddate['mon'] == $month && $enddate['year'] == $year ? intval($enddate['mday']) : calendar_days_in_month($month, $year); - - // Mark all days between $lowerbound and $upperbound (inclusive) as duration - for($i = $lowerbound + 1; $i <= $upperbound; ++$i) { - $durationbyday[$i][] = $event->id; - if($event->courseid == SITEID && $event->groupid == 0) { - $typesbyday[$i]['durationglobal'] = true; - } - else if($event->courseid != 0 && $event->courseid != SITEID && $event->groupid == 0) { - $typesbyday[$i]['durationcourse'] = true; - } - else if($event->groupid) { - $typesbyday[$i]['durationgroup'] = true; - } - else if($event->userid) { - $typesbyday[$i]['durationuser'] = true; - } - } - - } - return; -} - -/** - * Get current module cache - * - * @param array $coursecache list of course cache - * @param string $modulename name of the module - * @param int $instance module instance number - * @return stdClass|bool $module information - */ -function calendar_get_module_cached(&$coursecache, $modulename, $instance) { - $module = get_coursemodule_from_instance($modulename, $instance); - - if($module === false) return false; - if(!calendar_get_course_cached($coursecache, $module->course)) { - return false; - } - return $module; -} - -/** - * Get current course cache - * - * @param array $coursecache list of course cache - * @param int $courseid id of the course - * @return stdClass $coursecache[$courseid] return the specific course cache - */ -function calendar_get_course_cached(&$coursecache, $courseid) { - if (!isset($coursecache[$courseid])) { - $coursecache[$courseid] = get_course($courseid); - } - return $coursecache[$courseid]; -} - -/** - * Get group from groupid for calendar display - * - * @param int $groupid - * @return stdClass group object with fields 'id', 'name' and 'courseid' - */ -function calendar_get_group_cached($groupid) { - static $groupscache = array(); - if (!isset($groupscache[$groupid])) { - $groupscache[$groupid] = groups_get_group($groupid, 'id,name,courseid'); - } - return $groupscache[$groupid]; -} - -/** - * Returns the courses to load events for, the - * - * @param array $courseeventsfrom An array of courses to load calendar events for - * @param bool $ignorefilters specify the use of filters, false is set as default - * @return array An array of courses, groups, and user to load calendar events for based upon filters - */ -function calendar_set_filters(array $courseeventsfrom, $ignorefilters = false) { - global $USER, $CFG, $DB; - - // For backwards compatability we have to check whether the courses array contains - // just id's in which case we need to load course objects. - $coursestoload = array(); - foreach ($courseeventsfrom as $id => $something) { - if (!is_object($something)) { - $coursestoload[] = $id; - unset($courseeventsfrom[$id]); - } - } - if (!empty($coursestoload)) { - // TODO remove this in 2.2 - debugging('calendar_set_filters now preferes an array of course objects with preloaded contexts', DEBUG_DEVELOPER); - $courseeventsfrom = array_merge($courseeventsfrom, $DB->get_records_list('course', 'id', $coursestoload)); - } - - $courses = array(); - $user = false; - $group = false; - - // capabilities that allow seeing group events from all groups - // TODO: rewrite so that moodle/calendar:manageentries is not necessary here - $allgroupscaps = array('moodle/site:accessallgroups', 'moodle/calendar:manageentries'); - - $isloggedin = isloggedin(); - - if ($ignorefilters || calendar_show_event_type(CALENDAR_EVENT_COURSE)) { - $courses = array_keys($courseeventsfrom); - } - if ($ignorefilters || calendar_show_event_type(CALENDAR_EVENT_GLOBAL)) { - $courses[] = SITEID; - } - $courses = array_unique($courses); - sort($courses); - - if (!empty($courses) && in_array(SITEID, $courses)) { - // Sort courses for consistent colour highlighting - // Effectively ignoring SITEID as setting as last course id - $key = array_search(SITEID, $courses); - unset($courses[$key]); - $courses[] = SITEID; - } - - if ($ignorefilters || ($isloggedin && calendar_show_event_type(CALENDAR_EVENT_USER))) { - $user = $USER->id; - } - - if (!empty($courseeventsfrom) && (calendar_show_event_type(CALENDAR_EVENT_GROUP) || $ignorefilters)) { - - if (count($courseeventsfrom)==1) { - $course = reset($courseeventsfrom); - if (has_any_capability($allgroupscaps, context_course::instance($course->id))) { - $coursegroups = groups_get_all_groups($course->id, 0, 0, 'g.id'); - $group = array_keys($coursegroups); - } - } - if ($group === false) { - if (!empty($CFG->calendar_adminseesall) && has_any_capability($allgroupscaps, context_system::instance())) { - $group = true; - } else if ($isloggedin) { - $groupids = array(); - - // We already have the courses to examine in $courses - // For each course... - foreach ($courseeventsfrom as $courseid => $course) { - // If the user is an editing teacher in there, - if (!empty($USER->groupmember[$course->id])) { - // We've already cached the users groups for this course so we can just use that - $groupids = array_merge($groupids, $USER->groupmember[$course->id]); - } else if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) { - // If this course has groups, show events from all of those related to the current user - $coursegroups = groups_get_user_groups($course->id, $USER->id); - $groupids = array_merge($groupids, $coursegroups['0']); - } - } - if (!empty($groupids)) { - $group = $groupids; - } - } - } - } - if (empty($courses)) { - $courses = false; - } - - return array($courses, $group, $user); -} - -/** - * Return the capability for editing calendar event - * - * @param calendar_event $event event object - * @return bool capability to edit event - */ -function calendar_edit_event_allowed($event) { - global $USER, $DB; - - // Must be logged in - if (!isloggedin()) { - return false; - } - - // can not be using guest account - if (isguestuser()) { - return false; - } - - // You cannot edit URL based calendar subscription events presently. - if (isset($event->subscriptionid)) { - if (!empty($event->subscription->url)) { - // This event can be updated externally, so it cannot be edited. - return false; - } - } - - $sitecontext = context_system::instance(); - // if user has manageentries at site level, return true - if (has_capability('moodle/calendar:manageentries', $sitecontext)) { - return true; - } - - // if groupid is set, it's definitely a group event - if (!empty($event->groupid)) { - // Allow users to add/edit group events if: - // 1) They have manageentries (= entries for whole course) - // 2) They have managegroupentries AND are in the group - $group = $DB->get_record('groups', array('id'=>$event->groupid)); - return $group && ( - has_capability('moodle/calendar:manageentries', $event->context) || - (has_capability('moodle/calendar:managegroupentries', $event->context) - && groups_is_member($event->groupid))); - } else if (!empty($event->courseid)) { - // if groupid is not set, but course is set, - // it's definiely a course event - return has_capability('moodle/calendar:manageentries', $event->context); - } else if (!empty($event->userid) && $event->userid == $USER->id) { - // if course is not set, but userid id set, it's a user event - return (has_capability('moodle/calendar:manageownentries', $event->context)); - } else if (!empty($event->userid)) { - return (has_capability('moodle/calendar:manageentries', $event->context)); - } - return false; -} - -/** - * Returns the default courses to display on the calendar when there isn't a specific - * course to display. - * - * @return array $courses Array of courses to display - */ -function calendar_get_default_courses() { - global $CFG, $DB; - - if (!isloggedin()) { - return array(); - } - - $courses = array(); - if (!empty($CFG->calendar_adminseesall) && has_capability('moodle/calendar:manageentries', context_system::instance())) { - $select = ', ' . context_helper::get_preload_record_columns_sql('ctx'); - $join = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)"; - $sql = "SELECT c.* $select - FROM {course} c - $join - WHERE EXISTS (SELECT 1 FROM {event} e WHERE e.courseid = c.id) - "; - $courses = $DB->get_records_sql($sql, array('contextlevel' => CONTEXT_COURSE), 0, 20); - foreach ($courses as $course) { - context_helper::preload_from_record($course); - } - return $courses; - } - - $courses = enrol_get_my_courses(); - - return $courses; -} - -/** - * Display calendar preference button - * - * @param stdClass $course course object - * @deprecated since Moodle 3.2 - * @todo MDL-55875 This will be deleted in Moodle 3.6. - * @return string return preference button in html - */ -function calendar_preferences_button(stdClass $course) { - global $OUTPUT; - - // Guests have no preferences - if (!isloggedin() || isguestuser()) { - return ''; - } - debugging('This should no longer be used, the calendar preferences are now linked to the user preferences page'); - - return $OUTPUT->single_button(new moodle_url('/user/calendar.php'), get_string("preferences", "calendar")); -} - -/** - * Get event format time - * - * @param calendar_event $event event object - * @param int $now current time in gmt - * @param array $linkparams list of params for event link - * @param bool $usecommonwords the words as formatted date/time. - * @param int $showtime determine the show time GMT timestamp - * @return string $eventtime link/string for event time - */ -function calendar_format_event_time($event, $now, $linkparams = null, $usecommonwords = true, $showtime = 0) { - $starttime = $event->timestart; - $endtime = $event->timestart + $event->timeduration; - - if (empty($linkparams) || !is_array($linkparams)) { - $linkparams = array(); - } - - $linkparams['view'] = 'day'; - - // OK, now to get a meaningful display... - // Check if there is a duration for this event. - if ($event->timeduration) { - // Get the midnight of the day the event will start. - $usermidnightstart = usergetmidnight($starttime); - // Get the midnight of the day the event will end. - $usermidnightend = usergetmidnight($endtime); - // Check if we will still be on the same day. - if ($usermidnightstart == $usermidnightend) { - // Check if we are running all day. - if ($event->timeduration == DAYSECS) { - $time = get_string('allday', 'calendar'); - } else { // Specify the time we will be running this from. - $datestart = calendar_time_representation($starttime); - $dateend = calendar_time_representation($endtime); - $time = $datestart . ' » ' . $dateend; - } - - // Set printable representation. - if (!$showtime) { - $day = calendar_day_representation($event->timestart, $now, $usecommonwords); - $url = calendar_get_link_href(new moodle_url(CALENDAR_URL . 'view.php', $linkparams), 0, 0, 0, $endtime); - $eventtime = html_writer::link($url, $day) . ', ' . $time; - } else { - $eventtime = $time; - } - } else { // It must spans two or more days. - $daystart = calendar_day_representation($event->timestart, $now, $usecommonwords) . ', '; - if ($showtime == $usermidnightstart) { - $daystart = ''; - } - $timestart = calendar_time_representation($event->timestart); - $dayend = calendar_day_representation($event->timestart + $event->timeduration, $now, $usecommonwords) . ', '; - if ($showtime == $usermidnightend) { - $dayend = ''; - } - $timeend = calendar_time_representation($event->timestart + $event->timeduration); - - // Set printable representation. - if ($now >= $usermidnightstart && $now < strtotime('+1 day', $usermidnightstart)) { - $url = calendar_get_link_href(new moodle_url(CALENDAR_URL . 'view.php', $linkparams), 0, 0, 0, $endtime); - $eventtime = $timestart . ' » ' . html_writer::link($url, $dayend) . $timeend; - } else { - // The event is in the future, print start and end links. - $url = calendar_get_link_href(new moodle_url(CALENDAR_URL . 'view.php', $linkparams), 0, 0, 0, $starttime); - $eventtime = html_writer::link($url, $daystart) . $timestart . ' » '; - - $url = calendar_get_link_href(new moodle_url(CALENDAR_URL . 'view.php', $linkparams), 0, 0, 0, $endtime); - $eventtime .= html_writer::link($url, $dayend) . $timeend; - } - } - } else { // There is no time duration. - $time = calendar_time_representation($event->timestart); - // Set printable representation. - if (!$showtime) { - $day = calendar_day_representation($event->timestart, $now, $usecommonwords); - $url = calendar_get_link_href(new moodle_url(CALENDAR_URL . 'view.php', $linkparams), 0, 0, 0, $starttime); - $eventtime = html_writer::link($url, $day) . ', ' . trim($time); - } else { - $eventtime = $time; - } - } - - // Check if It has expired. - if ($event->timestart + $event->timeduration < $now) { - $eventtime = '' . str_replace(' href=', ' class="dimmed" href=', $eventtime) . ''; - } - - return $eventtime; -} - -/** - * Display month selector options - * - * @param string $name for the select element - * @param string|array $selected options for select elements - */ -function calendar_print_month_selector($name, $selected) { - $months = array(); - for ($i=1; $i<=12; $i++) { - $months[$i] = userdate(gmmktime(12, 0, 0, $i, 15, 2000), '%B'); - } - echo html_writer::label(get_string('months'), 'menu'. $name, false, array('class' => 'accesshide')); - echo html_writer::select($months, $name, $selected, false); -} - -/** - * Checks to see if the requested type of event should be shown for the given user. - * - * @param CALENDAR_EVENT_GLOBAL|CALENDAR_EVENT_COURSE|CALENDAR_EVENT_GROUP|CALENDAR_EVENT_USER $type - * The type to check the display for (default is to display all) - * @param stdClass|int|null $user The user to check for - by default the current user - * @return bool True if the tyep should be displayed false otherwise - */ -function calendar_show_event_type($type, $user = null) { - $default = CALENDAR_EVENT_GLOBAL + CALENDAR_EVENT_COURSE + CALENDAR_EVENT_GROUP + CALENDAR_EVENT_USER; - if (get_user_preferences('calendar_persistflt', 0, $user) === 0) { - global $SESSION; - if (!isset($SESSION->calendarshoweventtype)) { - $SESSION->calendarshoweventtype = $default; - } - return $SESSION->calendarshoweventtype & $type; - } else { - return get_user_preferences('calendar_savedflt', $default, $user) & $type; - } -} - -/** - * Sets the display of the event type given $display. - * - * If $display = true the event type will be shown. - * If $display = false the event type will NOT be shown. - * If $display = null the current value will be toggled and saved. - * - * @param CALENDAR_EVENT_GLOBAL|CALENDAR_EVENT_COURSE|CALENDAR_EVENT_GROUP|CALENDAR_EVENT_USER $type object of CALENDAR_EVENT_XXX - * @param bool $display option to display event type - * @param stdClass|int $user moodle user object or id, null means current user - */ -function calendar_set_event_type_display($type, $display = null, $user = null) { - $persist = get_user_preferences('calendar_persistflt', 0, $user); - $default = CALENDAR_EVENT_GLOBAL + CALENDAR_EVENT_COURSE + CALENDAR_EVENT_GROUP + CALENDAR_EVENT_USER; - if ($persist === 0) { - global $SESSION; - if (!isset($SESSION->calendarshoweventtype)) { - $SESSION->calendarshoweventtype = $default; - } - $preference = $SESSION->calendarshoweventtype; - } else { - $preference = get_user_preferences('calendar_savedflt', $default, $user); - } - $current = $preference & $type; - if ($display === null) { - $display = !$current; - } - if ($display && !$current) { - $preference += $type; - } else if (!$display && $current) { - $preference -= $type; - } - if ($persist === 0) { - $SESSION->calendarshoweventtype = $preference; - } else { - if ($preference == $default) { - unset_user_preference('calendar_savedflt', $user); - } else { - set_user_preference('calendar_savedflt', $preference, $user); - } - } -} - -/** - * Get calendar's allowed types - * - * @param stdClass $allowed list of allowed edit for event type - * @param stdClass|int $course object of a course or course id - */ -function calendar_get_allowed_types(&$allowed, $course = null) { - global $USER, $CFG, $DB; - $allowed = new stdClass(); - $allowed->user = has_capability('moodle/calendar:manageownentries', context_system::instance()); - $allowed->groups = false; // This may change just below - $allowed->courses = false; // This may change just below - $allowed->site = has_capability('moodle/calendar:manageentries', context_course::instance(SITEID)); - - if (!empty($course)) { - if (!is_object($course)) { - $course = $DB->get_record('course', array('id' => $course), '*', MUST_EXIST); - } - if ($course->id != SITEID) { - $coursecontext = context_course::instance($course->id); - $allowed->user = has_capability('moodle/calendar:manageownentries', $coursecontext); - - if (has_capability('moodle/calendar:manageentries', $coursecontext)) { - $allowed->courses = array($course->id => 1); - - if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) { - if (has_capability('moodle/site:accessallgroups', $coursecontext)) { - $allowed->groups = groups_get_all_groups($course->id); - } else { - $allowed->groups = groups_get_all_groups($course->id, $USER->id); - } - } - } else if (has_capability('moodle/calendar:managegroupentries', $coursecontext)) { - if($course->groupmode != NOGROUPS || !$course->groupmodeforce) { - if (has_capability('moodle/site:accessallgroups', $coursecontext)) { - $allowed->groups = groups_get_all_groups($course->id); - } else { - $allowed->groups = groups_get_all_groups($course->id, $USER->id); - } - } - } - } - } -} - -/** - * See if user can add calendar entries at all - * used to print the "New Event" button - * - * @param stdClass $course object of a course or course id - * @return bool has the capability to add at least one event type - */ -function calendar_user_can_add_event($course) { - if (!isloggedin() || isguestuser()) { - return false; - } - calendar_get_allowed_types($allowed, $course); - return (bool)($allowed->user || $allowed->groups || $allowed->courses || $allowed->site); -} - -/** - * Check wether the current user is permitted to add events - * - * @param stdClass $event object of event - * @return bool has the capability to add event - */ -function calendar_add_event_allowed($event) { - global $USER, $DB; - - // can not be using guest account - if (!isloggedin() or isguestuser()) { - return false; - } - - $sitecontext = context_system::instance(); - // if user has manageentries at site level, always return true - if (has_capability('moodle/calendar:manageentries', $sitecontext)) { - return true; - } - - switch ($event->eventtype) { - case 'course': - return has_capability('moodle/calendar:manageentries', $event->context); - - case 'group': - // Allow users to add/edit group events if: - // 1) They have manageentries (= entries for whole course) - // 2) They have managegroupentries AND are in the group - $group = $DB->get_record('groups', array('id'=>$event->groupid)); - return $group && ( - has_capability('moodle/calendar:manageentries', $event->context) || - (has_capability('moodle/calendar:managegroupentries', $event->context) - && groups_is_member($event->groupid))); - - case 'user': - if ($event->userid == $USER->id) { - return (has_capability('moodle/calendar:manageownentries', $event->context)); - } - //there is no 'break;' intentionally - - case 'site': - return has_capability('moodle/calendar:manageentries', $event->context); - - default: - return has_capability('moodle/calendar:manageentries', $event->context); - } -} - -/** - * Manage calendar events + * Manage calendar events. * * This class provides the required functionality in order to manage calendar events. * It was introduced as part of Moodle 2.0 and was created in order to provide a * better framework for dealing with calendar events in particular regard to file - * handling through the new file API + * handling through the new file API. * * @package core_calendar * @category calendar @@ -2075,34 +172,33 @@ class calendar_event { /** @var array An object containing the event properties can be accessed via the magic __get/set methods */ protected $properties = null; - /** - * @var string The converted event discription with file paths resolved. This gets populated when someone requests description for the first time */ + /** @var string The converted event discription with file paths resolved. + * This gets populated when someone requests description for the first time */ protected $_description = null; /** @var array The options to use with this description editor */ protected $editoroptions = array( - 'subdirs'=>false, - 'forcehttps'=>false, - 'maxfiles'=>-1, - 'maxbytes'=>null, - 'trusttext'=>false); + 'subdirs' => false, + 'forcehttps' => false, + 'maxfiles' => -1, + 'maxbytes' => null, + 'trusttext' => false); /** @var object The context to use with the description editor */ protected $editorcontext = null; /** - * Instantiates a new event and optionally populates its properties with the - * data provided + * Instantiates a new event and optionally populates its properties with the data provided. * - * @param stdClass $data Optional. An object containing the properties to for + * @param \stdClass $data Optional. An object containing the properties to for * an event */ - public function __construct($data=null) { + public function __construct($data = null) { global $CFG, $USER; - // First convert to object if it is not already (should either be object or assoc array) + // First convert to object if it is not already (should either be object or assoc array). if (!is_object($data)) { - $data = (object)$data; + $data = (object) $data; } $this->editoroptions['maxbytes'] = $CFG->maxbytes; @@ -2117,19 +213,22 @@ class calendar_event { $data->subscription = calendar_get_subscription($data->subscriptionid); } - // Default to a user event + // Default to a user event. if (empty($data->eventtype)) { $data->eventtype = 'user'; } - // Default to the current user + // Default to the current user. if (empty($data->userid)) { $data->userid = $USER->id; } if (!empty($data->timeduration) && is_array($data->timeduration)) { - $data->timeduration = make_timestamp($data->timeduration['year'], $data->timeduration['month'], $data->timeduration['day'], $data->timeduration['hour'], $data->timeduration['minute']) - $data->timestart; + $data->timeduration = make_timestamp( + $data->timeduration['year'], $data->timeduration['month'], $data->timeduration['day'], + $data->timeduration['hour'], $data->timeduration['minute']) - $data->timestart; } + if (!empty($data->description) && is_array($data->description)) { $data->format = $data->description['format']; $data->description = $data->description['text']; @@ -2137,22 +236,24 @@ class calendar_event { $data->description = ''; $data->format = editors_get_preferred_format(); } - // Ensure form is defaulted correctly + + // Ensure form is defaulted correctly. if (empty($data->format)) { $data->format = editors_get_preferred_format(); } - if (empty($data->context)) { - $data->context = $this->calculate_context($data); - } $this->properties = $data; + + if (empty($data->context)) { + $this->properties->context = $this->calculate_context(); + } } /** - * Magic property method + * Magic set method. * * Attempts to call a set_$key method if one exists otherwise falls back - * to simply set the property + * to simply set the property. * * @param string $key property name * @param mixed $value value of the property @@ -2165,27 +266,30 @@ class calendar_event { } /** - * Magic get method + * Magic get method. * * Attempts to call a get_$key method to return the property and ralls over - * to return the raw property + * to return the raw property. * * @param string $key property name * @return mixed property value + * @throws \coding_exception */ public function __get($key) { if (method_exists($this, 'get_'.$key)) { return $this->{'get_'.$key}(); } - if (!isset($this->properties->{$key})) { - throw new coding_exception('Undefined property requested'); + if (!property_exists($this->properties, $key)) { + throw new \coding_exception('Undefined property requested'); } return $this->properties->{$key}; } /** - * Stupid PHP needs an isset magic method if you use the get magic method and - * still want empty calls to work.... blah ~! + * Magic isset method. + * + * PHP needs an isset magic method if you use the get magic method and + * still want empty calls to work. * * @param string $key $key property name * @return bool|mixed property value, false if property is not exist @@ -2195,42 +299,44 @@ class calendar_event { } /** - * Calculate the context value needed for calendar_event. + * Calculate the context value needed for an event. + * * Event's type can be determine by the available value store in $data * It is important to check for the existence of course/courseid to determine * the course event. * Default value is set to CONTEXT_USER * - * @param stdClass $data information about event - * @return stdClass The context object. + * @return \stdClass The context object. */ - protected function calculate_context(stdClass $data) { + protected function calculate_context() { global $USER, $DB; $context = null; - if (isset($data->courseid) && $data->courseid > 0) { - $context = context_course::instance($data->courseid); - } else if (isset($data->course) && $data->course > 0) { - $context = context_course::instance($data->course); - } else if (isset($data->groupid) && $data->groupid > 0) { - $group = $DB->get_record('groups', array('id'=>$data->groupid)); - $context = context_course::instance($group->courseid); - } else if (isset($data->userid) && $data->userid > 0 && $data->userid == $USER->id) { - $context = context_user::instance($data->userid); - } else if (isset($data->userid) && $data->userid > 0 && $data->userid != $USER->id && - isset($data->instance) && $data->instance > 0) { - $cm = get_coursemodule_from_instance($data->modulename, $data->instance, 0, false, MUST_EXIST); - $context = context_course::instance($cm->course); + if (isset($this->properties->courseid) && $this->properties->courseid > 0) { + $context = \context_course::instance($this->properties->courseid); + } else if (isset($this->properties->course) && $this->properties->course > 0) { + $context = \context_course::instance($this->properties->course); + } else if (isset($this->properties->groupid) && $this->properties->groupid > 0) { + $group = $DB->get_record('groups', array('id' => $this->properties->groupid)); + $context = \context_course::instance($group->courseid); + } else if (isset($this->properties->userid) && $this->properties->userid > 0 + && $this->properties->userid == $USER->id) { + $context = \context_user::instance($this->properties->userid); + } else if (isset($this->properties->userid) && $this->properties->userid > 0 + && $this->properties->userid != $USER->id && + isset($this->properties->instance) && $this->properties->instance > 0) { + $cm = get_coursemodule_from_instance($this->properties->modulename, $this->properties->instance, 0, + false, MUST_EXIST); + $context = \context_course::instance($cm->course); } else { - $context = context_user::instance($data->userid); + $context = \context_user::instance($this->properties->userid); } return $context; } /** - * Returns an array of editoroptions for this event: Called by __get - * Please use $blah = $event->editoroptions; + * Returns an array of editoroptions for this event. * * @return array event editor options */ @@ -2250,44 +356,46 @@ class calendar_event { require_once($CFG->libdir . '/filelib.php'); if ($this->_description === null) { - // Check if we have already resolved the context for this event + // Check if we have already resolved the context for this event. if ($this->editorcontext === null) { - // Switch on the event type to decide upon the appropriate context - // to use for this event + // Switch on the event type to decide upon the appropriate context to use for this event. $this->editorcontext = $this->properties->context; if ($this->properties->eventtype != 'user' && $this->properties->eventtype != 'course' - && $this->properties->eventtype != 'site' && $this->properties->eventtype != 'group') { + && $this->properties->eventtype != 'site' && $this->properties->eventtype != 'group') { return clean_text($this->properties->description, $this->properties->format); } } - // Work out the item id for the editor, if this is a repeated event then the files will - // be associated with the original + // Work out the item id for the editor, if this is a repeated event + // then the files will be associated with the original. if (!empty($this->properties->repeatid) && $this->properties->repeatid > 0) { $itemid = $this->properties->repeatid; } else { $itemid = $this->properties->id; } - // Convert file paths in the description so that things display correctly - $this->_description = file_rewrite_pluginfile_urls($this->properties->description, 'pluginfile.php', $this->editorcontext->id, 'calendar', 'event_description', $itemid); - // Clean the text so no nasties get through + // Convert file paths in the description so that things display correctly. + $this->_description = file_rewrite_pluginfile_urls($this->properties->description, 'pluginfile.php', + $this->editorcontext->id, 'calendar', 'event_description', $itemid); + // Clean the text so no nasties get through. $this->_description = clean_text($this->_description, $this->properties->format); } - // Finally return the description + + // Finally return the description. return $this->_description; } /** - * Return the number of repeat events there are in this events series + * Return the number of repeat events there are in this events series. * * @return int number of event repeated */ public function count_repeats() { global $DB; if (!empty($this->properties->repeatid)) { - $this->properties->eventrepeats = $DB->count_records('event', array('repeatid'=>$this->properties->repeatid)); - // We don't want to count ourselves + $this->properties->eventrepeats = $DB->count_records('event', + array('repeatid' => $this->properties->repeatid)); + // We don't want to count ourselves. $this->properties->eventrepeats--; } return $this->properties->eventrepeats; @@ -2302,14 +410,14 @@ class calendar_event { * @see self::create() * @see self::update() * - * @param stdClass $data object of event + * @param \stdClass $data object of event * @param bool $checkcapability if moodle should check calendar managing capability or not * @return bool event updated */ public function update($data, $checkcapability=true) { global $DB, $USER; - foreach ($data as $key=>$value) { + foreach ($data as $key => $value) { $this->properties->$key = $value; } @@ -2357,8 +465,7 @@ class calendar_event { $this->properties->userid = $USER->id; break; default: - // Ewww we should NEVER get here, but just incase we do lets - // fail gracefully + // We should NEVER get here, but just incase we do lets fail gracefully. $usingeditor = false; break; } @@ -2366,7 +473,7 @@ class calendar_event { // If we are actually using the editor, we recalculate the context because some default values // were set when calculate_context() was called from the constructor. if ($usingeditor) { - $this->properties->context = $this->calculate_context($this->properties); + $this->properties->context = $this->calculate_context(); $this->editorcontext = $this->properties->context; } @@ -2375,20 +482,21 @@ class calendar_event { $this->properties->description = $this->properties->description['text']; } - // Insert the event into the database + // Insert the event into the database. $this->properties->id = $DB->insert_record('event', $this->properties); if ($usingeditor) { $this->properties->description = file_save_draft_area_files( - $editor['itemid'], - $this->editorcontext->id, - 'calendar', - 'event_description', - $this->properties->id, - $this->editoroptions, - $editor['text'], - $this->editoroptions['forcehttps']); - $DB->set_field('event', 'description', $this->properties->description, array('id'=>$this->properties->id)); + $editor['itemid'], + $this->editorcontext->id, + 'calendar', + 'event_description', + $this->properties->id, + $this->editoroptions, + $editor['text'], + $this->editoroptions['forcehttps']); + $DB->set_field('event', 'description', $this->properties->description, + array('id' => $this->properties->id)); } // Log the event entry. @@ -2401,28 +509,29 @@ class calendar_event { if (!empty($this->properties->repeat)) { $this->properties->repeatid = $this->properties->id; - $DB->set_field('event', 'repeatid', $this->properties->repeatid, array('id'=>$this->properties->id)); + $DB->set_field('event', 'repeatid', $this->properties->repeatid, array('id' => $this->properties->id)); $eventcopy = clone($this->properties); unset($eventcopy->id); - $timestart = new DateTime('@' . $eventcopy->timestart); - $timestart->setTimezone(core_date::get_user_timezone_object()); + $timestart = new \DateTime('@' . $eventcopy->timestart); + $timestart->setTimezone(\core_date::get_user_timezone_object()); - for($i = 1; $i < $eventcopy->repeats; $i++) { + for ($i = 1; $i < $eventcopy->repeats; $i++) { - $timestart->add(new DateInterval('P7D')); + $timestart->add(new \DateInterval('P7D')); $eventcopy->timestart = $timestart->getTimestamp(); // Get the event id for the log record. $eventcopyid = $DB->insert_record('event', $eventcopy); - // If the context has been set delete all associated files + // If the context has been set delete all associated files. if ($usingeditor) { $fs = get_file_storage(); - $files = $fs->get_area_files($this->editorcontext->id, 'calendar', 'event_description', $this->properties->id); + $files = $fs->get_area_files($this->editorcontext->id, 'calendar', 'event_description', + $this->properties->id); foreach ($files as $file) { - $fs->create_file_from_storedfile(array('itemid'=>$eventcopyid), $file); + $fs->create_file_from_storedfile(array('itemid' => $eventcopyid), $file); } } @@ -2436,13 +545,11 @@ class calendar_event { } } - // Hook for tracking added events - self::calendar_event_hook('add_event', array($this->properties, $repeatedids)); return true; } else { if ($checkcapability) { - if(!calendar_edit_event_allowed($this->properties)) { + if (!calendar_edit_event_allowed($this->properties)) { print_error('nopermissiontoupdatecalendar'); } } @@ -2450,26 +557,26 @@ class calendar_event { if ($usingeditor) { if ($this->editorcontext !== null) { $this->properties->description = file_save_draft_area_files( - $this->properties->description['itemid'], - $this->editorcontext->id, - 'calendar', - 'event_description', - $this->properties->id, - $this->editoroptions, - $this->properties->description['text'], - $this->editoroptions['forcehttps']); + $this->properties->description['itemid'], + $this->editorcontext->id, + 'calendar', + 'event_description', + $this->properties->id, + $this->editoroptions, + $this->properties->description['text'], + $this->editoroptions['forcehttps']); } else { $this->properties->format = $this->properties->description['format']; $this->properties->description = $this->properties->description['text']; } } - $event = $DB->get_record('event', array('id'=>$this->properties->id)); + $event = $DB->get_record('event', array('id' => $this->properties->id)); $updaterepeated = (!empty($this->properties->repeatid) && !empty($this->properties->repeateditall)); if ($updaterepeated) { - // Update all + // Update all. if ($this->properties->timestart != $event->timestart) { $timestartoffset = $this->properties->timestart - $event->timestart; $sql = "UPDATE {event} @@ -2479,33 +586,35 @@ class calendar_event { timeduration = ?, timemodified = ? WHERE repeatid = ?"; - $params = array($this->properties->name, $this->properties->description, $timestartoffset, $this->properties->timeduration, time(), $event->repeatid); + $params = array($this->properties->name, $this->properties->description, $timestartoffset, + $this->properties->timeduration, time(), $event->repeatid); } else { $sql = "UPDATE {event} SET name = ?, description = ?, timeduration = ?, timemodified = ? WHERE repeatid = ?"; - $params = array($this->properties->name, $this->properties->description, $this->properties->timeduration, time(), $event->repeatid); + $params = array($this->properties->name, $this->properties->description, + $this->properties->timeduration, time(), $event->repeatid); } $DB->execute($sql, $params); // Trigger an update event for each of the calendar event. - $events = $DB->get_records('event', array('repeatid' => $event->repeatid), '', 'id,timestart'); - foreach ($events as $event) { - $eventargs['objectid'] = $event->id; - $eventargs['other']['timestart'] = $event->timestart; + $events = $DB->get_records('event', array('repeatid' => $event->repeatid), '', '*'); + foreach ($events as $calendarevent) { + $eventargs['objectid'] = $calendarevent->id; + $eventargs['other']['timestart'] = $calendarevent->timestart; $event = \core\event\calendar_event_updated::create($eventargs); + $event->add_record_snapshot('event', $calendarevent); $event->trigger(); } } else { $DB->update_record('event', $this->properties); - $event = calendar_event::load($this->properties->id); + $event = self::load($this->properties->id); $this->properties = $event->properties(); // Trigger an update event. $event = \core\event\calendar_event_updated::create($eventargs); + $event->add_record_snapshot('event', $this->properties); $event->trigger(); } - // Hook for tracking event updates - self::calendar_event_hook('update_event', array($this->properties, $updaterepeated)); return true; } } @@ -2521,17 +630,17 @@ class calendar_event { * @param bool $deleterepeated delete event repeatedly * @return bool succession of deleting event */ - public function delete($deleterepeated=false) { + public function delete($deleterepeated = false) { global $DB; - // If $this->properties->id is not set then something is wrong + // If $this->properties->id is not set then something is wrong. if (empty($this->properties->id)) { debugging('Attempting to delete an event before it has been loaded', DEBUG_DEVELOPER); return false; } $calevent = $DB->get_record('event', array('id' => $this->properties->id), '*', MUST_EXIST); - // Delete the event - $DB->delete_records('event', array('id'=>$this->properties->id)); + // Delete the event. + $DB->delete_records('event', array('id' => $this->properties->id)); // Trigger an event for the delete action. $eventargs = array( @@ -2546,32 +655,33 @@ class calendar_event { $event->add_record_snapshot('event', $calevent); $event->trigger(); - // If we are deleting parent of a repeated event series, promote the next event in the series as parent + // If we are deleting parent of a repeated event series, promote the next event in the series as parent. if (($this->properties->id == $this->properties->repeatid) && !$deleterepeated) { - $newparent = $DB->get_field_sql("SELECT id from {event} where repeatid = ? order by id ASC", array($this->properties->id), IGNORE_MULTIPLE); + $newparent = $DB->get_field_sql("SELECT id from {event} where repeatid = ? order by id ASC", + array($this->properties->id), IGNORE_MULTIPLE); if (!empty($newparent)) { - $DB->execute("UPDATE {event} SET repeatid = ? WHERE repeatid = ?", array($newparent, $this->properties->id)); - // Get all records where the repeatid is the same as the event being removed + $DB->execute("UPDATE {event} SET repeatid = ? WHERE repeatid = ?", + array($newparent, $this->properties->id)); + // Get all records where the repeatid is the same as the event being removed. $events = $DB->get_records('event', array('repeatid' => $newparent)); - // For each of the returned events trigger the event_update hook and an update event. - foreach ($events as $event) { + // For each of the returned events trigger an update event. + foreach ($events as $calendarevent) { // Trigger an event for the update. - $eventargs['objectid'] = $event->id; - $eventargs['other']['timestart'] = $event->timestart; + $eventargs['objectid'] = $calendarevent->id; + $eventargs['other']['timestart'] = $calendarevent->timestart; $event = \core\event\calendar_event_updated::create($eventargs); + $event->add_record_snapshot('event', $calendarevent); $event->trigger(); - - self::calendar_event_hook('update_event', array($event, false)); } } } - // If the editor context hasn't already been set then set it now + // If the editor context hasn't already been set then set it now. if ($this->editorcontext === null) { $this->editorcontext = $this->properties->context; } - // If the context has been set delete all associated files + // If the context has been set delete all associated files. if ($this->editorcontext !== null) { $fs = get_file_storage(); $files = $fs->get_area_files($this->editorcontext->id, 'calendar', 'event_description', $this->properties->id); @@ -2580,15 +690,12 @@ class calendar_event { } } - // Fire the event deleted hook - self::calendar_event_hook('delete_event', array($this->properties->id, $deleterepeated)); - - // If we need to delete repeated events then we will fetch them all and delete one by one + // If we need to delete repeated events then we will fetch them all and delete one by one. if ($deleterepeated && !empty($this->properties->repeatid) && $this->properties->repeatid > 0) { - // Get all records where the repeatid is the same as the event being removed - $events = $DB->get_records('event', array('repeatid'=>$this->properties->repeatid)); - // For each of the returned events populate a calendar_event object and call delete - // make sure the arg passed is false as we are already deleting all repeats + // Get all records where the repeatid is the same as the event being removed. + $events = $DB->get_records('event', array('repeatid' => $this->properties->repeatid)); + // For each of the returned events populate an event object and call delete. + // make sure the arg passed is false as we are already deleting all repeats. foreach ($events as $event) { $event = new calendar_event($event); $event->delete(false); @@ -2599,7 +706,7 @@ class calendar_event { } /** - * Fetch all event properties + * Fetch all event properties. * * This function returns all of the events properties as an object and optionally * can prepare an editor for the description field at the same time. This is @@ -2608,47 +715,47 @@ class calendar_event { * * @param bool $prepareeditor If set to true a editor is prepared for use with * the mforms editor element. (for description) - * @return stdClass Object containing event properties + * @return \stdClass Object containing event properties */ - public function properties($prepareeditor=false) { - global $USER, $CFG, $DB; + public function properties($prepareeditor = false) { + global $DB; // First take a copy of the properties. We don't want to actually change the // properties or we'd forever be converting back and forwards between an - // editor formatted description and not + // editor formatted description and not. $properties = clone($this->properties); - // Clean the description here + // Clean the description here. $properties->description = clean_text($properties->description, $properties->format); // If set to true we need to prepare the properties for use with an editor - // and prepare the file area + // and prepare the file area. if ($prepareeditor) { // We may or may not have a property id. If we do then we need to work - // out the context so we can copy the existing files to the draft area + // out the context so we can copy the existing files to the draft area. if (!empty($properties->id)) { if ($properties->eventtype === 'site') { - // Site context + // Site context. $this->editorcontext = $this->properties->context; } else if ($properties->eventtype === 'user') { - // User context + // User context. $this->editorcontext = $this->properties->context; } else if ($properties->eventtype === 'group' || $properties->eventtype === 'course') { - // First check the course is valid - $course = $DB->get_record('course', array('id'=>$properties->courseid)); + // First check the course is valid. + $course = $DB->get_record('course', array('id' => $properties->courseid)); if (!$course) { print_error('invalidcourse'); } - // Course context + // Course context. $this->editorcontext = $this->properties->context; // We have a course and are within the course context so we had - // better use the courses max bytes value + // better use the courses max bytes value. $this->editoroptions['maxbytes'] = $course->maxbytes; } else { // If we get here we have a custom event type as used by some // modules. In this case the event will have been added by - // code and we won't need the editor + // code and we won't need the editor. $this->editoroptions['maxbytes'] = 0; $this->editoroptions['maxfiles'] = 0; } @@ -2656,7 +763,7 @@ class calendar_event { if (empty($this->editorcontext) || empty($this->editorcontext->id)) { $contextid = false; } else { - // Get the context id that is what we really want + // Get the context id that is what we really want. $contextid = $this->editorcontext->id; } } else { @@ -2667,21 +774,23 @@ class calendar_event { } // If the contextid === false we don't support files so no preparing - // a draft area + // a draft area. if ($contextid !== false) { - // Just encase it has already been submitted + // Just encase it has already been submitted. $draftiddescription = file_get_submitted_draft_itemid('description'); - // Prepare the draft area, this copies existing files to the draft area as well - $properties->description = file_prepare_draft_area($draftiddescription, $contextid, 'calendar', 'event_description', $properties->id, $this->editoroptions, $properties->description); + // Prepare the draft area, this copies existing files to the draft area as well. + $properties->description = file_prepare_draft_area($draftiddescription, $contextid, 'calendar', + 'event_description', $properties->id, $this->editoroptions, $properties->description); } else { $draftiddescription = 0; } - // Structure the description field as the editor requires - $properties->description = array('text'=>$properties->description, 'format'=>$properties->format, 'itemid'=>$draftiddescription); + // Structure the description field as the editor requires. + $properties->description = array('text' => $properties->description, 'format' => $properties->format, + 'itemid' => $draftiddescription); } - // Finally return the properties + // Finally return the properties. return $properties; } @@ -2693,92 +802,69 @@ class calendar_event { * is made visible. * @return bool if event is successfully updated, toggle will be visible */ - public function toggle_visibility($force=null) { - global $CFG, $DB; + public function toggle_visibility($force = null) { + global $DB; - // Set visible to the default if it is not already set + // Set visible to the default if it is not already set. if (empty($this->properties->visible)) { $this->properties->visible = 1; } if ($force === true || ($force !== false && $this->properties->visible == 0)) { - // Make this event visible + // Make this event visible. $this->properties->visible = 1; - // Fire the hook - self::calendar_event_hook('show_event', array($this->properties)); } else { - // Make this event hidden + // Make this event hidden. $this->properties->visible = 0; - // Fire the hook - self::calendar_event_hook('hide_event', array($this->properties)); } - // Update the database to reflect this change - return $DB->set_field('event', 'visible', $this->properties->visible, array('id'=>$this->properties->id)); + // Update the database to reflect this change. + $success = $DB->set_field('event', 'visible', $this->properties->visible, array('id' => $this->properties->id)); + $calendarevent = $DB->get_record('event', array('id' => $this->properties->id), '*', MUST_EXIST); + + // Prepare event data. + $eventargs = array( + 'context' => $this->properties->context, + 'objectid' => $this->properties->id, + 'other' => array( + 'repeatid' => empty($this->properties->repeatid) ? 0 : $this->properties->repeatid, + 'timestart' => $this->properties->timestart, + 'name' => $this->properties->name + ) + ); + $event = \core\event\calendar_event_updated::create($eventargs); + $event->add_record_snapshot('event', $calendarevent); + $event->trigger(); + + return $success; } /** - * Attempts to call the hook for the specified action should a calendar type - * by set $CFG->calendar, and the appopriate function defined - * - * @param string $action One of `update_event`, `add_event`, `delete_event`, `show_event`, `hide_event` - * @param array $args The args to pass to the hook, usually the event is the first element - * @return bool attempts to call event hook - */ - public static function calendar_event_hook($action, array $args) { - global $CFG; - static $extcalendarinc; - if ($extcalendarinc === null) { - if (!empty($CFG->calendar)) { - if (is_readable($CFG->dirroot .'/calendar/'. $CFG->calendar .'/lib.php')) { - include_once($CFG->dirroot .'/calendar/'. $CFG->calendar .'/lib.php'); - $extcalendarinc = true; - } else { - debugging("Calendar lib file missing or not readable at /calendar/{$CFG->calendar}/lib.php.", - DEBUG_DEVELOPER); - $extcalendarinc = false; - } - } else { - $extcalendarinc = false; - } - } - if($extcalendarinc === false) { - return false; - } - $hook = $CFG->calendar .'_'.$action; - if (function_exists($hook)) { - call_user_func_array($hook, $args); - return true; - } - return false; - } - - /** - * Returns a calendar_event object when provided with an event id + * Returns an event object when provided with an event id. * * This function makes use of MUST_EXIST, if the event id passed in is invalid - * it will result in an exception being thrown + * it will result in an exception being thrown. * * @param int|object $param event object or event id - * @return calendar_event|false status for loading calendar_event + * @return calendar_event */ public static function load($param) { global $DB; if (is_object($param)) { $event = new calendar_event($param); } else { - $event = $DB->get_record('event', array('id'=>(int)$param), '*', MUST_EXIST); + $event = $DB->get_record('event', array('id' => (int)$param), '*', MUST_EXIST); $event = new calendar_event($event); } return $event; } /** - * Creates a new event and returns a calendar_event object + * Creates a new event and returns an event object * - * @param stdClass|array $properties An object containing event properties + * @param \stdClass|array $properties An object containing event properties * @param bool $checkcapability Check caps or not - * @throws coding_exception + * @throws \coding_exception * * @return calendar_event|bool The event object or false if it failed */ @@ -2787,7 +873,7 @@ class calendar_event { $properties = (object)$properties; } if (!is_object($properties)) { - throw new coding_exception('When creating an event properties should be either an object or an assoc array'); + throw new \coding_exception('When creating an event properties should be either an object or an assoc array'); } $event = new calendar_event($properties); if ($event->update($properties, $checkcapability)) { @@ -2799,6 +885,7 @@ class calendar_event { /** * Format the text using the external API. + * * This function should we used when text formatting is required in external functions. * * @return array an array containing the text formatted and the text format @@ -2810,7 +897,7 @@ class calendar_event { $this->editorcontext = $this->properties->context; if ($this->properties->eventtype != 'user' && $this->properties->eventtype != 'course' - && $this->properties->eventtype != 'site' && $this->properties->eventtype != 'group') { + && $this->properties->eventtype != 'site' && $this->properties->eventtype != 'group') { // We don't have a context here, do a normal format_text. return external_format_text($this->properties->description, $this->properties->format, $this->editorcontext->id); } @@ -2824,7 +911,7 @@ class calendar_event { } return external_format_text($this->properties->description, $this->properties->format, $this->editorcontext->id, - 'calendar', 'event_description', $itemid); + 'calendar', 'event_description', $itemid); } } @@ -2975,6 +1062,1826 @@ class calendar_information { } } +/** + * Get calendar events. + * + * @param int $tstart Start time of time range for events + * @param int $tend End time of time range for events + * @param array|int|boolean $users array of users, user id or boolean for all/no user events + * @param array|int|boolean $groups array of groups, group id or boolean for all/no group events + * @param array|int|boolean $courses array of courses, course id or boolean for all/no course events + * @param boolean $withduration whether only events starting within time range selected + * or events in progress/already started selected as well + * @param boolean $ignorehidden whether to select only visible events or all events + * @return array $events of selected events or an empty array if there aren't any (or there was an error) + */ +function calendar_get_events($tstart, $tend, $users, $groups, $courses, $withduration=true, $ignorehidden=true) { + // We have a new implementation of this function in the calendar API class, which has slightly different behaviour + // so the old implementation must remain here. + global $DB; + $params = array(); + + // Quick test. + if (empty($users) && empty($groups) && empty($courses)) { + return array(); + } + + // Array of filter conditions. To be concatenated by the OR operator. + $filters = []; + + // User filter. + if ((is_array($users) && !empty($users)) or is_numeric($users)) { + // Events from a number of users. + list($insqlusers, $inparamsusers) = $DB->get_in_or_equal($users, SQL_PARAMS_NAMED); + $filters[] = "(e.userid $insqlusers AND e.courseid = 0 AND e.groupid = 0)"; + $params = array_merge($params, $inparamsusers); + } else if ($users === true) { + // Events from ALL users. + $filters[] = "(e.userid != 0 AND e.courseid = 0 AND e.groupid = 0)"; + } + + // Boolean false (no users at all): We don't need to do anything. + // Group filter. + if ((is_array($groups) && !empty($groups)) or is_numeric($groups)) { + // Events from a number of groups. + list($insqlgroups, $inparamsgroups) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED); + $filters[] = "e.groupid $insqlgroups"; + $params = array_merge($params, $inparamsgroups); + } else if ($groups === true) { + // Events from ALL groups. + $filters[] = "e.groupid != 0"; + } + + // Boolean false (no groups at all): We don't need to do anything. + // Course filter. + if ((is_array($courses) && !empty($courses)) or is_numeric($courses)) { + list($insqlcourses, $inparamscourses) = $DB->get_in_or_equal($courses, SQL_PARAMS_NAMED); + $filters[] = "(e.groupid = 0 AND e.courseid $insqlcourses)"; + $params = array_merge($params, $inparamscourses); + } else if ($courses === true) { + // Events from ALL courses. + $filters[] = "(e.groupid = 0 AND e.courseid != 0)"; + } + + // Security check: if, by now, we have NOTHING in $whereclause, then it means + // that NO event-selecting clauses were defined. Thus, we won't be returning ANY + // events no matter what. Allowing the code to proceed might return a completely + // valid query with only time constraints, thus selecting ALL events in that time frame! + if (empty($filters)) { + return array(); + } + + // Build our clause for the filters. + $filterclause = implode(' OR ', $filters); + + // Array of where conditions for our query. To be concatenated by the AND operator. + $whereconditions = ["($filterclause)"]; + + // Time clause. + if ($withduration) { + $timeclause = "((e.timestart >= :tstart1 OR e.timestart + e.timeduration > :tstart2) AND e.timestart <= :tend)"; + $params['tstart1'] = $tstart; + $params['tstart2'] = $tstart; + $params['tend'] = $tend; + } else { + $timeclause = "(e.timestart >= :tstart AND e.timestart <= :tend)"; + $params['tstart'] = $tstart; + $params['tend'] = $tend; + } + $whereconditions[] = $timeclause; + + // Show visible only. + if ($ignorehidden) { + $whereconditions[] = "(e.visible = 1)"; + } + + // Build the main query's WHERE clause. + $whereclause = implode(' AND ', $whereconditions); + + // Build SQL subquery and conditions for filtered events based on priorities. + $subquerywhere = ''; + $subqueryconditions = []; + + // Get the user's courses. Otherwise, get the default courses being shown by the calendar. + $usercourses = calendar_get_default_courses(); + + // Set calendar filters. + list($usercourses, $usergroups, $user) = calendar_set_filters($usercourses, true); + $subqueryparams = []; + + // Flag to indicate whether the query needs to exclude group overrides. + $viewgroupsonly = false; + if ($user) { + // Set filter condition for the user's events. + $subqueryconditions[] = "(ev.userid = :user AND ev.courseid = 0 AND ev.groupid = 0)"; + $subqueryparams['user'] = $user; + foreach ($usercourses as $courseid) { + if (has_capability('moodle/site:accessallgroups', context_course::instance($courseid))) { + $usergroupmembership = groups_get_all_groups($courseid, $user, 0, 'g.id'); + if (count($usergroupmembership) == 0) { + $viewgroupsonly = true; + break; + } + } + } + } + + // Set filter condition for the user's group events. + if ($usergroups === true || $viewgroupsonly) { + // Fetch group events, but not group overrides. + $subqueryconditions[] = "(ev.groupid != 0 AND ev.eventtype = 'group')"; + } else if (!empty($usergroups)) { + // Fetch group events and group overrides. + list($inusergroups, $inusergroupparams) = $DB->get_in_or_equal($usergroups, SQL_PARAMS_NAMED); + $subqueryconditions[] = "(ev.groupid $inusergroups)"; + $subqueryparams = array_merge($subqueryparams, $inusergroupparams); + } + + // Get courses to be used for the subquery. + $subquerycourses = []; + if (is_array($courses)) { + $subquerycourses = $courses; + } else if (is_numeric($courses)) { + $subquerycourses[] = $courses; + } + + // Merge with user courses, if necessary. + if (!empty($usercourses)) { + $subquerycourses = array_merge($subquerycourses, $usercourses); + // Make sure we remove duplicate values. + $subquerycourses = array_unique($subquerycourses); + } + + // Set subquery filter condition for the courses. + if (!empty($subquerycourses)) { + list($incourses, $incoursesparams) = $DB->get_in_or_equal($subquerycourses, SQL_PARAMS_NAMED); + $subqueryconditions[] = "(ev.groupid = 0 AND ev.courseid $incourses)"; + $subqueryparams = array_merge($subqueryparams, $incoursesparams); + } + + // Build the WHERE condition for the sub-query. + if (!empty($subqueryconditions)) { + $subquerywhere = 'WHERE ' . implode(" OR ", $subqueryconditions); + } + + // Merge subquery parameters to the parameters of the main query. + if (!empty($subqueryparams)) { + $params = array_merge($params, $subqueryparams); + } + + // Sub-query that fetches the list of unique events that were filtered based on priority. + $subquery = "SELECT ev.modulename, + ev.instance, + ev.eventtype, + MAX(ev.priority) as priority + FROM {event} ev + $subquerywhere + GROUP BY ev.modulename, ev.instance, ev.eventtype"; + + // Build the main query. + $sql = "SELECT e.* + FROM {event} e + INNER JOIN ($subquery) fe + ON e.modulename = fe.modulename + AND e.instance = fe.instance + AND e.eventtype = fe.eventtype + AND (e.priority = fe.priority OR (e.priority IS NULL AND fe.priority IS NULL)) + LEFT JOIN {modules} m + ON e.modulename = m.name + WHERE (m.visible = 1 OR m.visible IS NULL) AND $whereclause + ORDER BY e.timestart"; + $events = $DB->get_records_sql($sql, $params); + + if ($events === false) { + $events = array(); + } + + return $events; +} + +/** + * Return the days of the week. + * + * @return array array of days + */ +function calendar_get_days() { + $calendartype = \core_calendar\type_factory::get_calendar_instance(); + return $calendartype->get_weekdays(); +} + +/** + * Get the subscription from a given id. + * + * @since Moodle 2.5 + * @param int $id id of the subscription + * @return stdClass Subscription record from DB + * @throws moodle_exception for an invalid id + */ +function calendar_get_subscription($id) { + global $DB; + + $cache = \cache::make('core', 'calendar_subscriptions'); + $subscription = $cache->get($id); + if (empty($subscription)) { + $subscription = $DB->get_record('event_subscriptions', array('id' => $id), '*', MUST_EXIST); + $cache->set($id, $subscription); + } + + return $subscription; +} + +/** + * Gets the first day of the week. + * + * Used to be define('CALENDAR_STARTING_WEEKDAY', blah); + * + * @return int + */ +function calendar_get_starting_weekday() { + $calendartype = \core_calendar\type_factory::get_calendar_instance(); + return $calendartype->get_starting_weekday(); +} + +/** + * Generates the HTML for a miniature calendar. + * + * @param array $courses list of course to list events from + * @param array $groups list of group + * @param array $users user's info + * @param int|bool $calmonth calendar month in numeric, default is set to false + * @param int|bool $calyear calendar month in numeric, default is set to false + * @param string|bool $placement the place/page the calendar is set to appear - passed on the the controls function + * @param int|bool $courseid id of the course the calendar is displayed on - passed on the the controls function + * @param int $time the unixtimestamp representing the date we want to view, this is used instead of $calmonth + * and $calyear to support multiple calendars + * @return string $content return html table for mini calendar + */ +function calendar_get_mini($courses, $groups, $users, $calmonth = false, $calyear = false, $placement = false, + $courseid = false, $time = 0) { + global $CFG, $OUTPUT; + + // Get the calendar type we are using. + $calendartype = \core_calendar\type_factory::get_calendar_instance(); + + $display = new \stdClass; + + // Assume we are not displaying this month for now. + $display->thismonth = false; + + $content = ''; + + // Do this check for backwards compatibility. + // The core should be passing a timestamp rather than month and year. + // If a month and year are passed they will be in Gregorian. + if (!empty($calmonth) && !empty($calyear)) { + // Ensure it is a valid date, else we will just set it to the current timestamp. + if (checkdate($calmonth, 1, $calyear)) { + $time = make_timestamp($calyear, $calmonth, 1); + } else { + $time = time(); + } + $date = usergetdate($time); + if ($calmonth == $date['mon'] && $calyear == $date['year']) { + $display->thismonth = true; + } + // We can overwrite date now with the date used by the calendar type, + // if it is not Gregorian, otherwise there is no need as it is already in Gregorian. + if ($calendartype->get_name() != 'gregorian') { + $date = $calendartype->timestamp_to_date_array($time); + } + } else if (!empty($time)) { + // Get the specified date in the calendar type being used. + $date = $calendartype->timestamp_to_date_array($time); + $thisdate = $calendartype->timestamp_to_date_array(time()); + if ($date['month'] == $thisdate['month'] && $date['year'] == $thisdate['year']) { + $display->thismonth = true; + // If we are the current month we want to set the date to the current date, not the start of the month. + $date = $thisdate; + } + } else { + // Get the current date in the calendar type being used. + $time = time(); + $date = $calendartype->timestamp_to_date_array($time); + $display->thismonth = true; + } + + list($d, $m, $y) = array($date['mday'], $date['mon'], $date['year']); // This is what we want to display. + + // Get Gregorian date for the start of the month. + $gregoriandate = $calendartype->convert_to_gregorian($date['year'], $date['mon'], 1); + + // Store the gregorian date values to be used later. + list($gy, $gm, $gd, $gh, $gmin) = array($gregoriandate['year'], $gregoriandate['month'], $gregoriandate['day'], + $gregoriandate['hour'], $gregoriandate['minute']); + + // Get the max number of days in this month for this calendar type. + $display->maxdays = calendar_days_in_month($m, $y); + // Get the starting week day for this month. + $startwday = dayofweek(1, $m, $y); + // Get the days in a week. + $daynames = calendar_get_days(); + // Store the number of days in a week. + $numberofdaysinweek = $calendartype->get_num_weekdays(); + + // Set the min and max weekday. + $display->minwday = calendar_get_starting_weekday(); + $display->maxwday = $display->minwday + ($numberofdaysinweek - 1); + + // These are used for DB queries, so we want unixtime, so we need to use Gregorian dates. + $display->tstart = make_timestamp($gy, $gm, $gd, $gh, $gmin, 0); + $display->tend = $display->tstart + ($display->maxdays * DAYSECS) - 1; + + // Align the starting weekday to fall in our display range. + // This is simple, not foolproof. + if ($startwday < $display->minwday) { + $startwday += $numberofdaysinweek; + } + + // Get the events matching our criteria. Don't forget to offset the timestamps for the user's TZ. + $events = \core_calendar\local\api::get_legacy_events($display->tstart, $display->tend, $users, $groups, $courses); + + // Set event course class for course events. + if (!empty($events)) { + foreach ($events as $eventid => $event) { + if (!empty($event->modulename)) { + $cm = get_coursemodule_from_instance($event->modulename, $event->instance); + if (!\core_availability\info_module::is_user_visible($cm, 0, false)) { + unset($events[$eventid]); + } + } + } + } + + // This is either a genius idea or an idiot idea: in order to not complicate things, we use this rule: if, after + // possibly removing SITEID from $courses, there is only one course left, then clicking on a day in the month + // will also set the $SESSION->cal_courses_shown variable to that one course. Otherwise, we 'd need to add extra + // arguments to this function. + $hrefparams = array(); + if (!empty($courses)) { + $courses = array_diff($courses, array(SITEID)); + if (count($courses) == 1) { + $hrefparams['course'] = reset($courses); + } + } + + // We want to have easy access by day, since the display is on a per-day basis. + calendar_events_by_day($events, $m, $y, $eventsbyday, $durationbyday, $typesbyday, $courses); + + // Accessibility: added summary and elements. + $summary = get_string('calendarheading', 'calendar', userdate($display->tstart, get_string('strftimemonthyear'))); + // Begin table. + $content .= ''; + if (($placement !== false) && ($courseid !== false)) { + $content .= ''; + } + $content .= ''; // Header row: day names. + + // Print out the names of the weekdays. + for ($i = $display->minwday; $i <= $display->maxwday; $i++) { + $pos = $i % $numberofdaysinweek; + $content .= '\n"; + } + + $content .= ''; // End of day names; prepare for day numbers. + + // For the table display. $week is the row; $dayweek is the column. + $dayweek = $startwday; + + // Padding (the first week may have blank days in the beginning). + for ($i = $display->minwday; $i < $startwday; ++$i) { + $content .= '' ."\n"; + } + + $weekend = CALENDAR_DEFAULT_WEEKEND; + if (isset($CFG->calendar_weekend)) { + $weekend = intval($CFG->calendar_weekend); + } + + // Now display all the calendar. + $daytime = strtotime('-1 day', $display->tstart); + for ($day = 1; $day <= $display->maxdays; ++$day, ++$dayweek) { + $cellattributes = array(); + $daytime = strtotime('+1 day', $daytime); + if ($dayweek > $display->maxwday) { + // We need to change week (table row). + $content .= ''; + $dayweek = $display->minwday; + } + + // Reset vars. + if ($weekend & (1 << ($dayweek % $numberofdaysinweek))) { + // Weekend. This is true no matter what the exact range is. + $class = 'weekend day'; + } else { + // Normal working day. + $class = 'day'; + } + + $eventids = array(); + if (!empty($eventsbyday[$day])) { + $eventids = $eventsbyday[$day]; + } + + if (!empty($durationbyday[$day])) { + $eventids = array_unique(array_merge($eventids, $durationbyday[$day])); + } + + $finishclass = false; + + if (!empty($eventids)) { + // There is at least one event on this day. + $class .= ' hasevent'; + $hrefparams['view'] = 'day'; + $dayhref = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', $hrefparams), 0, 0, 0, $daytime); + + $popupcontent = ''; + foreach ($eventids as $eventid) { + if (!isset($events[$eventid])) { + continue; + } + $event = new \calendar_event($events[$eventid]); + $popupalt = ''; + $component = 'moodle'; + if (!empty($event->modulename)) { + $popupicon = 'icon'; + $popupalt = $event->modulename; + $component = $event->modulename; + } else if ($event->courseid == SITEID) { // Site event. + $popupicon = 'i/siteevent'; + } else if ($event->courseid != 0 && $event->courseid != SITEID + && $event->groupid == 0) { // Course event. + $popupicon = 'i/courseevent'; + } else if ($event->groupid) { // Group event. + $popupicon = 'i/groupevent'; + } else { // Must be a user event. + $popupicon = 'i/userevent'; + } + + if ($event->timeduration) { + $startdate = $calendartype->timestamp_to_date_array($event->timestart); + $enddate = $calendartype->timestamp_to_date_array($event->timestart + $event->timeduration - 1); + if ($enddate['mon'] == $m && $enddate['year'] == $y && $enddate['mday'] == $day) { + $finishclass = true; + } + } + + $dayhref->set_anchor('event_' . $event->id); + + $popupcontent .= \html_writer::start_tag('div'); + $popupcontent .= $OUTPUT->pix_icon($popupicon, $popupalt, $component); + // Show ical source if needed. + if (!empty($event->subscription) && $CFG->calendar_showicalsource) { + $a = new \stdClass(); + $a->name = format_string($event->name, true); + $a->source = $event->subscription->name; + $name = get_string('namewithsource', 'calendar', $a); + } else { + if ($finishclass) { + $samedate = $startdate['mon'] == $enddate['mon'] && + $startdate['year'] == $enddate['year'] && + $startdate['mday'] == $enddate['mday']; + + if ($samedate) { + $name = format_string($event->name, true); + } else { + $name = format_string($event->name, true) . ' (' . get_string('eventendtime', 'calendar') . ')'; + } + } else { + $name = format_string($event->name, true); + } + } + $popupcontent .= \html_writer::link($dayhref, $name); + $popupcontent .= \html_writer::end_tag('div'); + } + + if ($display->thismonth && $day == $d) { + $popupdata = calendar_get_popup(true, $daytime, $popupcontent); + } else { + $popupdata = calendar_get_popup(false, $daytime, $popupcontent); + } + + // Class and cell content. + if (isset($typesbyday[$day]['startglobal'])) { + $class .= ' calendar_event_global'; + } else if (isset($typesbyday[$day]['startcourse'])) { + $class .= ' calendar_event_course'; + } else if (isset($typesbyday[$day]['startgroup'])) { + $class .= ' calendar_event_group'; + } else if (isset($typesbyday[$day]['startuser'])) { + $class .= ' calendar_event_user'; + } + if ($finishclass) { + $class .= ' duration_finish'; + } + $data = array( + 'url' => $dayhref, + 'day' => $day, + 'content' => $popupdata['data-core_calendar-popupcontent'], + 'title' => $popupdata['data-core_calendar-title'] + ); + $cell = $OUTPUT->render_from_template('core_calendar/minicalendar_day_link', $data); + } else { + $cell = $day; + } + + $durationclass = false; + if (isset($typesbyday[$day]['durationglobal'])) { + $durationclass = ' duration_global'; + } else if (isset($typesbyday[$day]['durationcourse'])) { + $durationclass = ' duration_course'; + } else if (isset($typesbyday[$day]['durationgroup'])) { + $durationclass = ' duration_group'; + } else if (isset($typesbyday[$day]['durationuser'])) { + $durationclass = ' duration_user'; + } + if ($durationclass) { + $class .= ' duration ' . $durationclass; + } + + // If event has a class set then add it to the table day '; + } + $content .= ''; // Last row ends. + + $content .= '
    ' . calendar_top_controls($placement, + array('id' => $courseid, 'time' => $time)) . '
    ' . + $daynames[$pos]['shortname'] . "
     
    tag. + // Note: only one colour for minicalendar. + if (isset($eventsbyday[$day])) { + foreach ($eventsbyday[$day] as $eventid) { + if (!isset($events[$eventid])) { + continue; + } + $event = $events[$eventid]; + if (!empty($event->class)) { + $class .= ' ' . $event->class; + } + break; + } + } + + if ($display->thismonth && $day == $d) { + // The current cell is for today - add appropriate classes and additional information for styling. + $class .= ' today'; + $today = get_string('today', 'calendar') . ' ' . userdate(time(), get_string('strftimedayshort')); + + if (!isset($eventsbyday[$day]) && !isset($durationbyday[$day])) { + $class .= ' eventnone'; + $popupdata = calendar_get_popup(true, false); + $data = array( + 'url' => '#', + 'day' => $day, + 'content' => $popupdata['data-core_calendar-popupcontent'], + 'title' => $popupdata['data-core_calendar-title'] + ); + $cell = $OUTPUT->render_from_template('core_calendar/minicalendar_day_link', $data); + } + $cell = get_accesshide($today . ' ') . $cell; + } + + // Just display it. + $cellattributes['class'] = $class; + $content .= \html_writer::tag('td', $cell, $cellattributes); + } + + // Padding (the last week may have blank days at the end). + for ($i = $dayweek; $i <= $display->maxwday; ++$i) { + $content .= ' 
    '; // Tabular display of days ends. + return $content; +} + +/** + * Gets the calendar popup. + * + * It called at multiple points in from calendar_get_mini. + * Copied and modified from calendar_get_mini. + * + * @param bool $today false except when called on the current day. + * @param mixed $timestart $events[$eventid]->timestart, OR false if there are no events. + * @param string $popupcontent content for the popup window/layout. + * @return string eventid for the calendar_tooltip popup window/layout. + */ +function calendar_get_popup($today = false, $timestart, $popupcontent = '') { + $popupcaption = ''; + if ($today) { + $popupcaption = get_string('today', 'calendar') . ' '; + } + + if (false === $timestart) { + $popupcaption .= userdate(time(), get_string('strftimedayshort')); + $popupcontent = get_string('eventnone', 'calendar'); + + } else { + $popupcaption .= get_string('eventsfor', 'calendar', userdate($timestart, get_string('strftimedayshort'))); + } + + return array( + 'data-core_calendar-title' => $popupcaption, + 'data-core_calendar-popupcontent' => $popupcontent, + ); +} + +/** + * Gets the calendar upcoming event. + * + * @param array $courses array of courses + * @param array|int|bool $groups array of groups, group id or boolean for all/no group events + * @param array|int|bool $users array of users, user id or boolean for all/no user events + * @param int $daysinfuture number of days in the future we 'll look + * @param int $maxevents maximum number of events + * @param int $fromtime start time + * @return array $output array of upcoming events + */ +function calendar_get_upcoming($courses, $groups, $users, $daysinfuture, $maxevents, $fromtime=0) { + global $COURSE; + + $display = new \stdClass; + $display->range = $daysinfuture; // How many days in the future we 'll look. + $display->maxevents = $maxevents; + + $output = array(); + + $processed = 0; + $now = time(); // We 'll need this later. + $usermidnighttoday = usergetmidnight($now); + + if ($fromtime) { + $display->tstart = $fromtime; + } else { + $display->tstart = $usermidnighttoday; + } + + // This works correctly with respect to the user's DST, but it is accurate + // only because $fromtime is always the exact midnight of some day! + $display->tend = usergetmidnight($display->tstart + DAYSECS * $display->range + 3 * HOURSECS) - 1; + + // Get the events matching our criteria. + $events = \core_calendar\local\api::get_legacy_events($display->tstart, $display->tend, $users, $groups, $courses); + + // This is either a genius idea or an idiot idea: in order to not complicate things, we use this rule: if, after + // possibly removing SITEID from $courses, there is only one course left, then clicking on a day in the month + // will also set the $SESSION->cal_courses_shown variable to that one course. Otherwise, we 'd need to add extra + // arguments to this function. + $hrefparams = array(); + if (!empty($courses)) { + $courses = array_diff($courses, array(SITEID)); + if (count($courses) == 1) { + $hrefparams['course'] = reset($courses); + } + } + + if ($events !== false) { + $modinfo = get_fast_modinfo($COURSE); + foreach ($events as $event) { + if (!empty($event->modulename)) { + if ($event->courseid == $COURSE->id) { + if (isset($modinfo->instances[$event->modulename][$event->instance])) { + $cm = $modinfo->instances[$event->modulename][$event->instance]; + if (!$cm->uservisible) { + continue; + } + } + } else { + if (!$cm = get_coursemodule_from_instance($event->modulename, $event->instance)) { + continue; + } + if (!\core_availability\info_module::is_user_visible($cm, 0, false)) { + continue; + } + } + } + + if ($processed >= $display->maxevents) { + break; + } + + $event->time = calendar_format_event_time($event, $now, $hrefparams); + $output[] = $event; + $processed++; + } + } + + return $output; +} + +/** + * Get a HTML link to a course. + * + * @param int $courseid the course id + * @return string a link to the course (as HTML); empty if the course id is invalid + */ +function calendar_get_courselink($courseid) { + if (!$courseid) { + return ''; + } + + calendar_get_course_cached($coursecache, $courseid); + $context = \context_course::instance($courseid); + $fullname = format_string($coursecache[$courseid]->fullname, true, array('context' => $context)); + $url = new \moodle_url('/course/view.php', array('id' => $courseid)); + $link = \html_writer::link($url, $fullname); + + return $link; +} + +/** + * Get current module cache. + * + * @param array $modulecache in memory module cache + * @param string $modulename name of the module + * @param int $instance module instance number + * @return stdClass|bool $module information + */ +function calendar_get_module_cached(&$modulecache, $modulename, $instance) { + if (!isset($modulecache[$modulename . '_' . $instance])) { + $modulecache[$modulename . '_' . $instance] = get_coursemodule_from_instance($modulename, $instance); + } + + return $modulecache[$modulename . '_' . $instance]; +} + +/** + * Get current course cache. + * + * @param array $coursecache list of course cache + * @param int $courseid id of the course + * @return stdClass $coursecache[$courseid] return the specific course cache + */ +function calendar_get_course_cached(&$coursecache, $courseid) { + if (!isset($coursecache[$courseid])) { + $coursecache[$courseid] = get_course($courseid); + } + return $coursecache[$courseid]; +} + +/** + * Get group from groupid for calendar display + * + * @param int $groupid + * @return stdClass group object with fields 'id', 'name' and 'courseid' + */ +function calendar_get_group_cached($groupid) { + static $groupscache = array(); + if (!isset($groupscache[$groupid])) { + $groupscache[$groupid] = groups_get_group($groupid, 'id,name,courseid'); + } + return $groupscache[$groupid]; +} + +/** + * Add calendar event metadata + * + * @param stdClass $event event info + * @return stdClass $event metadata + */ +function calendar_add_event_metadata($event) { + global $CFG, $OUTPUT; + + // Support multilang in event->name. + $event->name = format_string($event->name, true); + + if (!empty($event->modulename)) { // Activity event. + // The module name is set. I will assume that it has to be displayed, and + // also that it is an automatically-generated event. And of course that the + // fields for get_coursemodule_from_instance are set correctly. + $module = calendar_get_module_cached($coursecache, $event->modulename, $event->instance); + + if ($module === false) { + return; + } + + $modulename = get_string('modulename', $event->modulename); + if (get_string_manager()->string_exists($event->eventtype, $event->modulename)) { + // Will be used as alt text if the event icon. + $eventtype = get_string($event->eventtype, $event->modulename); + } else { + $eventtype = ''; + } + $icon = $OUTPUT->image_url('icon', $event->modulename) . ''; + + $event->icon = '' . $eventtype . ''; + $event->referer = '' . $event->name . ''; + $event->courselink = calendar_get_courselink($module->course); + $event->cmid = $module->id; + } else if ($event->courseid == SITEID) { // Site event. + $event->icon = '' .
+            get_string('globalevent', 'calendar') . ''; + $event->cssclass = 'calendar_event_global'; + } else if ($event->courseid != 0 && $event->courseid != SITEID && $event->groupid == 0) { // Course event. + $event->icon = '' .
+            get_string('courseevent', 'calendar') . ''; + $event->courselink = calendar_get_courselink($event->courseid); + $event->cssclass = 'calendar_event_course'; + } else if ($event->groupid) { // Group event. + if ($group = calendar_get_group_cached($event->groupid)) { + $groupname = format_string($group->name, true, \context_course::instance($group->courseid)); + } else { + $groupname = ''; + } + $event->icon = \html_writer::empty_tag('image', array('src' => $OUTPUT->image_url('i/groupevent'), + 'alt' => get_string('groupevent', 'calendar'), 'title' => $groupname, 'class' => 'icon')); + $event->courselink = calendar_get_courselink($event->courseid) . ', ' . $groupname; + $event->cssclass = 'calendar_event_group'; + } else if ($event->userid) { // User event. + $event->icon = '' .
+            get_string('userevent', 'calendar') . ''; + $event->cssclass = 'calendar_event_user'; + } + + return $event; +} + +/** + * Get calendar events by id. + * + * @since Moodle 2.5 + * @param array $eventids list of event ids + * @return array Array of event entries, empty array if nothing found + */ +function calendar_get_events_by_id($eventids) { + global $DB; + + if (!is_array($eventids) || empty($eventids)) { + return array(); + } + + list($wheresql, $params) = $DB->get_in_or_equal($eventids); + $wheresql = "id $wheresql"; + + return $DB->get_records_select('event', $wheresql, $params); +} + +/** + * Get control options for calendar. + * + * @param string $type of calendar + * @param array $data calendar information + * @return string $content return available control for the calender in html + */ +function calendar_top_controls($type, $data) { + global $PAGE, $OUTPUT; + + // Get the calendar type we are using. + $calendartype = \core_calendar\type_factory::get_calendar_instance(); + + $content = ''; + + // Ensure course id passed if relevant. + $courseid = ''; + if (!empty($data['id'])) { + $courseid = '&course=' . $data['id']; + } + + // If we are passing a month and year then we need to convert this to a timestamp to + // support multiple calendars. No where in core should these be passed, this logic + // here is for third party plugins that may use this function. + if (!empty($data['m']) && !empty($date['y'])) { + if (!isset($data['d'])) { + $data['d'] = 1; + } + if (!checkdate($data['m'], $data['d'], $data['y'])) { + $time = time(); + } else { + $time = make_timestamp($data['y'], $data['m'], $data['d']); + } + } else if (!empty($data['time'])) { + $time = $data['time']; + } else { + $time = time(); + } + + // Get the date for the calendar type. + $date = $calendartype->timestamp_to_date_array($time); + + $urlbase = $PAGE->url; + + // We need to get the previous and next months in certain cases. + if ($type == 'frontpage' || $type == 'course' || $type == 'month') { + $prevmonth = calendar_sub_month($date['mon'], $date['year']); + $prevmonthtime = $calendartype->convert_to_gregorian($prevmonth[1], $prevmonth[0], 1); + $prevmonthtime = make_timestamp($prevmonthtime['year'], $prevmonthtime['month'], $prevmonthtime['day'], + $prevmonthtime['hour'], $prevmonthtime['minute']); + + $nextmonth = calendar_add_month($date['mon'], $date['year']); + $nextmonthtime = $calendartype->convert_to_gregorian($nextmonth[1], $nextmonth[0], 1); + $nextmonthtime = make_timestamp($nextmonthtime['year'], $nextmonthtime['month'], $nextmonthtime['day'], + $nextmonthtime['hour'], $nextmonthtime['minute']); + } + + switch ($type) { + case 'frontpage': + $prevlink = calendar_get_link_previous(get_string('monthprev', 'access'), $urlbase, false, false, false, + true, $prevmonthtime); + $nextlink = calendar_get_link_next(get_string('monthnext', 'access'), $urlbase, false, false, false, true, + $nextmonthtime); + $calendarlink = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', array('view' => 'month')), + false, false, false, $time); + + if (!empty($data['id'])) { + $calendarlink->param('course', $data['id']); + } + + $right = $nextlink; + + $content .= \html_writer::start_tag('div', array('class' => 'calendar-controls')); + $content .= $prevlink . ' | '; + $content .= \html_writer::tag('span', \html_writer::link($calendarlink, + userdate($time, get_string('strftimemonthyear')), array('title' => get_string('monththis', 'calendar')) + ), array('class' => 'current')); + $content .= ' | ' . $right; + $content .= "\n"; + $content .= \html_writer::end_tag('div'); + + break; + case 'course': + $prevlink = calendar_get_link_previous(get_string('monthprev', 'access'), $urlbase, false, false, false, + true, $prevmonthtime); + $nextlink = calendar_get_link_next(get_string('monthnext', 'access'), $urlbase, false, false, false, + true, $nextmonthtime); + $calendarlink = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', array('view' => 'month')), + false, false, false, $time); + + if (!empty($data['id'])) { + $calendarlink->param('course', $data['id']); + } + + $content .= \html_writer::start_tag('div', array('class' => 'calendar-controls')); + $content .= $prevlink . ' | '; + $content .= \html_writer::tag('span', \html_writer::link($calendarlink, + userdate($time, get_string('strftimemonthyear')), array('title' => get_string('monththis', 'calendar')) + ), array('class' => 'current')); + $content .= ' | ' . $nextlink; + $content .= ""; + $content .= \html_writer::end_tag('div'); + break; + case 'upcoming': + $calendarlink = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', array('view' => 'upcoming')), + false, false, false, $time); + if (!empty($data['id'])) { + $calendarlink->param('course', $data['id']); + } + $calendarlink = \html_writer::link($calendarlink, userdate($time, get_string('strftimemonthyear'))); + $content .= \html_writer::tag('div', $calendarlink, array('class' => 'centered')); + break; + case 'display': + $calendarlink = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', array('view' => 'month')), + false, false, false, $time); + if (!empty($data['id'])) { + $calendarlink->param('course', $data['id']); + } + $calendarlink = \html_writer::link($calendarlink, userdate($time, get_string('strftimemonthyear'))); + $content .= \html_writer::tag('h3', $calendarlink); + break; + case 'month': + $prevlink = calendar_get_link_previous(userdate($prevmonthtime, get_string('strftimemonthyear')), + 'view.php?view=month' . $courseid . '&', false, false, false, false, $prevmonthtime); + $nextlink = calendar_get_link_next(userdate($nextmonthtime, get_string('strftimemonthyear')), + 'view.php?view=month' . $courseid . '&', false, false, false, false, $nextmonthtime); + + $content .= \html_writer::start_tag('div', array('class' => 'calendar-controls')); + $content .= $prevlink . ' | '; + $content .= $OUTPUT->heading(userdate($time, get_string('strftimemonthyear')), 2, 'current'); + $content .= ' | ' . $nextlink; + $content .= ''; + $content .= \html_writer::end_tag('div')."\n"; + break; + case 'day': + $days = calendar_get_days(); + + $prevtimestamp = strtotime('-1 day', $time); + $nexttimestamp = strtotime('+1 day', $time); + + $prevdate = $calendartype->timestamp_to_date_array($prevtimestamp); + $nextdate = $calendartype->timestamp_to_date_array($nexttimestamp); + + $prevname = $days[$prevdate['wday']]['fullname']; + $nextname = $days[$nextdate['wday']]['fullname']; + $prevlink = calendar_get_link_previous($prevname, 'view.php?view=day' . $courseid . '&', false, false, + false, false, $prevtimestamp); + $nextlink = calendar_get_link_next($nextname, 'view.php?view=day' . $courseid . '&', false, false, false, + false, $nexttimestamp); + + $content .= \html_writer::start_tag('div', array('class' => 'calendar-controls')); + $content .= $prevlink; + $content .= ' | ' .userdate($time, + get_string('strftimedaydate')) . ''; + $content .= ' | ' . $nextlink; + $content .= ""; + $content .= \html_writer::end_tag('div') . "\n"; + + break; + } + + return $content; +} + +/** + * Formats a filter control element. + * + * @param moodle_url $url of the filter + * @param int $type constant defining the type filter + * @return string html content of the element + */ +function calendar_filter_controls_element(moodle_url $url, $type) { + global $OUTPUT; + + switch ($type) { + case CALENDAR_EVENT_GLOBAL: + $typeforhumans = 'global'; + $class = 'calendar_event_global'; + break; + case CALENDAR_EVENT_COURSE: + $typeforhumans = 'course'; + $class = 'calendar_event_course'; + break; + case CALENDAR_EVENT_GROUP: + $typeforhumans = 'groups'; + $class = 'calendar_event_group'; + break; + case CALENDAR_EVENT_USER: + $typeforhumans = 'user'; + $class = 'calendar_event_user'; + break; + } + + if (calendar_show_event_type($type)) { + $icon = $OUTPUT->pix_icon('t/hide', get_string('hide')); + $str = get_string('hide' . $typeforhumans . 'events', 'calendar'); + } else { + $icon = $OUTPUT->pix_icon('t/show', get_string('show')); + $str = get_string('show' . $typeforhumans . 'events', 'calendar'); + } + $content = \html_writer::start_tag('li', array('class' => 'calendar_event')); + $content .= \html_writer::start_tag('a', array('href' => $url, 'rel' => 'nofollow')); + $content .= \html_writer::tag('span', $icon, array('class' => $class)); + $content .= \html_writer::tag('span', $str, array('class' => 'eventname')); + $content .= \html_writer::end_tag('a'); + $content .= \html_writer::end_tag('li'); + + return $content; +} + +/** + * Get the controls filter for calendar. + * + * Filter is used to hide calendar info from the display page. + * + + * @param moodle_url $returnurl return-url for filter controls + * @return string $content return filter controls in html + */ +function calendar_filter_controls(moodle_url $returnurl) { + $groupevents = true; + + $seturl = new \moodle_url('/calendar/set.php', array('return' => base64_encode($returnurl->out_as_local_url(false)), + 'sesskey' => sesskey())); + $content = \html_writer::start_tag('ul'); + + $seturl->param('var', 'showglobal'); + $content .= calendar_filter_controls_element($seturl, CALENDAR_EVENT_GLOBAL); + + $seturl->param('var', 'showcourses'); + $content .= calendar_filter_controls_element($seturl, CALENDAR_EVENT_COURSE); + + if (isloggedin() && !isguestuser()) { + if ($groupevents) { + // This course MIGHT have group events defined, so show the filter. + $seturl->param('var', 'showgroups'); + $content .= calendar_filter_controls_element($seturl, CALENDAR_EVENT_GROUP); + } + $seturl->param('var', 'showuser'); + $content .= calendar_filter_controls_element($seturl, CALENDAR_EVENT_USER); + } + $content .= \html_writer::end_tag('ul'); + + return $content; +} + +/** + * Return the representation day. + * + * @param int $tstamp Timestamp in GMT + * @param int|bool $now current Unix timestamp + * @param bool $usecommonwords + * @return string the formatted date/time + */ +function calendar_day_representation($tstamp, $now = false, $usecommonwords = true) { + static $shortformat; + + if (empty($shortformat)) { + $shortformat = get_string('strftimedayshort'); + } + + if ($now === false) { + $now = time(); + } + + // To have it in one place, if a change is needed. + $formal = userdate($tstamp, $shortformat); + + $datestamp = usergetdate($tstamp); + $datenow = usergetdate($now); + + if ($usecommonwords == false) { + // We don't want words, just a date. + return $formal; + } else if ($datestamp['year'] == $datenow['year'] && $datestamp['yday'] == $datenow['yday']) { + return get_string('today', 'calendar'); + } else if (($datestamp['year'] == $datenow['year'] && $datestamp['yday'] == $datenow['yday'] - 1 ) || + ($datestamp['year'] == $datenow['year'] - 1 && $datestamp['mday'] == 31 && $datestamp['mon'] == 12 + && $datenow['yday'] == 1)) { + return get_string('yesterday', 'calendar'); + } else if (($datestamp['year'] == $datenow['year'] && $datestamp['yday'] == $datenow['yday'] + 1 ) || + ($datestamp['year'] == $datenow['year'] + 1 && $datenow['mday'] == 31 && $datenow['mon'] == 12 + && $datestamp['yday'] == 1)) { + return get_string('tomorrow', 'calendar'); + } else { + return $formal; + } +} + +/** + * return the formatted representation time. + * + + * @param int $time the timestamp in UTC, as obtained from the database + * @return string the formatted date/time + */ +function calendar_time_representation($time) { + static $langtimeformat = null; + + if ($langtimeformat === null) { + $langtimeformat = get_string('strftimetime'); + } + + $timeformat = get_user_preferences('calendar_timeformat'); + if (empty($timeformat)) { + $timeformat = get_config(null, 'calendar_site_timeformat'); + } + + return userdate($time, empty($timeformat) ? $langtimeformat : $timeformat); +} + +/** + * Adds day, month, year arguments to a URL and returns a moodle_url object. + * + * @param string|moodle_url $linkbase + * @param int $d The number of the day. + * @param int $m The number of the month. + * @param int $y The number of the year. + * @param int $time the unixtime, used for multiple calendar support. The values $d, + * $m and $y are kept for backwards compatibility. + * @return moodle_url|null $linkbase + */ +function calendar_get_link_href($linkbase, $d, $m, $y, $time = 0) { + if (empty($linkbase)) { + return null; + } + + if (!($linkbase instanceof \moodle_url)) { + $linkbase = new \moodle_url($linkbase); + } + + // If a day, month and year were passed then convert it to a timestamp. If these were passed + // then we can assume the day, month and year are passed as Gregorian, as no where in core + // should we be passing these values rather than the time. + if (!empty($d) && !empty($m) && !empty($y)) { + if (checkdate($m, $d, $y)) { + $time = make_timestamp($y, $m, $d); + } else { + $time = time(); + } + } else if (empty($time)) { + $time = time(); + } + + $linkbase->param('time', $time); + + return $linkbase; +} + +/** + * Build and return a previous month HTML link, with an arrow. + * + * @param string $text The text label. + * @param string|moodle_url $linkbase The URL stub. + * @param int $d The number of the date. + * @param int $m The number of the month. + * @param int $y year The number of the year. + * @param bool $accesshide Default visible, or hide from all except screenreaders. + * @param int $time the unixtime, used for multiple calendar support. The values $d, + * $m and $y are kept for backwards compatibility. + * @return string HTML string. + */ +function calendar_get_link_previous($text, $linkbase, $d, $m, $y, $accesshide = false, $time = 0) { + $href = calendar_get_link_href(new \moodle_url($linkbase), $d, $m, $y, $time); + + if (empty($href)) { + return $text; + } + + return link_arrow_left($text, (string)$href, $accesshide, 'previous'); +} + +/** + * Build and return a next month HTML link, with an arrow. + * + * @param string $text The text label. + * @param string|moodle_url $linkbase The URL stub. + * @param int $d the number of the Day + * @param int $m The number of the month. + * @param int $y The number of the year. + * @param bool $accesshide Default visible, or hide from all except screenreaders. + * @param int $time the unixtime, used for multiple calendar support. The values $d, + * $m and $y are kept for backwards compatibility. + * @return string HTML string. + */ +function calendar_get_link_next($text, $linkbase, $d, $m, $y, $accesshide = false, $time = 0) { + $href = calendar_get_link_href(new \moodle_url($linkbase), $d, $m, $y, $time); + + if (empty($href)) { + return $text; + } + + return link_arrow_right($text, (string)$href, $accesshide, 'next'); +} + +/** + * Return the number of days in month. + * + * @param int $month the number of the month. + * @param int $year the number of the year + * @return int + */ +function calendar_days_in_month($month, $year) { + $calendartype = \core_calendar\type_factory::get_calendar_instance(); + return $calendartype->get_num_days_in_month($year, $month); +} + +/** + * Get the next following month. + * + * @param int $month the number of the month. + * @param int $year the number of the year. + * @return array the following month + */ +function calendar_add_month($month, $year) { + $calendartype = \core_calendar\type_factory::get_calendar_instance(); + return $calendartype->get_next_month($year, $month); +} + +/** + * Get the previous month. + * + * @param int $month the number of the month. + * @param int $year the number of the year. + * @return array previous month + */ +function calendar_sub_month($month, $year) { + $calendartype = \core_calendar\type_factory::get_calendar_instance(); + return $calendartype->get_prev_month($year, $month); +} + +/** + * Get per-day basis events + * + * @param array $events list of events + * @param int $month the number of the month + * @param int $year the number of the year + * @param array $eventsbyday event on specific day + * @param array $durationbyday duration of the event in days + * @param array $typesbyday event type (eg: global, course, user, or group) + * @param array $courses list of courses + * @return void + */ +function calendar_events_by_day($events, $month, $year, &$eventsbyday, &$durationbyday, &$typesbyday, &$courses) { + $calendartype = \core_calendar\type_factory::get_calendar_instance(); + + $eventsbyday = array(); + $typesbyday = array(); + $durationbyday = array(); + + if ($events === false) { + return; + } + + foreach ($events as $event) { + $startdate = $calendartype->timestamp_to_date_array($event->timestart); + if ($event->timeduration) { + $enddate = $calendartype->timestamp_to_date_array($event->timestart + $event->timeduration - 1); + } else { + $enddate = $startdate; + } + + // Simple arithmetic: $year * 13 + $month is a distinct integer for each distinct ($year, $month) pair. + if (!($startdate['year'] * 13 + $startdate['mon'] <= $year * 13 + $month) && + ($enddate['year'] * 13 + $enddate['mon'] >= $year * 13 + $month)) { + continue; + } + + $eventdaystart = intval($startdate['mday']); + + if ($startdate['mon'] == $month && $startdate['year'] == $year) { + // Give the event to its day. + $eventsbyday[$eventdaystart][] = $event->id; + + // Mark the day as having such an event. + if ($event->courseid == SITEID && $event->groupid == 0) { + $typesbyday[$eventdaystart]['startglobal'] = true; + // Set event class for global event. + $events[$event->id]->class = 'calendar_event_global'; + } else if ($event->courseid != 0 && $event->courseid != SITEID && $event->groupid == 0) { + $typesbyday[$eventdaystart]['startcourse'] = true; + // Set event class for course event. + $events[$event->id]->class = 'calendar_event_course'; + } else if ($event->groupid) { + $typesbyday[$eventdaystart]['startgroup'] = true; + // Set event class for group event. + $events[$event->id]->class = 'calendar_event_group'; + } else if ($event->userid) { + $typesbyday[$eventdaystart]['startuser'] = true; + // Set event class for user event. + $events[$event->id]->class = 'calendar_event_user'; + } + } + + if ($event->timeduration == 0) { + // Proceed with the next. + continue; + } + + // The event starts on $month $year or before. + if ($startdate['mon'] == $month && $startdate['year'] == $year) { + $lowerbound = intval($startdate['mday']); + } else { + $lowerbound = 0; + } + + // Also, it ends on $month $year or later. + if ($enddate['mon'] == $month && $enddate['year'] == $year) { + $upperbound = intval($enddate['mday']); + } else { + $upperbound = calendar_days_in_month($month, $year); + } + + // Mark all days between $lowerbound and $upperbound (inclusive) as duration. + for ($i = $lowerbound + 1; $i <= $upperbound; ++$i) { + $durationbyday[$i][] = $event->id; + if ($event->courseid == SITEID && $event->groupid == 0) { + $typesbyday[$i]['durationglobal'] = true; + } else if ($event->courseid != 0 && $event->courseid != SITEID && $event->groupid == 0) { + $typesbyday[$i]['durationcourse'] = true; + } else if ($event->groupid) { + $typesbyday[$i]['durationgroup'] = true; + } else if ($event->userid) { + $typesbyday[$i]['durationuser'] = true; + } + } + + } + return; +} + +/** + * Returns the courses to load events for. + * + * @param array $courseeventsfrom An array of courses to load calendar events for + * @param bool $ignorefilters specify the use of filters, false is set as default + * @return array An array of courses, groups, and user to load calendar events for based upon filters + */ +function calendar_set_filters(array $courseeventsfrom, $ignorefilters = false) { + global $USER, $CFG; + + // For backwards compatability we have to check whether the courses array contains + // just id's in which case we need to load course objects. + $coursestoload = array(); + foreach ($courseeventsfrom as $id => $something) { + if (!is_object($something)) { + $coursestoload[] = $id; + unset($courseeventsfrom[$id]); + } + } + + $courses = array(); + $user = false; + $group = false; + + // Get the capabilities that allow seeing group events from all groups. + $allgroupscaps = array('moodle/site:accessallgroups', 'moodle/calendar:manageentries'); + + $isloggedin = isloggedin(); + + if ($ignorefilters || calendar_show_event_type(CALENDAR_EVENT_COURSE)) { + $courses = array_keys($courseeventsfrom); + } + if ($ignorefilters || calendar_show_event_type(CALENDAR_EVENT_GLOBAL)) { + $courses[] = SITEID; + } + $courses = array_unique($courses); + sort($courses); + + if (!empty($courses) && in_array(SITEID, $courses)) { + // Sort courses for consistent colour highlighting. + // Effectively ignoring SITEID as setting as last course id. + $key = array_search(SITEID, $courses); + unset($courses[$key]); + $courses[] = SITEID; + } + + if ($ignorefilters || ($isloggedin && calendar_show_event_type(CALENDAR_EVENT_USER))) { + $user = $USER->id; + } + + if (!empty($courseeventsfrom) && (calendar_show_event_type(CALENDAR_EVENT_GROUP) || $ignorefilters)) { + + if (count($courseeventsfrom) == 1) { + $course = reset($courseeventsfrom); + if (has_any_capability($allgroupscaps, \context_course::instance($course->id))) { + $coursegroups = groups_get_all_groups($course->id, 0, 0, 'g.id'); + $group = array_keys($coursegroups); + } + } + if ($group === false) { + if (!empty($CFG->calendar_adminseesall) && has_any_capability($allgroupscaps, \context_system::instance())) { + $group = true; + } else if ($isloggedin) { + $groupids = array(); + foreach ($courseeventsfrom as $courseid => $course) { + // If the user is an editing teacher in there. + if (!empty($USER->groupmember[$course->id])) { + // We've already cached the users groups for this course so we can just use that. + $groupids = array_merge($groupids, $USER->groupmember[$course->id]); + } else if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) { + // If this course has groups, show events from all of those related to the current user. + $coursegroups = groups_get_user_groups($course->id, $USER->id); + $groupids = array_merge($groupids, $coursegroups['0']); + } + } + if (!empty($groupids)) { + $group = $groupids; + } + } + } + } + if (empty($courses)) { + $courses = false; + } + + return array($courses, $group, $user); +} + +/** + * Return the capability for editing calendar event. + * + * @param calendar_event $event event object + * @return bool capability to edit event + */ +function calendar_edit_event_allowed($event) { + global $USER, $DB; + + // Must be logged in. + if (!isloggedin()) { + return false; + } + + // Can not be using guest account. + if (isguestuser()) { + return false; + } + + // You cannot edit URL based calendar subscription events presently. + if (!empty($event->subscriptionid)) { + if (!empty($event->subscription->url)) { + // This event can be updated externally, so it cannot be edited. + return false; + } + } + + $sitecontext = \context_system::instance(); + + // If user has manageentries at site level, return true. + if (has_capability('moodle/calendar:manageentries', $sitecontext)) { + return true; + } + + // If groupid is set, it's definitely a group event. + if (!empty($event->groupid)) { + // Allow users to add/edit group events if - + // 1) They have manageentries for the course OR + // 2) They have managegroupentries AND are in the group. + $group = $DB->get_record('groups', array('id' => $event->groupid)); + return $group && ( + has_capability('moodle/calendar:manageentries', $event->context) || + (has_capability('moodle/calendar:managegroupentries', $event->context) + && groups_is_member($event->groupid))); + } else if (!empty($event->courseid)) { + // If groupid is not set, but course is set, it's definiely a course event. + return has_capability('moodle/calendar:manageentries', $event->context); + } else if (!empty($event->userid) && $event->userid == $USER->id) { + // If course is not set, but userid id set, it's a user event. + return (has_capability('moodle/calendar:manageownentries', $event->context)); + } else if (!empty($event->userid)) { + return (has_capability('moodle/calendar:manageentries', $event->context)); + } + + return false; +} + +/** + * Returns the default courses to display on the calendar when there isn't a specific + * course to display. + * + * @return array $courses Array of courses to display + */ +function calendar_get_default_courses() { + global $CFG, $DB; + + if (!isloggedin()) { + return array(); + } + + if (!empty($CFG->calendar_adminseesall) && has_capability('moodle/calendar:manageentries', \context_system::instance())) { + $select = ', ' . \context_helper::get_preload_record_columns_sql('ctx'); + $join = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)"; + $sql = "SELECT c.* $select + FROM {course} c + $join + WHERE EXISTS (SELECT 1 FROM {event} e WHERE e.courseid = c.id) + "; + $courses = $DB->get_records_sql($sql, array('contextlevel' => CONTEXT_COURSE), 0, 20); + foreach ($courses as $course) { + \context_helper::preload_from_record($course); + } + return $courses; + } + + $courses = enrol_get_my_courses(); + + return $courses; +} + +/** + * Get event format time. + * + * @param calendar_event $event event object + * @param int $now current time in gmt + * @param array $linkparams list of params for event link + * @param bool $usecommonwords the words as formatted date/time. + * @param int $showtime determine the show time GMT timestamp + * @return string $eventtime link/string for event time + */ +function calendar_format_event_time($event, $now, $linkparams = null, $usecommonwords = true, $showtime = 0) { + $starttime = $event->timestart; + $endtime = $event->timestart + $event->timeduration; + + if (empty($linkparams) || !is_array($linkparams)) { + $linkparams = array(); + } + + $linkparams['view'] = 'day'; + + // OK, now to get a meaningful display. + // Check if there is a duration for this event. + if ($event->timeduration) { + // Get the midnight of the day the event will start. + $usermidnightstart = usergetmidnight($starttime); + // Get the midnight of the day the event will end. + $usermidnightend = usergetmidnight($endtime); + // Check if we will still be on the same day. + if ($usermidnightstart == $usermidnightend) { + // Check if we are running all day. + if ($event->timeduration == DAYSECS) { + $time = get_string('allday', 'calendar'); + } else { // Specify the time we will be running this from. + $datestart = calendar_time_representation($starttime); + $dateend = calendar_time_representation($endtime); + $time = $datestart . ' » ' . $dateend; + } + + // Set printable representation. + if (!$showtime) { + $day = calendar_day_representation($event->timestart, $now, $usecommonwords); + $url = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', $linkparams), 0, 0, 0, $endtime); + $eventtime = \html_writer::link($url, $day) . ', ' . $time; + } else { + $eventtime = $time; + } + } else { // It must spans two or more days. + $daystart = calendar_day_representation($event->timestart, $now, $usecommonwords) . ', '; + if ($showtime == $usermidnightstart) { + $daystart = ''; + } + $timestart = calendar_time_representation($event->timestart); + $dayend = calendar_day_representation($event->timestart + $event->timeduration, $now, $usecommonwords) . ', '; + if ($showtime == $usermidnightend) { + $dayend = ''; + } + $timeend = calendar_time_representation($event->timestart + $event->timeduration); + + // Set printable representation. + if ($now >= $usermidnightstart && $now < strtotime('+1 day', $usermidnightstart)) { + $url = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', $linkparams), 0, 0, 0, $endtime); + $eventtime = $timestart . ' » ' . \html_writer::link($url, $dayend) . $timeend; + } else { + // The event is in the future, print start and end links. + $url = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', $linkparams), 0, 0, 0, $starttime); + $eventtime = \html_writer::link($url, $daystart) . $timestart . ' » '; + + $url = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', $linkparams), 0, 0, 0, $endtime); + $eventtime .= \html_writer::link($url, $dayend) . $timeend; + } + } + } else { // There is no time duration. + $time = calendar_time_representation($event->timestart); + // Set printable representation. + if (!$showtime) { + $day = calendar_day_representation($event->timestart, $now, $usecommonwords); + $url = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', $linkparams), 0, 0, 0, $starttime); + $eventtime = \html_writer::link($url, $day) . ', ' . trim($time); + } else { + $eventtime = $time; + } + } + + // Check if It has expired. + if ($event->timestart + $event->timeduration < $now) { + $eventtime = '' . str_replace(' href=', ' class="dimmed" href=', $eventtime) . ''; + } + + return $eventtime; +} + +/** + * Checks to see if the requested type of event should be shown for the given user. + * + * @param int $type The type to check the display for (default is to display all) + * @param stdClass|int|null $user The user to check for - by default the current user + * @return bool True if the tyep should be displayed false otherwise + */ +function calendar_show_event_type($type, $user = null) { + $default = CALENDAR_EVENT_GLOBAL + CALENDAR_EVENT_COURSE + CALENDAR_EVENT_GROUP + CALENDAR_EVENT_USER; + + if (get_user_preferences('calendar_persistflt', 0, $user) === 0) { + global $SESSION; + if (!isset($SESSION->calendarshoweventtype)) { + $SESSION->calendarshoweventtype = $default; + } + return $SESSION->calendarshoweventtype & $type; + } else { + return get_user_preferences('calendar_savedflt', $default, $user) & $type; + } +} + +/** + * Sets the display of the event type given $display. + * + * If $display = true the event type will be shown. + * If $display = false the event type will NOT be shown. + * If $display = null the current value will be toggled and saved. + * + * @param int $type object of CALENDAR_EVENT_XXX + * @param bool $display option to display event type + * @param stdClass|int $user moodle user object or id, null means current user + */ +function calendar_set_event_type_display($type, $display = null, $user = null) { + $persist = get_user_preferences('calendar_persistflt', 0, $user); + $default = CALENDAR_EVENT_GLOBAL + CALENDAR_EVENT_COURSE + CALENDAR_EVENT_GROUP + CALENDAR_EVENT_USER; + if ($persist === 0) { + global $SESSION; + if (!isset($SESSION->calendarshoweventtype)) { + $SESSION->calendarshoweventtype = $default; + } + $preference = $SESSION->calendarshoweventtype; + } else { + $preference = get_user_preferences('calendar_savedflt', $default, $user); + } + $current = $preference & $type; + if ($display === null) { + $display = !$current; + } + if ($display && !$current) { + $preference += $type; + } else if (!$display && $current) { + $preference -= $type; + } + if ($persist === 0) { + $SESSION->calendarshoweventtype = $preference; + } else { + if ($preference == $default) { + unset_user_preference('calendar_savedflt', $user); + } else { + set_user_preference('calendar_savedflt', $preference, $user); + } + } +} + +/** + * Get calendar's allowed types. + * + * @param stdClass $allowed list of allowed edit for event type + * @param stdClass|int $course object of a course or course id + */ +function calendar_get_allowed_types(&$allowed, $course = null) { + global $USER, $DB; + + $allowed = new \stdClass(); + $allowed->user = has_capability('moodle/calendar:manageownentries', \context_system::instance()); + $allowed->groups = false; + $allowed->courses = false; + $allowed->site = has_capability('moodle/calendar:manageentries', \context_course::instance(SITEID)); + + if (!empty($course)) { + if (!is_object($course)) { + $course = $DB->get_record('course', array('id' => $course), '*', MUST_EXIST); + } + if ($course->id != SITEID) { + $coursecontext = \context_course::instance($course->id); + $allowed->user = has_capability('moodle/calendar:manageownentries', $coursecontext); + + if (has_capability('moodle/calendar:manageentries', $coursecontext)) { + $allowed->courses = array($course->id => 1); + + if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) { + if (has_capability('moodle/site:accessallgroups', $coursecontext)) { + $allowed->groups = groups_get_all_groups($course->id); + } else { + $allowed->groups = groups_get_all_groups($course->id, $USER->id); + } + } + } else if (has_capability('moodle/calendar:managegroupentries', $coursecontext)) { + if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) { + if (has_capability('moodle/site:accessallgroups', $coursecontext)) { + $allowed->groups = groups_get_all_groups($course->id); + } else { + $allowed->groups = groups_get_all_groups($course->id, $USER->id); + } + } + } + } + } +} + +/** + * See if user can add calendar entries at all used to print the "New Event" button. + * + * @param stdClass $course object of a course or course id + * @return bool has the capability to add at least one event type + */ +function calendar_user_can_add_event($course) { + if (!isloggedin() || isguestuser()) { + return false; + } + + calendar_get_allowed_types($allowed, $course); + + return (bool)($allowed->user || $allowed->groups || $allowed->courses || $allowed->site); +} + +/** + * Check wether the current user is permitted to add events. + * + * @param stdClass $event object of event + * @return bool has the capability to add event + */ +function calendar_add_event_allowed($event) { + global $USER, $DB; + + // Can not be using guest account. + if (!isloggedin() or isguestuser()) { + return false; + } + + $sitecontext = \context_system::instance(); + + // If user has manageentries at site level, always return true. + if (has_capability('moodle/calendar:manageentries', $sitecontext)) { + return true; + } + + switch ($event->eventtype) { + case 'course': + return has_capability('moodle/calendar:manageentries', $event->context); + case 'group': + // Allow users to add/edit group events if - + // 1) They have manageentries (= entries for whole course). + // 2) They have managegroupentries AND are in the group. + $group = $DB->get_record('groups', array('id' => $event->groupid)); + return $group && ( + has_capability('moodle/calendar:manageentries', $event->context) || + (has_capability('moodle/calendar:managegroupentries', $event->context) + && groups_is_member($event->groupid))); + case 'user': + if ($event->userid == $USER->id) { + return (has_capability('moodle/calendar:manageownentries', $event->context)); + } + // There is intentionally no 'break'. + case 'site': + return has_capability('moodle/calendar:manageentries', $event->context); + default: + return has_capability('moodle/calendar:manageentries', $event->context); + } +} + /** * Returns option list for the poll interval setting. * @@ -2982,12 +2889,12 @@ class calendar_information { */ function calendar_get_pollinterval_choices() { return array( - '0' => new lang_string('never', 'calendar'), - HOURSECS => new lang_string('hourly', 'calendar'), - DAYSECS => new lang_string('daily', 'calendar'), - WEEKSECS => new lang_string('weekly', 'calendar'), - '2628000' => new lang_string('monthly', 'calendar'), - YEARSECS => new lang_string('annually', 'calendar') + '0' => new \lang_string('never', 'calendar'), + HOURSECS => new \lang_string('hourly', 'calendar'), + DAYSECS => new \lang_string('daily', 'calendar'), + WEEKSECS => new \lang_string('weekly', 'calendar'), + '2628000' => new \lang_string('monthly', 'calendar'), + YEARSECS => new \lang_string('annually', 'calendar') ); } @@ -2999,7 +2906,7 @@ function calendar_get_pollinterval_choices() { */ function calendar_get_eventtype_choices($courseid) { $choices = array(); - $allowed = new stdClass; + $allowed = new \stdClass; calendar_get_allowed_types($allowed, $courseid); if ($allowed->user) { @@ -3045,7 +2952,7 @@ function calendar_add_subscription($sub) { if (!empty($sub->name)) { if (empty($sub->id)) { $id = $DB->insert_record('event_subscriptions', $sub); - // we cannot cache the data here because $sub is not complete. + // We cannot cache the data here because $sub is not complete. $sub->id = $id; // Trigger event, calendar subscription added. $eventparams = array('objectid' => $sub->id, @@ -3088,7 +2995,7 @@ function calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timez $name = str_replace('\\', '', $name); $name = preg_replace('/\s+/u', ' ', $name); - $eventrecord = new stdClass; + $eventrecord = new \stdClass; $eventrecord->name = clean_param($name, PARAM_NOTAGS); if (empty($event->properties['DESCRIPTION'][0]->value)) { @@ -3107,16 +3014,22 @@ function calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timez return 0; } - $tz = isset($event->properties['DTSTART'][0]->parameters['TZID']) ? $event->properties['DTSTART'][0]->parameters['TZID'] : - $timezone; - $tz = core_date::normalise_timezone($tz); + if (isset($event->properties['DTSTART'][0]->parameters['TZID'])) { + $tz = $event->properties['DTSTART'][0]->parameters['TZID']; + } else { + $tz = $timezone; + } + $tz = \core_date::normalise_timezone($tz); $eventrecord->timestart = strtotime($event->properties['DTSTART'][0]->value . ' ' . $tz); if (empty($event->properties['DTEND'])) { - $eventrecord->timeduration = 0; // no duration if no end time specified + $eventrecord->timeduration = 0; // No duration if no end time specified. } else { - $endtz = isset($event->properties['DTEND'][0]->parameters['TZID']) ? $event->properties['DTEND'][0]->parameters['TZID'] : - $timezone; - $endtz = core_date::normalise_timezone($endtz); + if (isset($event->properties['DTEND'][0]->parameters['TZID'])) { + $endtz = $event->properties['DTEND'][0]->parameters['TZID']; + } else { + $endtz = $timezone; + } + $endtz = \core_date::normalise_timezone($endtz); $eventrecord->timeduration = strtotime($event->properties['DTEND'][0]->value . ' ' . $endtz) - $eventrecord->timestart; } @@ -3128,7 +3041,7 @@ function calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timez // This event should be an all day event. $eventrecord->timeduration = 0; } - core_date::set_default_server_timezone(); + \core_date::set_default_server_timezone(); } $eventrecord->uuid = $event->properties['UID'][0]->value; @@ -3143,20 +3056,21 @@ function calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timez $eventrecord->courseid = $sub->courseid; $eventrecord->eventtype = $sub->eventtype; - if ($updaterecord = $DB->get_record('event', array('uuid' => $eventrecord->uuid, 'subscriptionid' => $eventrecord->subscriptionid))) { + if ($updaterecord = $DB->get_record('event', array('uuid' => $eventrecord->uuid, + 'subscriptionid' => $eventrecord->subscriptionid))) { $eventrecord->id = $updaterecord->id; $return = CALENDAR_IMPORT_EVENT_UPDATED; // Update. } else { $return = CALENDAR_IMPORT_EVENT_INSERTED; // Insert. } - if ($createdevent = calendar_event::create($eventrecord, false)) { + if ($createdevent = \calendar_event::create($eventrecord, false)) { if (!empty($event->properties['RRULE'])) { // Repeating events. date_default_timezone_set($tz); // Change time zone to parse all events. - $rrule = new \core_calendar\rrule_manager($event->properties['RRULE'][0]->value); + $rrule = new rrule_manager($event->properties['RRULE'][0]->value); $rrule->parse_rrule(); $rrule->create_events($createdevent); - core_date::set_default_server_timezone(); // Change time zone back to what it was. + \core_date::set_default_server_timezone(); // Change time zone back to what it was. } return $return; } else { @@ -3174,7 +3088,6 @@ function calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timez * @return string A log of the import progress, including errors */ function calendar_process_subscription_row($subscriptionid, $pollinterval, $action) { - // Fetch the subscription from the database making sure it exists. $sub = calendar_get_subscription($subscriptionid); @@ -3189,13 +3102,12 @@ function calendar_process_subscription_row($subscriptionid, $pollinterval, $acti calendar_update_subscription($sub); // Update the events. - return "

    ".get_string('subscriptionupdated', 'calendar', $sub->name)."

    " . calendar_update_subscription_events($subscriptionid); - + return "

    " . get_string('subscriptionupdated', 'calendar', $sub->name) . "

    " . + calendar_update_subscription_events($subscriptionid); case CALENDAR_SUBSCRIPTION_REMOVE: calendar_delete_subscription($subscriptionid); return get_string('subscriptionremoved', 'calendar', $sub->name); break; - default: break; } @@ -3213,10 +3125,11 @@ function calendar_delete_subscription($subscription) { if (!is_object($subscription)) { $subscription = $DB->get_record('event_subscriptions', array('id' => $subscription), '*', MUST_EXIST); } + // Delete subscription and related events. $DB->delete_records('event', array('subscriptionid' => $subscription->id)); $DB->delete_records('event_subscriptions', array('id' => $subscription->id)); - cache_helper::invalidate_by_definition('core', 'calendar_subscriptions', array(), array($subscription->id)); + \cache_helper::invalidate_by_definition('core', 'calendar_subscriptions', array(), array($subscription->id)); // Trigger event, calendar subscription deleted. $eventparams = array('objectid' => $subscription->id, @@ -3226,47 +3139,51 @@ function calendar_delete_subscription($subscription) { $event = \core\event\calendar_subscription_deleted::create($eventparams); $event->trigger(); } + /** * From a URL, fetch the calendar and return an iCalendar object. * * @param string $url The iCalendar URL - * @return stdClass The iCalendar object + * @return iCalendar The iCalendar object */ function calendar_get_icalendar($url) { global $CFG; - require_once($CFG->libdir.'/filelib.php'); + require_once($CFG->libdir . '/filelib.php'); - $curl = new curl(); + $curl = new \curl(); $curl->setopt(array('CURLOPT_FOLLOWLOCATION' => 1, 'CURLOPT_MAXREDIRS' => 5)); $calendar = $curl->get($url); + // Http code validation should actually be the job of curl class. if (!$calendar || $curl->info['http_code'] != 200 || !empty($curl->errorno)) { - throw new moodle_exception('errorinvalidicalurl', 'calendar'); + throw new \moodle_exception('errorinvalidicalurl', 'calendar'); } - $ical = new iCalendar(); + $ical = new \iCalendar(); $ical->unserialize($calendar); + return $ical; } /** * Import events from an iCalendar object into a course calendar. * - * @param stdClass $ical The iCalendar object. + * @param iCalendar $ical The iCalendar object. * @param int $courseid The course ID for the calendar. * @param int $subscriptionid The subscription ID. * @return string A log of the import progress, including errors. */ function calendar_import_icalendar_events($ical, $courseid, $subscriptionid = null) { global $DB; + $return = ''; $eventcount = 0; $updatecount = 0; // Large calendars take a while... if (!CLI_SCRIPT) { - core_php_time_limit::raise(300); + \core_php_time_limit::raise(300); } // Mark all events in a subscription with a zero timestamp. @@ -3274,36 +3191,45 @@ function calendar_import_icalendar_events($ical, $courseid, $subscriptionid = nu $sql = "UPDATE {event} SET timemodified = :time WHERE subscriptionid = :id"; $DB->execute($sql, array('time' => 0, 'id' => $subscriptionid)); } + // Grab the timezone from the iCalendar file to be used later. if (isset($ical->properties['X-WR-TIMEZONE'][0]->value)) { $timezone = $ical->properties['X-WR-TIMEZONE'][0]->value; } else { $timezone = 'UTC'; } + + $return = ''; foreach ($ical->components['VEVENT'] as $event) { $res = calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timezone); switch ($res) { - case CALENDAR_IMPORT_EVENT_UPDATED: - $updatecount++; - break; - case CALENDAR_IMPORT_EVENT_INSERTED: - $eventcount++; - break; - case 0: - $return .= '

    '.get_string('erroraddingevent', 'calendar').': '.(empty($event->properties['SUMMARY'])?'('.get_string('notitle', 'calendar').')':$event->properties['SUMMARY'][0]->value)."

    \n"; - break; + case CALENDAR_IMPORT_EVENT_UPDATED: + $updatecount++; + break; + case CALENDAR_IMPORT_EVENT_INSERTED: + $eventcount++; + break; + case 0: + $return .= '

    ' . get_string('erroraddingevent', 'calendar') . ': '; + if (empty($event->properties['SUMMARY'])) { + $return .= '(' . get_string('notitle', 'calendar') . ')'; + } else { + $return .= $event->properties['SUMMARY'][0]->value; + } + $return .= "

    \n"; + break; } } - $return .= "

    ".get_string('eventsimported', 'calendar', $eventcount)."

    "; - $return .= "

    ".get_string('eventsupdated', 'calendar', $updatecount)."

    "; + + $return .= "

    " . get_string('eventsimported', 'calendar', $eventcount) . "

    "; + $return .= "

    " . get_string('eventsupdated', 'calendar', $updatecount) . "

    "; // Delete remaining zero-marked events since they're not in remote calendar. if (!empty($subscriptionid)) { $deletecount = $DB->count_records('event', array('timemodified' => 0, 'subscriptionid' => $subscriptionid)); if (!empty($deletecount)) { - $sql = "DELETE FROM {event} WHERE timemodified = :time AND subscriptionid = :id"; - $DB->execute($sql, array('time' => 0, 'id' => $subscriptionid)); - $return .= "

    ".get_string('eventsdeleted', 'calendar').": {$deletecount}

    \n"; + $DB->delete_records('event', array('timemodified' => 0, 'subscriptionid' => $subscriptionid)); + $return .= "

    " . get_string('eventsdeleted', 'calendar') . ": {$deletecount}

    \n"; } } @@ -3317,17 +3243,19 @@ function calendar_import_icalendar_events($ical, $courseid, $subscriptionid = nu * @return string A log of the import progress, including errors. */ function calendar_update_subscription_events($subscriptionid) { - global $DB; - $sub = calendar_get_subscription($subscriptionid); - // Don't update a file subscription. TODO: Update from a new uploaded file. + + // Don't update a file subscription. if (empty($sub->url)) { return 'File subscription not updated.'; } + $ical = calendar_get_icalendar($sub->url); $return = calendar_import_icalendar_events($ical, $sub->courseid, $subscriptionid); $sub->lastupdated = time(); + calendar_update_subscription($sub); + return $return; } @@ -3345,19 +3273,21 @@ function calendar_update_subscription($subscription) { $subscription = (object)$subscription; } if (empty($subscription->id) || !$DB->record_exists('event_subscriptions', array('id' => $subscription->id))) { - throw new coding_exception('Cannot update a subscription without a valid id'); + throw new \coding_exception('Cannot update a subscription without a valid id'); } $DB->update_record('event_subscriptions', $subscription); + // Update cache. - $cache = cache::make('core', 'calendar_subscriptions'); + $cache = \cache::make('core', 'calendar_subscriptions'); $cache->set($subscription->id, $subscription); + // Trigger event, calendar subscription updated. $eventparams = array('userid' => $subscription->userid, 'objectid' => $subscription->id, 'context' => calendar_get_calendar_context($subscription), 'other' => array('eventtype' => $subscription->eventtype, 'courseid' => $subscription->courseid) - ); + ); $event = \core\event\calendar_subscription_updated::create($eventparams); $event->trigger(); } @@ -3369,8 +3299,6 @@ function calendar_update_subscription($subscription) { * @return bool true if current user can edit the subscription else false */ function calendar_can_edit_subscription($subscriptionorid) { - global $DB; - if (is_array($subscriptionorid)) { $subscription = (object)$subscriptionorid; } else if (is_object($subscriptionorid)) { @@ -3378,9 +3306,11 @@ function calendar_can_edit_subscription($subscriptionorid) { } else { $subscription = calendar_get_subscription($subscriptionorid); } - $allowed = new stdClass; + + $allowed = new \stdClass; $courseid = $subscription->courseid; $groupid = $subscription->groupid; + calendar_get_allowed_types($allowed, $courseid); switch ($subscription->eventtype) { case 'user': @@ -3404,37 +3334,6 @@ function calendar_can_edit_subscription($subscriptionorid) { } } -/** - * Update calendar subscriptions. - * - * @return bool - */ -function calendar_cron() { - global $CFG, $DB; - - // In order to execute this we need bennu. - require_once($CFG->libdir.'/bennu/bennu.inc.php'); - - mtrace('Updating calendar subscriptions:'); - cron_trace_time_and_memory(); - - $time = time(); - $subscriptions = $DB->get_records_sql('SELECT * FROM {event_subscriptions} WHERE pollinterval > 0 AND lastupdated + pollinterval < ?', array($time)); - foreach ($subscriptions as $sub) { - mtrace("Updating calendar subscription {$sub->name} in course {$sub->courseid}"); - try { - $log = calendar_update_subscription_events($sub->id); - mtrace(trim(strip_tags($log))); - } catch (moodle_exception $ex) { - mtrace('Error updating calendar subscription: ' . $ex->getMessage()); - } - } - - mtrace('Finished updating calendar subscriptions.'); - - return true; -} - /** * Helper function to determine the context of a calendar subscription. * Subscriptions can be created in two contexts COURSE, or USER. @@ -3443,14 +3342,13 @@ function calendar_cron() { * @return context instance */ function calendar_get_calendar_context($subscription) { - // Determine context based on calendar type. if ($subscription->eventtype === 'site') { - $context = context_course::instance(SITEID); + $context = \context_course::instance(SITEID); } else if ($subscription->eventtype === 'group' || $subscription->eventtype === 'course') { - $context = context_course::instance($subscription->courseid); + $context = \context_course::instance($subscription->courseid); } else { - $context = context_user::instance($subscription->userid); + $context = \context_user::instance($subscription->userid); } return $context; } diff --git a/calendar/renderer.php b/calendar/renderer.php index c68ca73608b..014262c15a5 100644 --- a/calendar/renderer.php +++ b/calendar/renderer.php @@ -64,7 +64,8 @@ class core_calendar_renderer extends plugin_renderer_base { public function fake_block_filters($courseid, $day, $month, $year, $view, $courses) { $returnurl = $this->page->url; $returnurl->param('course', $courseid); - return html_writer::tag('div', calendar_filter_controls($returnurl), array('class'=>'calendar_filters filters')); + return html_writer::tag('div', calendar_filter_controls($returnurl), + array('class' => 'calendar_filters filters')); } /** @@ -92,13 +93,16 @@ class core_calendar_renderer extends plugin_renderer_base { $nextmonthtime['hour'], $nextmonthtime['minute']); $content = html_writer::start_tag('div', array('class' => 'minicalendarblock')); - $content .= calendar_get_mini($calendar->courses, $calendar->groups, $calendar->users, false, false, 'display', $calendar->courseid, $prevmonthtime); + $content .= calendar_get_mini($calendar->courses, $calendar->groups, $calendar->users, + false, false, 'display', $calendar->courseid, $prevmonthtime); $content .= html_writer::end_tag('div'); $content .= html_writer::start_tag('div', array('class' => 'minicalendarblock')); - $content .= calendar_get_mini($calendar->courses, $calendar->groups, $calendar->users, false, false, 'display', $calendar->courseid, $calendar->time); + $content .= calendar_get_mini($calendar->courses, $calendar->groups, $calendar->users, + false, false, 'display', $calendar->courseid, $calendar->time); $content .= html_writer::end_tag('div'); $content .= html_writer::start_tag('div', array('class' => 'minicalendarblock')); - $content .= calendar_get_mini($calendar->courses, $calendar->groups, $calendar->users, false, false, 'display', $calendar->courseid, $nextmonthtime); + $content .= calendar_get_mini($calendar->courses, $calendar->groups, $calendar->users, + false, false, 'display', $calendar->courseid, $nextmonthtime); $content .= html_writer::end_tag('div'); return $content; } @@ -164,7 +168,8 @@ class core_calendar_renderer extends plugin_renderer_base { $returnurl = $this->page->url; } - $events = calendar_get_upcoming($calendar->courses, $calendar->groups, $calendar->users, 1, 100, $calendar->timestamp_today()); + $events = calendar_get_upcoming($calendar->courses, $calendar->groups, $calendar->users, + 1, 100, $calendar->timestamp_today()); $output = html_writer::start_tag('div', array('class'=>'header')); $output .= $this->course_filter_selector($returnurl, get_string('dayviewfor', 'calendar')); @@ -173,7 +178,8 @@ class core_calendar_renderer extends plugin_renderer_base { } $output .= html_writer::end_tag('div'); // Controls - $output .= html_writer::tag('div', calendar_top_controls('day', array('id' => $calendar->courseid, 'time' => $calendar->time)), array('class'=>'controls')); + $output .= html_writer::tag('div', calendar_top_controls('day', array('id' => $calendar->courseid, + 'time' => $calendar->time)), array('class' => 'controls')); if (empty($events)) { // There is nothing to display today. @@ -186,7 +192,8 @@ class core_calendar_renderer extends plugin_renderer_base { $event = new calendar_event($event); $event->calendarcourseid = $calendar->courseid; if ($event->timestart >= $calendar->timestamp_today() && $event->timestart <= $calendar->timestamp_tomorrow()-1) { // Print it now - $event->time = calendar_format_event_time($event, time(), null, false, $calendar->timestamp_today()); + $event->time = calendar_format_event_time($event, time(), null, false, + $calendar->timestamp_today()); $output .= $this->event($event); } else { // Save this for later $underway[] = $event; @@ -198,7 +205,8 @@ class core_calendar_renderer extends plugin_renderer_base { $output .= html_writer::span(get_string('spanningevents', 'calendar'), 'calendar-information calendar-span-multiple-days'); foreach ($underway as $event) { - $event->time = calendar_format_event_time($event, time(), null, false, $calendar->timestamp_today()); + $event->time = calendar_format_event_time($event, time(), null, false, + $calendar->timestamp_today()); $output .= $this->event($event); } } @@ -363,7 +371,8 @@ class core_calendar_renderer extends plugin_renderer_base { } // Get events from database - $events = calendar_get_events($display->tstart, $display->tend, $calendar->users, $calendar->groups, $calendar->courses); + $events = \core_calendar\local\api::get_legacy_events($display->tstart, $display->tend, $calendar->users, $calendar->groups, + $calendar->courses); if (!empty($events)) { foreach($events as $eventid => $event) { $event = new calendar_event($event); @@ -377,7 +386,8 @@ class core_calendar_renderer extends plugin_renderer_base { } // Extract information: events vs. time - calendar_events_by_day($events, $date['mon'], $date['year'], $eventsbyday, $durationbyday, $typesbyday, $calendar->courses); + calendar_events_by_day($events, $date['mon'], $date['year'], $eventsbyday, $durationbyday, + $typesbyday, $calendar->courses); $output = html_writer::start_tag('div', array('class'=>'header')); $output .= $this->course_filter_selector($returnurl, get_string('detailedmonthviewfor', 'calendar')); @@ -386,7 +396,8 @@ class core_calendar_renderer extends plugin_renderer_base { } $output .= html_writer::end_tag('div', array('class'=>'header')); // Controls - $output .= html_writer::tag('div', calendar_top_controls('month', array('id' => $calendar->courseid, 'time' => $calendar->time)), array('class' => 'controls')); + $output .= html_writer::tag('div', calendar_top_controls('month', array('id' => $calendar->courseid, + 'time' => $calendar->time)), array('class' => 'controls')); $table = new html_table(); $table->attributes = array('class'=>'calendarmonth calendartable'); @@ -432,7 +443,8 @@ class core_calendar_renderer extends plugin_renderer_base { // Reset vars $cell = new html_table_cell(); - $dayhref = calendar_get_link_href(new moodle_url(CALENDAR_URL.'view.php', array('view' => 'day', 'course' => $calendar->courseid)), 0, 0, 0, $daytime); + $dayhref = calendar_get_link_href(new moodle_url(CALENDAR_URL . 'view.php', + array('view' => 'day', 'course' => $calendar->courseid)), 0, 0, 0, $daytime); $cellclasses = array(); @@ -527,7 +539,8 @@ class core_calendar_renderer extends plugin_renderer_base { $returnurl = $this->page->url; } - $events = calendar_get_upcoming($calendar->courses, $calendar->groups, $calendar->users, $futuredays, $maxevents); + $events = calendar_get_upcoming($calendar->courses, $calendar->groups, $calendar->users, + $futuredays, $maxevents); $output = html_writer::start_tag('div', array('class'=>'header')); $output .= $this->course_filter_selector($returnurl, get_string('upcomingeventsfor', 'calendar')); @@ -539,8 +552,7 @@ class core_calendar_renderer extends plugin_renderer_base { if ($events) { $output .= html_writer::start_tag('div', array('class' => 'eventlist')); foreach ($events as $event) { - // Convert to calendar_event object so that we transform description - // accordingly + // Convert to calendar_event object so that we transform description accordingly. $event = new calendar_event($event); $event->calendarcourseid = $calendar->courseid; $output .= $this->event($event); diff --git a/calendar/set.php b/calendar/set.php index fab8aa54c02..c69de934a1a 100644 --- a/calendar/set.php +++ b/calendar/set.php @@ -1,43 +1,26 @@ . -///////////////////////////////////////////////////////////////////////////// -// // -// NOTICE OF COPYRIGHT // -// // -// Moodle - Calendar extension // -// // -// Copyright (C) 2003-2004 Greek School Network www.sch.gr // -// // -// Designed by: // -// Avgoustos Tsinakos (tsinakos@teikav.edu.gr) // -// Jon Papaioannou (pj@moodle.org) // -// // -// Programming and development: // -// Jon Papaioannou (pj@moodle.org) // -// // -// For bugs, suggestions, etc contact: // -// Jon Papaioannou (pj@moodle.org) // -// // -// The current module was developed at the University of Macedonia // -// (www.uom.gr) under the funding of the Greek School Network (www.sch.gr) // -// The aim of this project is to provide additional and improved // -// functionality to the Asynchronous Distance Education service that the // -// Greek School Network deploys. // -// // -// This program 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 2 of the License, or // -// (at your option) any later version. // -// // -// This program 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: // -// // -// http://www.gnu.org/copyleft/gpl.html // -// // -///////////////////////////////////////////////////////////////////////////// - +/** + * Sets the events filter for the calendar view. + * + * @package core_calendar + * @copyright 2003 Jon Papaioannou (pj@moodle.org) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ require_once('../config.php'); require_once($CFG->dirroot.'/calendar/lib.php'); diff --git a/calendar/tests/action_event_test.php b/calendar/tests/action_event_test.php new file mode 100644 index 00000000000..c655f84e01e --- /dev/null +++ b/calendar/tests/action_event_test.php @@ -0,0 +1,188 @@ +. + +/** + * Action event tests. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\local\event\entities\action_event; +use core_calendar\local\event\value_objects\action; +use core_calendar\local\event\value_objects\event_description; +use core_calendar\local\event\value_objects\event_times; +use core_calendar\local\event\entities\event_collection_interface; +use core_calendar\local\event\entities\event_interface; + +/** + * Action event testcase. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_calendar_action_event_testcase extends advanced_testcase { + /** + * Test event class getters. + * + * @dataProvider getters_testcases() + * @param array $constructorparams Associative array of constructor parameters. + */ + public function test_getters($constructorparams) { + $event = new action_event( + $constructorparams['event'], + $constructorparams['action'] + ); + + foreach ($constructorparams as $name => $value) { + if ($name !== 'event') { + $this->assertEquals($event->{'get_' . $name}(), $value); + } + } + } + + /** + * Test cases for getters test. + */ + public function getters_testcases() { + return [ + 'Dataset 1' => [ + 'constructorparams' => [ + 'event' => new core_calendar_action_event_test_event(), + 'action' => new action( + 'action 1', + new moodle_url('http://example.com'), + 2, + true + ) + ] + ], + 'Dataset 2' => [ + 'constructorparams' => [ + 'event' => new core_calendar_action_event_test_event(), + 'action' => new action( + 'action 2', + new moodle_url('http://example.com'), + 5, + false + ) + ] + ], + ]; + } +} + +/** + * Test event. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_calendar_action_event_test_event implements event_interface { + + public function get_id() { + return 1729; + } + + public function get_name() { + return 'Jeff'; + } + + public function get_description() { + return new event_description('asdf', 1); + } + + public function get_course() { + return new \stdClass(); + } + + public function get_course_module() { + return new \stdClass(); + } + + public function get_group() { + return new \stdClass(); + } + + public function get_user() { + return new \stdClass(); + } + + public function get_type() { + return 'asdf'; + } + + public function get_times() { + return new event_times( + (new \DateTimeImmutable())->setTimestamp('-2461276800'), + (new \DateTimeImmutable())->setTimestamp('115776000'), + (new \DateTimeImmutable())->setTimestamp('115776000'), + (new \DateTimeImmutable())->setTimestamp(time()) + ); + } + + public function get_repeats() { + return new core_calendar_action_event_test_event_collection(); + } + + public function get_subscription() { + return new \stdClass(); + } + + public function is_visible() { + return true; + } +} + +/** + * Test event collection. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_calendar_action_event_test_event_collection implements event_collection_interface { + /** + * @var array + */ + protected $events; + + /** + * core_calendar_action_event_test_event_collection constructor. + */ + public function __construct() { + $this->events = [ + 'not really an event hahaha', + 'also not really. gottem.' + ]; + } + + public function get_id() { + return 1729; + } + + public function get_num() { + return 2; + } + + public function getIterator() { + foreach ($this->events as $event) { + yield $event; + } + } +} diff --git a/calendar/tests/action_factory_test.php b/calendar/tests/action_factory_test.php new file mode 100644 index 00000000000..d58a4775b94 --- /dev/null +++ b/calendar/tests/action_factory_test.php @@ -0,0 +1,51 @@ +. + +/** + * Action factory test. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\action_factory; +use core_calendar\local\event\entities\action_interface; + +/** + * Action factory testcase. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_calendar_action_factory_test extends advanced_testcase { + /** + * Test action factory. + */ + public function test_action_factory() { + $factory = new action_factory(); + $instance = $factory->create_instance( + 'test', + new \moodle_url('http://example.com'), + 1729, + true + ); + + $this->assertInstanceOf(action_interface::class, $instance); + } +} diff --git a/calendar/tests/action_test.php b/calendar/tests/action_test.php new file mode 100644 index 00000000000..cc4dd6a3ccd --- /dev/null +++ b/calendar/tests/action_test.php @@ -0,0 +1,82 @@ +. + +/** + * Action tests. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\local\event\value_objects\action; + +/** + * Action testcase. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_calendar_action_testcase extends advanced_testcase { + /** + * Test action class getters. + * + * @dataProvider getters_testcases() + * @param array $constructorparams Associative array of constructor parameters. + */ + public function test_getters($constructorparams) { + $action = new action( + $constructorparams['name'], + $constructorparams['url'], + $constructorparams['item_count'], + $constructorparams['actionable'] + ); + + foreach ($constructorparams as $name => $value) { + if ($name == 'actionable') { + $this->assertEquals($action->is_actionable(), $value); + } else { + $this->assertEquals($action->{'get_' . $name}(), $value); + } + } + } + + /** + * Test cases for getters test. + */ + public function getters_testcases() { + return [ + 'Dataset 1' => [ + 'constructorparams' => [ + 'name' => 'Hello', + 'url' => new moodle_url('http://example.com'), + 'item_count' => 1, + 'actionable' => true + ] + ], + 'Dataset 2' => [ + 'constructorparams' => [ + 'name' => 'Goodbye', + 'url' => new moodle_url('http://example.com'), + 'item_count' => 2, + 'actionable' => false + ] + ] + ]; + } +} diff --git a/calendar/tests/behat/calendar.feature b/calendar/tests/behat/calendar.feature index c026cfe4360..81689dbef7c 100644 --- a/calendar/tests/behat/calendar.feature +++ b/calendar/tests/behat/calendar.feature @@ -24,9 +24,7 @@ Feature: Perform basic calendar functionality | user | group | | student1 | G1 | When I log in as "admin" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Calendar" block Scenario: Create a site event @@ -36,8 +34,7 @@ Feature: Perform basic calendar functionality | Description | Come join this awesome event, sucka! | And I log out And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "This month" And I should see "Really awesome event!" And I log out @@ -52,8 +49,7 @@ Feature: Perform basic calendar functionality | Description | Come join this awesome event, sucka! | And I log out And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "This month" And I should see "Really awesome event!" And I log out @@ -69,8 +65,7 @@ Feature: Perform basic calendar functionality | Description | Come join this awesome event | And I log out And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "This month" And I follow "Really awesome event!" And "Group 1" "text" should exist in the ".eventlist" "css_element" @@ -86,8 +81,7 @@ Feature: Perform basic calendar functionality | Description | Come join this awesome event, sucka! | And I log out And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "This month" And I should not see "Really awesome event!" diff --git a/calendar/tests/behat/calendar_lookahead.feature b/calendar/tests/behat/calendar_lookahead.feature index 9c6c046fd0d..819d03e2b30 100644 --- a/calendar/tests/behat/calendar_lookahead.feature +++ b/calendar/tests/behat/calendar_lookahead.feature @@ -17,8 +17,7 @@ Feature: Limit displayed upcoming events And I log in as "teacher1" Scenario: I view calendar details for a future event - Given I follow "C1" - And I turn editing mode on + Given I am on "Course 1" course homepage with editing mode on And I add the "Calendar" block And I add the "Upcoming events" block And I follow "This month" @@ -36,6 +35,5 @@ Feature: Limit displayed upcoming events | Upcoming events look-ahead | 3 months | And I press "Save changes" And I wait to be redirected - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Two months away event" diff --git a/calendar/tests/calendartype_test.php b/calendar/tests/calendartype_test.php index c097cfd0759..5d9af035811 100644 --- a/calendar/tests/calendartype_test.php +++ b/calendar/tests/calendartype_test.php @@ -33,9 +33,6 @@ require_once($CFG->dirroot . '/calendar/tests/calendartype_test_example.php'); require_once($CFG->libdir . '/form/dateselector.php'); require_once($CFG->libdir . '/form/datetimeselector.php'); -// Used to test the calendar/lib.php functions. -require_once($CFG->dirroot . '/calendar/lib.php'); - // Used to test the user datetime profile field. require_once($CFG->dirroot . '/user/profile/lib.php'); require_once($CFG->dirroot . '/user/profile/definelib.php'); @@ -108,8 +105,6 @@ class core_calendar_type_testcase extends advanced_testcase { * different calendar types. */ public function test_calendar_type_dateselector_elements() { - global $CFG; - // We want to reset the test data after this run. $this->resetAfterTest(); diff --git a/calendar/tests/container_test.php b/calendar/tests/container_test.php new file mode 100644 index 00000000000..8f4f5a7f47f --- /dev/null +++ b/calendar/tests/container_test.php @@ -0,0 +1,322 @@ +. + +/** + * Event container tests. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/calendar/lib.php'); + +use core_calendar\local\event\entities\action_event; +use core_calendar\local\event\entities\event; +use core_calendar\local\event\entities\event_interface; +use core_calendar\local\event\factories\event_factory; +use core_calendar\local\event\factories\event_factory_interface; +use core_calendar\local\event\mappers\event_mapper; +use core_calendar\local\event\mappers\event_mapper_interface; + +/** + * Core container testcase. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_calendar_container_testcase extends advanced_testcase { + + /** + * Test setup. + */ + public function setUp() { + $this->resetAfterTest(); + $this->setAdminUser(); + } + + /** + * Test getting the event factory. + */ + public function test_get_event_factory() { + $factory = \core_calendar\local\event\container::get_event_factory(); + + // Test that the container is returning the right type. + $this->assertInstanceOf(event_factory_interface::class, $factory); + // Test that the container is returning the right implementation. + $this->assertInstanceOf(event_factory::class, $factory); + + // Test that getting the factory a second time returns the same instance. + $factory2 = \core_calendar\local\event\container::get_event_factory(); + $this->assertTrue($factory === $factory2); + } + + /** + * Test that the event factory correctly creates instances of events. + * + * @dataProvider get_event_factory_testcases() + * @param \stdClass $dbrow Row from the "database". + */ + public function test_event_factory_create_instance($dbrow) { + $legacyevent = $this->create_event($dbrow); + $factory = \core_calendar\local\event\container::get_event_factory(); + $course = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $moduleinstance = $generator->create_instance(['course' => $course->id]); + + // Set some of the fake dbrow properties to match real data in the DB + // this is necessary as the factory hides things that modinfo doesn't + // know about. + $dbrow->id = $legacyevent->id; + $dbrow->courseid = $course->id; + $dbrow->instance = $moduleinstance->id; + $dbrow->modulename = 'assign'; + $event = $factory->create_instance($dbrow); + + // Test that the factory is returning the right type. + $this->assertInstanceOf(event_interface::class, $event); + // Test that the factory is returning the right implementation. + $this->assertTrue($event instanceof event || $event instanceof action_event); + + // Test that the event created has the correct properties. + $this->assertEquals($legacyevent->id, $event->get_id()); + $this->assertEquals($dbrow->description, $event->get_description()->get_value()); + $this->assertEquals($dbrow->format, $event->get_description()->get_format()); + $this->assertEquals($dbrow->courseid, $event->get_course()->get_id()); + + if ($dbrow->groupid == 0) { + $this->assertNull($event->get_group()); + } else { + $this->assertEquals($dbrow->groupid, $event->get_group()->get_id()); + } + + $this->assertEquals($dbrow->userid, $event->get_user()->get_id()); + $this->assertEquals($legacyevent->id, $event->get_repeats()->get_id()); + $this->assertEquals($dbrow->modulename, $event->get_course_module()->get('modname')); + $this->assertEquals($dbrow->instance, $event->get_course_module()->get('instance')); + $this->assertEquals($dbrow->timestart, $event->get_times()->get_start_time()->getTimestamp()); + $this->assertEquals($dbrow->timemodified, $event->get_times()->get_modified_time()->getTimestamp()); + $this->assertEquals($dbrow->timesort, $event->get_times()->get_sort_time()->getTimestamp()); + + if ($dbrow->visible == 1) { + $this->assertTrue($event->is_visible()); + } else { + $this->assertFalse($event->is_visible()); + } + + if (!$dbrow->subscriptionid) { + $this->assertNull($event->get_subscription()); + } else { + $this->assertEquals($event->get_subscription()->get_id()); + } + } + + /** + * Test that the event factory deals with invisible modules properly as admin. + * + * @dataProvider get_event_factory_testcases() + * @param \stdClass $dbrow Row from the "database". + */ + public function test_event_factory_when_module_visibility_is_toggled_as_admin($dbrow) { + $legacyevent = $this->create_event($dbrow); + $factory = \core_calendar\local\event\container::get_event_factory(); + $course = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $moduleinstance = $generator->create_instance(['course' => $course->id]); + + $dbrow->id = $legacyevent->id; + $dbrow->courseid = $course->id; + $dbrow->instance = $moduleinstance->id; + $dbrow->modulename = 'assign'; + + set_coursemodule_visible($moduleinstance->cmid, 0); + + $event = $factory->create_instance($dbrow); + + // Test that the factory is returning an event as the admin can see hidden course modules. + $this->assertInstanceOf(event_interface::class, $event); + } + + /** + * Test that the event factory deals with invisible modules properly as a guest. + * + * @dataProvider get_event_factory_testcases() + * @param \stdClass $dbrow Row from the "database". + */ + public function test_event_factory_when_module_visibility_is_toggled_as_guest($dbrow) { + $legacyevent = $this->create_event($dbrow); + $factory = \core_calendar\local\event\container::get_event_factory(); + $course = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $moduleinstance = $generator->create_instance(['course' => $course->id]); + + $dbrow->id = $legacyevent->id; + $dbrow->courseid = $course->id; + $dbrow->instance = $moduleinstance->id; + $dbrow->modulename = 'assign'; + + set_coursemodule_visible($moduleinstance->cmid, 0); + + // Set to a user who can not view hidden course modules. + $this->setGuestUser(); + + $event = $factory->create_instance($dbrow); + + // Module is invisible to guest users so this should return null. + $this->assertNull($event); + } + + /** + * Test that the event factory deals with completion related events properly. + */ + public function test_event_factory_with_completion_related_event() { + global $CFG; + + $CFG->enablecompletion = true; + + // Create the course we will be using. + $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); + + // Add the assignment. + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $assign = $generator->create_instance(array('course' => $course->id), array('completion' => 1)); + + // Create a completion event. + $event = new \stdClass(); + $event->name = 'An event'; + $event->description = 'Event description'; + $event->format = FORMAT_HTML; + $event->eventtype = \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED; + $event->userid = 1; + $event->modulename = 'assign'; + $event->instance = $assign->id; + $event->courseid = $course->id; + $event->groupid = 0; + $event->timestart = time(); + $event->timesort = time(); + $event->timemodified = time(); + $event->timeduration = 0; + $event->subscriptionid = null; + $event->repeatid = 0; + $legacyevent = $this->create_event($event); + + // Update the id of the event that was created. + $event->id = $legacyevent->id; + + // Create the factory we are going to be testing the behaviour of. + $factory = \core_calendar\local\event\container::get_event_factory(); + + // Check that we get the correct instance. + $this->assertInstanceOf(event_interface::class, $factory->create_instance($event)); + + // Now, disable completion. + $CFG->enablecompletion = false; + + // The result should now be null since we have disabled completion. + $this->assertNull($factory->create_instance($event)); + } + + /** + * Test getting the event mapper. + */ + public function test_get_event_mapper() { + $mapper = \core_calendar\local\event\container::get_event_mapper(); + + $this->assertInstanceOf(event_mapper_interface::class, $mapper); + $this->assertInstanceOf(event_mapper::class, $mapper); + + $mapper2 = \core_calendar\local\event\container::get_event_mapper(); + + $this->assertTrue($mapper === $mapper2); + } + + /** + * Test cases for the get event factory test. + */ + public function get_event_factory_testcases() { + return [ + 'Data set 1' => [ + 'dbrow' => (object)[ + 'name' => 'Test event', + 'description' => 'Hello', + 'format' => 1, + 'courseid' => 1, + 'groupid' => 0, + 'userid' => 1, + 'repeatid' => 0, + 'modulename' => 'assign', + 'instance' => 2, + 'eventtype' => 'due', + 'timestart' => 1486396800, + 'timeduration' => 0, + 'timesort' => 1486396800, + 'visible' => 1, + 'timemodified' => 1485793098, + 'subscriptionid' => null + ] + ], + + 'Data set 2' => [ + 'dbrow' => (object)[ + 'name' => 'Test event', + 'description' => 'Hello', + 'format' => 1, + 'courseid' => 1, + 'groupid' => 1, + 'userid' => 1, + 'repeatid' => 0, + 'modulename' => 'assign', + 'instance' => 2, + 'eventtype' => 'due', + 'timestart' => 1486396800, + 'timeduration' => 0, + 'timesort' => 1486396800, + 'visible' => 1, + 'timemodified' => 1485793098, + 'subscriptionid' => null + ] + ] + ]; + } + + /** + * Helper function to create calendar events using the old code. + * + * @param array $properties A list of calendar event properties to set + * @return calendar_event|bool + */ + protected function create_event($properties = []) { + $record = new \stdClass(); + $record->name = 'event name'; + $record->eventtype = 'global'; + $record->timestart = time(); + $record->timeduration = 0; + $record->timesort = 0; + $record->type = 1; + $record->courseid = 0; + + foreach ($properties as $name => $value) { + $record->$name = $value; + } + + $event = new calendar_event($record); + return $event->create($record, false); + } +} diff --git a/calendar/tests/event_description_test.php b/calendar/tests/event_description_test.php new file mode 100644 index 00000000000..b7c0e40ea4a --- /dev/null +++ b/calendar/tests/event_description_test.php @@ -0,0 +1,71 @@ +. + +/** + * Event description tests. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\local\event\value_objects\event_description; + +/** + * Action testcase. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_calendar_event_description_testcase extends advanced_testcase { + /** + * Test event description class getters. + * + * @dataProvider getters_testcases() + * @param array $constructorparams Associative array of constructor parameters. + */ + public function test_getters($constructorparams) { + $eventdescription = new event_description( + $constructorparams['value'], + $constructorparams['format'] + ); + foreach ($constructorparams as $name => $value) { + $this->assertEquals($eventdescription->{'get_' . $name}(), $value); + } + } + + /** + * Test cases for getters test. + */ + public function getters_testcases() { + return [ + 'Dataset 1' => [ + 'constructorparams' => [ + 'value' => 'Hello', + 'format' => 1 + ] + ], + 'Dataset 2' => [ + 'constructorparams' => [ + 'value' => 'Goodbye', + 'format' => 2 + ] + ] + ]; + } +} diff --git a/calendar/tests/event_factory_test.php b/calendar/tests/event_factory_test.php new file mode 100644 index 00000000000..b2f3039f764 --- /dev/null +++ b/calendar/tests/event_factory_test.php @@ -0,0 +1,468 @@ +. + +/** + * Event factory test. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/calendar/lib.php'); + +use core_calendar\local\event\factories\event_factory; +use core_calendar\local\event\entities\event_interface; + +/** + * Event factory testcase. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_calendar_event_factory_testcase extends advanced_testcase { + /** + * Test event class getters. + * + * @dataProvider create_instance_testcases() + * @param \stdClass $dbrow Row from the event table. + * @param callable $actioncallbackapplier Action callback applier. + * @param callable $visibilitycallbackapplier Visibility callback applier. + * @param callable $bailoutcheck Early bail out check function. + * @param string $expectedclass Class the factory is expected to produce. + * @param mixed $expectedattributevalue Expected value of the modified attribute. + */ + public function test_create_instance( + $dbrow, + callable $actioncallbackapplier, + callable $visibilitycallbackapplier, + callable $bailoutcheck, + $expectedclass, + $expectedattributevalue + ) { + $this->resetAfterTest(true); + $this->setAdminUser(); + $event = $this->create_event(); + $coursecache = []; + $modulecache = []; + $factory = new event_factory( + $actioncallbackapplier, + $visibilitycallbackapplier, + $bailoutcheck, + $coursecache, + $modulecache + ); + $dbrow->id = $event->id; + $instance = $factory->create_instance($dbrow); + + if ($expectedclass) { + $this->assertInstanceOf($expectedclass, $instance); + } + + if (is_null($expectedclass)) { + $this->assertNull($instance); + } + + if ($expectedattributevalue) { + $this->assertEquals($instance->testattribute, $expectedattributevalue); + } + } + + /** + * Test invalid callback exception. + */ + public function test_invalid_action_callback() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $event = $this->create_event(); + $coursecache = []; + $modulecache = []; + $factory = new event_factory( + function () { + return 'hello'; + }, + function () { + return true; + }, + function () { + return false; + }, + $coursecache, + $modulecache + ); + + $this->expectException('\core_calendar\local\event\exceptions\invalid_callback_exception'); + $factory->create_instance( + (object)[ + 'id' => $event->id, + 'name' => 'test', + 'description' => 'Test description', + 'format' => 2, + 'courseid' => 1, + 'groupid' => 1, + 'userid' => 1, + 'repeatid' => 0, + 'modulename' => 'assign', + 'instance' => 1, + 'eventtype' => 'due', + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'timestart' => 123456789, + 'timeduration' => 12, + 'timemodified' => 123456789, + 'timesort' => 123456789, + 'visible' => 1, + 'subscriptionid' => 1 + ] + ); + } + + /** + * Test invalid callback exception. + */ + public function test_invalid_visibility_callback() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $event = $this->create_event(); + $coursecache = []; + $modulecache = []; + $factory = new event_factory( + function ($event) { + return $event; + }, + function () { + return 'asdf'; + }, + function () { + return false; + }, + $coursecache, + $modulecache + ); + + $this->expectException('\core_calendar\local\event\exceptions\invalid_callback_exception'); + $factory->create_instance( + (object)[ + 'id' => $event->id, + 'name' => 'test', + 'description' => 'Test description', + 'format' => 2, + 'courseid' => 1, + 'groupid' => 1, + 'userid' => 1, + 'repeatid' => 0, + 'modulename' => 'assign', + 'instance' => 1, + 'eventtype' => 'due', + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'timestart' => 123456789, + 'timeduration' => 12, + 'timemodified' => 123456789, + 'timesort' => 123456789, + 'visible' => 1, + 'subscriptionid' => 1 + ] + ); + } + + /** + * Test invalid callback exception. + */ + public function test_invalid_bail_callback() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $event = $this->create_event(); + $coursecache = []; + $modulecache = []; + $factory = new event_factory( + function ($event) { + return $event; + }, + function () { + return true; + }, + function () { + return 'asdf'; + }, + $coursecache, + $modulecache + ); + + $this->expectException('\core_calendar\local\event\exceptions\invalid_callback_exception'); + $factory->create_instance( + (object)[ + 'id' => $event->id, + 'name' => 'test', + 'description' => 'Test description', + 'format' => 2, + 'courseid' => 1, + 'groupid' => 1, + 'userid' => 1, + 'repeatid' => 0, + 'modulename' => 'assign', + 'instance' => 1, + 'eventtype' => 'due', + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'timestart' => 123456789, + 'timeduration' => 12, + 'timemodified' => 123456789, + 'timesort' => 123456789, + 'visible' => 1, + 'subscriptionid' => 1 + ] + ); + } + + /** + * Test the factory's course cache. + */ + public function test_course_cache() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $course = self::getDataGenerator()->create_course(); + $event = $this->create_event(['courseid' => $course->id]); + $coursecache = []; + $modulecache = []; + $factory = new event_factory( + function ($event) { + return $event; + }, + function () { + return true; + }, + function () { + return false; + }, + $coursecache, + $modulecache + ); + + $instance = $factory->create_instance( + (object)[ + 'id' => $event->id, + 'name' => 'test', + 'description' => 'Test description', + 'format' => 2, + 'courseid' => $course->id, + 'groupid' => 1, + 'userid' => 1, + 'repeatid' => 0, + 'modulename' => 'assign', + 'instance' => 1, + 'eventtype' => 'due', + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'timestart' => 123456789, + 'timeduration' => 12, + 'timemodified' => 123456789, + 'timesort' => 123456789, + 'visible' => 1, + 'subscriptionid' => 1 + ] + ); + + $instance->get_course()->get('fullname'); + $this->assertArrayHasKey($course->id, $coursecache); + } + + /** + * Test the factory's module cache. + */ + public function test_module_cache() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $course = self::getDataGenerator()->create_course(); + $event = $this->create_event(['courseid' => $course->id]); + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $assigninstance = $plugingenerator->create_instance(['course' => $course->id]); + + $coursecache = []; + $modulecache = []; + $factory = new event_factory( + function ($event) { + return $event; + }, + function () { + return true; + }, + function () { + return false; + }, + $coursecache, + $modulecache + ); + + $instance = $factory->create_instance( + (object)[ + 'id' => $event->id, + 'name' => 'test', + 'description' => 'Test description', + 'format' => 2, + 'courseid' => $course->id, + 'groupid' => 1, + 'userid' => 1, + 'repeatid' => 0, + 'modulename' => 'assign', + 'instance' => $assigninstance->id, + 'eventtype' => 'due', + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'timestart' => 123456789, + 'timeduration' => 12, + 'timemodified' => 123456789, + 'timesort' => 123456789, + 'visible' => 1, + 'subscriptionid' => 1 + ] + ); + + $instance->get_course_module()->get('course'); + $this->assertArrayHasKey('assign' . '_' . $assigninstance->id, $modulecache); + } + + /** + * Testcases for the create instance test. + * + * @return array Array of testcases. + */ + public function create_instance_testcases() { + return [ + 'Sample event record with event exposed' => [ + 'dbrow' => (object)[ + 'name' => 'Test event', + 'description' => 'Hello', + 'format' => 1, + 'courseid' => 1, + 'groupid' => 1, + 'userid' => 1, + 'repeatid' => 0, + 'modulename' => 'Test module', + 'instance' => 1, + 'eventtype' => 'Due', + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'timestart' => 123456789, + 'timeduration' => 123456789, + 'timemodified' => 123456789, + 'timesort' => 123456789, + 'visible' => true, + 'subscriptionid' => 1 + ], + 'actioncallbackapplier' => function(event_interface $event) { + $event->testattribute = 'Hello'; + return $event; + }, + 'visibilitycallbackapplier' => function(event_interface $event) { + return true; + }, + 'bailoutcheck' => function() { + return false; + }, + event_interface::class, + 'Hello' + ], + 'Sample event record with event hidden' => [ + 'dbrow' => (object)[ + 'name' => 'Test event', + 'description' => 'Hello', + 'format' => 1, + 'courseid' => 1, + 'groupid' => 1, + 'userid' => 1, + 'repeatid' => 0, + 'modulename' => 'Test module', + 'instance' => 1, + 'eventtype' => 'Due', + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'timestart' => 123456789, + 'timeduration' => 123456789, + 'timemodified' => 123456789, + 'timesort' => 123456789, + 'visible' => true, + 'subscriptionid' => 1 + ], + 'actioncallbackapplier' => function(event_interface $event) { + $event->testattribute = 'Hello'; + return $event; + }, + 'visibilitycallbackapplier' => function(event_interface $event) { + return false; + }, + 'bailoutcheck' => function() { + return false; + }, + null, + null + ], + 'Sample event record with early bail' => [ + 'dbrow' => (object)[ + 'name' => 'Test event', + 'description' => 'Hello', + 'format' => 1, + 'courseid' => 1, + 'groupid' => 1, + 'userid' => 1, + 'repeatid' => 0, + 'modulename' => 'Test module', + 'instance' => 1, + 'eventtype' => 'Due', + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'timestart' => 123456789, + 'timeduration' => 123456789, + 'timemodified' => 123456789, + 'timesort' => 123456789, + 'visible' => true, + 'subscriptionid' => 1 + ], + 'actioncallbackapplier' => function(event_interface $event) { + $event->testattribute = 'Hello'; + return $event; + }, + 'visibilitycallbackapplier' => function(event_interface $event) { + return true; + }, + 'bailoutcheck' => function() { + return true; + }, + null, + null + ] + ]; + } + + /** + * Helper function to create calendar events using the old code. + * + * @param array $properties A list of calendar event properties to set + * @return calendar_event + */ + protected function create_event($properties = []) { + $record = new \stdClass(); + $record->name = 'event name'; + $record->eventtype = 'global'; + $record->timestart = time(); + $record->timeduration = 0; + $record->timesort = 0; + $record->type = 1; + $record->courseid = 0; + + foreach ($properties as $name => $value) { + $record->$name = $value; + } + + $event = new calendar_event($record); + return $event->create($record, false); + } +} diff --git a/calendar/tests/event_mapper_test.php b/calendar/tests/event_mapper_test.php new file mode 100644 index 00000000000..67738f4f190 --- /dev/null +++ b/calendar/tests/event_mapper_test.php @@ -0,0 +1,434 @@ +. + +/** + * Event mapper test. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/calendar/lib.php'); + +use core_calendar\local\event\mappers\event_mapper; +use core_calendar\local\event\value_objects\action; +use core_calendar\local\event\value_objects\event_description; +use core_calendar\local\event\value_objects\event_times; +use core_calendar\local\event\factories\action_factory_interface; +use core_calendar\local\event\entities\event_collection_interface; +use core_calendar\local\event\factories\event_factory_interface; +use core_calendar\local\event\entities\event_interface; +use core_calendar\local\event\entities\action_event_interface; +use core_calendar\local\event\proxies\proxy_interface; + +/** + * Event mapper testcase. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_calendar_event_mapper_testcase extends advanced_testcase { + /** + * Test legacy event -> event. + */ + public function test_from_legacy_event_to_event() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $legacyevent = $this->create_event(); + $mapper = new event_mapper( + new event_mapper_test_event_factory() + ); + $event = $mapper->from_legacy_event_to_event($legacyevent); + $this->assertInstanceOf(event_interface::class, $event); + } + + /** + * Test event -> legacy event. + */ + public function test_from_event_to_legacy_event() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $legacyevent = $this->create_event(['modname' => 'assign', 'instance' => 1]); + $event = new event_mapper_test_event($legacyevent); + $mapper = new event_mapper( + new event_mapper_test_event_factory() + ); + $legacyevent = $mapper->from_event_to_legacy_event($event); + $this->assertInstanceOf(calendar_event::class, $legacyevent); + } + + /** + * Test event -> stdClass. + */ + public function test_from_event_to_stdclass() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $legacyevent = $this->create_event(['modname' => 'assign', 'instance' => 1]); + $event = new event_mapper_test_event($legacyevent); + $mapper = new event_mapper( + new event_mapper_test_event_factory() + ); + $obj = $mapper->from_event_to_stdclass($event); + $this->assertInstanceOf(\stdClass::class, $obj); + $this->assertEquals($obj->name, $event->get_name()); + $this->assertEquals($obj->eventtype, $event->get_type()); + $this->assertEquals($obj->timestart, $event->get_times()->get_start_time()->getTimestamp()); + } + + /** + * Test event -> array. + */ + public function test_from_event_to_assoc_array() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $legacyevent = $this->create_event(['modname' => 'assign', 'instance' => 1]); + $event = new event_mapper_test_event($legacyevent); + $mapper = new event_mapper( + new event_mapper_test_event_factory() + ); + $arr = $mapper->from_event_to_assoc_array($event); + $this->assertTrue(is_array($arr)); + $this->assertEquals($arr['name'], $event->get_name()); + $this->assertEquals($arr['eventtype'], $event->get_type()); + $this->assertEquals($arr['timestart'], $event->get_times()->get_start_time()->getTimestamp()); + } + + /** + * Test for action event -> legacy event. + */ + public function test_from_action_event_to_legacy_event() { + $this->resetAfterTest(true); + $this->setAdminUser(); + $legacyevent = $this->create_event(['modname' => 'assign', 'instance' => 1]); + $event = new event_mapper_test_action_event( + new event_mapper_test_event($legacyevent) + ); + $mapper = new event_mapper( + new event_mapper_test_event_factory() + ); + $legacyevent = $mapper->from_event_to_legacy_event($event); + + $this->assertInstanceOf(calendar_event::class, $legacyevent); + $this->assertEquals($legacyevent->actionname, 'test action'); + $this->assertInstanceOf(\moodle_url::class, $legacyevent->actionurl); + $this->assertEquals($legacyevent->actionnum, 1729); + $this->assertEquals($legacyevent->actionactionable, $event->get_action()->is_actionable()); + } + + /** + * Helper function to create calendar events using the old code. + * + * @param array $properties A list of calendar event properties to set + * @return calendar_event + */ + protected function create_event($properties = []) { + $record = new \stdClass(); + $record->name = 'event name'; + $record->eventtype = 'global'; + $record->timestart = time(); + $record->timeduration = 0; + $record->timesort = 0; + $record->type = 1; + $record->courseid = 0; + + foreach ($properties as $name => $value) { + $record->$name = $value; + } + + $event = new calendar_event($record); + return $event->create($record, false); + } +} + +/** + * A test event factory. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class event_mapper_test_event_factory implements event_factory_interface { + + public function create_instance(\stdClass $dbrow) { + return new event_mapper_test_event(); + } +} + +/** + * A test action event + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class event_mapper_test_action_event implements action_event_interface { + /** + * @var event_interface $event The event to delegate to. + */ + protected $event; + + /** + * event_mapper_test_action_event constructor. + * @param event_interface $event + */ + public function __construct(event_interface $event) { + $this->event = $event; + } + + public function get_id() { + return $this->event->get_id(); + } + + public function get_name() { + return $this->event->get_name(); + } + + public function get_description() { + return $this->event->get_description(); + } + + public function get_course() { + return $this->event->get_course(); + } + + public function get_course_module() { + return $this->event->get_course_module(); + } + + public function get_group() { + return $this->event->get_group(); + } + + public function get_user() { + return $this->event->get_user(); + } + + public function get_type() { + return $this->event->get_type(); + } + + public function get_times() { + return $this->event->get_times(); + } + + public function get_repeats() { + return $this->event->get_repeats(); + } + + public function get_subscription() { + return $this->event->get_subscription(); + } + + public function is_visible() { + return $this->event->is_visible(); + } + + public function get_action() { + return new action( + 'test action', + new \moodle_url('http://example.com'), + 1729, + true + ); + } +} + +/** + * A test event. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class event_mapper_test_event implements event_interface { + /** + * @var proxy_interface $courseproxy Course proxy. + */ + protected $courseproxy; + + /** + * @var proxy_interface $cmproxy Course module proxy. + */ + protected $cmproxy; + + /** + * @var proxy_interface $groupproxy Group proxy. + */ + protected $groupproxy; + + /** + * @var proxy_interface $userproxy User proxy. + */ + protected $userproxy; + + /** + * @var proxy_interface $subscriptionproxy Subscription proxy. + */ + protected $subscriptionproxy; + + /** + * Constructor. + * + * @param calendar_event $legacyevent Legacy event to exctract IDs etc from. + */ + public function __construct($legacyevent = null) { + if ($legacyevent) { + $this->courseproxy = new event_mapper_test_proxy($legacyevent->courseid); + $this->cmproxy = new event_mapper_test_proxy(1729, + [ + 'modname' => $legacyevent->modname, + 'instance' => $legacyevent->instance + ] + ); + $this->groupproxy = new event_mapper_test_proxy(0); + $this->userproxy = new event_mapper_test_proxy($legacyevent->userid); + $this->subscriptionproxy = new event_mapper_test_proxy(null); + } + } + + public function get_id() { + return 1729; + } + + public function get_name() { + return 'Jeff'; + } + + public function get_description() { + return new event_description('asdf', 1); + } + + public function get_course() { + return $this->courseproxy; + } + + public function get_course_module() { + return $this->cmproxy; + } + + public function get_group() { + return $this->groupproxy; + } + + public function get_user() { + return $this->userproxy; + } + + public function get_type() { + return 'asdf'; + } + + public function get_times() { + return new event_times( + (new \DateTimeImmutable())->setTimestamp('-2461276800'), + (new \DateTimeImmutable())->setTimestamp('115776000'), + (new \DateTimeImmutable())->setTimestamp('115776000'), + (new \DateTimeImmutable())->setTimestamp(time()) + ); + } + + public function get_repeats() { + return new core_calendar_event_mapper_test_event_collection(); + } + + public function get_subscription() { + return $this->subscriptionproxy; + } + + public function is_visible() { + return true; + } +} + +/** + * A test proxy. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class event_mapper_test_proxy implements proxy_interface { + /** + * @var int $id Proxied ID. + */ + protected $id; + + /** + * @var array $params Params to proxy. + */ + protected $params; + + /** + * Constructor. + * + * @param int $id Proxied ID. + * @param array $params Params to proxy. + */ + public function __construct($id, $params = []) { + $this->params = $params; + } + + public function get($member) { + return isset($params[$member]) ? $params[$member] : null; + } + + public function get_id() { + return $this->id; + } + + public function set($member, $value) { + } + + public function get_proxied_instance() { + } +} + +/** + * A test event. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_calendar_event_mapper_test_event_collection implements event_collection_interface { + /** + * @var array $events Array of events. + */ + protected $events; + + /** + * Constructor. + */ + public function __construct() { + $this->events = [ + 'not really an event hahaha', + 'also not really. gottem.' + ]; + } + + public function get_id() { + return 1729; + } + + public function get_num() { + return 2; + } + + public function getIterator() { + foreach ($this->events as $event) { + yield $event; + } + } +} diff --git a/calendar/tests/event_test.php b/calendar/tests/event_test.php new file mode 100644 index 00000000000..c1ddaf96f27 --- /dev/null +++ b/calendar/tests/event_test.php @@ -0,0 +1,140 @@ +. + +/** + * Event tests. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\local\event\entities\event; +use core_calendar\local\event\proxies\std_proxy; +use core_calendar\local\event\value_objects\event_description; +use core_calendar\local\event\value_objects\event_times; +use core_calendar\local\event\entities\event_collection_interface; + +/** + * Event testcase. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_calendar_event_testcase extends advanced_testcase { + /** + * Test event class getters. + * + * @dataProvider getters_testcases() + * @param array $constructorparams Associative array of constructor parameters. + */ + public function test_getters($constructorparams) { + $event = new event( + $constructorparams['id'], + $constructorparams['name'], + $constructorparams['description'], + $constructorparams['course'], + $constructorparams['group'], + $constructorparams['user'], + $constructorparams['repeats'], + $constructorparams['course_module'], + $constructorparams['type'], + $constructorparams['times'], + $constructorparams['visible'], + $constructorparams['subscription'] + ); + + foreach ($constructorparams as $name => $value) { + if ($name !== 'visible') { + $this->assertEquals($event->{'get_' . $name}(), $value); + } + } + + $this->assertEquals($event->is_visible(), $constructorparams['visible']); + } + + /** + * Test cases for getters test. + */ + public function getters_testcases() { + $lamecallable = function($id) { + return (object)['id' => $id]; + }; + + return [ + 'Dataset 1' => [ + 'constructorparams' => [ + 'id' => 1, + 'name' => 'Test event 1', + 'description' => new event_description('asdf', 1), + 'course' => new std_proxy(1, $lamecallable), + 'group' => new std_proxy(1, $lamecallable), + 'user' => new std_proxy(1, $lamecallable), + 'repeats' => new core_calendar_event_test_event_collection(), + 'course_module' => new std_proxy(1, $lamecallable), + 'type' => 'dunno what this actually is meant to be', + 'times' => new event_times( + (new \DateTimeImmutable())->setTimestamp('-2461276800'), + (new \DateTimeImmutable())->setTimestamp('115776000'), + (new \DateTimeImmutable())->setTimestamp('115776000'), + (new \DateTimeImmutable())->setTimestamp(time()) + ), + 'visible' => true, + 'subscription' => new std_proxy(1, $lamecallable) + ] + ], + ]; + } +} + +/** + * Test event class. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_calendar_event_test_event_collection implements event_collection_interface { + /** + * @var array $events Array of events. + */ + protected $events; + + /** + * Constructor. + */ + public function __construct() { + $this->events = [ + 'not really an event hahaha', + 'also not really. gottem.' + ]; + } + + public function get_id() { + return 1729; + } + + public function get_num() { + return 2; + } + + public function getIterator() { + foreach ($this->events as $event) { + yield $event; + } + } +} diff --git a/calendar/tests/event_times_test.php b/calendar/tests/event_times_test.php new file mode 100644 index 00000000000..9a6c55cf4b3 --- /dev/null +++ b/calendar/tests/event_times_test.php @@ -0,0 +1,80 @@ +. + +/** + * Event times tests. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\local\event\value_objects\event_times; + +/** + * Event times testcase. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_calendar_event_times_testcase extends advanced_testcase { + /** + * Test event times class getters. + * + * @dataProvider getters_testcases() + * @param array $constructorparams Associative array of constructor parameters. + */ + public function test_getters($constructorparams) { + $eventtimes = new event_times( + $constructorparams['start_time'], + $constructorparams['end_time'], + $constructorparams['sort_time'], + $constructorparams['modified_time'] + ); + + foreach ($constructorparams as $name => $value) { + $this->assertEquals($eventtimes->{'get_' . $name}(), $value); + } + + $this->assertEquals($eventtimes->get_duration(), $constructorparams['end_time']->diff($constructorparams['start_time'])); + } + + /** + * Test cases for getters test. + */ + public function getters_testcases() { + return [ + 'Dataset 1' => [ + 'constructorparams' => [ + 'start_time' => (new \DateTimeImmutable())->setTimestamp('-2461276800'), + 'end_time' => (new \DateTimeImmutable())->setTimestamp('115776000'), + 'sort_time' => (new \DateTimeImmutable())->setTimestamp('115776000'), + 'modified_time' => (new \DateTimeImmutable())->setTimestamp(time()) + ] + ], + 'Dataset 2' => [ + 'constructorparams' => [ + 'start_time' => (new \DateTimeImmutable())->setTimestamp('123456'), + 'end_time' => (new \DateTimeImmutable())->setTimestamp('12345678'), + 'sort_time' => (new \DateTimeImmutable())->setTimestamp('1111'), + 'modified_time' => (new \DateTimeImmutable())->setTimestamp(time()) + ] + ] + ]; + } +} diff --git a/calendar/tests/event_vault_test.php b/calendar/tests/event_vault_test.php new file mode 100644 index 00000000000..1d57d1db9d2 --- /dev/null +++ b/calendar/tests/event_vault_test.php @@ -0,0 +1,901 @@ +. + +/** + * This file contains the class that handles testing of the calendar event vault. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/calendar/tests/helpers.php'); + +use core_calendar\local\event\data_access\event_vault; +use core_calendar\local\event\strategies\raw_event_retrieval_strategy; + +/** + * This file contains the class that handles testing of the calendar event vault. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_calendar_event_vault_testcase extends advanced_testcase { + + /** + * Test that get_action_events_by_timesort returns events after the + * provided timesort value. + */ + public function test_get_action_events_by_timesort_after_time() { + $this->resetAfterTest(true); + + $user = $this->getDataGenerator()->create_user(); + $factory = new action_event_test_factory(); + $strategy = new raw_event_retrieval_strategy(); + $vault = new event_vault($factory, $strategy); + + $this->setUser($user); + + for ($i = 1; $i < 6; $i++) { + create_event([ + 'name' => sprintf('Event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION + ]); + } + + $events = $vault->get_action_events_by_timesort($user, 3); + + $this->assertCount(3, $events); + $this->assertEquals('Event 3', $events[0]->get_name()); + $this->assertEquals('Event 4', $events[1]->get_name()); + $this->assertEquals('Event 5', $events[2]->get_name()); + + $events = $vault->get_action_events_by_timesort($user, 3, null, null, 1); + + $this->assertCount(1, $events); + $this->assertEquals('Event 3', $events[0]->get_name()); + + $events = $vault->get_action_events_by_timesort($user, 6); + + $this->assertCount(0, $events); + } + + /** + * Test that get_action_events_by_timesort returns events before the + * provided timesort value. + */ + public function test_get_action_events_by_timesort_before_time() { + $this->resetAfterTest(true); + $this->setAdminuser(); + + $user = $this->getDataGenerator()->create_user(); + $factory = new action_event_test_factory(); + $strategy = new raw_event_retrieval_strategy(); + $vault = new event_vault($factory, $strategy); + + for ($i = 1; $i < 6; $i++) { + create_event([ + 'name' => sprintf('Event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => 1 + ]); + } + + $events = $vault->get_action_events_by_timesort($user, null, 3); + + $this->assertCount(3, $events); + $this->assertEquals('Event 1', $events[0]->get_name()); + $this->assertEquals('Event 2', $events[1]->get_name()); + $this->assertEquals('Event 3', $events[2]->get_name()); + + $events = $vault->get_action_events_by_timesort($user, null, 3, null, 1); + + $this->assertCount(1, $events); + $this->assertEquals('Event 1', $events[0]->get_name()); + + $events = $vault->get_action_events_by_timesort($user, 6); + + $this->assertCount(0, $events); + } + + /** + * Test that get_action_events_by_timesort returns events between the + * provided timesort values. + */ + public function test_get_action_events_by_timesort_between_time() { + $this->resetAfterTest(true); + $this->setAdminuser(); + + $user = $this->getDataGenerator()->create_user(); + $factory = new action_event_test_factory(); + $strategy = new raw_event_retrieval_strategy(); + $vault = new event_vault($factory, $strategy); + + for ($i = 1; $i < 6; $i++) { + create_event([ + 'name' => sprintf('Event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => 1 + ]); + } + + $events = $vault->get_action_events_by_timesort($user, 2, 4); + + $this->assertCount(3, $events); + $this->assertEquals('Event 2', $events[0]->get_name()); + $this->assertEquals('Event 3', $events[1]->get_name()); + $this->assertEquals('Event 4', $events[2]->get_name()); + + $events = $vault->get_action_events_by_timesort($user, 2, 4, null, 1); + + $this->assertCount(1, $events); + $this->assertEquals('Event 2', $events[0]->get_name()); + } + + /** + * Test that get_action_events_by_timesort returns events between the + * provided timesort values and after the last seen event when one is + * provided. + */ + public function test_get_action_events_by_timesort_between_time_after_event() { + $this->resetAfterTest(true); + $this->setAdminuser(); + + $user = $this->getDataGenerator()->create_user(); + $factory = new action_event_test_factory(); + $strategy = new raw_event_retrieval_strategy(); + $vault = new event_vault($factory, $strategy); + + $records = []; + for ($i = 1; $i < 21; $i++) { + $records[] = create_event([ + 'name' => sprintf('Event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => 1 + ]); + } + + $aftereventid = $records[6]->id; + $afterevent = $vault->get_event_by_id($aftereventid); + $events = $vault->get_action_events_by_timesort($user, 3, 15, $afterevent); + + $this->assertCount(8, $events); + $this->assertEquals('Event 8', $events[0]->get_name()); + + $events = $vault->get_action_events_by_timesort($user, 3, 15, $afterevent, 3); + + $this->assertCount(3, $events); + } + + /** + * Test that get_action_events_by_timesort returns events between the + * provided timesort values and the last seen event can be provided to + * get paginated results. + */ + public function test_get_action_events_by_timesort_between_time_skip_even_records() { + $this->resetAfterTest(true); + $this->setAdminuser(); + + $user = $this->getDataGenerator()->create_user(); + // The factory will skip events with even ids. + $factory = new action_event_test_factory(function($actionevent) { + return ($actionevent->get_id() % 2) ? false : true; + }); + $strategy = new raw_event_retrieval_strategy(); + $vault = new event_vault($factory, $strategy); + + for ($i = 1; $i < 41; $i++) { + create_event([ + 'name' => sprintf('Event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => 1 + ]); + } + + $events = $vault->get_action_events_by_timesort($user, 3, 35, null, 5); + + $this->assertCount(5, $events); + $this->assertEquals('Event 3', $events[0]->get_name()); + $this->assertEquals('Event 5', $events[1]->get_name()); + $this->assertEquals('Event 7', $events[2]->get_name()); + $this->assertEquals('Event 9', $events[3]->get_name()); + $this->assertEquals('Event 11', $events[4]->get_name()); + + $afterevent = $events[4]; + $events = $vault->get_action_events_by_timesort($user, 3, 35, $afterevent, 5); + + $this->assertCount(5, $events); + $this->assertEquals('Event 13', $events[0]->get_name()); + $this->assertEquals('Event 15', $events[1]->get_name()); + $this->assertEquals('Event 17', $events[2]->get_name()); + $this->assertEquals('Event 19', $events[3]->get_name()); + $this->assertEquals('Event 21', $events[4]->get_name()); + } + + /** + * Test that get_action_events_by_timesort returns events between the + * provided timesort values. The database will continue to be read until the + * number of events requested has been satisfied. In this case the first + * five events are rejected so it should require two database requests. + */ + public function test_get_action_events_by_timesort_between_time_skip_first_records() { + $this->resetAfterTest(true); + $this->setAdminuser(); + + $user = $this->getDataGenerator()->create_user(); + $limit = 5; + $seen = 0; + // The factory will skip the first $limit events. + $factory = new action_event_test_factory(function($actionevent) use (&$seen, $limit) { + if ($seen < $limit) { + $seen++; + return false; + } else { + return true; + } + }); + $strategy = new raw_event_retrieval_strategy(); + $vault = new event_vault($factory, $strategy); + + for ($i = 1; $i < 21; $i++) { + create_event([ + 'name' => sprintf('Event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => 1 + ]); + } + + $events = $vault->get_action_events_by_timesort($user, 1, 20, null, $limit); + + $this->assertCount($limit, $events); + $this->assertEquals(sprintf('Event %d', $limit + 1), $events[0]->get_name()); + $this->assertEquals(sprintf('Event %d', $limit + 2), $events[1]->get_name()); + $this->assertEquals(sprintf('Event %d', $limit + 3), $events[2]->get_name()); + $this->assertEquals(sprintf('Event %d', $limit + 4), $events[3]->get_name()); + $this->assertEquals(sprintf('Event %d', $limit + 5), $events[4]->get_name()); + } + + /** + * Test that get_action_events_by_timesort returns events between the + * provided timesort values and after the last seen event when one is + * provided. This should work even when the event ids aren't ordered the + * same as the timesort order. + */ + public function test_get_action_events_by_timesort_non_consecutive_ids() { + $this->resetAfterTest(true); + $this->setAdminuser(); + + $user = $this->getDataGenerator()->create_user(); + $factory = new action_event_test_factory(); + $strategy = new raw_event_retrieval_strategy(); + $vault = new event_vault($factory, $strategy); + + /* + * The events should be ordered by timesort as follows: + * + * 1 event 1 + * 2 event 1 + * 1 event 2 + * 2 event 2 + * 1 event 3 + * 2 event 3 + * 1 event 4 + * 2 event 4 + * 1 event 5 + * 2 event 5 + * 1 event 6 + * 2 event 6 + * 1 event 7 + * 2 event 7 + * 1 event 8 + * 2 event 8 + * 1 event 9 + * 2 event 9 + * 1 event 10 + * 2 event 10 + */ + $records = []; + for ($i = 1; $i < 11; $i++) { + $records[] = create_event([ + 'name' => sprintf('1 event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => 1 + ]); + } + + for ($i = 1; $i < 11; $i++) { + $records[] = create_event([ + 'name' => sprintf('2 event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => 1 + ]); + } + + /* + * Expected result set: + * + * 2 event 4 + * 1 event 5 + * 2 event 5 + * 1 event 6 + * 2 event 6 + * 1 event 7 + * 2 event 7 + * 1 event 8 + * 2 event 8 + */ + $aftereventid = $records[3]->id; + $afterevent = $vault->get_event_by_id($aftereventid); + // Offset results by event with name "1 event 4" which has the same timesort + // value as the lower boundary of this query (3). Confirm that the given + // $afterevent is used to ignore events with the same timesortfrom values. + $events = $vault->get_action_events_by_timesort($user, 3, 8, $afterevent); + + $this->assertCount(9, $events); + $this->assertEquals('2 event 4', $events[0]->get_name()); + $this->assertEquals('2 event 8', $events[8]->get_name()); + + /* + * Expected result set: + * + * 2 event 4 + * 1 event 5 + */ + $events = $vault->get_action_events_by_timesort($user, 3, 8, $afterevent, 2); + + $this->assertCount(2, $events); + $this->assertEquals('2 event 4', $events[0]->get_name()); + $this->assertEquals('1 event 5', $events[1]->get_name()); + + /* + * Expected result set: + * + * 2 event 8 + */ + $aftereventid = $records[7]->id; + $afterevent = $vault->get_event_by_id($aftereventid); + // Offset results by event with name "1 event 8" which has the same timesort + // value as the upper boundary of this query (8). Confirm that the given + // $afterevent is used to ignore events with the same timesortto values. + $events = $vault->get_action_events_by_timesort($user, 3, 8, $afterevent); + + $this->assertCount(1, $events); + $this->assertEquals('2 event 8', $events[0]->get_name()); + + /* + * Expected empty result set. + */ + $aftereventid = $records[18]->id; + $afterevent = $vault->get_event_by_id($aftereventid); + // Offset results by event with name "2 event 9" which has a timesort + // value larger than the upper boundary of this query (9 > 8). Confirm + // that the given $afterevent is used for filtering events. + $events = $vault->get_action_events_by_timesort($user, 3, 8, $afterevent); + $this->assertEmpty($events); + } + + /** + * Test that get_action_events_by_course returns events after the + * provided timesort value. + */ + public function test_get_action_events_by_course_after_time() { + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $factory = new action_event_test_factory(); + $strategy = new raw_event_retrieval_strategy(); + $vault = new event_vault($factory, $strategy); + + $this->resetAfterTest(true); + $this->setAdminuser(); + $this->getDataGenerator()->enrol_user($user->id, $course1->id); + $this->getDataGenerator()->enrol_user($user->id, $course2->id); + + for ($i = 1; $i < 6; $i++) { + create_event([ + 'name' => sprintf('Event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $course1->id, + ]); + } + + for ($i = 6; $i < 12; $i++) { + create_event([ + 'name' => sprintf('Event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $course2->id, + ]); + } + + $events = $vault->get_action_events_by_course($user, $course1, 3); + $this->assertCount(3, $events); + $this->assertEquals('Event 3', $events[0]->get_name()); + $this->assertEquals('Event 4', $events[1]->get_name()); + $this->assertEquals('Event 5', $events[2]->get_name()); + + $events = $vault->get_action_events_by_course($user, $course1, 3, null, null, 1); + + $this->assertCount(1, $events); + $this->assertEquals('Event 3', $events[0]->get_name()); + + $events = $vault->get_action_events_by_course($user, $course1, 6); + + $this->assertCount(0, $events); + } + + /** + * Test that get_action_events_by_course returns events before the + * provided timesort value. + */ + public function test_get_action_events_by_course_before_time() { + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $factory = new action_event_test_factory(); + $strategy = new raw_event_retrieval_strategy(); + $vault = new event_vault($factory, $strategy); + + $this->resetAfterTest(true); + $this->setAdminuser(); + $this->getDataGenerator()->enrol_user($user->id, $course1->id); + $this->getDataGenerator()->enrol_user($user->id, $course2->id); + + for ($i = 1; $i < 6; $i++) { + create_event([ + 'name' => sprintf('Event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $course1->id, + ]); + } + + for ($i = 6; $i < 12; $i++) { + create_event([ + 'name' => sprintf('Event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $course2->id, + ]); + } + + $events = $vault->get_action_events_by_course($user, $course1, null, 3); + + $this->assertCount(3, $events); + $this->assertEquals('Event 1', $events[0]->get_name()); + $this->assertEquals('Event 2', $events[1]->get_name()); + $this->assertEquals('Event 3', $events[2]->get_name()); + + $events = $vault->get_action_events_by_course($user, $course1, null, 3, null, 1); + + $this->assertCount(1, $events); + $this->assertEquals('Event 1', $events[0]->get_name()); + + $events = $vault->get_action_events_by_course($user, $course1, 6); + + $this->assertCount(0, $events); + } + + /** + * Test that get_action_events_by_course returns events between the + * provided timesort values. + */ + public function test_get_action_events_by_course_between_time() { + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $factory = new action_event_test_factory(); + $strategy = new raw_event_retrieval_strategy(); + $vault = new event_vault($factory, $strategy); + + $this->resetAfterTest(true); + $this->setAdminuser(); + $this->getDataGenerator()->enrol_user($user->id, $course1->id); + $this->getDataGenerator()->enrol_user($user->id, $course2->id); + + for ($i = 1; $i < 6; $i++) { + create_event([ + 'name' => sprintf('Event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $course1->id, + ]); + } + + for ($i = 6; $i < 12; $i++) { + create_event([ + 'name' => sprintf('Event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $course2->id, + ]); + } + + $events = $vault->get_action_events_by_course($user, $course1, 2, 4); + + $this->assertCount(3, $events); + $this->assertEquals('Event 2', $events[0]->get_name()); + $this->assertEquals('Event 3', $events[1]->get_name()); + $this->assertEquals('Event 4', $events[2]->get_name()); + + $events = $vault->get_action_events_by_course($user, $course1, 2, 4, null, 1); + + $this->assertCount(1, $events); + $this->assertEquals('Event 2', $events[0]->get_name()); + } + + /** + * Test that get_action_events_by_course returns events between the + * provided timesort values and after the last seen event when one is + * provided. + */ + public function test_get_action_events_by_course_between_time_after_event() { + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $factory = new action_event_test_factory(); + $strategy = new raw_event_retrieval_strategy(); + $vault = new event_vault($factory, $strategy); + $records = []; + + $this->resetAfterTest(true); + $this->setAdminuser(); + $this->getDataGenerator()->enrol_user($user->id, $course1->id); + $this->getDataGenerator()->enrol_user($user->id, $course2->id); + + for ($i = 1; $i < 21; $i++) { + $records[] = create_event([ + 'name' => sprintf('Event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $course1->id, + ]); + } + + for ($i = 21; $i < 41; $i++) { + $records[] = create_event([ + 'name' => sprintf('Event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $course2->id, + ]); + } + + $aftereventid = $records[6]->id; + $afterevent = $vault->get_event_by_id($aftereventid); + $events = $vault->get_action_events_by_course($user, $course1, 3, 15, $afterevent); + + $this->assertCount(8, $events); + $this->assertEquals('Event 8', $events[0]->get_name()); + + $events = $vault->get_action_events_by_course($user, $course1, 3, 15, $afterevent, 3); + + $this->assertCount(3, $events); + } + + /** + * Test that get_action_events_by_course returns events between the + * provided timesort values and the last seen event can be provided to + * get paginated results. + */ + public function test_get_action_events_by_course_between_time_skip_even_records() { + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + // The factory will skip events with even ids. + $factory = new action_event_test_factory(function($actionevent) { + return ($actionevent->get_id() % 2) ? false : true; + }); + $strategy = new raw_event_retrieval_strategy(); + $vault = new event_vault($factory, $strategy); + + $this->resetAfterTest(true); + $this->setAdminuser(); + $this->getDataGenerator()->enrol_user($user->id, $course1->id); + $this->getDataGenerator()->enrol_user($user->id, $course2->id); + + for ($i = 1; $i < 41; $i++) { + create_event([ + 'name' => sprintf('Event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $course1->id, + ]); + } + + for ($i = 41; $i < 81; $i++) { + create_event([ + 'name' => sprintf('Event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $course2->id, + ]); + } + + $events = $vault->get_action_events_by_course($user, $course1, 3, 35, null, 5); + + $this->assertCount(5, $events); + $this->assertEquals('Event 3', $events[0]->get_name()); + $this->assertEquals('Event 5', $events[1]->get_name()); + $this->assertEquals('Event 7', $events[2]->get_name()); + $this->assertEquals('Event 9', $events[3]->get_name()); + $this->assertEquals('Event 11', $events[4]->get_name()); + + $afterevent = $events[4]; + $events = $vault->get_action_events_by_course($user, $course1, 3, 35, $afterevent, 5); + + $this->assertCount(5, $events); + $this->assertEquals('Event 13', $events[0]->get_name()); + $this->assertEquals('Event 15', $events[1]->get_name()); + $this->assertEquals('Event 17', $events[2]->get_name()); + $this->assertEquals('Event 19', $events[3]->get_name()); + $this->assertEquals('Event 21', $events[4]->get_name()); + } + + /** + * Test that get_action_events_by_course returns events between the + * provided timesort values. The database will continue to be read until the + * number of events requested has been satisfied. In this case the first + * five events are rejected so it should require two database requests. + */ + public function test_get_action_events_by_course_between_time_skip_first_records() { + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $limit = 5; + $seen = 0; + // The factory will skip the first $limit events. + $factory = new action_event_test_factory(function($actionevent) use (&$seen, $limit) { + if ($seen < $limit) { + $seen++; + return false; + } else { + return true; + } + }); + $strategy = new raw_event_retrieval_strategy(); + $vault = new event_vault($factory, $strategy); + + $this->resetAfterTest(true); + $this->setAdminuser(); + $this->getDataGenerator()->enrol_user($user->id, $course1->id); + $this->getDataGenerator()->enrol_user($user->id, $course2->id); + + for ($i = 1; $i < 21; $i++) { + create_event([ + 'name' => sprintf('Event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $course1->id, + ]); + } + + for ($i = 21; $i < 41; $i++) { + create_event([ + 'name' => sprintf('Event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $course2->id, + ]); + } + + $events = $vault->get_action_events_by_course($user, $course1, 1, 20, null, $limit); + + $this->assertCount($limit, $events); + $this->assertEquals(sprintf('Event %d', $limit + 1), $events[0]->get_name()); + $this->assertEquals(sprintf('Event %d', $limit + 2), $events[1]->get_name()); + $this->assertEquals(sprintf('Event %d', $limit + 3), $events[2]->get_name()); + $this->assertEquals(sprintf('Event %d', $limit + 4), $events[3]->get_name()); + $this->assertEquals(sprintf('Event %d', $limit + 5), $events[4]->get_name()); + } + + /** + * Test that get_action_events_by_course returns events between the + * provided timesort values and after the last seen event when one is + * provided. This should work even when the event ids aren't ordered the + * same as the timesort order. + */ + public function test_get_action_events_by_course_non_consecutive_ids() { + $this->resetAfterTest(true); + $this->setAdminuser(); + + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $factory = new action_event_test_factory(); + $strategy = new raw_event_retrieval_strategy(); + $vault = new event_vault($factory, $strategy); + + $this->setAdminuser(); + $this->getDataGenerator()->enrol_user($user->id, $course1->id); + $this->getDataGenerator()->enrol_user($user->id, $course2->id); + + /* + * The events should be ordered by timesort as follows: + * + * 1 event 1 + * 2 event 1 + * 1 event 2 + * 2 event 2 + * 1 event 3 + * 2 event 3 + * 1 event 4 + * 2 event 4 + * 1 event 5 + * 2 event 5 + * 1 event 6 + * 2 event 6 + * 1 event 7 + * 2 event 7 + * 1 event 8 + * 2 event 8 + * 1 event 9 + * 2 event 9 + * 1 event 10 + * 2 event 10 + */ + $records = []; + for ($i = 1; $i < 11; $i++) { + $records[] = create_event([ + 'name' => sprintf('1 event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $course1->id, + ]); + } + + for ($i = 1; $i < 11; $i++) { + $records[] = create_event([ + 'name' => sprintf('2 event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $course1->id, + ]); + } + + // Create events for the other course. + for ($i = 1; $i < 11; $i++) { + $records[] = create_event([ + 'name' => sprintf('3 event %d', $i), + 'eventtype' => 'user', + 'userid' => $user->id, + 'timesort' => $i, + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $course2->id, + ]); + } + + /* + * Expected result set: + * + * 2 event 4 + * 1 event 5 + * 2 event 5 + * 1 event 6 + * 2 event 6 + * 1 event 7 + * 2 event 7 + * 1 event 8 + * 2 event 8 + */ + $aftereventid = $records[3]->id; + $afterevent = $vault->get_event_by_id($aftereventid); + // Offset results by event with name "1 event 4" which has the same timesort + // value as the lower boundary of this query (3). Confirm that the given + // $afterevent is used to ignore events with the same timesortfrom values. + $events = $vault->get_action_events_by_course($user, $course1, 3, 8, $afterevent); + + $this->assertCount(9, $events); + $this->assertEquals('2 event 4', $events[0]->get_name()); + $this->assertEquals('2 event 8', $events[8]->get_name()); + + /* + * Expected result set: + * + * 2 event 4 + * 1 event 5 + */ + $events = $vault->get_action_events_by_course($user, $course1, 3, 8, $afterevent, 2); + + $this->assertCount(2, $events); + $this->assertEquals('2 event 4', $events[0]->get_name()); + $this->assertEquals('1 event 5', $events[1]->get_name()); + + /* + * Expected result set: + * + * 2 event 8 + */ + $aftereventid = $records[7]->id; + $afterevent = $vault->get_event_by_id($aftereventid); + // Offset results by event with name "1 event 8" which has the same timesort + // value as the upper boundary of this query (8). Confirm that the given + // $afterevent is used to ignore events with the same timesortto values. + $events = $vault->get_action_events_by_course($user, $course1, 3, 8, $afterevent); + + $this->assertCount(1, $events); + $this->assertEquals('2 event 8', $events[0]->get_name()); + + /* + * Expected empty result set. + */ + $aftereventid = $records[18]->id; + $afterevent = $vault->get_event_by_id($aftereventid); + // Offset results by event with name "2 event 9" which has a timesort + // value larger than the upper boundary of this query (9 > 8). Confirm + // that the given $afterevent is used for filtering events. + $events = $vault->get_action_events_by_course($user, $course1, 3, 8, $afterevent); + + $this->assertEmpty($events); + } +} diff --git a/calendar/tests/events_test.php b/calendar/tests/events_test.php index 207d66a6a4f..6f78848a059 100644 --- a/calendar/tests/events_test.php +++ b/calendar/tests/events_test.php @@ -219,6 +219,41 @@ class core_calendar_events_testcase extends advanced_testcase { } } + /** + * Tests for calendar_event_updated event. + */ + public function test_calendar_event_updated_toggle_visibility() { + global $DB, $SITE; + + $this->resetAfterTest(); + + // Create a calendar event. + $time = time(); + $calevent = core_calendar_externallib_testcase::create_calendar_event('Some wickedly awesome event yo!', + $this->user->id, 'user', 0, $time); + + // Updated the visibility of the calendar event. + $sink = $this->redirectEvents(); + $calevent->toggle_visibility(); + $dbrecord = $DB->get_record('event', array('id' => $calevent->id), '*', MUST_EXIST); + $events = $sink->get_events(); + + // Validate the calendar_event_updated event. + $event = $events[0]; + $this->assertInstanceOf('\core\event\calendar_event_updated', $event); + $this->assertEquals('event', $event->objecttable); + $this->assertEquals($SITE->id, $event->courseid); + $this->assertEquals($calevent->context, $event->get_context()); + $expectedlog = array($SITE->id, 'calendar', 'edit', 'event.php?action=edit&id=' . $calevent->id , + $calevent->name); + $this->assertEventLegacyLogData($expectedlog, $event); + $other = array('repeatid' => 0, 'timestart' => $time, 'name' => 'Some wickedly awesome event yo!'); + $this->assertEquals($other, $event->other); + $this->assertEventContextNotUsed($event); + $this->assertEquals($dbrecord, $event->get_record_snapshot('event', $event->objectid)); + + } + /** * Tests for event validations related to calendar_event_created event. */ diff --git a/calendar/tests/externallib_test.php b/calendar/tests/externallib_test.php index 9baf24918be..62f93363104 100644 --- a/calendar/tests/externallib_test.php +++ b/calendar/tests/externallib_test.php @@ -85,6 +85,12 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase { if (empty($prop->timeduration)) { $prop->timeduration = 0; } + if (empty($prop->timesort)) { + $prop->timesort = 0; + } + if (empty($prop->type)) { + $prop->type = CALENDAR_EVENT_TYPE_STANDARD; + } if (empty($prop->repeats)) { $prop->repeat = 0; } else { @@ -542,4 +548,657 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase { $this->assertEquals(1, count($eventsret['events'])); $this->assertEquals(2, count($eventsret['warnings'])); } + + /** + * Requesting calendar events from a given time should return all events with a sort + * time at or after the requested time. All events prior to that time should not + * be return. + * + * If there are no events on or after the given time then an empty result set should + * be returned. + */ + public function test_get_calendar_action_events_by_timesort_after_time() { + $user = $this->getDataGenerator()->create_user(); + $course = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $moduleinstance = $generator->create_instance(['course' => $course->id]); + + $this->getDataGenerator()->enrol_user($user->id, $course->id); + $this->resetAfterTest(true); + $this->setUser($user); + + $params = [ + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'modulename' => 'assign', + 'instance' => $moduleinstance->id, + 'courseid' => $course->id, + ]; + + $event1 = $this->create_calendar_event('Event 1', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 1])); + $event2 = $this->create_calendar_event('Event 2', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 2])); + $event3 = $this->create_calendar_event('Event 3', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 3])); + $event4 = $this->create_calendar_event('Event 4', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 4])); + $event5 = $this->create_calendar_event('Event 5', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 5])); + $event6 = $this->create_calendar_event('Event 6', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 6])); + $event7 = $this->create_calendar_event('Event 7', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 7])); + $event8 = $this->create_calendar_event('Event 8', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 8])); + + $result = core_calendar_external::get_calendar_action_events_by_timesort(5); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_timesort_returns(), + $result + ); + $events = $result['events']; + + $this->assertCount(4, $events); + $this->assertEquals('Event 5', $events[0]['name']); + $this->assertEquals('Event 6', $events[1]['name']); + $this->assertEquals('Event 7', $events[2]['name']); + $this->assertEquals('Event 8', $events[3]['name']); + $this->assertEquals($event5->id, $result['firstid']); + $this->assertEquals($event8->id, $result['lastid']); + + $result = core_calendar_external::get_calendar_action_events_by_timesort(9); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_timesort_returns(), + $result + ); + + $this->assertEmpty($result['events']); + $this->assertNull($result['firstid']); + $this->assertNull($result['lastid']); + } + + /** + * Requesting calendar events before a given time should return all events with a sort + * time at or before the requested time (inclusive). All events after that time + * should not be returned. + * + * If there are no events before the given time then an empty result set should be + * returned. + */ + public function test_get_calendar_action_events_by_timesort_before_time() { + $user = $this->getDataGenerator()->create_user(); + $course = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $moduleinstance = $generator->create_instance(['course' => $course->id]); + + $this->getDataGenerator()->enrol_user($user->id, $course->id); + $this->resetAfterTest(true); + $this->setUser($user); + + $params = [ + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'modulename' => 'assign', + 'instance' => $moduleinstance->id, + 'courseid' => $course->id, + ]; + + $event1 = $this->create_calendar_event('Event 1', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 2])); + $event2 = $this->create_calendar_event('Event 2', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 3])); + $event3 = $this->create_calendar_event('Event 3', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 4])); + $event4 = $this->create_calendar_event('Event 4', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 5])); + $event5 = $this->create_calendar_event('Event 5', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 6])); + $event6 = $this->create_calendar_event('Event 6', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 7])); + $event7 = $this->create_calendar_event('Event 7', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 8])); + $event8 = $this->create_calendar_event('Event 8', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 9])); + + $result = core_calendar_external::get_calendar_action_events_by_timesort(null, 5); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_timesort_returns(), + $result + ); + $events = $result['events']; + + $this->assertCount(4, $events); + $this->assertEquals('Event 1', $events[0]['name']); + $this->assertEquals('Event 2', $events[1]['name']); + $this->assertEquals('Event 3', $events[2]['name']); + $this->assertEquals('Event 4', $events[3]['name']); + $this->assertEquals($event1->id, $result['firstid']); + $this->assertEquals($event4->id, $result['lastid']); + + $result = core_calendar_external::get_calendar_action_events_by_timesort(null, 1); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_timesort_returns(), + $result + ); + + $this->assertEmpty($result['events']); + $this->assertNull($result['firstid']); + $this->assertNull($result['lastid']); + } + + /** + * Requesting calendar events within a given time range should return all events with + * a sort time between the lower and upper time bound (inclusive). + * + * If there are no events in the given time range then an empty result set should be + * returned. + */ + public function test_get_calendar_action_events_by_timesort_time_range() { + $user = $this->getDataGenerator()->create_user(); + $course = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $moduleinstance = $generator->create_instance(['course' => $course->id]); + + $this->getDataGenerator()->enrol_user($user->id, $course->id); + $this->resetAfterTest(true); + $this->setUser($user); + + $params = [ + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'modulename' => 'assign', + 'instance' => $moduleinstance->id, + 'courseid' => $course->id, + ]; + + $event1 = $this->create_calendar_event('Event 1', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 1])); + $event2 = $this->create_calendar_event('Event 2', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 2])); + $event3 = $this->create_calendar_event('Event 3', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 3])); + $event4 = $this->create_calendar_event('Event 4', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 4])); + $event5 = $this->create_calendar_event('Event 5', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 5])); + $event6 = $this->create_calendar_event('Event 6', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 6])); + $event7 = $this->create_calendar_event('Event 7', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 7])); + $event8 = $this->create_calendar_event('Event 8', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 8])); + + $result = core_calendar_external::get_calendar_action_events_by_timesort(3, 6); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_timesort_returns(), + $result + ); + $events = $result['events']; + + $this->assertCount(4, $events); + $this->assertEquals('Event 3', $events[0]['name']); + $this->assertEquals('Event 4', $events[1]['name']); + $this->assertEquals('Event 5', $events[2]['name']); + $this->assertEquals('Event 6', $events[3]['name']); + $this->assertEquals($event3->id, $result['firstid']); + $this->assertEquals($event6->id, $result['lastid']); + + $result = core_calendar_external::get_calendar_action_events_by_timesort(10, 15); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_timesort_returns(), + $result + ); + + $this->assertEmpty($result['events']); + $this->assertNull($result['firstid']); + $this->assertNull($result['lastid']); + } + + /** + * Requesting calendar events within a given time range and a limit and offset should return + * the number of events up to the given limit value that have a sort time between the lower + * and uppper time bound (inclusive) where the result set is shifted by the offset value. + * + * If there are no events in the given time range then an empty result set should be + * returned. + */ + public function test_get_calendar_action_events_by_timesort_time_limit_offset() { + $user = $this->getDataGenerator()->create_user(); + $course = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $moduleinstance = $generator->create_instance(['course' => $course->id]); + + $this->getDataGenerator()->enrol_user($user->id, $course->id); + $this->resetAfterTest(true); + $this->setUser($user); + + $params = [ + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'modulename' => 'assign', + 'instance' => $moduleinstance->id, + 'courseid' => $course->id, + ]; + + $event1 = $this->create_calendar_event('Event 1', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 1])); + $event2 = $this->create_calendar_event('Event 2', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 2])); + $event3 = $this->create_calendar_event('Event 3', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 3])); + $event4 = $this->create_calendar_event('Event 4', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 4])); + $event5 = $this->create_calendar_event('Event 5', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 5])); + $event6 = $this->create_calendar_event('Event 6', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 6])); + $event7 = $this->create_calendar_event('Event 7', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 7])); + $event8 = $this->create_calendar_event('Event 8', $user->id, 'user', 0, 1, array_merge($params, ['timesort' => 8])); + + $result = core_calendar_external::get_calendar_action_events_by_timesort(2, 7, $event3->id, 2); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_timesort_returns(), + $result + ); + $events = $result['events']; + + $this->assertCount(2, $events); + $this->assertEquals('Event 4', $events[0]['name']); + $this->assertEquals('Event 5', $events[1]['name']); + $this->assertEquals($event4->id, $result['firstid']); + $this->assertEquals($event5->id, $result['lastid']); + + $result = core_calendar_external::get_calendar_action_events_by_timesort(2, 7, $event5->id, 2); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_timesort_returns(), + $result + ); + $events = $result['events']; + + $this->assertCount(2, $events); + $this->assertEquals('Event 6', $events[0]['name']); + $this->assertEquals('Event 7', $events[1]['name']); + $this->assertEquals($event6->id, $result['firstid']); + $this->assertEquals($event7->id, $result['lastid']); + + $result = core_calendar_external::get_calendar_action_events_by_timesort(2, 7, $event7->id, 2); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_timesort_returns(), + $result + ); + + $this->assertEmpty($result['events']); + $this->assertNull($result['firstid']); + $this->assertNull($result['lastid']); + } + + /** + * Requesting calendar events from a given course and time should return all + * events with a sort time at or after the requested time. All events prior + * to that time should not be return. + * + * If there are no events on or after the given time then an empty result set should + * be returned. + */ + public function test_get_calendar_action_events_by_course_after_time() { + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $instance1 = $generator->create_instance(['course' => $course1->id]); + $instance2 = $generator->create_instance(['course' => $course2->id]); + $records = []; + + $this->getDataGenerator()->enrol_user($user->id, $course1->id); + $this->getDataGenerator()->enrol_user($user->id, $course2->id); + $this->resetAfterTest(true); + $this->setUser($user); + + for ($i = 1; $i < 19; $i++) { + $courseid = ($i < 9) ? $course1->id : $course2->id; + $instance = ($i < 9) ? $instance1->id : $instance2->id; + $records[] = $this->create_calendar_event( + sprintf('Event %d', $i), + $user->id, + 'user', + 0, + 1, + [ + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $courseid, + 'timesort' => $i, + 'modulename' => 'assign', + 'instance' => $instance, + ] + ); + } + + $result = core_calendar_external::get_calendar_action_events_by_course($course1->id, 5); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_course_returns(), + $result + ); + $result = $result['events']; + + $this->assertCount(4, $result); + $this->assertEquals('Event 5', $result[0]['name']); + $this->assertEquals('Event 6', $result[1]['name']); + $this->assertEquals('Event 7', $result[2]['name']); + $this->assertEquals('Event 8', $result[3]['name']); + + $result = core_calendar_external::get_calendar_action_events_by_course($course1->id, 9); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_course_returns(), + $result + ); + $result = $result['events']; + + $this->assertEmpty($result); + } + + /** + * Requesting calendar events for a course and before a given time should return + * all events with a sort time at or before the requested time (inclusive). All + * events after that time should not be returned. + * + * If there are no events before the given time then an empty result set should be + * returned. + */ + public function test_get_calendar_action_events_by_course_before_time() { + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $instance1 = $generator->create_instance(['course' => $course1->id]); + $instance2 = $generator->create_instance(['course' => $course2->id]); + $records = []; + + $this->getDataGenerator()->enrol_user($user->id, $course1->id); + $this->getDataGenerator()->enrol_user($user->id, $course2->id); + $this->resetAfterTest(true); + $this->setUser($user); + + for ($i = 1; $i < 19; $i++) { + $courseid = ($i < 9) ? $course1->id : $course2->id; + $instance = ($i < 9) ? $instance1->id : $instance2->id; + $records[] = $this->create_calendar_event( + sprintf('Event %d', $i), + $user->id, + 'user', + 0, + 1, + [ + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $courseid, + 'timesort' => $i + 1, + 'modulename' => 'assign', + 'instance' => $instance, + ] + ); + } + + $result = core_calendar_external::get_calendar_action_events_by_course($course1->id, null, 5); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_course_returns(), + $result + ); + $result = $result['events']; + + $this->assertCount(4, $result); + $this->assertEquals('Event 1', $result[0]['name']); + $this->assertEquals('Event 2', $result[1]['name']); + $this->assertEquals('Event 3', $result[2]['name']); + $this->assertEquals('Event 4', $result[3]['name']); + + $result = core_calendar_external::get_calendar_action_events_by_course($course1->id, null, 1); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_course_returns(), + $result + ); + $result = $result['events']; + + $this->assertEmpty($result); + } + + /** + * Requesting calendar events for a course and within a given time range should + * return all events with a sort time between the lower and upper time bound + * (inclusive). + * + * If there are no events in the given time range then an empty result set should be + * returned. + */ + public function test_get_calendar_action_events_by_course_time_range() { + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $instance1 = $generator->create_instance(['course' => $course1->id]); + $instance2 = $generator->create_instance(['course' => $course2->id]); + $records = []; + + $this->getDataGenerator()->enrol_user($user->id, $course1->id); + $this->getDataGenerator()->enrol_user($user->id, $course2->id); + $this->resetAfterTest(true); + $this->setUser($user); + + for ($i = 1; $i < 19; $i++) { + $courseid = ($i < 9) ? $course1->id : $course2->id; + $instance = ($i < 9) ? $instance1->id : $instance2->id; + $records[] = $this->create_calendar_event( + sprintf('Event %d', $i), + $user->id, + 'user', + 0, + 1, + [ + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $courseid, + 'timesort' => $i, + 'modulename' => 'assign', + 'instance' => $instance, + ] + ); + } + + $result = core_calendar_external::get_calendar_action_events_by_course($course1->id, 3, 6); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_course_returns(), + $result + ); + $result = $result['events']; + + $this->assertCount(4, $result); + $this->assertEquals('Event 3', $result[0]['name']); + $this->assertEquals('Event 4', $result[1]['name']); + $this->assertEquals('Event 5', $result[2]['name']); + $this->assertEquals('Event 6', $result[3]['name']); + + $result = core_calendar_external::get_calendar_action_events_by_course($course1->id, 10, 15); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_course_returns(), + $result + ); + $result = $result['events']; + + $this->assertEmpty($result); + } + + /** + * Requesting calendar events for a course and within a given time range and a limit + * and offset should return the number of events up to the given limit value that have + * a sort time between the lower and uppper time bound (inclusive) where the result + * set is shifted by the offset value. + * + * If there are no events in the given time range then an empty result set should be + * returned. + */ + public function test_get_calendar_action_events_by_course_time_limit_offset() { + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $instance1 = $generator->create_instance(['course' => $course1->id]); + $instance2 = $generator->create_instance(['course' => $course2->id]); + $records = []; + + $this->getDataGenerator()->enrol_user($user->id, $course1->id); + $this->getDataGenerator()->enrol_user($user->id, $course2->id); + $this->resetAfterTest(true); + $this->setUser($user); + + for ($i = 1; $i < 19; $i++) { + $courseid = ($i < 9) ? $course1->id : $course2->id; + $instance = ($i < 9) ? $instance1->id : $instance2->id; + $records[] = $this->create_calendar_event( + sprintf('Event %d', $i), + $user->id, + 'user', + 0, + 1, + [ + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $courseid, + 'timesort' => $i, + 'modulename' => 'assign', + 'instance' => $instance, + ] + ); + } + + $result = core_calendar_external::get_calendar_action_events_by_course( + $course1->id, 2, 7, $records[2]->id, 2); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_course_returns(), + $result + ); + $result = $result['events']; + + $this->assertCount(2, $result); + $this->assertEquals('Event 4', $result[0]['name']); + $this->assertEquals('Event 5', $result[1]['name']); + + $result = core_calendar_external::get_calendar_action_events_by_course( + $course1->id, 2, 7, $records[4]->id, 2); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_course_returns(), + $result + ); + $result = $result['events']; + + $this->assertCount(2, $result); + $this->assertEquals('Event 6', $result[0]['name']); + $this->assertEquals('Event 7', $result[1]['name']); + + $result = core_calendar_external::get_calendar_action_events_by_course( + $course1->id, 2, 7, $records[6]->id, 2); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_course_returns(), + $result + ); + $result = $result['events']; + + $this->assertEmpty($result); + } + + /** + * Test that get_action_events_by_courses will return a list of events for each + * course you provided as long as the user is enrolled in the course. + */ + public function test_get_action_events_by_courses() { + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course3 = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $instance1 = $generator->create_instance(['course' => $course1->id]); + $instance2 = $generator->create_instance(['course' => $course2->id]); + $instance3 = $generator->create_instance(['course' => $course3->id]); + $records = []; + $mapresult = function($result) { + $groupedbycourse = []; + foreach ($result['groupedbycourse'] as $group) { + $events = $group['events']; + $courseid = $group['courseid']; + $groupedbycourse[$courseid] = $events; + } + + return $groupedbycourse; + }; + + $this->getDataGenerator()->enrol_user($user->id, $course1->id); + $this->getDataGenerator()->enrol_user($user->id, $course2->id); + $this->resetAfterTest(true); + $this->setUser($user); + + for ($i = 1; $i < 10; $i++) { + if ($i < 3) { + $courseid = $course1->id; + $instance = $instance1->id; + } else if ($i < 6) { + $courseid = $course2->id; + $instance = $instance2->id; + } else { + $courseid = $course3->id; + $instance = $instance3->id; + } + + $records[] = $this->create_calendar_event( + sprintf('Event %d', $i), + $user->id, + 'user', + 0, + 1, + [ + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $courseid, + 'timesort' => $i, + 'modulename' => 'assign', + 'instance' => $instance, + ] + ); + } + + $result = core_calendar_external::get_calendar_action_events_by_courses([], 1); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_courses_returns(), + $result + ); + $result = $result['groupedbycourse']; + + $this->assertEmpty($result); + + $result = core_calendar_external::get_calendar_action_events_by_courses([$course1->id], 3); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_courses_returns(), + $result + ); + + $groupedbycourse = $mapresult($result); + + $this->assertEmpty($groupedbycourse[$course1->id]); + + $result = core_calendar_external::get_calendar_action_events_by_courses([$course1->id], 1); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_courses_returns(), + $result + ); + $groupedbycourse = $mapresult($result); + + $this->assertCount(2, $groupedbycourse[$course1->id]); + $this->assertEquals('Event 1', $groupedbycourse[$course1->id][0]['name']); + $this->assertEquals('Event 2', $groupedbycourse[$course1->id][1]['name']); + + $result = core_calendar_external::get_calendar_action_events_by_courses( + [$course1->id, $course2->id], 1); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_courses_returns(), + $result + ); + $groupedbycourse = $mapresult($result); + + $this->assertCount(2, $groupedbycourse[$course1->id]); + $this->assertEquals('Event 1', $groupedbycourse[$course1->id][0]['name']); + $this->assertEquals('Event 2', $groupedbycourse[$course1->id][1]['name']); + $this->assertCount(3, $groupedbycourse[$course2->id]); + $this->assertEquals('Event 3', $groupedbycourse[$course2->id][0]['name']); + $this->assertEquals('Event 4', $groupedbycourse[$course2->id][1]['name']); + $this->assertEquals('Event 5', $groupedbycourse[$course2->id][2]['name']); + + $result = core_calendar_external::get_calendar_action_events_by_courses( + [$course1->id, $course2->id], 2, 4); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_courses_returns(), + $result + ); + $groupedbycourse = $mapresult($result); + + $this->assertCount(2, $groupedbycourse); + $this->assertCount(1, $groupedbycourse[$course1->id]); + $this->assertEquals('Event 2', $groupedbycourse[$course1->id][0]['name']); + $this->assertCount(2, $groupedbycourse[$course2->id]); + $this->assertEquals('Event 3', $groupedbycourse[$course2->id][0]['name']); + $this->assertEquals('Event 4', $groupedbycourse[$course2->id][1]['name']); + + $result = core_calendar_external::get_calendar_action_events_by_courses( + [$course1->id, $course2->id], 1, null, 1); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_courses_returns(), + $result + ); + $groupedbycourse = $mapresult($result); + + $this->assertCount(2, $groupedbycourse); + $this->assertCount(1, $groupedbycourse[$course1->id]); + $this->assertEquals('Event 1', $groupedbycourse[$course1->id][0]['name']); + $this->assertCount(1, $groupedbycourse[$course2->id]); + $this->assertEquals('Event 3', $groupedbycourse[$course2->id][0]['name']); + } } diff --git a/calendar/tests/helpers.php b/calendar/tests/helpers.php new file mode 100644 index 00000000000..c990d4de37d --- /dev/null +++ b/calendar/tests/helpers.php @@ -0,0 +1,160 @@ +. + +/** + * This file contains helper classes and functions for testing. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/calendar/lib.php'); + +use core_calendar\local\event\entities\action_event; +use core_calendar\local\event\entities\event; +use core_calendar\local\event\entities\repeat_event_collection; +use core_calendar\local\event\proxies\std_proxy; +use core_calendar\local\event\value_objects\action; +use core_calendar\local\event\value_objects\event_description; +use core_calendar\local\event\value_objects\event_times; +use core_calendar\local\event\factories\event_factory_interface; + +/** + * Create a calendar event with the given properties. + * + * @param array $properties The properties to set on the event + * @return \calendar_event + */ +function create_event($properties) { + $record = new \stdClass(); + $record->name = 'event name'; + $record->eventtype = 'global'; + $record->repeat = 0; + $record->repeats = 0; + $record->timestart = time(); + $record->timeduration = 0; + $record->timesort = 0; + $record->type = CALENDAR_EVENT_TYPE_STANDARD; + $record->courseid = 0; + + foreach ($properties as $name => $value) { + $record->$name = $value; + } + + $event = new \calendar_event($record); + return $event->create($record); +} + +/** + * A test factory that will create action events. + * + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or late + */ +class action_event_test_factory implements event_factory_interface { + + /** + * @var callable $callback. + */ + private $callback; + + /** + * A test factory that will create action events. The factory accepts a callback + * that will be used to determine if the event should be returned or not. + * + * The callback will be given the event and should return true if the event + * should be returned and false otherwise. + * + * @param callable $callback The callback. + */ + public function __construct($callback = null) { + $this->callback = $callback; + } + + public function create_instance(\stdClass $record) { + $module = null; + $subscription = null; + + if ($record->instance && $record->modulename) { + $modulename = $record->modulename; + $module = new std_proxy($record->instance, function($id) use ($modulename) { + return get_coursemodule_from_instance($modulename, $id); + }, + (object)[ + 'modname' => $modulename, + 'instance' => $record->instance + ]); + } + + if ($record->subscriptionid) { + $subscription = new std_proxy($record->subscriptionid, function($id) { + return (object)['id' => $id]; + }); + } + + $event = new event( + $record->id, + $record->name, + new event_description($record->description, $record->format), + new std_proxy($record->courseid, function($id) { + $course = new \stdClass(); + $course->id = $id; + return $course; + }), + new std_proxy($record->groupid, function($id) { + $group = new \stdClass(); + $group->id = $id; + return $group; + }), + new std_proxy($record->userid, function($id) { + $user = new \stdClass(); + $user->id = $id; + return $user; + }), + new repeat_event_collection($record->id, null, $this), + $module, + $record->eventtype, + new event_times( + (new \DateTimeImmutable())->setTimestamp($record->timestart), + (new \DateTimeImmutable())->setTimestamp($record->timestart + $record->timeduration), + (new \DateTimeImmutable())->setTimestamp($record->timesort ? $record->timesort : $record->timestart), + (new \DateTimeImmutable())->setTimestamp($record->timemodified) + ), + !empty($record->visible), + $subscription + ); + + $action = new action( + 'Test action', + new \moodle_url('/'), + 1, + true + ); + + $actionevent = new action_event($event, $action); + + if ($callback = $this->callback) { + return $callback($actionevent) ? $actionevent : false; + } else { + return $actionevent; + } + } +} diff --git a/calendar/tests/ical_test.php b/calendar/tests/ical_test.php deleted file mode 100644 index d2223b538c2..00000000000 --- a/calendar/tests/ical_test.php +++ /dev/null @@ -1,138 +0,0 @@ -. - -/** - * Calendar Ical unit tests - * - * @package core_calendar - * @copyright 2013 Ankit Agarwal - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -defined('MOODLE_INTERNAL') || die(); - -global $CFG; - - -/** - * Unit tests for ical APIs. - * - * @package core_calendar - * @copyright 2013 Ankit Agarwal - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.5 - */ -class core_calendar_ical_testcase extends advanced_testcase { - - /** - * Tests set up - */ - protected function setUp() { - global $CFG; - require_once($CFG->dirroot . '/calendar/lib.php'); - } - - /** - * @expectedException coding_exception - */ - public function test_calendar_update_subscription() { - $this->resetAfterTest(true); - - $subscription = new stdClass(); - $subscription->eventtype = 'site'; - $subscription->name = 'test'; - $id = calendar_add_subscription($subscription); - - $subscription = new stdClass(); - $subscription = calendar_get_subscription($id); - $subscription->name = 'awesome'; - calendar_update_subscription($subscription); - $sub = calendar_get_subscription($id); - $this->assertEquals($subscription->name, $sub->name); - - $subscription = new stdClass(); - $subscription = calendar_get_subscription($id); - $subscription->name = 'awesome2'; - $subscription->pollinterval = 604800; - calendar_update_subscription($subscription); - $sub = calendar_get_subscription($id); - $this->assertEquals($subscription->name, $sub->name); - $this->assertEquals($subscription->pollinterval, $sub->pollinterval); - - $subscription = new stdClass(); - $subscription->name = 'awesome4'; - calendar_update_subscription($subscription); - } - - public function test_calendar_add_subscription() { - global $DB, $CFG; - - require_once($CFG->dirroot . '/lib/bennu/bennu.inc.php'); - - $this->resetAfterTest(true); - - // Test for Microsoft Outlook 2010. - $subscription = new stdClass(); - $subscription->name = 'Microsoft Outlook 2010'; - $subscription->importfrom = CALENDAR_IMPORT_FROM_FILE; - $subscription->eventtype = 'site'; - $id = calendar_add_subscription($subscription); - - $calendar = file_get_contents($CFG->dirroot . '/lib/tests/fixtures/ms_outlook_2010.ics'); - $ical = new iCalendar(); - $ical->unserialize($calendar); - $this->assertEquals($ical->parser_errors, array()); - - $sub = calendar_get_subscription($id); - $result = calendar_import_icalendar_events($ical, $sub->courseid, $sub->id); - $count = $DB->count_records('event', array('subscriptionid' => $sub->id)); - $this->assertEquals($count, 1); - - // Test for OSX Yosemite. - $subscription = new stdClass(); - $subscription->name = 'OSX Yosemite'; - $subscription->importfrom = CALENDAR_IMPORT_FROM_FILE; - $subscription->eventtype = 'site'; - $id = calendar_add_subscription($subscription); - - $calendar = file_get_contents($CFG->dirroot . '/lib/tests/fixtures/osx_yosemite.ics'); - $ical = new iCalendar(); - $ical->unserialize($calendar); - $this->assertEquals($ical->parser_errors, array()); - - $sub = calendar_get_subscription($id); - $result = calendar_import_icalendar_events($ical, $sub->courseid, $sub->id); - $count = $DB->count_records('event', array('subscriptionid' => $sub->id)); - $this->assertEquals($count, 1); - - // Test for Google Gmail. - $subscription = new stdClass(); - $subscription->name = 'Google Gmail'; - $subscription->importfrom = CALENDAR_IMPORT_FROM_FILE; - $subscription->eventtype = 'site'; - $id = calendar_add_subscription($subscription); - - $calendar = file_get_contents($CFG->dirroot . '/lib/tests/fixtures/google_gmail.ics'); - $ical = new iCalendar(); - $ical->unserialize($calendar); - $this->assertEquals($ical->parser_errors, array()); - - $sub = calendar_get_subscription($id); - $result = calendar_import_icalendar_events($ical, $sub->courseid, $sub->id); - $count = $DB->count_records('event', array('subscriptionid' => $sub->id)); - $this->assertEquals($count, 1); - } -} diff --git a/calendar/tests/lib_test.php b/calendar/tests/lib_test.php index 3e3bb394824..797dc891c42 100644 --- a/calendar/tests/lib_test.php +++ b/calendar/tests/lib_test.php @@ -15,116 +15,48 @@ // along with Moodle. If not, see . /** - * Calendar lib unit tests + * Contains the class containing unit tests for the calendar lib. * * @package core_calendar - * @copyright 2013 Dan Poltawski + * @copyright 2017 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); -global $CFG; -require_once($CFG->dirroot . '/calendar/lib.php'); + +require_once(__DIR__ . '/helpers.php'); /** - * Unit tests for calendar lib + * Class contaning unit tests for the calendar lib. * * @package core_calendar - * @copyright 2013 Dan Poltawski + * @copyright 2017 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class core_calendar_lib_testcase extends advanced_testcase { + /** + * Tests set up + */ protected function setUp() { - $this->resetAfterTest(true); - } - - public function test_calendar_get_course_cached() { - // Setup some test courses. - $course1 = $this->getDataGenerator()->create_course(); - $course2 = $this->getDataGenerator()->create_course(); - $course3 = $this->getDataGenerator()->create_course(); - - // Load courses into cache. - $coursecache = null; - calendar_get_course_cached($coursecache, $course1->id); - calendar_get_course_cached($coursecache, $course2->id); - calendar_get_course_cached($coursecache, $course3->id); - - // Verify the cache. - $this->assertArrayHasKey($course1->id, $coursecache); - $cachedcourse1 = $coursecache[$course1->id]; - $this->assertEquals($course1->id, $cachedcourse1->id); - $this->assertEquals($course1->shortname, $cachedcourse1->shortname); - $this->assertEquals($course1->fullname, $cachedcourse1->fullname); - - $this->assertArrayHasKey($course2->id, $coursecache); - $cachedcourse2 = $coursecache[$course2->id]; - $this->assertEquals($course2->id, $cachedcourse2->id); - $this->assertEquals($course2->shortname, $cachedcourse2->shortname); - $this->assertEquals($course2->fullname, $cachedcourse2->fullname); - - $this->assertArrayHasKey($course3->id, $coursecache); - $cachedcourse3 = $coursecache[$course3->id]; - $this->assertEquals($course3->id, $cachedcourse3->id); - $this->assertEquals($course3->shortname, $cachedcourse3->shortname); - $this->assertEquals($course3->fullname, $cachedcourse3->fullname); + $this->resetAfterTest(); } /** - * Test calendar cron with a working subscription URL. + * Test that the get_events() function only returns activity events that are enabled. */ - public function test_calendar_cron_working_url() { - global $CFG; - require_once($CFG->dirroot . '/lib/cronlib.php'); - - // ICal URL from external test repo. - $subscriptionurl = $this->getExternalTestFileUrl('/ical.ics'); - - $subscription = new stdClass(); - $subscription->eventtype = 'site'; - $subscription->name = 'test'; - $subscription->url = $subscriptionurl; - $subscription->pollinterval = 86400; - $subscription->lastupdated = 0; - calendar_add_subscription($subscription); - - $this->expectOutputRegex('/Events imported: .* Events updated:/'); - calendar_cron(); - } - - /** - * Test calendar cron with a broken subscription URL. - */ - public function test_calendar_cron_broken_url() { - global $CFG; - require_once($CFG->dirroot . '/lib/cronlib.php'); - - $subscription = new stdClass(); - $subscription->eventtype = 'site'; - $subscription->name = 'test'; - $subscription->url = 'brokenurl'; - $subscription->pollinterval = 86400; - $subscription->lastupdated = 0; - calendar_add_subscription($subscription); - - $this->expectOutputRegex('/Error updating calendar subscription: The given iCal URL is invalid/'); - calendar_cron(); - } - - /** - * Test the calendar_get_events() function only returns activity - * events that are enabled. - */ - public function test_calendar_get_events_with_disabled_module() { + public function test_get_events_with_disabled_module() { global $DB; - + $this->setAdminUser(); $generator = $this->getDataGenerator(); $course = $generator->create_course(); + $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $assigninstance = $assigngenerator->create_instance(['course' => $course->id]); + $lessongenerator = $this->getDataGenerator()->get_plugin_generator('mod_lesson'); + $lessoninstance = $lessongenerator->create_instance(['course' => $course->id]); $student = $generator->create_user(); $generator->enrol_user($student->id, $course->id, 'student'); $this->setUser($student); - $events = [ [ 'name' => 'Start of assignment', @@ -134,13 +66,12 @@ class core_calendar_lib_testcase extends advanced_testcase { 'groupid' => 0, 'userid' => 2, 'modulename' => 'assign', - 'instance' => 1, + 'instance' => $assigninstance->id, 'eventtype' => 'due', 'timestart' => time(), 'timeduration' => 86400, 'visible' => 1 ], [ - 'name' => 'Start of lesson', 'description' => '', 'format' => 1, @@ -148,30 +79,25 @@ class core_calendar_lib_testcase extends advanced_testcase { 'groupid' => 0, 'userid' => 2, 'modulename' => 'lesson', - 'instance' => 1, + 'instance' => $lessoninstance->id, 'eventtype' => 'end', 'timestart' => time(), 'timeduration' => 86400, 'visible' => 1 ] ]; - foreach ($events as $event) { calendar_event::create($event, false); } - $timestart = time() - 60; $timeend = time() + 60; - // Get all events. $events = calendar_get_events($timestart, $timeend, true, 0, true); $this->assertCount(2, $events); - // Disable the lesson module. $modulerecord = $DB->get_record('modules', ['name' => 'lesson']); $modulerecord->visible = 0; $DB->update_record('modules', $modulerecord); - // Check that we only return the assign event. $events = calendar_get_events($timestart, $timeend, true, 0, true); $this->assertCount(1, $events); @@ -184,7 +110,6 @@ class core_calendar_lib_testcase extends advanced_testcase { */ public function test_calendar_get_events_with_overrides() { global $DB; - $generator = $this->getDataGenerator(); $course = $generator->create_course(); $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); @@ -192,31 +117,26 @@ class core_calendar_lib_testcase extends advanced_testcase { $params['course'] = $course->id; } $instance = $plugingenerator->create_instance($params); - // Create users. $useroverridestudent = $generator->create_user(); $group1student = $generator->create_user(); $group2student = $generator->create_user(); $group12student = $generator->create_user(); $nogroupstudent = $generator->create_user(); - // Enrol users. $generator->enrol_user($useroverridestudent->id, $course->id, 'student'); $generator->enrol_user($group1student->id, $course->id, 'student'); $generator->enrol_user($group2student->id, $course->id, 'student'); $generator->enrol_user($group12student->id, $course->id, 'student'); $generator->enrol_user($nogroupstudent->id, $course->id, 'student'); - // Create groups. $group1 = $generator->create_group(['courseid' => $course->id]); $group2 = $generator->create_group(['courseid' => $course->id]); - // Add members to groups. $generator->create_group_member(['groupid' => $group1->id, 'userid' => $group1student->id]); $generator->create_group_member(['groupid' => $group2->id, 'userid' => $group2student->id]); $generator->create_group_member(['groupid' => $group1->id, 'userid' => $group12student->id]); $generator->create_group_member(['groupid' => $group2->id, 'userid' => $group12student->id]); - $now = time(); // Events with the same module name, instance and event type. $events = [ @@ -277,48 +197,39 @@ class core_calendar_lib_testcase extends advanced_testcase { 'priority' => 2, ], ]; - foreach ($events as $event) { calendar_event::create($event, false); } - $timestart = $now - 100; $timeend = $now + (3 * 86400); - $groups = [$group1->id, $group2->id]; - // Get user override events. $this->setUser($useroverridestudent); $events = calendar_get_events($timestart, $timeend, $useroverridestudent->id, $groups, $course->id); $this->assertCount(1, $events); $event = reset($events); $this->assertEquals('Assignment 1 due date - User override', $event->name); - // Get event for user with override but with the timestart and timeend parameters only covering the original event. $events = calendar_get_events($timestart, $now, $useroverridestudent->id, $groups, $course->id); $this->assertCount(0, $events); - // Get events for user that does not belong to any group and has no user override events. $this->setUser($nogroupstudent); $events = calendar_get_events($timestart, $timeend, $nogroupstudent->id, $groups, $course->id); $this->assertCount(1, $events); $event = reset($events); $this->assertEquals('Assignment 1 due date', $event->name); - // Get events for user that belongs to groups A and B and has no user override events. $this->setUser($group12student); $events = calendar_get_events($timestart, $timeend, $group12student->id, $groups, $course->id); $this->assertCount(1, $events); $event = reset($events); $this->assertEquals('Assignment 1 due date - Group B override', $event->name); - // Get events for user that belongs to group A and has no user override events. $this->setUser($group1student); $events = calendar_get_events($timestart, $timeend, $group1student->id, $groups, $course->id); $this->assertCount(1, $events); $event = reset($events); $this->assertEquals('Assignment 1 due date - Group A override', $event->name); - // Add repeating events. $repeatingevents = [ [ @@ -328,7 +239,7 @@ class core_calendar_lib_testcase extends advanced_testcase { 'courseid' => SITEID, 'groupid' => 0, 'userid' => 2, - 'repeatid' => 1, + 'repeatid' => $event->id, 'modulename' => '0', 'instance' => 0, 'eventtype' => 'site', @@ -343,7 +254,7 @@ class core_calendar_lib_testcase extends advanced_testcase { 'courseid' => SITEID, 'groupid' => 0, 'userid' => 2, - 'repeatid' => 1, + 'repeatid' => $event->id, 'modulename' => '0', 'instance' => 0, 'eventtype' => 'site', @@ -355,9 +266,130 @@ class core_calendar_lib_testcase extends advanced_testcase { foreach ($repeatingevents as $event) { calendar_event::create($event, false); } - // Make sure repeating events are not filtered out. $events = calendar_get_events($timestart, $timeend, true, true, true); $this->assertCount(3, $events); } -} + + public function test_get_course_cached() { + // Setup some test courses. + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course3 = $this->getDataGenerator()->create_course(); + + // Load courses into cache. + $coursecache = null; + calendar_get_course_cached($coursecache, $course1->id); + calendar_get_course_cached($coursecache, $course2->id); + calendar_get_course_cached($coursecache, $course3->id); + + // Verify the cache. + $this->assertArrayHasKey($course1->id, $coursecache); + $cachedcourse1 = $coursecache[$course1->id]; + $this->assertEquals($course1->id, $cachedcourse1->id); + $this->assertEquals($course1->shortname, $cachedcourse1->shortname); + $this->assertEquals($course1->fullname, $cachedcourse1->fullname); + + $this->assertArrayHasKey($course2->id, $coursecache); + $cachedcourse2 = $coursecache[$course2->id]; + $this->assertEquals($course2->id, $cachedcourse2->id); + $this->assertEquals($course2->shortname, $cachedcourse2->shortname); + $this->assertEquals($course2->fullname, $cachedcourse2->fullname); + + $this->assertArrayHasKey($course3->id, $coursecache); + $cachedcourse3 = $coursecache[$course3->id]; + $this->assertEquals($course3->id, $cachedcourse3->id); + $this->assertEquals($course3->shortname, $cachedcourse3->shortname); + $this->assertEquals($course3->fullname, $cachedcourse3->fullname); + } + + /** + * Test the update_subscription() function. + */ + public function test_update_subscription() { + $this->resetAfterTest(true); + + $subscription = new stdClass(); + $subscription->eventtype = 'site'; + $subscription->name = 'test'; + $id = calendar_add_subscription($subscription); + + $subscription = calendar_get_subscription($id); + $subscription->name = 'awesome'; + calendar_update_subscription($subscription); + $sub = calendar_get_subscription($id); + $this->assertEquals($subscription->name, $sub->name); + + $subscription = calendar_get_subscription($id); + $subscription->name = 'awesome2'; + $subscription->pollinterval = 604800; + calendar_update_subscription($subscription); + $sub = calendar_get_subscription($id); + $this->assertEquals($subscription->name, $sub->name); + $this->assertEquals($subscription->pollinterval, $sub->pollinterval); + + $subscription = new stdClass(); + $subscription->name = 'awesome4'; + $this->expectException('coding_exception'); + calendar_update_subscription($subscription); + } + + public function test_add_subscription() { + global $DB, $CFG; + + require_once($CFG->dirroot . '/lib/bennu/bennu.inc.php'); + + $this->resetAfterTest(true); + + // Test for Microsoft Outlook 2010. + $subscription = new stdClass(); + $subscription->name = 'Microsoft Outlook 2010'; + $subscription->importfrom = CALENDAR_IMPORT_FROM_FILE; + $subscription->eventtype = 'site'; + $id = calendar_add_subscription($subscription); + + $calendar = file_get_contents($CFG->dirroot . '/lib/tests/fixtures/ms_outlook_2010.ics'); + $ical = new iCalendar(); + $ical->unserialize($calendar); + $this->assertEquals($ical->parser_errors, array()); + + $sub = calendar_get_subscription($id); + calendar_import_icalendar_events($ical, $sub->courseid, $sub->id); + $count = $DB->count_records('event', array('subscriptionid' => $sub->id)); + $this->assertEquals($count, 1); + + // Test for OSX Yosemite. + $subscription = new stdClass(); + $subscription->name = 'OSX Yosemite'; + $subscription->importfrom = CALENDAR_IMPORT_FROM_FILE; + $subscription->eventtype = 'site'; + $id = calendar_add_subscription($subscription); + + $calendar = file_get_contents($CFG->dirroot . '/lib/tests/fixtures/osx_yosemite.ics'); + $ical = new iCalendar(); + $ical->unserialize($calendar); + $this->assertEquals($ical->parser_errors, array()); + + $sub = calendar_get_subscription($id); + calendar_import_icalendar_events($ical, $sub->courseid, $sub->id); + $count = $DB->count_records('event', array('subscriptionid' => $sub->id)); + $this->assertEquals($count, 1); + + // Test for Google Gmail. + $subscription = new stdClass(); + $subscription->name = 'Google Gmail'; + $subscription->importfrom = CALENDAR_IMPORT_FROM_FILE; + $subscription->eventtype = 'site'; + $id = calendar_add_subscription($subscription); + + $calendar = file_get_contents($CFG->dirroot . '/lib/tests/fixtures/google_gmail.ics'); + $ical = new iCalendar(); + $ical->unserialize($calendar); + $this->assertEquals($ical->parser_errors, array()); + + $sub = calendar_get_subscription($id); + calendar_import_icalendar_events($ical, $sub->courseid, $sub->id); + $count = $DB->count_records('event', array('subscriptionid' => $sub->id)); + $this->assertEquals($count, 1); + } +} \ No newline at end of file diff --git a/calendar/tests/local_api_test.php b/calendar/tests/local_api_test.php new file mode 100644 index 00000000000..599a6052048 --- /dev/null +++ b/calendar/tests/local_api_test.php @@ -0,0 +1,861 @@ +. + +/** + * Contains the class containing unit tests for the calendar local API. + * + * @package core_calendar + * @copyright 2017 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/helpers.php'); + +/** + * Class contaning unit tests for the calendar local API. + * + * @package core_calendar + * @copyright 2017 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_calendar_local_api_testcase extends advanced_testcase { + + /** + * Tests set up + */ + protected function setUp() { + $this->resetAfterTest(); + } + + /** + * Requesting calendar events from a given time should return all events with a sort + * time at or after the requested time. All events prior to that time should not + * be return. + * + * If there are no events on or after the given time then an empty result set should + * be returned. + */ + public function test_get_calendar_action_events_by_timesort_after_time() { + $user = $this->getDataGenerator()->create_user(); + $course = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $moduleinstance = $generator->create_instance(['course' => $course->id]); + + $this->getDataGenerator()->enrol_user($user->id, $course->id); + $this->resetAfterTest(true); + $this->setAdminUser(); + + $params = [ + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $course->id, + 'modulename' => 'assign', + 'instance' => $moduleinstance->id, + 'userid' => $user->id, + 'eventtype' => 'user', + 'repeats' => 0, + 'timestart' => 1, + ]; + + $event1 = create_event(array_merge($params, ['name' => 'Event 1', 'timesort' => 1])); + $event2 = create_event(array_merge($params, ['name' => 'Event 2', 'timesort' => 2])); + $event3 = create_event(array_merge($params, ['name' => 'Event 3', 'timesort' => 3])); + $event4 = create_event(array_merge($params, ['name' => 'Event 4', 'timesort' => 4])); + $event5 = create_event(array_merge($params, ['name' => 'Event 5', 'timesort' => 5])); + $event6 = create_event(array_merge($params, ['name' => 'Event 6', 'timesort' => 6])); + $event7 = create_event(array_merge($params, ['name' => 'Event 7', 'timesort' => 7])); + $event8 = create_event(array_merge($params, ['name' => 'Event 8', 'timesort' => 8])); + + $this->setUser($user); + $result = \core_calendar\local\api::get_action_events_by_timesort(5); + + $this->assertCount(4, $result); + $this->assertEquals('Event 5', $result[0]->get_name()); + $this->assertEquals('Event 6', $result[1]->get_name()); + $this->assertEquals('Event 7', $result[2]->get_name()); + $this->assertEquals('Event 8', $result[3]->get_name()); + + $result = \core_calendar\local\api::get_action_events_by_timesort(9); + + $this->assertEmpty($result); + } + + /** + * Requesting calendar events before a given time should return all events with a sort + * time at or before the requested time (inclusive). All events after that time + * should not be returned. + * + * If there are no events before the given time then an empty result set should be + * returned. + */ + public function test_get_calendar_action_events_by_timesort_before_time() { + $user = $this->getDataGenerator()->create_user(); + $course = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $moduleinstance = $generator->create_instance(['course' => $course->id]); + + $this->getDataGenerator()->enrol_user($user->id, $course->id); + $this->resetAfterTest(true); + $this->setAdminUser(); + + $params = [ + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $course->id, + 'modulename' => 'assign', + 'instance' => $moduleinstance->id, + 'userid' => 1, + 'eventtype' => 'user', + 'repeats' => 0, + 'timestart' => 1, + ]; + + $event1 = create_event(array_merge($params, ['name' => 'Event 1', 'timesort' => 2])); + $event2 = create_event(array_merge($params, ['name' => 'Event 2', 'timesort' => 3])); + $event3 = create_event(array_merge($params, ['name' => 'Event 3', 'timesort' => 4])); + $event4 = create_event(array_merge($params, ['name' => 'Event 4', 'timesort' => 5])); + $event5 = create_event(array_merge($params, ['name' => 'Event 5', 'timesort' => 6])); + $event6 = create_event(array_merge($params, ['name' => 'Event 6', 'timesort' => 7])); + $event7 = create_event(array_merge($params, ['name' => 'Event 7', 'timesort' => 8])); + $event8 = create_event(array_merge($params, ['name' => 'Event 8', 'timesort' => 9])); + + $this->setUser($user); + $result = \core_calendar\local\api::get_action_events_by_timesort(null, 5); + + $this->assertCount(4, $result); + $this->assertEquals('Event 1', $result[0]->get_name()); + $this->assertEquals('Event 2', $result[1]->get_name()); + $this->assertEquals('Event 3', $result[2]->get_name()); + $this->assertEquals('Event 4', $result[3]->get_name()); + + $result = \core_calendar\local\api::get_action_events_by_timesort(null, 1); + + $this->assertEmpty($result); + } + + /** + * Requesting calendar events within a given time range should return all events with + * a sort time between the lower and upper time bound (inclusive). + * + * If there are no events in the given time range then an empty result set should be + * returned. + */ + public function test_get_calendar_action_events_by_timesort_time_range() { + $user = $this->getDataGenerator()->create_user(); + $course = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $moduleinstance = $generator->create_instance(['course' => $course->id]); + + $this->getDataGenerator()->enrol_user($user->id, $course->id); + $this->resetAfterTest(true); + $this->setAdminUser(); + + $params = [ + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $course->id, + 'modulename' => 'assign', + 'instance' => $moduleinstance->id, + 'userid' => 1, + 'eventtype' => 'user', + 'repeats' => 0, + 'timestart' => 1, + ]; + + $event1 = create_event(array_merge($params, ['name' => 'Event 1', 'timesort' => 1])); + $event2 = create_event(array_merge($params, ['name' => 'Event 2', 'timesort' => 2])); + $event3 = create_event(array_merge($params, ['name' => 'Event 3', 'timesort' => 3])); + $event4 = create_event(array_merge($params, ['name' => 'Event 4', 'timesort' => 4])); + $event5 = create_event(array_merge($params, ['name' => 'Event 5', 'timesort' => 5])); + $event6 = create_event(array_merge($params, ['name' => 'Event 6', 'timesort' => 6])); + $event7 = create_event(array_merge($params, ['name' => 'Event 7', 'timesort' => 7])); + $event8 = create_event(array_merge($params, ['name' => 'Event 8', 'timesort' => 8])); + + $this->setUser($user); + $result = \core_calendar\local\api::get_action_events_by_timesort(3, 6); + + $this->assertCount(4, $result); + $this->assertEquals('Event 3', $result[0]->get_name()); + $this->assertEquals('Event 4', $result[1]->get_name()); + $this->assertEquals('Event 5', $result[2]->get_name()); + $this->assertEquals('Event 6', $result[3]->get_name()); + + $result = \core_calendar\local\api::get_action_events_by_timesort(10, 15); + + $this->assertEmpty($result); + } + + /** + * Requesting calendar events within a given time range and a limit and offset should return + * the number of events up to the given limit value that have a sort time between the lower + * and uppper time bound (inclusive) where the result set is shifted by the offset value. + * + * If there are no events in the given time range then an empty result set should be + * returned. + */ + public function test_get_calendar_action_events_by_timesort_time_limit_offset() { + $user = $this->getDataGenerator()->create_user(); + $course = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $moduleinstance = $generator->create_instance(['course' => $course->id]); + + $this->getDataGenerator()->enrol_user($user->id, $course->id); + $this->resetAfterTest(true); + $this->setAdminUser(); + + $params = [ + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $course->id, + 'modulename' => 'assign', + 'instance' => $moduleinstance->id, + 'userid' => 1, + 'eventtype' => 'user', + 'repeats' => 0, + 'timestart' => 1, + ]; + + $event1 = create_event(array_merge($params, ['name' => 'Event 1', 'timesort' => 1])); + $event2 = create_event(array_merge($params, ['name' => 'Event 2', 'timesort' => 2])); + $event3 = create_event(array_merge($params, ['name' => 'Event 3', 'timesort' => 3])); + $event4 = create_event(array_merge($params, ['name' => 'Event 4', 'timesort' => 4])); + $event5 = create_event(array_merge($params, ['name' => 'Event 5', 'timesort' => 5])); + $event6 = create_event(array_merge($params, ['name' => 'Event 6', 'timesort' => 6])); + $event7 = create_event(array_merge($params, ['name' => 'Event 7', 'timesort' => 7])); + $event8 = create_event(array_merge($params, ['name' => 'Event 8', 'timesort' => 8])); + + $this->setUser($user); + $result = \core_calendar\local\api::get_action_events_by_timesort(2, 7, $event3->id, 2); + + $this->assertCount(2, $result); + $this->assertEquals('Event 4', $result[0]->get_name()); + $this->assertEquals('Event 5', $result[1]->get_name()); + + $result = \core_calendar\local\api::get_action_events_by_timesort(2, 7, $event5->id, 2); + + $this->assertCount(2, $result); + $this->assertEquals('Event 6', $result[0]->get_name()); + $this->assertEquals('Event 7', $result[1]->get_name()); + + $result = \core_calendar\local\api::get_action_events_by_timesort(2, 7, $event7->id, 2); + + $this->assertEmpty($result); + } + + /** + * Requesting calendar events from a given course and time should return all + * events with a sort time at or after the requested time. All events prior + * to that time should not be return. + * + * If there are no events on or after the given time then an empty result set should + * be returned. + */ + public function test_get_calendar_action_events_by_course_after_time() { + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $moduleinstance1 = $generator->create_instance(['course' => $course1->id]); + $moduleinstance2 = $generator->create_instance(['course' => $course2->id]); + + $this->getDataGenerator()->enrol_user($user->id, $course1->id); + $this->getDataGenerator()->enrol_user($user->id, $course2->id); + $this->resetAfterTest(true); + $this->setUser($user); + + $params = [ + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'modulename' => 'assign', + 'instance' => $moduleinstance1->id, + 'userid' => $user->id, + 'courseid' => $course1->id, + 'eventtype' => 'user', + 'repeats' => 0, + 'timestart' => 1, + ]; + + $event1 = create_event(array_merge($params, ['name' => 'Event 1', 'timesort' => 1])); + $event2 = create_event(array_merge($params, ['name' => 'Event 2', 'timesort' => 2])); + $event3 = create_event(array_merge($params, ['name' => 'Event 3', 'timesort' => 3])); + $event4 = create_event(array_merge($params, ['name' => 'Event 4', 'timesort' => 4])); + $event5 = create_event(array_merge($params, ['name' => 'Event 5', 'timesort' => 5])); + $event6 = create_event(array_merge($params, ['name' => 'Event 6', 'timesort' => 6])); + $event7 = create_event(array_merge($params, ['name' => 'Event 7', 'timesort' => 7])); + $event8 = create_event(array_merge($params, ['name' => 'Event 8', 'timesort' => 8])); + + $params['courseid'] = $course2->id; + $params['instance'] = $moduleinstance2->id; + $event9 = create_event(array_merge($params, ['name' => 'Event 9', 'timesort' => 1])); + $event10 = create_event(array_merge($params, ['name' => 'Event 10', 'timesort' => 2])); + $event11 = create_event(array_merge($params, ['name' => 'Event 11', 'timesort' => 3])); + $event12 = create_event(array_merge($params, ['name' => 'Event 12', 'timesort' => 4])); + $event13 = create_event(array_merge($params, ['name' => 'Event 13', 'timesort' => 5])); + $event14 = create_event(array_merge($params, ['name' => 'Event 14', 'timesort' => 6])); + $event15 = create_event(array_merge($params, ['name' => 'Event 15', 'timesort' => 7])); + $event16 = create_event(array_merge($params, ['name' => 'Event 16', 'timesort' => 8])); + + $result = \core_calendar\local\api::get_action_events_by_course($course1, 5); + + $this->assertCount(4, $result); + $this->assertEquals('Event 5', $result[0]->get_name()); + $this->assertEquals('Event 6', $result[1]->get_name()); + $this->assertEquals('Event 7', $result[2]->get_name()); + $this->assertEquals('Event 8', $result[3]->get_name()); + + $result = \core_calendar\local\api::get_action_events_by_course($course1, 9); + + $this->assertEmpty($result); + } + + /** + * Requesting calendar events for a course and before a given time should return + * all events with a sort time at or before the requested time (inclusive). All + * events after that time should not be returned. + * + * If there are no events before the given time then an empty result set should be + * returned. + */ + public function test_get_calendar_action_events_by_course_before_time() { + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $moduleinstance1 = $generator->create_instance(['course' => $course1->id]); + $moduleinstance2 = $generator->create_instance(['course' => $course2->id]); + + $this->getDataGenerator()->enrol_user($user->id, $course1->id); + $this->getDataGenerator()->enrol_user($user->id, $course2->id); + $this->resetAfterTest(true); + $this->setUser($user); + + $params = [ + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'modulename' => 'assign', + 'instance' => $moduleinstance1->id, + 'userid' => $user->id, + 'courseid' => $course1->id, + 'eventtype' => 'user', + 'repeats' => 0, + 'timestart' => 1, + ]; + + $event1 = create_event(array_merge($params, ['name' => 'Event 1', 'timesort' => 2])); + $event2 = create_event(array_merge($params, ['name' => 'Event 2', 'timesort' => 3])); + $event3 = create_event(array_merge($params, ['name' => 'Event 3', 'timesort' => 4])); + $event4 = create_event(array_merge($params, ['name' => 'Event 4', 'timesort' => 5])); + $event5 = create_event(array_merge($params, ['name' => 'Event 5', 'timesort' => 6])); + $event6 = create_event(array_merge($params, ['name' => 'Event 6', 'timesort' => 7])); + $event7 = create_event(array_merge($params, ['name' => 'Event 7', 'timesort' => 8])); + $event8 = create_event(array_merge($params, ['name' => 'Event 8', 'timesort' => 9])); + + $params['courseid'] = $course2->id; + $params['instance'] = $moduleinstance2->id; + $event9 = create_event(array_merge($params, ['name' => 'Event 9', 'timesort' => 2])); + $event10 = create_event(array_merge($params, ['name' => 'Event 10', 'timesort' => 3])); + $event11 = create_event(array_merge($params, ['name' => 'Event 11', 'timesort' => 4])); + $event12 = create_event(array_merge($params, ['name' => 'Event 12', 'timesort' => 5])); + $event13 = create_event(array_merge($params, ['name' => 'Event 13', 'timesort' => 6])); + $event14 = create_event(array_merge($params, ['name' => 'Event 14', 'timesort' => 7])); + $event15 = create_event(array_merge($params, ['name' => 'Event 15', 'timesort' => 8])); + $event16 = create_event(array_merge($params, ['name' => 'Event 16', 'timesort' => 9])); + + $result = \core_calendar\local\api::get_action_events_by_course($course1, null, 5); + + $this->assertCount(4, $result); + $this->assertEquals('Event 1', $result[0]->get_name()); + $this->assertEquals('Event 2', $result[1]->get_name()); + $this->assertEquals('Event 3', $result[2]->get_name()); + $this->assertEquals('Event 4', $result[3]->get_name()); + + $result = \core_calendar\local\api::get_action_events_by_course($course1, null, 1); + + $this->assertEmpty($result); + } + + /** + * Requesting calendar events for a course and within a given time range should + * return all events with a sort time between the lower and upper time bound + * (inclusive). + * + * If there are no events in the given time range then an empty result set should be + * returned. + */ + public function test_get_calendar_action_events_by_course_time_range() { + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $moduleinstance1 = $generator->create_instance(['course' => $course1->id]); + $moduleinstance2 = $generator->create_instance(['course' => $course2->id]); + + $this->getDataGenerator()->enrol_user($user->id, $course1->id); + $this->getDataGenerator()->enrol_user($user->id, $course2->id); + $this->resetAfterTest(true); + $this->setUser($user); + + $params = [ + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'modulename' => 'assign', + 'instance' => $moduleinstance1->id, + 'userid' => $user->id, + 'courseid' => $course1->id, + 'eventtype' => 'user', + 'repeats' => 0, + 'timestart' => 1, + ]; + + $event1 = create_event(array_merge($params, ['name' => 'Event 1', 'timesort' => 1])); + $event2 = create_event(array_merge($params, ['name' => 'Event 2', 'timesort' => 2])); + $event3 = create_event(array_merge($params, ['name' => 'Event 3', 'timesort' => 3])); + $event4 = create_event(array_merge($params, ['name' => 'Event 4', 'timesort' => 4])); + $event5 = create_event(array_merge($params, ['name' => 'Event 5', 'timesort' => 5])); + $event6 = create_event(array_merge($params, ['name' => 'Event 6', 'timesort' => 6])); + $event7 = create_event(array_merge($params, ['name' => 'Event 7', 'timesort' => 7])); + $event8 = create_event(array_merge($params, ['name' => 'Event 8', 'timesort' => 8])); + + $params['courseid'] = $course2->id; + $params['instance'] = $moduleinstance2->id; + $event9 = create_event(array_merge($params, ['name' => 'Event 9', 'timesort' => 1])); + $event10 = create_event(array_merge($params, ['name' => 'Event 10', 'timesort' => 2])); + $event11 = create_event(array_merge($params, ['name' => 'Event 11', 'timesort' => 3])); + $event12 = create_event(array_merge($params, ['name' => 'Event 12', 'timesort' => 4])); + $event13 = create_event(array_merge($params, ['name' => 'Event 13', 'timesort' => 5])); + $event14 = create_event(array_merge($params, ['name' => 'Event 14', 'timesort' => 6])); + $event15 = create_event(array_merge($params, ['name' => 'Event 15', 'timesort' => 7])); + $event16 = create_event(array_merge($params, ['name' => 'Event 16', 'timesort' => 8])); + + $result = \core_calendar\local\api::get_action_events_by_course($course1, 3, 6); + + $this->assertCount(4, $result); + $this->assertEquals('Event 3', $result[0]->get_name()); + $this->assertEquals('Event 4', $result[1]->get_name()); + $this->assertEquals('Event 5', $result[2]->get_name()); + $this->assertEquals('Event 6', $result[3]->get_name()); + + $result = \core_calendar\local\api::get_action_events_by_course($course1, 10, 15); + + $this->assertEmpty($result); + } + + /** + * Requesting calendar events for a course and within a given time range and a limit + * and offset should return the number of events up to the given limit value that have + * a sort time between the lower and uppper time bound (inclusive) where the result + * set is shifted by the offset value. + * + * If there are no events in the given time range then an empty result set should be + * returned. + */ + public function test_get_calendar_action_events_by_course_time_limit_offset() { + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $moduleinstance1 = $generator->create_instance(['course' => $course1->id]); + $moduleinstance2 = $generator->create_instance(['course' => $course2->id]); + + $this->getDataGenerator()->enrol_user($user->id, $course1->id); + $this->getDataGenerator()->enrol_user($user->id, $course2->id); + $this->resetAfterTest(true); + $this->setUser($user); + + $params = [ + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'modulename' => 'assign', + 'instance' => $moduleinstance1->id, + 'userid' => $user->id, + 'courseid' => $course1->id, + 'eventtype' => 'user', + 'repeats' => 0, + 'timestart' => 1, + ]; + + $event1 = create_event(array_merge($params, ['name' => 'Event 1', 'timesort' => 1])); + $event2 = create_event(array_merge($params, ['name' => 'Event 2', 'timesort' => 2])); + $event3 = create_event(array_merge($params, ['name' => 'Event 3', 'timesort' => 3])); + $event4 = create_event(array_merge($params, ['name' => 'Event 4', 'timesort' => 4])); + $event5 = create_event(array_merge($params, ['name' => 'Event 5', 'timesort' => 5])); + $event6 = create_event(array_merge($params, ['name' => 'Event 6', 'timesort' => 6])); + $event7 = create_event(array_merge($params, ['name' => 'Event 7', 'timesort' => 7])); + $event8 = create_event(array_merge($params, ['name' => 'Event 8', 'timesort' => 8])); + + $params['courseid'] = $course2->id; + $params['instance'] = $moduleinstance2->id; + $event9 = create_event(array_merge($params, ['name' => 'Event 9', 'timesort' => 1])); + $event10 = create_event(array_merge($params, ['name' => 'Event 10', 'timesort' => 2])); + $event11 = create_event(array_merge($params, ['name' => 'Event 11', 'timesort' => 3])); + $event12 = create_event(array_merge($params, ['name' => 'Event 12', 'timesort' => 4])); + $event13 = create_event(array_merge($params, ['name' => 'Event 13', 'timesort' => 5])); + $event14 = create_event(array_merge($params, ['name' => 'Event 14', 'timesort' => 6])); + $event15 = create_event(array_merge($params, ['name' => 'Event 15', 'timesort' => 7])); + $event16 = create_event(array_merge($params, ['name' => 'Event 16', 'timesort' => 8])); + + $result = \core_calendar\local\api::get_action_events_by_course($course1, 2, 7, $event3->id, 2); + + $this->assertCount(2, $result); + $this->assertEquals('Event 4', $result[0]->get_name()); + $this->assertEquals('Event 5', $result[1]->get_name()); + + $result = \core_calendar\local\api::get_action_events_by_course($course1, 2, 7, $event5->id, 2); + + $this->assertCount(2, $result); + $this->assertEquals('Event 6', $result[0]->get_name()); + $this->assertEquals('Event 7', $result[1]->get_name()); + + $result = \core_calendar\local\api::get_action_events_by_course($course1, 2, 7, $event7->id, 2); + + $this->assertEmpty($result); + } + + /** + * Test that get_action_events_by_courses will return a list of events for each + * course you provided as long as the user is enrolled in the course. + */ + public function test_get_action_events_by_courses() { + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course3 = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $moduleinstance1 = $generator->create_instance(['course' => $course1->id]); + $moduleinstance2 = $generator->create_instance(['course' => $course2->id]); + $moduleinstance3 = $generator->create_instance(['course' => $course3->id]); + + $this->getDataGenerator()->enrol_user($user->id, $course1->id); + $this->getDataGenerator()->enrol_user($user->id, $course2->id); + $this->getDataGenerator()->enrol_user($user->id, $course3->id); + $this->resetAfterTest(true); + $this->setUser($user); + + $params = [ + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'modulename' => 'assign', + 'instance' => $moduleinstance1->id, + 'userid' => $user->id, + 'courseid' => $course1->id, + 'eventtype' => 'user', + 'repeats' => 0, + 'timestart' => 1, + ]; + + $event1 = create_event(array_merge($params, ['name' => 'Event 1', 'timesort' => 1])); + $event2 = create_event(array_merge($params, ['name' => 'Event 2', 'timesort' => 2])); + + $params['courseid'] = $course2->id; + $params['instance'] = $moduleinstance2->id; + $event3 = create_event(array_merge($params, ['name' => 'Event 3', 'timesort' => 3])); + $event4 = create_event(array_merge($params, ['name' => 'Event 4', 'timesort' => 4])); + $event5 = create_event(array_merge($params, ['name' => 'Event 5', 'timesort' => 5])); + + $params['courseid'] = $course3->id; + $params['instance'] = $moduleinstance3->id; + $event6 = create_event(array_merge($params, ['name' => 'Event 6', 'timesort' => 6])); + $event7 = create_event(array_merge($params, ['name' => 'Event 7', 'timesort' => 7])); + $event8 = create_event(array_merge($params, ['name' => 'Event 8', 'timesort' => 8])); + $event9 = create_event(array_merge($params, ['name' => 'Event 9', 'timesort' => 9])); + + $result = \core_calendar\local\api::get_action_events_by_courses([], 1); + + $this->assertEmpty($result); + + $result = \core_calendar\local\api::get_action_events_by_courses([$course1], 3); + + $this->assertEmpty($result[$course1->id]); + + $result = \core_calendar\local\api::get_action_events_by_courses([$course1], 1); + + $this->assertCount(2, $result[$course1->id]); + $this->assertEquals('Event 1', $result[$course1->id][0]->get_name()); + $this->assertEquals('Event 2', $result[$course1->id][1]->get_name()); + + $result = \core_calendar\local\api::get_action_events_by_courses([$course1, $course2], 1); + + $this->assertCount(2, $result[$course1->id]); + $this->assertEquals('Event 1', $result[$course1->id][0]->get_name()); + $this->assertEquals('Event 2', $result[$course1->id][1]->get_name()); + $this->assertCount(3, $result[$course2->id]); + $this->assertEquals('Event 3', $result[$course2->id][0]->get_name()); + $this->assertEquals('Event 4', $result[$course2->id][1]->get_name()); + $this->assertEquals('Event 5', $result[$course2->id][2]->get_name()); + + $result = \core_calendar\local\api::get_action_events_by_courses([$course1, $course2], 2, 4); + + $this->assertCount(1, $result[$course1->id]); + $this->assertEquals('Event 2', $result[$course1->id][0]->get_name()); + $this->assertCount(2, $result[$course2->id]); + $this->assertEquals('Event 3', $result[$course2->id][0]->get_name()); + $this->assertEquals('Event 4', $result[$course2->id][1]->get_name()); + + $result = \core_calendar\local\api::get_action_events_by_courses([$course1, $course2, $course3], 1, null, 1); + + $this->assertCount(1, $result[$course1->id]); + $this->assertEquals('Event 1', $result[$course1->id][0]->get_name()); + $this->assertCount(1, $result[$course2->id]); + $this->assertEquals('Event 3', $result[$course2->id][0]->get_name()); + $this->assertCount(1, $result[$course3->id]); + $this->assertEquals('Event 6', $result[$course3->id][0]->get_name()); + } + + /** + * Test that the get_legacy_events() function only returns activity events that are enabled. + */ + public function test_get_legacy_events_with_disabled_module() { + global $DB; + + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + + $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $assigninstance = $assigngenerator->create_instance(['course' => $course->id]); + + $lessongenerator = $this->getDataGenerator()->get_plugin_generator('mod_lesson'); + $lessoninstance = $lessongenerator->create_instance(['course' => $course->id]); + $student = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($student->id, $course->id, 'student'); + $this->setUser($student); + $events = [ + [ + 'name' => 'Start of assignment', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'assign', + 'instance' => $assigninstance->id, + 'eventtype' => 'due', + 'timestart' => time(), + 'timeduration' => 86400, + 'visible' => 1 + ], [ + 'name' => 'Start of lesson', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'lesson', + 'instance' => $lessoninstance->id, + 'eventtype' => 'end', + 'timestart' => time(), + 'timeduration' => 86400, + 'visible' => 1 + ] + ]; + foreach ($events as $event) { + calendar_event::create($event, false); + } + $timestart = time() - 60; + $timeend = time() + 60; + + // Get all events. + $events = \core_calendar\local\api::get_legacy_events($timestart, $timeend, true, 0, true); + $this->assertCount(2, $events); + + // Disable the lesson module. + $modulerecord = $DB->get_record('modules', ['name' => 'lesson']); + $modulerecord->visible = 0; + $DB->update_record('modules', $modulerecord); + + // Check that we only return the assign event. + $events = \core_calendar\local\api::get_legacy_events($timestart, $timeend, true, 0, true); + $this->assertCount(1, $events); + $event = reset($events); + $this->assertEquals('assign', $event->modulename); + } + + /** + * Test for \core_calendar\local\api::get_legacy_events() when there are user and group overrides. + */ + public function test_get_legacy_events_with_overrides() { + $generator = $this->getDataGenerator(); + + $course = $generator->create_course(); + + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + if (!isset($params['course'])) { + $params['course'] = $course->id; + } + + $instance = $plugingenerator->create_instance($params); + + // Create users. + $useroverridestudent = $generator->create_user(); + $group1student = $generator->create_user(); + $group2student = $generator->create_user(); + $group12student = $generator->create_user(); + $nogroupstudent = $generator->create_user(); + + // Enrol users. + $generator->enrol_user($useroverridestudent->id, $course->id, 'student'); + $generator->enrol_user($group1student->id, $course->id, 'student'); + $generator->enrol_user($group2student->id, $course->id, 'student'); + $generator->enrol_user($group12student->id, $course->id, 'student'); + $generator->enrol_user($nogroupstudent->id, $course->id, 'student'); + + // Create groups. + $group1 = $generator->create_group(['courseid' => $course->id]); + $group2 = $generator->create_group(['courseid' => $course->id]); + + // Add members to groups. + $generator->create_group_member(['groupid' => $group1->id, 'userid' => $group1student->id]); + $generator->create_group_member(['groupid' => $group2->id, 'userid' => $group2student->id]); + $generator->create_group_member(['groupid' => $group1->id, 'userid' => $group12student->id]); + $generator->create_group_member(['groupid' => $group2->id, 'userid' => $group12student->id]); + $now = time(); + + // Events with the same module name, instance and event type. + $events = [ + [ + 'name' => 'Assignment 1 due date', + 'description' => '', + 'format' => 0, + 'courseid' => $course->id, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'assign', + 'instance' => $instance->id, + 'eventtype' => 'due', + 'timestart' => $now, + 'timeduration' => 0, + 'visible' => 1 + ], [ + 'name' => 'Assignment 1 due date - User override', + 'description' => '', + 'format' => 1, + 'courseid' => 0, + 'groupid' => 0, + 'userid' => $useroverridestudent->id, + 'modulename' => 'assign', + 'instance' => $instance->id, + 'eventtype' => 'due', + 'timestart' => $now + 86400, + 'timeduration' => 0, + 'visible' => 1, + 'priority' => CALENDAR_EVENT_USER_OVERRIDE_PRIORITY + ], [ + 'name' => 'Assignment 1 due date - Group A override', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => $group1->id, + 'userid' => 2, + 'modulename' => 'assign', + 'instance' => $instance->id, + 'eventtype' => 'due', + 'timestart' => $now + (2 * 86400), + 'timeduration' => 0, + 'visible' => 1, + 'priority' => 1, + ], [ + 'name' => 'Assignment 1 due date - Group B override', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => $group2->id, + 'userid' => 2, + 'modulename' => 'assign', + 'instance' => $instance->id, + 'eventtype' => 'due', + 'timestart' => $now + (3 * 86400), + 'timeduration' => 0, + 'visible' => 1, + 'priority' => 2, + ], + ]; + + foreach ($events as $event) { + calendar_event::create($event, false); + } + + $timestart = $now - 100; + $timeend = $now + (3 * 86400); + $groups = [$group1->id, $group2->id]; + + // Get user override events. + $this->setUser($useroverridestudent); + $events = \core_calendar\local\api::get_legacy_events($timestart, $timeend, $useroverridestudent->id, $groups, $course->id); + $this->assertCount(1, $events); + $event = reset($events); + $this->assertEquals('Assignment 1 due date - User override', $event->name); + + // Get event for user with override but with the timestart and timeend parameters only covering the original event. + $events = \core_calendar\local\api::get_legacy_events($timestart, $now, $useroverridestudent->id, $groups, $course->id); + $this->assertCount(0, $events); + + // Get events for user that does not belong to any group and has no user override events. + $this->setUser($nogroupstudent); + $events = \core_calendar\local\api::get_legacy_events($timestart, $timeend, $nogroupstudent->id, $groups, $course->id); + $this->assertCount(1, $events); + $event = reset($events); + $this->assertEquals('Assignment 1 due date', $event->name); + + // Get events for user that belongs to groups A and B and has no user override events. + $this->setUser($group12student); + $events = \core_calendar\local\api::get_legacy_events($timestart, $timeend, $group12student->id, $groups, $course->id); + $this->assertCount(1, $events); + $event = reset($events); + $this->assertEquals('Assignment 1 due date - Group B override', $event->name); + + // Get events for user that belongs to group A and has no user override events. + $this->setUser($group1student); + $events = \core_calendar\local\api::get_legacy_events($timestart, $timeend, $group1student->id, $groups, $course->id); + $this->assertCount(1, $events); + $event = reset($events); + $this->assertEquals('Assignment 1 due date - Group A override', $event->name); + + // Add repeating events. + $repeatingevents = [ + [ + 'name' => 'Repeating site event', + 'description' => '', + 'format' => 1, + 'courseid' => SITEID, + 'groupid' => 0, + 'userid' => 2, + 'repeatid' => $event->id, + 'modulename' => '0', + 'instance' => 0, + 'eventtype' => 'site', + 'timestart' => $now + 86400, + 'timeduration' => 0, + 'visible' => 1, + ], + [ + 'name' => 'Repeating site event', + 'description' => '', + 'format' => 1, + 'courseid' => SITEID, + 'groupid' => 0, + 'userid' => 2, + 'repeatid' => $event->id, + 'modulename' => '0', + 'instance' => 0, + 'eventtype' => 'site', + 'timestart' => $now + (2 * 86400), + 'timeduration' => 0, + 'visible' => 1, + ], + ]; + + foreach ($repeatingevents as $event) { + calendar_event::create($event, false); + } + + // Make sure repeating events are not filtered out. + $events = \core_calendar\local\api::get_legacy_events($timestart, $timeend, true, true, true); + $this->assertCount(3, $events); + } +} diff --git a/calendar/tests/module_std_proxy_test.php b/calendar/tests/module_std_proxy_test.php new file mode 100644 index 00000000000..c279eec806b --- /dev/null +++ b/calendar/tests/module_std_proxy_test.php @@ -0,0 +1,219 @@ +. + +/** + * module_std_proxy tests. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\local\event\proxies\module_std_proxy; + +/** + * std_proxy testcase. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_calendar_module_std_proxy_testcase extends advanced_testcase { + /** + * @var \stdClass[] $objects Array of objects to proxy. + */ + public $objects; + + /** + * Sets up the fixture. This method is called before a test is executed. + */ + public function setUp() { + $this->objects = [ + 'somemodule_someinstance' => (object) [ + 'member1' => 'Hello', + 'member2' => 1729, + 'member3' => 'Something else' + ], + 'anothermodule_anotherinstance' => (object) [ + 'member1' => 'Hej', + 'member2' => 87539319, + 'member3' => 'nagot annat' + ] + ]; + } + + /** + * Test proxying. + * + * @dataProvider test_proxy_testcases() + * @param string $modulename Object module name. + * @param string $instance Object instance. + * @param string $member Object member to retrieve. + * @param mixed $expected Expected value of member. + */ + public function test_proxy($modulename, $instance, $member, $expected) { + $proxy = new module_std_proxy( + $modulename, + $instance, + function($modulename, $instance) { + return $this->objects[$modulename . '_' . $instance]; + } + ); + + $this->assertEquals($proxy->get($member), $expected); + + // Test changing the value. + $proxy->set($member, 'something even more else'); + $this->assertEquals($proxy->get($member), 'something even more else'); + } + + /** + * Test setting values with a base class. + * + * @dataProvider test_proxy_testcases() + * @param string $modulename Object module name. + * @param string $instance Object instance. + * @param string $member Object member to retrieve. + * @param mixed $storedvalue Value as would be stored externally. + */ + public function test_base_values($modulename, $instance, $member, $storedvalue) { + $proxy = new module_std_proxy( + $modulename, + $instance, + function($module, $instance) { + return $this->objects[$module . '_' . $instance]; + }, + (object)['member1' => 'should clobber 1'] + ); + + $expected = $member == 'member1' ? 'should clobber 1' : $storedvalue; + $this->assertEquals($proxy->get($member), $expected); + } + + /** + * Test getting a non existant member. + * + * @dataProvider test_get_set_testcases() + * @param string $modulename Object module name. + * @param string $instance Object instance. + */ + public function test_get_invalid_member($modulename, $instance) { + $proxy = new module_std_proxy( + $modulename, + $instance, + function($modulename, $instance) { + return $this->objects[$modulename . '_' . $instance]; + } + ); + + $this->expectException('\core_calendar\local\event\exceptions\member_does_not_exist_exception'); + $proxy->get('thisdoesnotexist'); + } + + /** + * Test setting a non existant member. + * + * @dataProvider test_get_set_testcases() + * @param string $modulename Object module name. + * @param string $instance Object instance. + */ + public function test_set_invalid_member($modulename, $instance) { + $proxy = new module_std_proxy( + $modulename, + $instance, + function($modulename, $instance) { + return $this->objects[$modulename . '_' . $instance]; + } + ); + + $this->expectException('\core_calendar\local\event\exceptions\member_does_not_exist_exception'); + $proxy->set('thisdoesnotexist', 'should break'); + } + + /** + * Test get proxied instance. + * + * @dataProvider test_get_set_testcases() + * @param string $modulename Object module name. + * @param string $instance Object instance. + */ + public function test_get_proxied_instance($modulename, $instance) { + $proxy = new module_std_proxy( + $modulename, + $instance, + function($modulename, $instance) { + return $this->objects[$modulename . '_' . $instance]; + } + ); + + $this->assertEquals($proxy->get_proxied_instance(), $this->objects[$modulename . '_' . $instance]); + } + + /** + * Test cases for proxying test. + */ + public function test_proxy_testcases() { + return [ + 'Object 1 member 1' => [ + 'somemodule', + 'someinstance', + 'member1', + 'Hello' + ], + 'Object 1 member 2' => [ + 'somemodule', + 'someinstance', + 'member2', + 1729 + ], + 'Object 1 member 3' => [ + 'somemodule', + 'someinstance', + 'member3', + 'Something else' + ], + 'Object 2 member 1' => [ + 'anothermodule', + 'anotherinstance', + 'member1', + 'Hej' + ], + 'Object 2 member 2' => [ + 'anothermodule', + 'anotherinstance', + 'member2', + 87539319 + ], + 'Object 3 member 3' => [ + 'anothermodule', + 'anotherinstance', + 'member3', + 'nagot annat' + ] + ]; + } + + /** + * Test cases for getting and setting tests. + */ + public function test_get_set_testcases() { + return [ + 'Object 1' => ['somemodule', 'someinstance'], + 'Object 2' => ['anothermodule', 'anotherinstance'] + ]; + } +} diff --git a/calendar/tests/raw_event_retrieval_strategy_test.php b/calendar/tests/raw_event_retrieval_strategy_test.php new file mode 100644 index 00000000000..cbf8d706d40 --- /dev/null +++ b/calendar/tests/raw_event_retrieval_strategy_test.php @@ -0,0 +1,280 @@ +. + +/** + * Raw event retrieval strategy tests. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/calendar/tests/helpers.php'); + +use core_calendar\local\event\strategies\raw_event_retrieval_strategy; + +/** + * Raw event retrieval strategy testcase. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_calendar_raw_event_retrieval_strategy_testcase extends advanced_testcase { + /** + * Test retrieval strategy when module is disabled. + */ + public function test_get_raw_events_with_disabled_module() { + global $DB; + + $this->resetAfterTest(); + $retrievalstrategy = new raw_event_retrieval_strategy(); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $student = $generator->create_user(); + $generator->enrol_user($student->id, $course->id, 'student'); + $this->setUser($student); + $events = [ + [ + 'name' => 'Start of assignment', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'assign', + 'instance' => 1, + 'eventtype' => 'due', + 'timestart' => time(), + 'timeduration' => 86400, + 'visible' => 1 + ], [ + 'name' => 'Start of lesson', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'lesson', + 'instance' => 1, + 'eventtype' => 'end', + 'timestart' => time(), + 'timeduration' => 86400, + 'visible' => 1 + ] + ]; + + foreach ($events as $event) { + calendar_event::create($event, false); + } + + // Get all events. + $events = $retrievalstrategy->get_raw_events(null, [0], null); + $this->assertCount(2, $events); + + // Disable the lesson module. + $modulerecord = $DB->get_record('modules', ['name' => 'lesson']); + $modulerecord->visible = 0; + $DB->update_record('modules', $modulerecord); + + // Check that we only return the assign event. + $events = $retrievalstrategy->get_raw_events(null, [0], null); + $this->assertCount(1, $events); + $event = reset($events); + $this->assertEquals('assign', $event->modulename); + } + + /** + * Test retrieval strategy when there are overrides. + */ + public function test_get_raw_event_strategy_with_overrides() { + $this->resetAfterTest(); + + $retrievalstrategy = new raw_event_retrieval_strategy(); + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + + $instance = $plugingenerator->create_instance(['course' => $course->id]); + + // Create users. + $useroverridestudent = $generator->create_user(); + $group1student = $generator->create_user(); + $group2student = $generator->create_user(); + $group12student = $generator->create_user(); + $nogroupstudent = $generator->create_user(); + + // Enrol users. + $generator->enrol_user($useroverridestudent->id, $course->id, 'student'); + $generator->enrol_user($group1student->id, $course->id, 'student'); + $generator->enrol_user($group2student->id, $course->id, 'student'); + $generator->enrol_user($group12student->id, $course->id, 'student'); + + $generator->enrol_user($nogroupstudent->id, $course->id, 'student'); + + // Create groups. + $group1 = $generator->create_group(['courseid' => $course->id, 'name' => 'Group 1']); + $group2 = $generator->create_group(['courseid' => $course->id, 'name' => 'Group 2']); + + // Add members to groups. + $generator->create_group_member(['groupid' => $group1->id, 'userid' => $group1student->id]); + $generator->create_group_member(['groupid' => $group2->id, 'userid' => $group2student->id]); + $generator->create_group_member(['groupid' => $group1->id, 'userid' => $group12student->id]); + $generator->create_group_member(['groupid' => $group2->id, 'userid' => $group12student->id]); + + $now = time(); + + // Events with the same module name, instance and event type. + $events = [ + [ + 'name' => 'Assignment 1 due date', + 'description' => '', + 'format' => 0, + 'courseid' => $course->id, + 'groupid' => 0, + 'userid' => 2, + 'modulename' => 'assign', + 'instance' => $instance->id, + 'eventtype' => 'due', + 'timestart' => $now, + 'timeduration' => 0, + 'visible' => 1 + ], [ + 'name' => 'Assignment 1 due date - User override', + 'description' => '', + 'format' => 1, + 'courseid' => 0, + 'groupid' => 0, + 'userid' => $useroverridestudent->id, + 'modulename' => 'assign', + 'instance' => $instance->id, + 'eventtype' => 'due', + 'timestart' => $now + 86400, + 'timeduration' => 0, + 'visible' => 1, + 'priority' => CALENDAR_EVENT_USER_OVERRIDE_PRIORITY + ], [ + 'name' => 'Assignment 1 due date - Group A override', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => $group1->id, + 'userid' => 2, + 'modulename' => 'assign', + 'instance' => $instance->id, + 'eventtype' => 'due', + 'timestart' => $now + (2 * 86400), + 'timeduration' => 0, + 'visible' => 1, + 'priority' => 1, + ], [ + 'name' => 'Assignment 1 due date - Group B override', + 'description' => '', + 'format' => 1, + 'courseid' => $course->id, + 'groupid' => $group2->id, + 'userid' => 2, + 'modulename' => 'assign', + 'instance' => $instance->id, + 'eventtype' => 'due', + 'timestart' => $now + (3 * 86400), + 'timeduration' => 0, + 'visible' => 1, + 'priority' => 2, + ], + ]; + + foreach ($events as $event) { + calendar_event::create($event, false); + } + + $timestart = $now - 100; + $timeend = $now + (3 * 86400); + $groups = [$group1->id, $group2->id]; + + // Get user override events. + $this->setUser($useroverridestudent); + $events = $retrievalstrategy->get_raw_events([$useroverridestudent->id], $groups, [$course->id]); + $this->assertCount(1, $events); + $event = reset($events); + $this->assertEquals('Assignment 1 due date - User override', $event->name); + + // Get events for user that does not belong to any group and has no user override events. + $this->setUser($nogroupstudent); + $events = $retrievalstrategy->get_raw_events([$nogroupstudent->id], $groups, [$course->id]); + $this->assertCount(1, $events); + $event = reset($events); + $this->assertEquals('Assignment 1 due date', $event->name); + + // Get events for user that belongs to groups A and B and has no user override events. + $this->setUser($group12student); + $events = $retrievalstrategy->get_raw_events([$group12student->id], $groups, [$course->id]); + $this->assertCount(1, $events); + $event = reset($events); + $this->assertEquals('Assignment 1 due date - Group B override', $event->name); + + // Get events for user that belongs to group A and has no user override events. + $this->setUser($group1student); + $events = $retrievalstrategy->get_raw_events([$group1student->id], $groups, [$course->id]); + $this->assertCount(1, $events); + $event = reset($events); + $this->assertEquals('Assignment 1 due date - Group A override', $event->name); + + // Add repeating events. + $repeatingevents = [ + [ + 'name' => 'Repeating site event', + 'description' => '', + 'format' => 1, + 'courseid' => SITEID, + 'groupid' => 0, + 'userid' => 2, + 'repeatid' => 1, + 'modulename' => '0', + 'instance' => 0, + 'eventtype' => 'site', + 'timestart' => $now + 86400, + 'timeduration' => 0, + 'visible' => 1, + ], + [ + 'name' => 'Repeating site event', + 'description' => '', + 'format' => 1, + 'courseid' => SITEID, + 'groupid' => 0, + 'userid' => 2, + 'repeatid' => 1, + 'modulename' => '0', + 'instance' => 0, + 'eventtype' => 'site', + 'timestart' => $now + (2 * 86400), + 'timeduration' => 0, + 'visible' => 1, + ], + ]; + + foreach ($repeatingevents as $event) { + calendar_event::create($event, false); + } + + // Make sure repeating events are not filtered out. + $events = $retrievalstrategy->get_raw_events(); + $this->assertCount(3, $events); + } +} diff --git a/calendar/tests/repeat_event_collection_test.php b/calendar/tests/repeat_event_collection_test.php new file mode 100644 index 00000000000..f59fdd9cb00 --- /dev/null +++ b/calendar/tests/repeat_event_collection_test.php @@ -0,0 +1,180 @@ +. + +/** + * Repeat event collection tests. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/calendar/lib.php'); + +use core_calendar\local\event\entities\event; +use core_calendar\local\event\entities\repeat_event_collection; +use core_calendar\local\event\proxies\std_proxy; +use core_calendar\local\event\value_objects\event_description; +use core_calendar\local\event\value_objects\event_times; +use core_calendar\local\event\factories\event_factory_interface; + +/** + * Repeat event collection tests. + * + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_calendar_repeat_event_collection_testcase extends advanced_testcase { + /** + * Test that creating a repeat collection for a parent that doesn't + * exist throws an exception. + */ + public function test_no_parent_collection() { + $this->resetAfterTest(true); + $parentid = 123122131; + $factory = new core_calendar_repeat_event_collection_event_test_factory(); + $this->expectException('\core_calendar\local\event\exceptions\no_repeat_parent_exception'); + $collection = new repeat_event_collection($parentid, null, $factory); + } + + /** + * Test that an empty collection is valid. + */ + public function test_empty_collection() { + $this->resetAfterTest(true); + $this->setAdminUser(); + + $event = $this->create_event([ + // This causes the code to set the repeat id on this record + // but not create any repeat event records. + 'repeat' => 1, + 'repeats' => 0 + ]); + $parentid = $event->id; + $factory = new core_calendar_repeat_event_collection_event_test_factory(); + + // Event collection with no repeats. + $collection = new repeat_event_collection($parentid, null, $factory); + + $this->assertEquals($parentid, $collection->get_id()); + $this->assertEquals(0, $collection->get_num()); + $this->assertNull($collection->getIterator()->next()); + } + + /** + * Test that a collection with values behaves correctly. + */ + public function test_values_collection() { + $this->resetAfterTest(true); + $this->setAdminUser(); + + $factory = new core_calendar_repeat_event_collection_event_test_factory(); + $event = $this->create_event([ + // This causes the code to set the repeat id on this record + // but not create any repeat event records. + 'repeat' => 1, + 'repeats' => 0 + ]); + $parentid = $event->id; + $repeats = []; + + for ($i = 1; $i < 4; $i++) { + $record = $this->create_event([ + 'name' => sprintf('repeat %d', $i), + 'repeatid' => $parentid + ]); + + // Index by name so that we don't have to rely on sorting + // when doing the comparison later. + $repeats[$record->name] = $record; + } + + // Event collection with no repeats. + $collection = new repeat_event_collection($parentid, null, $factory); + + $this->assertEquals($parentid, $collection->get_id()); + $this->assertEquals(count($repeats), $collection->get_num()); + + foreach ($collection as $index => $event) { + $name = $event->get_name(); + $this->assertEquals($repeats[$name]->name, $name); + } + } + + /** + * Helper function to create calendar events using the old code. + * + * @param array $properties A list of calendar event properties to set + * @return calendar_event + */ + protected function create_event($properties = []) { + $record = new \stdClass(); + $record->name = 'event name'; + $record->eventtype = 'global'; + $record->repeat = 0; + $record->repeats = 0; + $record->timestart = time(); + $record->timeduration = 0; + $record->timesort = 0; + $record->type = 1; + $record->courseid = 0; + + foreach ($properties as $name => $value) { + $record->$name = $value; + } + + $event = new calendar_event($record); + return $event->create($record, false); + } +} + +/** + * Test event factory. + * + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_calendar_repeat_event_collection_event_test_factory implements event_factory_interface { + + public function create_instance(\stdClass $dbrow) { + $identity = function($id) { + return $id; + }; + return new event( + $dbrow->id, + $dbrow->name, + new event_description($dbrow->description, $dbrow->format), + new std_proxy($dbrow->courseid, $identity), + new std_proxy($dbrow->groupid, $identity), + new std_proxy($dbrow->userid, $identity), + new repeat_event_collection($dbrow->id, null, $this), + new std_proxy($dbrow->instance, $identity), + $dbrow->type, + new event_times( + (new \DateTimeImmutable())->setTimestamp($dbrow->timestart), + (new \DateTimeImmutable())->setTimestamp($dbrow->timestart + $dbrow->timeduration), + (new \DateTimeImmutable())->setTimestamp($dbrow->timesort ? $dbrow->timesort : $dbrow->timestart), + (new \DateTimeImmutable())->setTimestamp($dbrow->timemodified) + ), + !empty($dbrow->visible), + new std_proxy($dbrow->subscriptionid, $identity) + ); + } +} diff --git a/calendar/tests/std_proxy_test.php b/calendar/tests/std_proxy_test.php new file mode 100644 index 00000000000..cc912b3b7cf --- /dev/null +++ b/calendar/tests/std_proxy_test.php @@ -0,0 +1,188 @@ +. + +/** + * std_proxy tests. + * + * @package core_calendar + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\local\event\proxies\std_proxy; + +/** + * std_proxy testcase. + * + * @copyright 2017 Cameron Ball + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_calendar_std_proxy_testcase extends advanced_testcase { + /** + * @var \stdClass[] $objects Array of objects to proxy. + */ + public $objects; + + public function setUp() { + $this->objects = [ + 1 => (object) [ + 'member1' => 'Hello', + 'member2' => 1729, + 'member3' => 'Something else' + ], + 5 => (object) [ + 'member1' => 'Hej', + 'member2' => 87539319, + 'member3' => 'nagot annat' + ] + ]; + } + + /** + * Test proxying. + * + * @dataProvider test_proxy_testcases() + * @param int $id Object ID. + * @param string $member Object member to retrieve. + * @param mixed $expected Expected value of member. + */ + public function test_proxy($id, $member, $expected) { + $proxy = new std_proxy($id, function($id) { + return $this->objects[$id]; + }); + + $this->assertEquals($proxy->get($member), $expected); + + // Test changing the value. + $proxy->set($member, 'something even more else'); + $this->assertEquals($proxy->get($member), 'something even more else'); + } + + /** + * Test setting values with a base class. + * + * @dataProvider test_proxy_testcases() + * @param int $id Object ID. + * @param string $member Object member to retrieve. + * @param mixed $storedvalue Value as would be stored externally. + */ + public function test_base_values($id, $member, $storedvalue) { + $proxy = new std_proxy( + $id, + function($id) { + return $this->objects[$id]; + }, + (object)['member1' => 'should clobber 1'] + ); + + $expected = $member == 'member1' ? 'should clobber 1' : $storedvalue; + $this->assertEquals($proxy->get($member), $expected); + } + + /** + * Test getting a non existant member. + * + * @dataProvider test_get_set_testcases() + * @param int $id ID of the object being proxied. + */ + public function test_get_invalid_member($id) { + $proxy = new std_proxy($id, function($id) { + return $this->objects[$id]; + }); + + $this->expectException('\core_calendar\local\event\exceptions\member_does_not_exist_exception'); + $proxy->get('thisdoesnotexist'); + } + + /** + * Test setting a non existant member. + * + * @dataProvider test_get_set_testcases() + * @param int $id ID of the object being proxied. + */ + public function test_set_invalid_member($id) { + $proxy = new std_proxy($id, function($id) { + return $this->objects[$id]; + }); + + $this->expectException('\core_calendar\local\event\exceptions\member_does_not_exist_exception'); + $proxy->set('thisdoesnotexist', 'should break'); + } + + /** + * Test get proxied instance. + * + * @dataProvider test_get_set_testcases() + * @param int $id Object ID. + */ + public function test_get_proxied_instance($id) { + $proxy = new std_proxy($id, function($id) { + return $this->objects[$id]; + }); + + $this->assertEquals($proxy->get_proxied_instance(), $this->objects[$id]); + } + + /** + * Test cases for proxying test. + */ + public function test_proxy_testcases() { + return [ + 'Object 1 member 1' => [ + 1, + 'member1', + 'Hello' + ], + 'Object 1 member 2' => [ + 1, + 'member2', + 1729 + ], + 'Object 1 member 3' => [ + 1, + 'member3', + 'Something else' + ], + 'Object 2 member 1' => [ + 5, + 'member1', + 'Hej' + ], + 'Object 2 member 2' => [ + 5, + 'member2', + 87539319 + ], + 'Object 3 member 3' => [ + 5, + 'member3', + 'nagot annat' + ] + ]; + } + + /** + * Test cases for getting and setting tests. + */ + public function test_get_set_testcases() { + return [ + 'Object 1' => [1], + 'Object 2' => [5] + ]; + } +} diff --git a/calendar/upgrade.txt b/calendar/upgrade.txt index 4a37e2c205b..e99279e8d13 100644 --- a/calendar/upgrade.txt +++ b/calendar/upgrade.txt @@ -1,8 +1,16 @@ This files describes API changes in /calendar/* , information provided here is intended especially for developers. +=== 3.3 === +* calendar_event_hook() has been removed. Developers should be using the Moodle events system to achieve this behaviour, + rather than using a hacky calendar specific implementation. +* calendar_wday_name() is deprecated and no longer used in core. +* calendar_get_block_upcoming() is deprecated, please use block_calendar_upcoming::get_upcoming_content() instead. +* calendar_print_month_selector() is deprecated and no longer used in core. +* calendar_cron() is deprecated and should not be used. Please use the core\task\calendar_cron_task instead. + === 3.2 === -* calendar_preferences_button() is now depreciated. Calendar preferences have been moved to the user preferences page. +* calendar_preferences_button() is now deprecated. Calendar preferences have been moved to the user preferences page. === 2.9 === default values changes in code: diff --git a/cohort/tests/behat/access_visible_cohorts.feature b/cohort/tests/behat/access_visible_cohorts.feature index e9131933857..946bd04b849 100644 --- a/cohort/tests/behat/access_visible_cohorts.feature +++ b/cohort/tests/behat/access_visible_cohorts.feature @@ -46,7 +46,7 @@ Feature: Access visible and hidden cohorts Scenario: Teacher can see visible cohorts defined in the above contexts When I log in as "teacher" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Enrolment methods" node in "Course administration > Users" And I select "Cohort sync" from the "Add method" singleselect Then the "Cohort" select box should contain "Cohort in category 1" @@ -58,10 +58,10 @@ Feature: Access visible and hidden cohorts And the "Cohort" select box should contain "System empty cohort" And I set the field "Cohort" to "System cohort" And I press "Add method" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Enrolled users" node in "Course administration > Users" And I should see "student@example.com" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Groups" node in "Course administration > Users" And I press "Auto-create groups" And the "Select members from cohort" select box should contain "Cohort in category 1" @@ -74,8 +74,7 @@ Feature: Access visible and hidden cohorts Scenario: System manager can see all cohorts defined in the above contexts When I log in as "user1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Enrolment methods" node in "Course administration > Users" And I select "Cohort sync" from the "Add method" singleselect Then the "Cohort" select box should contain "Cohort in category 1" @@ -101,8 +100,7 @@ Feature: Access visible and hidden cohorts Scenario: Category manager can see all cohorts defined in his category and visible cohorts defined above When I log in as "user2" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Enrolment methods" node in "Course administration > Users" And I select "Cohort sync" from the "Add method" singleselect Then the "Cohort" select box should contain "Cohort in category 1" diff --git a/cohort/tests/behat/upload_cohort_users.feature b/cohort/tests/behat/upload_cohort_users.feature index 68adda85f83..1b1edb4086a 100644 --- a/cohort/tests/behat/upload_cohort_users.feature +++ b/cohort/tests/behat/upload_cohort_users.feature @@ -15,12 +15,10 @@ Feature: Upload users to a cohort | Course 1 | C1 | 0 | | Course 2 | C2 | 0 | And I log in as "admin" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I add "Cohort sync" enrolment method with: | Cohort | Cohort 1 | - And I am on site homepage - And I follow "Course 2" + And I am on "Course 2" course homepage And I add "Cohort sync" enrolment method with: | Cohort | Cohort 2 | When I navigate to "Upload users" node in "Site administration > Users > Accounts" @@ -36,14 +34,12 @@ Feature: Upload users to a cohort And I click on "Assign" "link" in the "Cohort 2" "table_row" And the "Current users" select box should contain "Mary Smith (marysmith@example.com)" And the "Current users" select box should contain "Alice Smith (alicesmith@example.com)" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Users > Enrolled users" in current page administration And I should see "Tom Jones" And I should see "Bob Jones" And I should not see "Mary Smith" - And I am on site homepage - And I follow "Course 2" + And I am on "Course 2" course homepage And I navigate to "Users > Enrolled users" in current page administration And I should see "Mary Smith" And I should see "Alice Smith" diff --git a/completion/classes/api.php b/completion/classes/api.php new file mode 100644 index 00000000000..418d8498866 --- /dev/null +++ b/completion/classes/api.php @@ -0,0 +1,116 @@ +. + +/** + * Contains class containing completion API. + * + * @package core_completion + * @copyright 2017 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_completion; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class containing completion API. + * + * @package core_completion + * @copyright 2017 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class api { + + /** + * @var string The completion expected on event. + */ + const COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED = 'expectcompletionon'; + + /** + * Creates, updates or deletes an event for the expected completion date. + * + * @param int $cmid The course module id + * @param string $modulename The name of the module (eg. assign, quiz) + * @param int $instanceid The instance idLOL + * @param int|null $completionexpectedtime The time completion is expected, null if not set + * @return bool + */ + public static function update_completion_date_event($cmid, $modulename, $instanceid, $completionexpectedtime) { + global $CFG, $DB; + + // Required for calendar constant CALENDAR_EVENT_TYPE_ACTION. + require_once($CFG->dirroot . '/calendar/lib.php'); + + $instance = $DB->get_record($modulename, array('id' => $instanceid), '*', MUST_EXIST); + $course = $DB->get_record('course', array('id' => $instance->course), '*', MUST_EXIST); + + $completion = new \completion_info($course); + + // Can not create/update an event if completion is disabled. + if (!$completion->is_enabled() && $completionexpectedtime !== null) { + return true; + } + + // Create the \stdClass we will be using for our language strings. + $lang = new \stdClass(); + $lang->modulename = get_string('pluginname', $modulename); + $lang->instancename = $instance->name; + + // Create the calendar event. + $event = new \stdClass(); + $event->type = CALENDAR_EVENT_TYPE_ACTION; + $event->eventtype = self::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED; + if ($event->id = $DB->get_field('event', 'id', array('modulename' => $modulename, + 'instance' => $instance->id, 'eventtype' => $event->eventtype))) { + if ($completionexpectedtime !== null) { + // Calendar event exists so update it. + $event->name = get_string('completionexpectedfor', 'completion', $lang); + $event->description = format_module_intro($modulename, $instance, $cmid); + $event->timestart = $completionexpectedtime; + $event->timesort = $completionexpectedtime; + $event->visible = instance_is_visible($modulename, $instance); + $event->timeduration = 0; + + $calendarevent = \calendar_event::load($event->id); + $calendarevent->update($event); + } else { + // Calendar event is no longer needed. + $calendarevent = \calendar_event::load($event->id); + $calendarevent->delete(); + } + } else { + // Event doesn't exist so create one. + if ($completionexpectedtime !== null) { + $event->name = get_string('completionexpectedfor', 'completion', $lang); + $event->description = format_module_intro($modulename, $instance, $cmid); + $event->courseid = $instance->course; + $event->groupid = 0; + $event->userid = 0; + $event->modulename = $modulename; + $event->instance = $instance->id; + $event->timestart = $completionexpectedtime; + $event->timesort = $completionexpectedtime; + $event->visible = instance_is_visible($modulename, $instance); + $event->timeduration = 0; + + \calendar_event::create($event); + } + } + + return true; + } +} diff --git a/completion/classes/progress.php b/completion/classes/progress.php new file mode 100644 index 00000000000..803f2ef044e --- /dev/null +++ b/completion/classes/progress.php @@ -0,0 +1,84 @@ +. + +/** + * Contains class used to return completion progress information. + * + * @package core_completion + * @copyright 2017 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_completion; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/completionlib.php'); + +/** + * Class used to return completion progress information. + * + * @package core_completion + * @copyright 2017 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class progress { + + /** + * Returns the course percentage completed by a certain user, returns null if no completion data is available. + * + * @param \stdClass $course Moodle course object + * @param int $userid The id of the user, 0 for the current user + * @return null|float The percentage, or null if completion is not supported in the course, + * or there are no activities that support completion. + */ + public static function get_course_progress_percentage($course, $userid = 0) { + global $USER; + + // Make sure we continue with a valid userid. + if (empty($userid)) { + $userid = $USER->id; + } + + $completion = new \completion_info($course); + + // First, let's make sure completion is enabled. + if (!$completion->is_enabled()) { + return null; + } + + // Before we check how many modules have been completed see if the course has. + if ($completion->is_course_complete($userid)) { + return 100; + } + + // Get the number of modules that support completion. + $modules = $completion->get_activities(); + $count = count($modules); + if (!$count) { + return null; + } + + // Get the number of modules that have been completed. + $completed = 0; + foreach ($modules as $module) { + $data = $completion->get_data($module, false, $userid); + $completed += $data->completionstate == COMPLETION_INCOMPLETE ? 0 : 1; + } + + return ($completed / $count) * 100; + } +} diff --git a/completion/tests/api_test.php b/completion/tests/api_test.php new file mode 100644 index 00000000000..67624842086 --- /dev/null +++ b/completion/tests/api_test.php @@ -0,0 +1,218 @@ +. + +/** + * Test completion API. + * + * @package core_completion + * @category test + * @copyright 2017 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Test completion API. + * + * @package core_completion + * @category test + * @copyright 2017 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_completion_api_testcase extends advanced_testcase { + + /** + * Test setup. + */ + public function setUp() { + $this->resetAfterTest(); + } + + public function test_update_completion_date_event() { + global $CFG, $DB; + + $this->setAdminUser(); + + // Create a course. + $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); + + // Create an assign activity. + $time = time(); + $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id)); + + // Create the completion event. + $CFG->enablecompletion = true; + \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign->id, $time); + + // Check that there is now an event in the database. + $events = $DB->get_records('event'); + $this->assertCount(1, $events); + + // Get the event. + $event = reset($events); + + // Confirm the event is correct. + $this->assertEquals('assign', $event->modulename); + $this->assertEquals($assign->id, $event->instance); + $this->assertEquals(CALENDAR_EVENT_TYPE_ACTION, $event->type); + $this->assertEquals(\core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED, $event->eventtype); + $this->assertEquals($time, $event->timestart); + $this->assertEquals($time, $event->timesort); + } + + public function test_update_completion_date_event_update() { + global $CFG, $DB; + + $this->setAdminUser(); + + // Create a course. + $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); + + // Create an assign activity. + $time = time(); + $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id)); + + // Create the event. + $CFG->enablecompletion = true; + \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign->id, $time); + + // Call it again, but this time with a different time. + \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign->id, $time + DAYSECS); + + // Check that there is still only one event in the database. + $events = $DB->get_records('event'); + $this->assertCount(1, $events); + + // Get the event. + $event = reset($events); + + // Confirm that the event has been updated. + $this->assertEquals('assign', $event->modulename); + $this->assertEquals($assign->id, $event->instance); + $this->assertEquals(CALENDAR_EVENT_TYPE_ACTION, $event->type); + $this->assertEquals(\core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED, $event->eventtype); + $this->assertEquals($time + DAYSECS, $event->timestart); + $this->assertEquals($time + DAYSECS, $event->timesort); + } + + public function test_update_completion_date_event_delete() { + global $CFG, $DB; + + $this->setAdminUser(); + + // Create a course. + $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); + + // Create an assign activity. + $time = time(); + $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id)); + + // Create the event. + $CFG->enablecompletion = true; + \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign->id, $time); + + // Call it again, but the time specified as null. + \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign->id, null); + + // Check that there is no event in the database. + $this->assertEquals(0, $DB->count_records('event')); + } + + public function test_update_completion_date_event_completion_disabled() { + global $CFG, $DB; + + $this->setAdminUser(); + + // Create a course. + $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); + + // Create an assign activity. + $time = time(); + $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id)); + + // Try and create the completion event with completion disabled. + $CFG->enablecompletion = false; + \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign->id, $time); + + // Check that there is no event in the database. + $this->assertEquals(0, $DB->count_records('event')); + } + + public function test_update_completion_date_event_update_completion_disabled() { + global $CFG, $DB; + + $this->setAdminUser(); + + // Create a course. + $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); + + // Create an assign activity. + $time = time(); + $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id)); + + // Create the completion event. + $CFG->enablecompletion = true; + \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign->id, $time); + + // Disable completion. + $CFG->enablecompletion = false; + + // Try and update the completion date. + \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign->id, $time + DAYSECS); + + // Check that there is an event in the database. + $events = $DB->get_records('event'); + $this->assertCount(1, $events); + + // Get the event. + $event = reset($events); + + // Confirm the event has not changed. + $this->assertEquals('assign', $event->modulename); + $this->assertEquals($assign->id, $event->instance); + $this->assertEquals(CALENDAR_EVENT_TYPE_ACTION, $event->type); + $this->assertEquals(\core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED, $event->eventtype); + $this->assertEquals($time, $event->timestart); + $this->assertEquals($time, $event->timesort); + } + + public function test_update_completion_date_event_delete_completion_disabled() { + global $CFG, $DB; + + $this->setAdminUser(); + + // Create a course. + $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); + + // Create an assign activity. + $time = time(); + $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id)); + + // Create the completion event. + $CFG->enablecompletion = true; + \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign->id, $time); + + // Disable completion. + $CFG->enablecompletion = false; + + // Should still be able to delete completion events even when completion is disabled. + \core_completion\api::update_completion_date_event($assign->cmid, 'assign', $assign->id, null); + + // Check that there is now no event in the database. + $this->assertEquals(0, $DB->count_records('event')); + } +} diff --git a/completion/tests/behat/enable_manual_complete_mark.feature b/completion/tests/behat/enable_manual_complete_mark.feature index 848b3155566..6ba1be43405 100644 --- a/completion/tests/behat/enable_manual_complete_mark.feature +++ b/completion/tests/behat/enable_manual_complete_mark.feature @@ -18,26 +18,23 @@ Feature: Allow students to manually mark an activity as complete | teacher1 | C1 | editingteacher | | student1 | C1 | student | And I log in as "teacher1" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I navigate to "Edit settings" in current page administration And I set the following fields to these values: | Enable completion tracking | Yes | And I press "Save and display" And I add a "Forum" to section "1" and I fill the form with: - | Forum name | Test forum name | - | Description | Test forum description | + | Forum name | Test forum name | + | Description | Test forum description | + | Completion tracking | Students can manually mark the activity as completed | And "Student First" user has not completed "Test forum name" activity And I log out And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage When I click on "Not completed: Test forum name. Select to mark as complete." "icon" Then the "Test forum name" "forum" activity with "manual" completion should be marked as complete And I log out And I log in as "teacher1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Reports > Activity completion" in current page administration And "Student First" user has completed "Test forum name" activity diff --git a/completion/tests/behat/restrict_activity_by_date.feature b/completion/tests/behat/restrict_activity_by_date.feature index 3f94122bc0f..551243d8e5d 100644 --- a/completion/tests/behat/restrict_activity_by_date.feature +++ b/completion/tests/behat/restrict_activity_by_date.feature @@ -17,9 +17,7 @@ Feature: Restrict activity availability through date conditions | teacher1 | C1 | editingteacher | | student1 | C1 | student | And I log in as "teacher1" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on # Adding the page like this because id_available*_enabled needs to be clicked to trigger the action. And I add a "Assignment" to section "1" And I expand all fieldsets @@ -39,8 +37,7 @@ Feature: Restrict activity availability through date conditions And I press "Save and return to course" And I log out When I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage Then I should see "Available from 31 December 2037" And "Test assignment 1" activity should be dimmed And "Test assignment 1" "link" should not exist @@ -64,6 +61,5 @@ Feature: Restrict activity availability through date conditions And I press "Save and return to course" And I log out When I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage Then I should not see "Test assignment 2" diff --git a/completion/tests/behat/restrict_activity_by_grade.feature b/completion/tests/behat/restrict_activity_by_grade.feature index 6c970e87da2..3d6a6cdd11d 100644 --- a/completion/tests/behat/restrict_activity_by_grade.feature +++ b/completion/tests/behat/restrict_activity_by_grade.feature @@ -18,9 +18,7 @@ Feature: Restrict activity availability through grade conditions | teacher1 | C1 | editingteacher | | student1 | C1 | student | And I log in as "teacher1" - #And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Grade assignment | | Description | Grade this assignment to revoke restriction on restricted assignment | @@ -41,8 +39,7 @@ Feature: Restrict activity availability through grade conditions And I press "Save and return to course" And I log out When I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage Then I should see "Not available unless: You achieve a required score in Grade assignment" And "Test page name" activity should be dimmed And "Test page name" "link" should not exist @@ -54,8 +51,7 @@ Feature: Restrict activity availability through grade conditions And I should see "Submitted for grading" And I log out And I log in as "teacher1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Grade assignment" And I navigate to "View all submissions" in current page administration And I click on "Grade" "link" in the "Student First" "table_row" @@ -66,7 +62,6 @@ Feature: Restrict activity availability through grade conditions And I follow "Edit settings" And I log out And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And "Test page name" activity should be visible And I should not see "Not available unless: You achieve a required score in Grade assignment" diff --git a/completion/tests/behat/restrict_section_availability.feature b/completion/tests/behat/restrict_section_availability.feature index f00c39f4960..63d8422abea 100644 --- a/completion/tests/behat/restrict_section_availability.feature +++ b/completion/tests/behat/restrict_section_availability.feature @@ -20,9 +20,7 @@ Feature: Restrict sections availability through completion or grade conditions @javascript Scenario: Show section greyed-out to student when completion condition is not satisfied Given I log in as "teacher1" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I navigate to "Edit settings" in current page administration And I set the following fields to these values: | Enable completion tracking | Yes | @@ -44,8 +42,7 @@ Feature: Restrict sections availability through completion or grade conditions And I press "Save changes" And I log out And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage Then I should see "Not available unless: The activity Test label is marked complete" And I should not see "Test page name" And I click on "Not completed: Test label. Select to mark as complete." "icon" @@ -55,9 +52,7 @@ Feature: Restrict sections availability through completion or grade conditions @javascript Scenario: Show section greyed-out to student when grade condition is not satisfied Given I log in as "teacher1" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Grade assignment | | Description | Grade this assignment to revoke restriction on restricted assignment | @@ -78,8 +73,7 @@ Feature: Restrict sections availability through completion or grade conditions And I press "Save changes" And I log out When I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage Then I should see "Not available unless: You achieve a required score in Grade assignment" And "Test page name" activity should be hidden And I follow "Grade assignment" @@ -90,8 +84,7 @@ Feature: Restrict sections availability through completion or grade conditions And I should see "Submitted for grading" And I log out And I log in as "teacher1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Grade assignment" And I navigate to "View all submissions" in current page administration And I click on "Grade" "link" in the "Student First" "table_row" @@ -102,7 +95,6 @@ Feature: Restrict sections availability through completion or grade conditions And I follow "Edit settings" And I log out And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And "Test page name" activity should be visible And I should not see "Not available unless: You achieve a required score in Grade assignment" diff --git a/completion/tests/behat/teacher_manual_completion.feature b/completion/tests/behat/teacher_manual_completion.feature index ca0c35d90f0..13810f37546 100644 --- a/completion/tests/behat/teacher_manual_completion.feature +++ b/completion/tests/behat/teacher_manual_completion.feature @@ -17,8 +17,7 @@ Feature: Allow teachers to manually mark users as complete when configured | student1 | CC1 | student | | teacher1 | CC1 | editingteacher | And I log in as "admin" - And I am on site homepage - And I follow "Completion course" + And I am on "Completion course" course homepage And completion tracking is "Enabled" in current course And I follow "Course completion" And I set the field "Teacher" to "1" @@ -27,13 +26,11 @@ Feature: Allow teachers to manually mark users as complete when configured And I add the "Course completion status" block And I log out And I log in as "student1" - And I am on site homepage - And I follow "Completion course" + And I am on "Completion course" course homepage And I should see "Status: Not yet started" And I log out When I log in as "teacher1" - And I am on site homepage - And I follow "Completion course" + And I am on "Completion course" course homepage And I follow "View course report" And I should see "Student First" And I follow "Click to mark user complete" @@ -44,6 +41,5 @@ Feature: Allow teachers to manually mark users as complete when configured And I am on site homepage And I log out Then I log in as "student1" - And I am on site homepage - And I follow "Completion course" + And I am on "Completion course" course homepage And I should see "Status: Complete" diff --git a/completion/tests/progress_test.php b/completion/tests/progress_test.php new file mode 100644 index 00000000000..b2e5b97e56f --- /dev/null +++ b/completion/tests/progress_test.php @@ -0,0 +1,154 @@ +. + +/** + * Test completion progress API. + * + * @package core_completion + * @category test + * @copyright 2017 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Test completion progress API. + * + * @package core_completion + * @category test + * @copyright 2017 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_completion_progress_testcase extends advanced_testcase { + + /** + * Test setup. + */ + public function setUp() { + global $CFG; + + $CFG->enablecompletion = true; + $this->resetAfterTest(); + } + + /** + * Tests that the course progress percentage is returned correctly when we have only activity completion. + */ + public function test_course_progress_percentage_with_just_activities() { + global $DB; + + // Add a course that supports completion. + $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); + + // Enrol a user in the course. + $user = $this->getDataGenerator()->create_user(); + $studentrole = $DB->get_record('role', array('shortname' => 'student')); + $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id); + + // Add four activities that use completion. + $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id), + array('completion' => 1)); + $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), + array('completion' => 1)); + $this->getDataGenerator()->create_module('forum', array('course' => $course->id), + array('completion' => 1)); + $this->getDataGenerator()->create_module('forum', array('course' => $course->id), + array('completion' => 1)); + + // Add an activity that does *not* use completion. + $this->getDataGenerator()->create_module('assign', array('course' => $course->id)); + + // Mark two of them as completed for a user. + $cmassign = get_coursemodule_from_id('assign', $assign->cmid); + $cmdata = get_coursemodule_from_id('data', $data->cmid); + $completion = new completion_info($course); + $completion->update_state($cmassign, COMPLETION_COMPLETE, $user->id); + $completion->update_state($cmdata, COMPLETION_COMPLETE, $user->id); + + // Check we have received valid data. + // Note - only 4 out of the 5 activities support completion, and the user has completed 2 of those. + $this->assertEquals('50', \core_completion\progress::get_course_progress_percentage($course, $user->id)); + } + + /** + * Tests that the course progress percentage is returned correctly when we have a course and activity completion. + */ + public function test_course_progress_percentage_with_activities_and_course() { + global $DB; + + // Add a course that supports completion. + $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); + + // Enrol a user in the course. + $user = $this->getDataGenerator()->create_user(); + $studentrole = $DB->get_record('role', array('shortname' => 'student')); + $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id); + + // Add four activities that use completion. + $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id), + array('completion' => 1)); + $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), + array('completion' => 1)); + $this->getDataGenerator()->create_module('forum', array('course' => $course->id), + array('completion' => 1)); + $this->getDataGenerator()->create_module('forum', array('course' => $course->id), + array('completion' => 1)); + + // Add an activity that does *not* use completion. + $this->getDataGenerator()->create_module('assign', array('course' => $course->id)); + + // Mark two of them as completed for a user. + $cmassign = get_coursemodule_from_id('assign', $assign->cmid); + $cmdata = get_coursemodule_from_id('data', $data->cmid); + $completion = new completion_info($course); + $completion->update_state($cmassign, COMPLETION_COMPLETE, $user->id); + $completion->update_state($cmdata, COMPLETION_COMPLETE, $user->id); + + // Now, mark the course as completed. + $ccompletion = new completion_completion(array('course' => $course->id, 'userid' => $user->id)); + $ccompletion->mark_complete(); + + // Check we have received valid data. + // The course completion takes priority, so should return 100. + $this->assertEquals('100', \core_completion\progress::get_course_progress_percentage($course, $user->id)); + } + + /** + * Tests that the course progress returns null when the course does not support it. + */ + public function test_course_progress_course_not_using_completion() { + // Create a course that does not use completion. + $course = $this->getDataGenerator()->create_course(); + + // Check that the result was null. + $this->assertNull(\core_completion\progress::get_course_progress_percentage($course)); + } + + /** + * Tests that the course progress returns null when there are no activities that support it. + */ + public function test_course_progress_no_activities_using_completion() { + // Create a course that does support completion. + $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); + + // Add an activity that does *not* support completion. + $this->getDataGenerator()->create_module('assign', array('course' => $course->id)); + + // Check that the result was null. + $this->assertNull(\core_completion\progress::get_course_progress_percentage($course)); + } +} diff --git a/course/classes/external/course_summary_exporter.php b/course/classes/external/course_summary_exporter.php index afc96f34411..531ed8c5cfc 100644 --- a/course/classes/external/course_summary_exporter.php +++ b/course/classes/external/course_summary_exporter.php @@ -42,6 +42,7 @@ class course_summary_exporter extends \core\external\exporter { protected function get_other_values(renderer_base $output) { return array( + 'fullnamedisplay' => get_course_display_name_for_list($this->data), 'viewurl' => (new moodle_url('/course/view.php', array('id' => $this->data->id)))->out(false) ); } @@ -59,12 +60,24 @@ class course_summary_exporter extends \core\external\exporter { ), 'idnumber' => array( 'type' => PARAM_RAW, + ), + 'summary' => array( + 'type' => PARAM_RAW, + ), + 'startdate' => array( + 'type' => PARAM_INT, + ), + 'enddate' => array( + 'type' => PARAM_INT, ) ); } public static function define_other_properties() { return array( + 'fullnamedisplay' => array( + 'type' => PARAM_TEXT, + ), 'viewurl' => array( 'type' => PARAM_URL, ) diff --git a/course/externallib.php b/course/externallib.php index 73370acc2db..802cfd2fee5 100644 --- a/course/externallib.php +++ b/course/externallib.php @@ -2654,6 +2654,8 @@ class core_course_external extends external_api { /** * Returns description of method parameters * + * @deprecated since 3.3 + * * @return external_function_parameters * @since Moodle 3.2 */ @@ -2668,6 +2670,8 @@ class core_course_external extends external_api { /** * Return activities overview for the given courses. * + * @deprecated since 3.3 + * * @param array $courseids a list of course ids * @return array of warnings and the activities overview * @since Moodle 3.2 @@ -2725,6 +2729,8 @@ class core_course_external extends external_api { /** * Returns description of method result value * + * @deprecated since 3.3 + * * @return external_description * @since Moodle 3.2 */ @@ -2751,6 +2757,15 @@ class core_course_external extends external_api { ); } + /** + * Marking the method as deprecated. + * + * @return bool + */ + public static function get_activities_overview_is_deprecated() { + return true; + } + /** * Returns description of method parameters * diff --git a/course/format/social/tests/behat/social_adjust_discussion_count.feature b/course/format/social/tests/behat/social_adjust_discussion_count.feature index 4c83c5a320c..f7216c7f32b 100644 --- a/course/format/social/tests/behat/social_adjust_discussion_count.feature +++ b/course/format/social/tests/behat/social_adjust_discussion_count.feature @@ -15,14 +15,14 @@ Feature: Change number of discussions displayed | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I press "Add a new discussion topic" And I set the following fields to these values: | Subject | Forum Post 10 | | Message | This is forum post ten | And I press "Post to forum" And I wait to be redirected - And I follow "Course 1" + And I am on "Course 1" course homepage And I wait "1" seconds And I press "Add a new discussion topic" And I set the following fields to these values: @@ -30,7 +30,7 @@ Feature: Change number of discussions displayed | Message | This is forum post nine | And I press "Post to forum" And I wait to be redirected - And I follow "Course 1" + And I am on "Course 1" course homepage And I wait "1" seconds And I press "Add a new discussion topic" And I set the following fields to these values: @@ -38,7 +38,7 @@ Feature: Change number of discussions displayed | Message | This is forum post eight | And I press "Post to forum" And I wait to be redirected - And I follow "Course 1" + And I am on "Course 1" course homepage And I wait "1" seconds And I press "Add a new discussion topic" And I set the following fields to these values: @@ -46,7 +46,7 @@ Feature: Change number of discussions displayed | Message | This is forum post seven | And I press "Post to forum" And I wait to be redirected - And I follow "Course 1" + And I am on "Course 1" course homepage And I wait "1" seconds And I press "Add a new discussion topic" And I set the following fields to these values: @@ -54,7 +54,7 @@ Feature: Change number of discussions displayed | Message | This is forum post six | And I press "Post to forum" And I wait to be redirected - And I follow "Course 1" + And I am on "Course 1" course homepage And I wait "1" seconds And I press "Add a new discussion topic" And I set the following fields to these values: @@ -62,7 +62,7 @@ Feature: Change number of discussions displayed | Message | This is forum post five | And I press "Post to forum" And I wait to be redirected - And I follow "Course 1" + And I am on "Course 1" course homepage And I wait "1" seconds And I press "Add a new discussion topic" And I set the following fields to these values: @@ -70,7 +70,7 @@ Feature: Change number of discussions displayed | Message | This is forum post four | And I press "Post to forum" And I wait to be redirected - And I follow "Course 1" + And I am on "Course 1" course homepage And I wait "1" seconds And I press "Add a new discussion topic" And I set the following fields to these values: @@ -78,7 +78,7 @@ Feature: Change number of discussions displayed | Message | This is forum post three | And I press "Post to forum" And I wait to be redirected - And I follow "Course 1" + And I am on "Course 1" course homepage And I wait "1" seconds And I press "Add a new discussion topic" And I set the following fields to these values: @@ -86,7 +86,7 @@ Feature: Change number of discussions displayed | Message | This is forum post two | And I press "Post to forum" And I wait to be redirected - And I follow "Course 1" + And I am on "Course 1" course homepage And I wait "1" seconds And I press "Add a new discussion topic" And I set the following fields to these values: @@ -94,7 +94,7 @@ Feature: Change number of discussions displayed | Message | This is forum post one | And I press "Post to forum" And I wait to be redirected - And I follow "Course 1" + And I am on "Course 1" course homepage Scenario: When number of discussions is decreased fewer discussions appear Given I navigate to "Edit settings" in current page administration diff --git a/course/format/topics/tests/behat/edit_delete_sections.feature b/course/format/topics/tests/behat/edit_delete_sections.feature index 1c836478c7b..ee86fb452d1 100644 --- a/course/format/topics/tests/behat/edit_delete_sections.feature +++ b/course/format/topics/tests/behat/edit_delete_sections.feature @@ -21,8 +21,7 @@ Feature: Sections can be edited and deleted in topics format | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on Scenario: View the default name of the general section in topics format When I click on "Edit section" "link" in the "li#section-0" "css_element" @@ -62,7 +61,7 @@ Feature: Sections can be edited and deleted in topics format Then I should not see "Topic 1" in the "region-main" "region" And "New name for topic" "field" should not exist And I should see "Midterm evaluation" in the "li#section-1" "css_element" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should not see "Topic 1" in the "region-main" "region" And I should see "Midterm evaluation" in the "li#section-1" "css_element" diff --git a/course/format/weeks/tests/behat/edit_delete_sections.feature b/course/format/weeks/tests/behat/edit_delete_sections.feature index 6269da09f5a..74d25ce4f20 100644 --- a/course/format/weeks/tests/behat/edit_delete_sections.feature +++ b/course/format/weeks/tests/behat/edit_delete_sections.feature @@ -21,8 +21,7 @@ Feature: Sections can be edited and deleted in weeks format | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on Scenario: View the default name of the general section in weeks format When I click on "Edit section" "link" in the "li#section-0" "css_element" @@ -67,7 +66,7 @@ Feature: Sections can be edited and deleted in weeks format Then I should not see "1 May - 7 May" in the "region-main" "region" And "New name for week" "field" should not exist And I should see "Midterm evaluation" in the "li#section-1" "css_element" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should not see "1 May - 7 May" in the "region-main" "region" And I should see "Midterm evaluation" in the "li#section-1" "css_element" diff --git a/course/moodleform_mod.php b/course/moodleform_mod.php index 1886a28785f..46ed50affdb 100644 --- a/course/moodleform_mod.php +++ b/course/moodleform_mod.php @@ -636,7 +636,6 @@ abstract class moodleform_mod extends moodleform { } if ($completion->is_enabled()) { $mform->addElement('header', 'activitycompletionheader', get_string('activitycompletion', 'completion')); - // Unlock button for if people have completed it (will // be removed in definition_after_data if they haven't) $mform->addElement('submit', 'unlockcompletion', get_string('unlockcompletion', 'completion')); @@ -647,7 +646,13 @@ abstract class moodleform_mod extends moodleform { $trackingdefault = COMPLETION_TRACKING_NONE; // If system and activity default is on, set it. if ($CFG->completiondefault && $this->_features->defaultcompletion) { - $trackingdefault = COMPLETION_TRACKING_MANUAL; + $hasrules = plugin_supports('mod', $this->_modname, FEATURE_COMPLETION_HAS_RULES, true); + $tracksviews = plugin_supports('mod', $this->_modname, FEATURE_COMPLETION_TRACKS_VIEWS, true); + if ($hasrules || $tracksviews) { + $trackingdefault = COMPLETION_TRACKING_AUTOMATIC; + } else { + $trackingdefault = COMPLETION_TRACKING_MANUAL; + } } $mform->addElement('select', 'completion', get_string('completion', 'completion'), @@ -662,6 +667,10 @@ abstract class moodleform_mod extends moodleform { $mform->addElement('checkbox', 'completionview', get_string('completionview', 'completion'), get_string('completionview_desc', 'completion')); $mform->disabledIf('completionview', 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC); + // Check by default if automatic completion tracking is set. + if ($trackingdefault == COMPLETION_TRACKING_AUTOMATIC) { + $mform->setDefault('completionview', 1); + } $gotcompletionoptions = true; } diff --git a/course/tests/behat/activities_edit_completion.feature b/course/tests/behat/activities_edit_completion.feature index c4f44b7b959..ebca3b4f1f3 100644 --- a/course/tests/behat/activities_edit_completion.feature +++ b/course/tests/behat/activities_edit_completion.feature @@ -9,16 +9,14 @@ Feature: Edit completion settings of an activity | fullname | shortname | enablecompletion | | Course 1 | C1 | 1 | And I log in as "admin" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Page" to section "1" and I fill the form with: | Name | TestPage | | Description | x | | Page content | x | | Completion tracking | 2 | | Require view | 1 | - And I follow "Course 1" + And I am on "Course 1" course homepage Scenario: Completion is not locked when the activity has not yet been viewed Given I click on "Edit settings" "link" in the "TestPage" activity diff --git a/course/tests/behat/activities_edit_name.feature b/course/tests/behat/activities_edit_name.feature index 2d53b7cc8fc..29e8911821c 100644 --- a/course/tests/behat/activities_edit_name.feature +++ b/course/tests/behat/activities_edit_name.feature @@ -16,8 +16,7 @@ Feature: Edit activity name in-place | user | course | role | | teacher1 | C1 | editingteacher | When I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Forum" to section "1" and I fill the form with: | Forum name | Test forum name | | Description | Test forum description | @@ -28,7 +27,7 @@ Feature: Edit activity name in-place Then I should not see "Test forum name" in the ".course-content" "css_element" And "New name for activity Test forum name" "field" should not exist And I should see "Good news" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Good news" And I should not see "Test forum name" # Cancel renaming @@ -38,7 +37,7 @@ Feature: Edit activity name in-place And "New name for activity Good news" "field" should not exist And I should see "Good news" And I should not see "Terrible news" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "Good news" And I should not see "Terrible news" And I log out diff --git a/course/tests/behat/activities_edit_with_block_dock.feature b/course/tests/behat/activities_edit_with_block_dock.feature index 735288ec59c..4ed3330593b 100644 --- a/course/tests/behat/activities_edit_with_block_dock.feature +++ b/course/tests/behat/activities_edit_with_block_dock.feature @@ -16,8 +16,7 @@ Feature: Open the edit menu when a block is docked | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Glossary" to section "1" and I fill the form with: | Name | Test glossary name | | Description | Test glossary description | diff --git a/course/tests/behat/activities_group_icons.feature b/course/tests/behat/activities_group_icons.feature index 4429b2a1040..a863ff0c9e3 100644 --- a/course/tests/behat/activities_group_icons.feature +++ b/course/tests/behat/activities_group_icons.feature @@ -16,8 +16,7 @@ Feature: Toggle activities groups mode from the course page | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Forum" to section "1" and I fill the form with: | Forum name | Test forum name | | Description | Test forum description | diff --git a/course/tests/behat/activities_indentation.feature b/course/tests/behat/activities_indentation.feature index ded5b0ec551..8ef74131f60 100644 --- a/course/tests/behat/activities_indentation.feature +++ b/course/tests/behat/activities_indentation.feature @@ -18,8 +18,7 @@ Feature: Indent items on the course page | teacher1 | C1 | editingteacher | | student1 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Glossary" to section "1" and I fill the form with: | Name | Test glossary name | | Description | Test glossary description | diff --git a/course/tests/behat/activities_visibility_icons.feature b/course/tests/behat/activities_visibility_icons.feature index 6df1c37a828..4a45cec23aa 100644 --- a/course/tests/behat/activities_visibility_icons.feature +++ b/course/tests/behat/activities_visibility_icons.feature @@ -18,8 +18,7 @@ Feature: Toggle activities visibility from the course page | teacher1 | C1 | editingteacher | | student1 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Forum" to section "1" and I fill the form with: | Forum name | Test forum name | | Description | Test forum description | @@ -59,7 +58,7 @@ Feature: Toggle activities visibility from the course page And I log out # Student should not see this activity. And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I should not see "Test forum name" And I log out @@ -77,8 +76,7 @@ Feature: Toggle activities visibility from the course page | teacher1 | C1 | editingteacher | | student1 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Recent activity" block And I add a "Forum" to section "2" and I fill the form with: | Forum name | Test forum name | @@ -113,7 +111,7 @@ Feature: Toggle activities visibility from the course page And I log out # Student will not see the module on the course page but can access it from other reports and blocks: And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And "Test forum name" activity should be hidden And I click on "Test forum name" "link" in the "Recent activity" "block" And I should see "Test forum name" @@ -132,8 +130,7 @@ Feature: Toggle activities visibility from the course page | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Forum" to section "2" and I fill the form with: | Forum name | Test forum name | | Description | Test forum description | @@ -171,8 +168,7 @@ Feature: Toggle activities visibility from the course page | allowstealth | 1 | And I log out And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Recent activity" block And I add a "Assignment" to section "2" and I fill the form with: | Assignment name | Test assignment name | @@ -205,7 +201,7 @@ Feature: Toggle activities visibility from the course page And I log out # Student will not see the module on the course page but can access it from other reports and blocks: And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And "Test assignment name" activity should be hidden And I click on "Test assignment name" "link" in the "Recent activity" "block" And I should see "Test assignment name" diff --git a/course/tests/behat/add_activities.feature b/course/tests/behat/add_activities.feature index b318d8046d0..547162d4ae6 100644 --- a/course/tests/behat/add_activities.feature +++ b/course/tests/behat/add_activities.feature @@ -17,9 +17,7 @@ Feature: Add activities to courses | student1 | C1 | student | | student2 | C1 | student | And I log in as "admin" - And I am on site homepage - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on @javascript Scenario: Add an activity to a course @@ -48,8 +46,7 @@ Feature: Add activities to courses Scenario: Set activity description to required then add an activity supplying only the name Given I set the following administration settings values: | requiremodintro | Yes | - When I am on site homepage - And I follow "Course 1" + When I am on "Course 1" course homepage And I add a "Database" to section "3" and I fill the form with: | Name | Test name | Then I should see "Required" diff --git a/course/tests/behat/course_controls.feature b/course/tests/behat/course_controls.feature index fda72f91b5a..f28f75e9e0e 100644 --- a/course/tests/behat/course_controls.feature +++ b/course/tests/behat/course_controls.feature @@ -27,7 +27,7 @@ Feature: Course activity controls works as expected | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage When I follow And I turn editing mode on And I add the "Recent activity" block @@ -99,7 +99,7 @@ Feature: Course activity controls works as expected | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage When I follow And I turn editing mode on And I add the "Recent activity" block diff --git a/course/tests/behat/course_creation.feature b/course/tests/behat/course_creation.feature index 8b6c46c58bc..140fd3fcc18 100644 --- a/course/tests/behat/course_creation.feature +++ b/course/tests/behat/course_creation.feature @@ -18,8 +18,7 @@ Feature: Managers can create courses And I enrol "Student 1" user as "Student" And I log out When I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Latest announcements" block Then "Latest announcements" "block" should exist And I follow "Announcements" @@ -28,7 +27,7 @@ Feature: Managers can create courses And "Subscription mode > Forced subscription" "text" should exist in current page administration And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Announcements" And "Add a new topic" "button" should not exist And "Forced subscription" "text" should exist in current page administration diff --git a/course/tests/behat/coursetags.feature b/course/tests/behat/coursetags.feature index fe1d331bc0b..62ccbeda6b2 100644 --- a/course/tests/behat/coursetags.feature +++ b/course/tests/behat/coursetags.feature @@ -24,7 +24,7 @@ Feature: Tagging courses | teacher1 | c2 | editingteacher | | teacher2 | c2 | teacher | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Edit settings" in current page administration And I set the following fields to these values: | Tags | Mathematics | @@ -33,15 +33,14 @@ Feature: Tagging courses Scenario: Set course tags using the course edit form When I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Edit settings" node in "Course administration" And I expand all fieldsets Then I should see "Mathematics" in the ".form-autocomplete-selection" "css_element" And I set the following fields to these values: | Tags | Algebra | And I press "Save and display" - And I am on homepage - And I follow "Course 2" + And I am on "Course 2" course homepage And I navigate to "Edit settings" node in "Course administration" And I set the following fields to these values: | Tags | Mathematics, Geometry | @@ -71,14 +70,13 @@ Feature: Tagging courses | moodle/course:tag | Allow | And I log out When I log in as "teacher2" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Course tags" in current page administration Then I should see "Mathematics" in the ".form-autocomplete-selection" "css_element" And I set the following fields to these values: | Tags | Algebra | And I press "Save changes" - And I am on homepage - And I follow "Course 2" + And I am on "Course 2" course homepage And I navigate to "Course tags" in current page administration And I set the following fields to these values: | Tags | Mathematics, Geometry | diff --git a/course/tests/behat/edit_settings.feature b/course/tests/behat/edit_settings.feature index 1ef2c67cb64..e2ffa84e3cf 100644 --- a/course/tests/behat/edit_settings.feature +++ b/course/tests/behat/edit_settings.feature @@ -16,7 +16,7 @@ Feature: Edit course settings | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage When I navigate to "Edit settings" in current page administration And I set the following fields to these values: | Course full name | Edited course fullname | @@ -32,7 +32,7 @@ Feature: Edit course settings And the field "Course full name" matches value "Edited course fullname" And the field "Course short name" matches value "Edited course shortname" And the field "Course summary" matches value "Edited course summary" - And I am on homepage + And I am on site homepage And I should see "Edited course fullname" Scenario: Edit course settings and return to the management interface diff --git a/course/tests/behat/force_group_mode.feature b/course/tests/behat/force_group_mode.feature index 284a59dd171..1230ae95a6d 100644 --- a/course/tests/behat/force_group_mode.feature +++ b/course/tests/behat/force_group_mode.feature @@ -15,8 +15,7 @@ Feature: Force group mode in a course | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Chat" to section "1" and I fill the form with: | Name of this chat room | Chat room | | Description | Chat description | diff --git a/course/tests/behat/move_activities.feature b/course/tests/behat/move_activities.feature index ba900d446bf..b06c7f6a73b 100644 --- a/course/tests/behat/move_activities.feature +++ b/course/tests/behat/move_activities.feature @@ -15,8 +15,7 @@ Feature: Activities can be moved between sections | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add the "Recent activity" block And I follow "Delete Recent activity block" And I press "Yes" diff --git a/course/tests/behat/move_sections.feature b/course/tests/behat/move_sections.feature index d1efdde9dd1..0e9e097346c 100644 --- a/course/tests/behat/move_sections.feature +++ b/course/tests/behat/move_sections.feature @@ -15,8 +15,7 @@ Feature: Sections can be moved | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on Scenario: Move up and down a section with Javascript disabled in a single page course Given I add a "Forum" to section "1" and I fill the form with: diff --git a/course/tests/behat/paged_course_navigation.feature b/course/tests/behat/paged_course_navigation.feature index 8be6ae11c57..0f929bdcf7a 100644 --- a/course/tests/behat/paged_course_navigation.feature +++ b/course/tests/behat/paged_course_navigation.feature @@ -10,8 +10,7 @@ Feature: Course paged mode | fullname | shortname | category | format | coursedisplay | numsections | | Course 1 | C1 | 0 | | 1 | 3 | And I log in as "admin" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage Then I click on "link" in the "section" And I follow "C1" And I click on "link" in the "section" @@ -44,8 +43,7 @@ Feature: Course paged mode | fullname | shortname | category | format | coursedisplay | numsections | | Course 1 | C1 | 0 | | 1 | 3 | And I log in as "admin" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage Then I click on "link" in the "section" And I follow "C1" And I click on "link" in the "section" diff --git a/course/tests/behat/rename_roles.feature b/course/tests/behat/rename_roles.feature index 253aebdb49c..c16ead45273 100644 --- a/course/tests/behat/rename_roles.feature +++ b/course/tests/behat/rename_roles.feature @@ -20,7 +20,7 @@ Feature: Rename roles within a course | teacher2 | C1 | teacher | | student1 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage When I navigate to "Edit settings" in current page administration And I set the following fields to these values: | Your word for 'Non-editing teacher' | Tutor | @@ -33,7 +33,7 @@ Feature: Rename roles within a course And the "roleid" select box should contain "Tutor" And the "roleid" select box should contain "Learner" And the "roleid" select box should not contain "Student" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Edit settings" in current page administration And I set the following fields to these values: | Your word for 'Non-editing teacher' | | diff --git a/course/tests/behat/restrict_available_activities.feature b/course/tests/behat/restrict_available_activities.feature index dc240de8048..b31933fdf82 100644 --- a/course/tests/behat/restrict_available_activities.feature +++ b/course/tests/behat/restrict_available_activities.feature @@ -18,8 +18,7 @@ Feature: Restrict activities availability @javascript Scenario: Activities can be added with the default permissions Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I add a "Glossary" to section "1" and I fill the form with: | Name | Test glossary name | | Description | Test glossary description | @@ -33,14 +32,12 @@ Feature: Restrict activities availability Given I log in as "admin" And I set the following system permissions of "Teacher" role: | mod/chat:addinstance | Prohibit | - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Users > Permissions" in current page administration And I override the system permissions of "Teacher" role with: | mod/glossary:addinstance | Prohibit | And I log out And I log in as "teacher1" - And I follow "Course 1" - When I turn editing mode on + When I am on "Course 1" course homepage with editing mode on Then the "Add an activity to section 'Topic 1'" select box should not contain "Chat" Then the "Add an activity to section 'Topic 1'" select box should not contain "Glossary" diff --git a/course/tests/behat/role_renaming.feature b/course/tests/behat/role_renaming.feature index 31012650357..d4b7f3d0817 100644 --- a/course/tests/behat/role_renaming.feature +++ b/course/tests/behat/role_renaming.feature @@ -19,7 +19,7 @@ Feature: Rename roles in a course Scenario: Teacher can rename roles Given I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Edit settings" in current page administration And I should see "Role renaming" When I set the following fields to these values: @@ -37,7 +37,7 @@ Feature: Rename roles in a course | moodle/course:renameroles | Inherit | And I follow "Log out" When I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Edit settings" in current page administration Then I should not see "Role renaming" And I should not see "Your word for 'Teacher'" diff --git a/course/tests/behat/section_highlighting.feature b/course/tests/behat/section_highlighting.feature index af8c941d2a8..07b54235bf5 100644 --- a/course/tests/behat/section_highlighting.feature +++ b/course/tests/behat/section_highlighting.feature @@ -18,15 +18,13 @@ Feature: Topic's course sections highlighting | teacher1 | C1 | editingteacher | | student1 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I turn section "1" highlighting on Then section "1" should be highlighted And I turn section "2" highlighting on And section "2" should be highlighted And section "1" should not be highlighted - And I am on homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And section "2" should be highlighted And section "1" should not be highlighted And I turn section "2" highlighting off @@ -34,12 +32,11 @@ Feature: Topic's course sections highlighting And section "2" should not be highlighted And I reload the page And section "2" should not be highlighted - And I am on homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And section "2" should not be highlighted And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And section "1" should not be highlighted And section "2" should not be highlighted diff --git a/course/tests/behat/section_visibility.feature b/course/tests/behat/section_visibility.feature index b9f781a0d78..f11dc6dc6cc 100644 --- a/course/tests/behat/section_visibility.feature +++ b/course/tests/behat/section_visibility.feature @@ -18,8 +18,7 @@ Feature: Show/hide course sections | teacher1 | C1 | editingteacher | | student1 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Forum" to section "1" and I fill the form with: | Forum name | Test hidden forum 11 name | | Description | Test hidden forum 11 description | @@ -44,7 +43,8 @@ Feature: Show/hide course sections | Forum name | Test hidden forum 32 name | | Description | Test hidden forum 32 description | | Availability | Show on course page | - And I follow "Course 1" + | Visible | Show | + And I am on "Course 1" course homepage When I hide section "1" Then section "1" should be hidden And section "2" should be visible @@ -65,7 +65,7 @@ Feature: Show/hide course sections And all activities in section "1" should be hidden And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And section "1" should be hidden And all activities in section "1" should be hidden And section "2" should be visible diff --git a/course/tests/behat/view_subfolders_inline.feature b/course/tests/behat/view_subfolders_inline.feature index 14194c068b3..d447b2bb314 100644 --- a/course/tests/behat/view_subfolders_inline.feature +++ b/course/tests/behat/view_subfolders_inline.feature @@ -15,8 +15,7 @@ Feature: View subfolders in a course in-line | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Folder" to section "3" and I fill the form with: | Name | Test folder | | Display folder contents | On a separate page | @@ -30,7 +29,7 @@ Feature: View subfolders in a course in-line @javascript Scenario: Add a folder with two subfolders - view on separate page - Given I follow "Course 1" + Given I am on "Course 1" course homepage And I should not see "Test subfolder 1" And I follow "Test folder" And I should see "Test subfolder 1" @@ -39,7 +38,7 @@ Feature: View subfolders in a course in-line And I set the field "New folder name" to "Test subfolder 2" And I click on "button.fp-dlg-butcreate" "css_element" in the "div.fp-mkdir-dlg" "css_element" And I press "Save changes" - When I follow "Course 1" + When I am on "Course 1" course homepage Then I should not see "Test subfolder 2" And I follow "Test folder" And I should see "Test subfolder 2" diff --git a/course/tests/courselib_test.php b/course/tests/courselib_test.php index c726278b396..52edef8b1f9 100644 --- a/course/tests/courselib_test.php +++ b/course/tests/courselib_test.php @@ -97,6 +97,7 @@ class core_course_courselib_testcase extends advanced_testcase { $moduleinfo->sendlatenotifications = true; $moduleinfo->duedate = time() + (7 * 24 * 3600); $moduleinfo->cutoffdate = time() + (7 * 24 * 3600); + $moduleinfo->gradingduedate = time() + (7 * 24 * 3600); $moduleinfo->allowsubmissionsfromdate = time(); $moduleinfo->teamsubmission = true; $moduleinfo->requireallteammemberssubmit = true; diff --git a/course/tests/externallib_test.php b/course/tests/externallib_test.php index 9616ca2bf15..83f56997cf4 100644 --- a/course/tests/externallib_test.php +++ b/course/tests/externallib_test.php @@ -1823,6 +1823,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase { $courses = array($course1->id , $course2->id); $result = core_course_external::get_activities_overview($courses); + $this->assertDebuggingCalledCount(8); $result = external_api::clean_returnvalue(core_course_external::get_activities_overview_returns(), $result); // There should be one entry for course1, and no others. diff --git a/enrol/externallib.php b/enrol/externallib.php index 711cc69ceb8..588fbf96f69 100644 --- a/enrol/externallib.php +++ b/enrol/externallib.php @@ -326,6 +326,11 @@ class core_enrol_external extends external_api { $course->fullname = external_format_string($course->fullname, $context->id); $course->shortname = external_format_string($course->shortname, $context->id); + $progress = null; + if ($course->enablecompletion) { + $progress = \core_completion\progress::get_course_progress_percentage($course); + } + $result[] = array( 'id' => $course->id, 'shortname' => $course->shortname, @@ -339,7 +344,8 @@ class core_enrol_external extends external_api { 'showgrades' => $course->showgrades, 'lang' => $course->lang, 'enablecompletion' => $course->enablecompletion, - 'category' => $course->category + 'category' => $course->category, + 'progress' => $progress, ); } @@ -369,6 +375,7 @@ class core_enrol_external extends external_api { 'enablecompletion' => new external_value(PARAM_BOOL, 'true if completion is enabled, otherwise false', VALUE_OPTIONAL), 'category' => new external_value(PARAM_INT, 'course category id', VALUE_OPTIONAL), + 'progress' => new external_value(PARAM_FLOAT, 'Progress percentage', VALUE_OPTIONAL), ) ) ); diff --git a/enrol/guest/tests/behat/guest_access.feature b/enrol/guest/tests/behat/guest_access.feature index 621482a358a..1b05008bb50 100644 --- a/enrol/guest/tests/behat/guest_access.feature +++ b/enrol/guest/tests/behat/guest_access.feature @@ -16,8 +16,7 @@ Feature: Guest users can auto-enrol themself in courses where guest access is al | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Forum" to section "1" and I fill the form with: | Forum name | Test forum name | | Description | Test forum description | @@ -30,8 +29,7 @@ Feature: Guest users can auto-enrol themself in courses where guest access is al And I press "Save changes" And I log out And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage When I follow "Test forum name" Then I should not see "Subscribe to this forum" @@ -44,8 +42,7 @@ Feature: Guest users can auto-enrol themself in courses where guest access is al And I press "Save changes" And I log out And I log in as "student1" - And I am on site homepage - When I follow "Course 1" + When I am on "Course 1" course homepage Then I should see "Guest access" And I set the following fields to these values: | Password | moodle_rules | diff --git a/enrol/lti/tests/behat/basic_settings.feature b/enrol/lti/tests/behat/basic_settings.feature index eeb24b129d4..c2031792280 100644 --- a/enrol/lti/tests/behat/basic_settings.feature +++ b/enrol/lti/tests/behat/basic_settings.feature @@ -30,7 +30,7 @@ Feature: Check that settings are adhered to when creating an enrolment plugin Scenario: As an admin set site-wide settings for the enrolment plugin and ensure they are used Given I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Enrolment methods" node in "Course administration > Users" And I select "Publish as LTI tool" from the "Add method" singleselect When I expand all fieldsets diff --git a/enrol/lti/tests/behat/index_page.feature b/enrol/lti/tests/behat/index_page.feature index 46b553fd09a..0f7d81c44d6 100644 --- a/enrol/lti/tests/behat/index_page.feature +++ b/enrol/lti/tests/behat/index_page.feature @@ -21,8 +21,7 @@ Feature: Check that the page listing the shared external tools is functioning as Scenario: I want to edit an external tool Given I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment name | | Description | Submit your online text | diff --git a/enrol/meta/tests/behat/enrol_meta.feature b/enrol/meta/tests/behat/enrol_meta.feature index e28b005e25a..f8ba1731044 100644 --- a/enrol/meta/tests/behat/enrol_meta.feature +++ b/enrol/meta/tests/behat/enrol_meta.feature @@ -34,7 +34,7 @@ Feature: Enrolments are synchronised with meta courses And I am on course index Scenario: Add meta enrolment instance without groups - When I follow "Course 3" + When I am on "Course 3" course homepage And I add "Course meta link" enrolment method with: | Link course | C1C1 | And I navigate to "Enrolled users" node in "Course administration > Users" @@ -43,11 +43,11 @@ Feature: Enrolments are synchronised with meta courses And I should not see "Groupcourse" in the "table.userenrolment" "css_element" Scenario: Add meta enrolment instance with groups - When I follow "Course 3" + When I am on "Course 3" course homepage And I add "Course meta link" enrolment method with: | Link course | C1C1 | | Add to group | Groupcourse 1 | - And I follow "Course 3" + And I am on "Course 3" course homepage And I add "Course meta link" enrolment method with: | Link course | C2C2 | | Add to group | Groupcourse 2 | @@ -62,7 +62,7 @@ Feature: Enrolments are synchronised with meta courses And I should not see "Groupcourse 2" in the "Student 4" "table_row" Scenario: Add meta enrolment instance with auto-created groups - When I follow "Course 3" + When I am on "Course 3" course homepage And I add "Course meta link" enrolment method with: | Link course | C1C1 | | Add to group | Create new group | @@ -75,11 +75,11 @@ Feature: Enrolments are synchronised with meta courses And the "Groups" select box should contain "Course 1 course (4)" Scenario: Backup and restore of meta enrolment instance - When I follow "Course 3" + When I am on "Course 3" course homepage And I add "Course meta link" enrolment method with: | Link course | C1C1 | | Add to group | Groupcourse 1 | - And I follow "Course 3" + And I am on "Course 3" course homepage And I add "Course meta link" enrolment method with: | Link course | C2C2 | When I backup "Course 3" course using this options: diff --git a/enrol/self/tests/behat/key_holder.feature b/enrol/self/tests/behat/key_holder.feature index 31f4a8214eb..c9cd9d46a49 100644 --- a/enrol/self/tests/behat/key_holder.feature +++ b/enrol/self/tests/behat/key_holder.feature @@ -34,15 +34,13 @@ Feature: Users can be defined as key holders in courses where self enrolment is @javascript Scenario: The key holder name is displayed on site home page Given I log in as "admin" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage When I add "Self enrolment" enrolment method with: | Custom instance name | Test student enrolment | | Enrolment key | moodle_rules | And I log out And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I should see "You should have received this enrolment key from:" And I should see "Manager 1" And I set the following fields to these values: diff --git a/enrol/self/tests/behat/self_enrolment.feature b/enrol/self/tests/behat/self_enrolment.feature index a0c53dd7dde..b38fb108ae4 100644 --- a/enrol/self/tests/behat/self_enrolment.feature +++ b/enrol/self/tests/behat/self_enrolment.feature @@ -21,11 +21,11 @@ Feature: Users can auto-enrol themself in courses where self enrolment is allowe @javascript Scenario: Self-enrolment enabled as guest Given I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I add "Self enrolment" enrolment method with: | Custom instance name | Test student enrolment | And I log out - When I follow "Course 1" + When I am on "Course 1" course homepage And I press "Log in as a guest" Then I should see "Guests cannot access this course. Please log in." And I press "Continue" @@ -33,27 +33,25 @@ Feature: Users can auto-enrol themself in courses where self enrolment is allowe Scenario: Self-enrolment enabled Given I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage When I add "Self enrolment" enrolment method with: | Custom instance name | Test student enrolment | And I log out And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I press "Enrol me" Then I should see "Topic 1" And I should not see "Enrolment options" Scenario: Self-enrolment enabled requiring an enrolment key Given I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage When I add "Self enrolment" enrolment method with: | Custom instance name | Test student enrolment | | Enrolment key | moodle_rules | And I log out And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I set the following fields to these values: | Enrolment key | moodle_rules | And I press "Enrol me" @@ -63,18 +61,17 @@ Feature: Users can auto-enrol themself in courses where self enrolment is allowe Scenario: Self-enrolment disabled Given I log in as "student1" - And I am on site homepage - When I follow "Course 1" + When I am on "Course 1" course homepage Then I should see "You can not enrol yourself in this course" Scenario: Self-enrolment enabled requiring a group enrolment key Given I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage When I add "Self enrolment" enrolment method with: | Custom instance name | Test student enrolment | | Enrolment key | moodle_rules | | Use group enrolment keys | Yes | - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Users > Groups" in current page administration And I press "Create group" And I set the following fields to these values: @@ -83,8 +80,7 @@ Feature: Users can auto-enrol themself in courses where self enrolment is allowe And I press "Save changes" And I log out And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I set the following fields to these values: | Enrolment key | Test-groupenrolkey1 | And I press "Enrol me" diff --git a/enrol/tests/behat/add_to_group.feature b/enrol/tests/behat/add_to_group.feature index a57e441cff9..79f4b4c39c7 100644 --- a/enrol/tests/behat/add_to_group.feature +++ b/enrol/tests/behat/add_to_group.feature @@ -24,7 +24,7 @@ Feature: Users can be added to multiple groups at once Scenario: Adding a user to one group Given I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Enrolled users" node in "Course administration > Users" And I click on "Add user into group" "link" in the "student1" "table_row" When I set the field "Add user into group" to "Group 1" @@ -33,7 +33,7 @@ Feature: Users can be added to multiple groups at once Scenario: Adding a user to multiple group Given I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Enrolled users" node in "Course administration > Users" And I click on "Add user into group" "link" in the "student1" "table_row" When I set the field "Add user into group" to "Group 1, Group 2, Group 3" diff --git a/enrol/tests/behat/enrol_user.feature b/enrol/tests/behat/enrol_user.feature index 94a80c55196..aa88075b324 100644 --- a/enrol/tests/behat/enrol_user.feature +++ b/enrol/tests/behat/enrol_user.feature @@ -13,7 +13,7 @@ Feature: User can be enrolled into a course | Course 001 | C001 | And I log in as "admin" And I am on course index - And I follow "Course 001" + And I am on "Course 001" course homepage Scenario: User can be enrolled without javascript When I enrol "Studie One" user as "Student" diff --git a/enrol/tests/behat/filter_enrolled_users.feature b/enrol/tests/behat/filter_enrolled_users.feature index d2843056710..8edf4584e34 100644 --- a/enrol/tests/behat/filter_enrolled_users.feature +++ b/enrol/tests/behat/filter_enrolled_users.feature @@ -39,7 +39,7 @@ Feature: Enrolled users can be filtered by group Scenario Outline: Given I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Enrolled users" node in "Course administration > Users" When I set the field "Group" to "" diff --git a/enrol/tests/behat/manage_enrolments_from_participants.feature b/enrol/tests/behat/manage_enrolments_from_participants.feature index d50bde399d8..e2e77451afd 100644 --- a/enrol/tests/behat/manage_enrolments_from_participants.feature +++ b/enrol/tests/behat/manage_enrolments_from_participants.feature @@ -19,7 +19,7 @@ Feature: Manage enrollments from participants page | student2 | C1 | student | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to course participants Scenario: Check the participants link when "All partipants" selected diff --git a/enrol/tests/enrollib_test.php b/enrol/tests/enrollib_test.php index b3dcfc1df17..b9c5d7f3118 100644 --- a/enrol/tests/enrollib_test.php +++ b/enrol/tests/enrollib_test.php @@ -476,4 +476,49 @@ class core_enrollib_testcase extends advanced_testcase { $this->assertGreaterThan($userenrolorig, $userenrolpost); } + + /** + * Test to confirm that enrol_get_my_courses only return the courses that + * the logged in user is enrolled in. + */ + public function test_enrol_get_my_courses_only_enrolled_courses() { + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course3 = $this->getDataGenerator()->create_course(); + $course4 = $this->getDataGenerator()->create_course(); + + $this->getDataGenerator()->enrol_user($user->id, $course1->id); + $this->getDataGenerator()->enrol_user($user->id, $course2->id); + $this->getDataGenerator()->enrol_user($user->id, $course3->id); + $this->resetAfterTest(true); + $this->setUser($user); + + // By default this function should return all of the courses the user + // is enrolled in. + $courses = enrol_get_my_courses(); + + $this->assertCount(3, $courses); + $this->assertEquals($course1->id, $courses[$course1->id]->id); + $this->assertEquals($course2->id, $courses[$course2->id]->id); + $this->assertEquals($course3->id, $courses[$course3->id]->id); + + // If a set of course ids are provided then the result set will only contain + // these courses. + $courseids = [$course1->id, $course2->id]; + $courses = enrol_get_my_courses(['id'], 'visible DESC,sortorder ASC', 0, $courseids); + + $this->assertCount(2, $courses); + $this->assertEquals($course1->id, $courses[$course1->id]->id); + $this->assertEquals($course2->id, $courses[$course2->id]->id); + + // If the course ids list contains any ids for courses the user isn't enrolled in + // then they will be ignored (in this case $course4). + $courseids = [$course1->id, $course2->id, $course4->id]; + $courses = enrol_get_my_courses(['id'], 'visible DESC,sortorder ASC', 0, $courseids); + + $this->assertCount(2, $courses); + $this->assertEquals($course1->id, $courses[$course1->id]->id); + $this->assertEquals($course2->id, $courses[$course2->id]->id); + } } diff --git a/files/tests/behat/add_custom_file_type.feature b/files/tests/behat/add_custom_file_type.feature index 0ed9a61ced7..59f1773cfb3 100644 --- a/files/tests/behat/add_custom_file_type.feature +++ b/files/tests/behat/add_custom_file_type.feature @@ -28,13 +28,12 @@ Feature: Add a new custom file type And I should see "application/x-moodle-rules" And I log out And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on When I add a "File" to section "1" and I fill the form with: | Name | Test file | | Select files | files/tests/fixtures/custom_filetype.mdlr | | Show type | 1 | | Display resource description | 1 | - And I follow "Course 1" + And I am on "Course 1" course homepage Then I should see "Test file" And I should see "Moodle rules" in the "span.resourcelinkdetails" "css_element" diff --git a/files/tests/behat/course_files.feature b/files/tests/behat/course_files.feature index af9f65ab038..c44e15436fa 100644 --- a/files/tests/behat/course_files.feature +++ b/files/tests/behat/course_files.feature @@ -13,8 +13,7 @@ Feature: Course files | legacyfilesinnewcourses | 1 | | legacyfilesaddallowed | 1 | When I log in as "admin" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage Then "Legacy course files" "link" should exist in current page administration And I navigate to "Legacy course files" node in "Course administration" And I press "Edit legacy course files" @@ -30,8 +29,7 @@ Feature: Course files | legacyfilesinnewcourses | 1 | | legacyfilesaddallowed | 0 | When I log in as "admin" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage Then "Legacy course files" "link" should exist in current page administration And I navigate to "Legacy course files" node in "Course administration" And I press "Edit legacy course files" diff --git a/grade/export/txt/tests/behat/export.feature b/grade/export/txt/tests/behat/export.feature index ea92bf6ae29..1965c9acdf5 100644 --- a/grade/export/txt/tests/behat/export.feature +++ b/grade/export/txt/tests/behat/export.feature @@ -21,7 +21,7 @@ Feature: I need to export grades as text | assign | C1 | a1 | Test assignment name | Submit your online text | 1 | | assign | C1 | a2 | Test assignment name 2 | Submit your online text | 1 | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment name" diff --git a/grade/export/xml/tests/behat/export.feature b/grade/export/xml/tests/behat/export.feature index 9e6450a0181..d1ca5aacdee 100644 --- a/grade/export/xml/tests/behat/export.feature +++ b/grade/export/xml/tests/behat/export.feature @@ -20,7 +20,7 @@ Feature: I need to export grades as xml | activity | course | idnumber | name | intro | | assign | C1 | a1 | Test assignment name | Submit something! | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment name" diff --git a/grade/grading/form/guide/tests/behat/edit_guide.feature b/grade/grading/form/guide/tests/behat/edit_guide.feature index 37a256317d4..db90d9499b2 100644 --- a/grade/grading/form/guide/tests/behat/edit_guide.feature +++ b/grade/grading/form/guide/tests/behat/edit_guide.feature @@ -17,8 +17,7 @@ Feature: Marking guides can be created and edited | teacher1 | C1 | editingteacher | | student1 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment 1 name | | Description | Test assignment description | @@ -93,7 +92,7 @@ Feature: Marking guides can be created and edited And I log out # Viewing it as a student. And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1 name" And I should see "80" in the ".feedback" "css_element" And I should see "Marking guide test description" in the ".feedback" "css_element" diff --git a/grade/grading/form/rubric/tests/behat/edit_rubric.feature b/grade/grading/form/rubric/tests/behat/edit_rubric.feature index 6500d05d169..4fbde356c76 100644 --- a/grade/grading/form/rubric/tests/behat/edit_rubric.feature +++ b/grade/grading/form/rubric/tests/behat/edit_rubric.feature @@ -18,8 +18,7 @@ Feature: Rubrics can be created and edited | teacher1 | C1 | editingteacher | | student1 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment 1 name | | Description | Test assignment description | @@ -72,7 +71,7 @@ Feature: Rubrics can be created and edited And I log out # Viewing it as a student. And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1 name" And I should see "35" in the ".feedback" "css_element" And I should see "Rubric test description" in the ".feedback" "css_element" @@ -82,7 +81,7 @@ Feature: Rubrics can be created and edited And the level with "5" points is selected for the rubric criterion "Criterion 3" And I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage # Editing a rubric definition without regrading students. And I go to "Test assignment 1 name" advanced grading definition page And "Save as draft" "button" should not exist @@ -95,14 +94,14 @@ Feature: Rubrics can be created and edited And I log out # Check that the student still sees the grade. And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1 name" And I should see "35" in the ".feedback" "css_element" And the level with "20" points is selected for the rubric criterion "Criterion 1" And I log out # Editing a rubric with significant changes. And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I go to "Test assignment 1 name" advanced grading definition page And I click on "Move down" "button" in the "Criterion 2" "table_row" And I replace "1" rubric level with "60" in "Criterion 1" criterion @@ -112,14 +111,14 @@ Feature: Rubrics can be created and edited And I log out # Check that the student doesn't see the grade. And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1 name" And I should see "35" in the ".feedback" "css_element" And the level with "20" points is not selected for the rubric criterion "Criterion 1" And I log out # Regrade student. And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1 name" And I go to "Student 1" "Test assignment 1 name" activity advanced grading page And I should see "The rubric definition was changed after this student had been graded. The student can not see this rubric until you check the rubric and update the grade." @@ -127,14 +126,14 @@ Feature: Rubrics can be created and edited And I log out # Check that the student sees the grade again. And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1 name" And I should see "31.82" in the ".feedback" "css_element" And the level with "20" points is not selected for the rubric criterion "Criterion 1" # Hide all rubric info for students And I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I go to "Test assignment 1 name" advanced grading definition page And I set the field "Allow users to preview rubric (otherwise it will only be displayed after grading)" to "" And I set the field "Display rubric description during evaluation" to "" @@ -147,7 +146,7 @@ Feature: Rubrics can be created and edited And I log out # Students should not see anything. And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment 1 name" And I should not see "Criterion 1" in the ".submissionstatustable" "css_element" And I should not see "Criterion 2" in the ".submissionstatustable" "css_element" diff --git a/grade/grading/form/rubric/tests/behat/grade_calculation.feature b/grade/grading/form/rubric/tests/behat/grade_calculation.feature index 3e647147b31..687f1a49c7e 100644 --- a/grade/grading/form/rubric/tests/behat/grade_calculation.feature +++ b/grade/grading/form/rubric/tests/behat/grade_calculation.feature @@ -23,8 +23,7 @@ Feature: Converting rubric score to grades | activity | name | intro | course | idnumber | grade | advancedgradingmethod_submissions | | assign | Test assignment 1 | Test | C1 | assign1 | | rubric | When I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I go to "Test assignment 1" advanced grading definition page And I set the following fields to these values: | Name | Assignment 1 rubric | diff --git a/grade/grading/form/rubric/tests/behat/negative_points.feature b/grade/grading/form/rubric/tests/behat/negative_points.feature index 83ee60b9f8e..33d0e1e0deb 100644 --- a/grade/grading/form/rubric/tests/behat/negative_points.feature +++ b/grade/grading/form/rubric/tests/behat/negative_points.feature @@ -27,8 +27,7 @@ Feature: Rubrics can have levels with negative scores | activity | name | intro | course | idnumber | grade | advancedgradingmethod_submissions | | assign | Test assignment 1 | Test | C1 | assign1 | 100 | rubric | When I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I go to "Test assignment 1" advanced grading definition page And I set the following fields to these values: | Name | Assignment 1 rubric | diff --git a/grade/grading/form/rubric/tests/behat/publish_rubric_templates.feature b/grade/grading/form/rubric/tests/behat/publish_rubric_templates.feature index e755151d403..c1a5cbdf1e1 100644 --- a/grade/grading/form/rubric/tests/behat/publish_rubric_templates.feature +++ b/grade/grading/form/rubric/tests/behat/publish_rubric_templates.feature @@ -23,8 +23,7 @@ Feature: Publish rubrics as templates | user | role | contextlevel | reference | | manager1 | manager | System | | And I log in as "manager1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I go to "Test assignment 1 name" advanced grading definition page And I set the following fields to these values: | Name | Assignment 1 rubric | @@ -36,7 +35,7 @@ Feature: Publish rubrics as templates When I publish "Test assignment 1 name" grading form definition as a public template And I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I set "Test assignment 2 name" activity to use "Assignment 1 rubric" grading form Then I should see "Advanced grading: Test assignment 2 name (Submissions)" And I should see "Criterion 1" diff --git a/grade/grading/form/rubric/tests/behat/reuse_own_rubrics.feature b/grade/grading/form/rubric/tests/behat/reuse_own_rubrics.feature index 5effb3d0e32..f649d1129d6 100644 --- a/grade/grading/form/rubric/tests/behat/reuse_own_rubrics.feature +++ b/grade/grading/form/rubric/tests/behat/reuse_own_rubrics.feature @@ -15,8 +15,7 @@ Feature: Reuse my rubrics in other activities | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment 1 name | | Description | Test assignment 1 description | @@ -30,7 +29,7 @@ Feature: Reuse my rubrics in other activities | Criterion 2 | Level 21 | 21 | Level 22 | 22 | Level 3 | 23 | | Criterion 3 | Level 31 | 31 | Level 32 | 32 | | | And I press "Save rubric and make it ready" - And I follow "Course 1" + And I am on "Course 1" course homepage When I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment 2 name | | Description | Test assignment 2 description | @@ -40,7 +39,7 @@ Feature: Reuse my rubrics in other activities And I should see "Criterion 1" And I should see "Criterion 2" And I should see "Criterion 3" - And I follow "Course 1" + And I am on "Course 1" course homepage And I go to "Test assignment 1 name" advanced grading definition page And I should see "Criterion 1" And I should see "Criterion 2" diff --git a/grade/report/grader/tests/behat/ajax_grader.feature b/grade/report/grader/tests/behat/ajax_grader.feature index dddab82eb22..b7cbe9e6fb8 100644 --- a/grade/report/grader/tests/behat/ajax_grader.feature +++ b/grade/report/grader/tests/behat/ajax_grader.feature @@ -53,7 +53,7 @@ Feature: Using the AJAX grading feature of Grader report to update grades and fe | grade_overridecat | 1 | | grade_report_showquickfeedback | 0 | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I click on student "Student 2" for grade item "Item VU" Then I should see a grade field for "Student 2" and grade item "Item VU" @@ -105,7 +105,7 @@ Feature: Using the AJAX grading feature of Grader report to update grades and fe | grade_overridecat | 1 | | grade_report_showquickfeedback | 1 | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I click on student "Student 2" for grade item "Item VU" Then I should see a grade field for "Student 2" and grade item "Item VU" @@ -142,7 +142,7 @@ Feature: Using the AJAX grading feature of Grader report to update grades and fe | grade_overridecat | 0 | | grade_report_showquickfeedback | 1 | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I click on student "Student 2" for grade item "Item VU" Then I should see a grade field for "Student 2" and grade item "Item VU" @@ -162,7 +162,7 @@ Feature: Using the AJAX grading feature of Grader report to update grades and fe | grade_overridecat | 1 | | grade_report_showquickfeedback | 1 | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on Then I should not see a grade field for "Student 2" and grade item "Item VL" @@ -193,7 +193,7 @@ Feature: Using the AJAX grading feature of Grader report to update grades and fe | grade_overridecat | 0 | | grade_report_showquickfeedback | 1 | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I change window size to "large" diff --git a/grade/report/grader/tests/behat/switch_views.feature b/grade/report/grader/tests/behat/switch_views.feature index 54bed2bbf64..7cd066a0a98 100644 --- a/grade/report/grader/tests/behat/switch_views.feature +++ b/grade/report/grader/tests/behat/switch_views.feature @@ -18,8 +18,7 @@ Feature: We can change what we are viewing on the grader report | teacher1 | C1 | editingteacher | | student1 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment name 1 | | Description | Submit your online text | @@ -30,14 +29,14 @@ Feature: We can change what we are viewing on the grader report | assignsubmission_onlinetext_enabled | 1 | And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment name 1" When I press "Add submission" And I set the following fields to these values: | Online text | This is a submission for assignment 1 | And I press "Save changes" Then I should see "Submitted for grading" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment name 2" When I press "Add submission" And I set the following fields to these values: @@ -46,7 +45,7 @@ Feature: We can change what we are viewing on the grader report Then I should see "Submitted for grading" And I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment name 1" @@ -56,11 +55,10 @@ Feature: We can change what we are viewing on the grader report @javascript Scenario: View and minimise the grader report containing hidden activities - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I open "Test assignment name 2" actions menu And I click on "Hide" "link" in the "Test assignment name 2" activity - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I should see "Test assignment name 1" And I should see "Test assignment name 2" @@ -85,8 +83,7 @@ Feature: We can change what we are viewing on the grader report @javascript Scenario: View and minimise the grader report containing hidden activities without the 'moodle/grade:viewhidden' capability - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I open "Test assignment name 2" actions menu And I click on "Hide" "link" in the "Test assignment name 2" activity And I log out @@ -99,7 +96,7 @@ Feature: We can change what we are viewing on the grader report | user | course | role | | student2 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I should see "Test assignment name 1" And I should see "Test assignment name 2" diff --git a/grade/report/history/tests/behat/basic_functionality.feature b/grade/report/history/tests/behat/basic_functionality.feature index 3704c3ad420..119bd083f92 100644 --- a/grade/report/history/tests/behat/basic_functionality.feature +++ b/grade/report/history/tests/behat/basic_functionality.feature @@ -22,8 +22,7 @@ Feature: A teacher checks the grade history report in a course | student1 | C1 | student | | student2 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | The greatest assignment ever | | Description | Write a behat test for Moodle - it's amazing! | @@ -39,7 +38,7 @@ Feature: A teacher checks the grade history report in a course And I press "Save changes" And I log out And I log in as "teacher2" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I give the grade "70.00" to the user "Student 1" for the grade item "The greatest assignment ever" diff --git a/grade/report/singleview/tests/behat/bulk_insert_grades.feature b/grade/report/singleview/tests/behat/bulk_insert_grades.feature index fe94e1fcf2f..c99aefe1a2a 100644 --- a/grade/report/singleview/tests/behat/bulk_insert_grades.feature +++ b/grade/report/singleview/tests/behat/bulk_insert_grades.feature @@ -31,7 +31,7 @@ Feature: We can bulk insert grades for students in a course Scenario: I can bulk insert grades and check their override flags for grade view. Given I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment one" And I navigate to "View all submissions" in current page administration And I click on "Grade" "link" in the "Student 1" "table_row" @@ -39,7 +39,7 @@ Feature: We can bulk insert grades for students in a course | Grade out of 100 | 50 | And I press "Save changes" And I press "Ok" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I follow "Single view for Test assignment one" Then the field "Grade for james (Student) 1" matches value "50.00" @@ -72,7 +72,7 @@ Feature: We can bulk insert grades for students in a course Scenario: I can bulk insert grades and check their override flags for user view. Given I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment two" And I navigate to "View all submissions" in current page administration And I click on "Grade" "link" in the "Student 1" "table_row" @@ -80,7 +80,7 @@ Feature: We can bulk insert grades for students in a course | Grade out of 100 | 50 | And I press "Save changes" And I press "Ok" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook # And I click on "input[title='Dock Navigation block']" "css_element" # And I click on "input[title='Dock Administration block']" "css_element" diff --git a/grade/report/singleview/tests/behat/singleview.feature b/grade/report/singleview/tests/behat/singleview.feature index cd1774fd9a2..bfd7d7fc2db 100644 --- a/grade/report/singleview/tests/behat/singleview.feature +++ b/grade/report/singleview/tests/behat/singleview.feature @@ -51,7 +51,7 @@ Feature: We can use Single view | moodle/grade:edit | Allow | teacher | Course | C1 | | gradereport/singleview:view | Allow | teacher | Course | C1 | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage Given I navigate to "View > Grader report" in the course gradebook @javascript @@ -96,7 +96,7 @@ Feature: We can use Single view | james (Student) 1 | Very good | And I log out And I log in as "teacher2" - And I follow "Course 1" + And I am on "Course 1" course homepage Given I navigate to "View > Single view" in the course gradebook And I click on "Student 4" "option" And the "Exclude for Test assignment one" "checkbox" should be disabled diff --git a/grade/report/user/tests/behat/user_view.feature b/grade/report/user/tests/behat/user_view.feature index e54ac6f187e..eb9bef2d334 100644 --- a/grade/report/user/tests/behat/user_view.feature +++ b/grade/report/user/tests/behat/user_view.feature @@ -31,7 +31,7 @@ Feature: View the user report as the student will see it | assign | C1 | a5 | Test assignment five | Submit something! | 100 | | assign | C1 | a6 | Test assignment six | Submit something! | 100 | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Setup > Gradebook setup" in the course gradebook And I hide the grade item "Test assignment six" And I hide the grade item "Sub category 2" @@ -82,7 +82,7 @@ Feature: View the user report as the student will see it | Test assignment six | And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "User report" in the course gradebook Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | @@ -120,7 +120,7 @@ Feature: View the user report as the student will see it | Test assignment six | And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "User report" in the course gradebook Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | @@ -158,7 +158,7 @@ Feature: View the user report as the student will see it | Test assignment six | And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "User report" in the course gradebook Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | @@ -182,7 +182,7 @@ Feature: View the user report as the student will see it | moodle/grade:viewhidden | Allow | And I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Setup > Course grade settings" in the course gradebook And I set the field with xpath "//select[@name='report_user_showtotalsifcontainhidden']" to "Show totals excluding hidden items" And I press "Save changes" @@ -202,7 +202,7 @@ Feature: View the user report as the student will see it | Course total | - | 383.00 | 0–600 | 63.83 % | - | And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "User report" in the course gradebook Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | diff --git a/grade/report/user/tests/behat/view_usereport.feature b/grade/report/user/tests/behat/view_usereport.feature index 3fc9563afe5..3aaf02aefbc 100644 --- a/grade/report/user/tests/behat/view_usereport.feature +++ b/grade/report/user/tests/behat/view_usereport.feature @@ -10,8 +10,7 @@ Feature: We can use the user report Scenario: Verify we can view a user grade report with no users enrolled. Given I log in as "admin" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > User report" in the course gradebook And I select "All users (0)" from the "Select all or one user" singleselect Then I should see "No students enrolled in this course yet" diff --git a/grade/tests/behat/grade_UI_settings.feature b/grade/tests/behat/grade_UI_settings.feature index 5661e329b05..b354dae3e4f 100644 --- a/grade/tests/behat/grade_UI_settings.feature +++ b/grade/tests/behat/grade_UI_settings.feature @@ -18,8 +18,7 @@ Feature: Site settings can be used to hide parts of the gradebook UI | activity | course | idnumber | name | intro | | assign | C1 | assign1 | Assignment1 | Assignment 1 intro | And I log in as "admin" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on @@ -30,8 +29,7 @@ Feature: Site settings can be used to hide parts of the gradebook UI Then I navigate to "General settings" node in "Site administration > Grades" And I set the field "Show minimum grade" to "0" And I press "Save changes" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I click on "Edit assign Assignment1" "link" And I should not see "Minimum grade" @@ -42,8 +40,7 @@ Feature: Site settings can be used to hide parts of the gradebook UI When I navigate to "Grader report" node in "Site administration > Grades > Report settings" And I set the field "Show calculations" to "0" And I press "Save changes" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook Then "Edit calculation for Course total" "link" should not exist @@ -53,7 +50,6 @@ Feature: Site settings can be used to hide parts of the gradebook UI Then I navigate to "Grade category settings" node in "Site administration > Grades" And I set the field "Allow category grades to be manually overridden" to "0" And I press "Save changes" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And "tr .course input[type='text']" "css_element" should not exist diff --git a/grade/tests/behat/grade_aggregation.feature b/grade/tests/behat/grade_aggregation.feature index df80dd3d24f..68a91041234 100644 --- a/grade/tests/behat/grade_aggregation.feature +++ b/grade/tests/behat/grade_aggregation.feature @@ -41,7 +41,7 @@ Feature: We can use calculated grade totals | grade_aggregations_visible | Mean of grades,Weighted mean of grades,Simple weighted mean of grades,Mean of grades (with extra credits),Median of grades,Lowest grade,Highest grade,Mode of grades,Natural | And I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I change window size to "large" @@ -256,7 +256,7 @@ Feature: We can use calculated grade totals And "Test outcome item one" row "Grade" column of "user-grade" table should contain "Excellent (100.00 %)" And I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Setup > Gradebook setup" in the course gradebook And I set the following settings for grade item "Test outcome item one": | Extra credit | 1 | @@ -268,7 +268,7 @@ Feature: We can use calculated grade totals And "Test outcome item one" row "Grade" column of "user-grade" table should contain "Excellent (100.00 %)" And I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Setup > Gradebook setup" in the course gradebook And I set the following settings for grade item "Course 1": | Aggregation | Natural | @@ -370,8 +370,7 @@ Feature: We can use calculated grade totals Scenario: Natural aggregation with drop lowest When I log out And I log in as "admin" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I set the following settings for grade item "Sub category 1": diff --git a/grade/tests/behat/grade_aggregation_changes.feature b/grade/tests/behat/grade_aggregation_changes.feature index 89db8d5680e..68835bb361e 100644 --- a/grade/tests/behat/grade_aggregation_changes.feature +++ b/grade/tests/behat/grade_aggregation_changes.feature @@ -33,8 +33,7 @@ Feature: Changing the aggregation of an item affects its weight and extra credit And I log in as "admin" And I set the following administration settings values: | grade_aggregations_visible | Mean of grades,Weighted mean of grades,Simple weighted mean of grades,Mean of grades (with extra credits),Median of grades,Lowest grade,Highest grade,Mode of grades,Natural | - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I follow "Edit Cat mean" diff --git a/grade/tests/behat/grade_average.feature b/grade/tests/behat/grade_average.feature index f60cfa15821..abbfe4fb3ce 100644 --- a/grade/tests/behat/grade_average.feature +++ b/grade/tests/behat/grade_average.feature @@ -21,8 +21,7 @@ Feature: Average grades are displayed in the gradebook | student2 | C1 | student | | student3 | C1 | student | And I log in as "admin" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage # Enable averages And I navigate to "Setup > Course grade settings" in the course gradebook And I set the following fields to these values: @@ -58,8 +57,7 @@ Feature: Average grades are displayed in the gradebook # Check the user grade table And I log in as "student1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "User report" in the course gradebook Then I should see "50.00" in the ".level2.column-grade" "css_element" Then I should see "50.00" in the ".level2.column-average" "css_element" diff --git a/grade/tests/behat/grade_calculated_grade_items.feature b/grade/tests/behat/grade_calculated_grade_items.feature index 57046899f7b..e3cca8d185f 100644 --- a/grade/tests/behat/grade_calculated_grade_items.feature +++ b/grade/tests/behat/grade_calculated_grade_items.feature @@ -19,8 +19,7 @@ Feature: Calculated grade items can be used in the gradebook | student1 | C1 | student | | student2 | C1 | student | And I log in as "admin" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Setup > Gradebook setup" in the course gradebook @javascript diff --git a/grade/tests/behat/grade_calculated_grade_items_20150627.feature b/grade/tests/behat/grade_calculated_grade_items_20150627.feature index e1eb3fbf969..88558150a30 100644 --- a/grade/tests/behat/grade_calculated_grade_items_20150627.feature +++ b/grade/tests/behat/grade_calculated_grade_items_20150627.feature @@ -20,8 +20,7 @@ Feature: Gradebook calculations for calculated grade items before the fix 201506 | student1 | C1 | student | | student2 | C1 | student | And I log in as "admin" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Setup > Gradebook setup" in the course gradebook @javascript diff --git a/grade/tests/behat/grade_calculated_weights.feature b/grade/tests/behat/grade_calculated_weights.feature index 47a30400de8..88b20c35f96 100644 --- a/grade/tests/behat/grade_calculated_weights.feature +++ b/grade/tests/behat/grade_calculated_weights.feature @@ -29,7 +29,7 @@ Feature: We can understand the gradebook user report | grade_aggregations_visible | Mean of grades,Weighted mean of grades,Simple weighted mean of grades,Mean of grades (with extra credits),Median of grades,Lowest grade,Highest grade,Mode of grades,Natural | And I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I give the grade "60.00" to the user "Student 1" for the grade item "Test assignment one" diff --git a/grade/tests/behat/grade_category_validation.feature b/grade/tests/behat/grade_category_validation.feature index 74cff6208a5..ad270465406 100644 --- a/grade/tests/behat/grade_category_validation.feature +++ b/grade/tests/behat/grade_category_validation.feature @@ -32,8 +32,7 @@ Feature: Editing a grade item | grade_aggregations_visible | Mean of grades,Weighted mean of grades,Simple weighted mean of grades,Mean of grades (with extra credits),Median of grades,Lowest grade,Highest grade,Mode of grades,Natural | And I log out And I log in as "teacher1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Setup > Gradebook setup" in the course gradebook And I press "Add category" And I set the following fields to these values: diff --git a/grade/tests/behat/grade_contribution_with_extra_credit.feature b/grade/tests/behat/grade_contribution_with_extra_credit.feature index 05311952dec..cb53856f38c 100644 --- a/grade/tests/behat/grade_contribution_with_extra_credit.feature +++ b/grade/tests/behat/grade_contribution_with_extra_credit.feature @@ -19,8 +19,7 @@ Feature: Extra credit contributions are normalised when going out of bounds And I log in as "admin" And I set the following administration settings values: | grade_aggregations_visible | Simple weighted mean of grades,Mean of grades (with extra credits),Natural | - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Setup > Gradebook setup" in the course gradebook And I press "Add grade item" And I set the following fields to these values: @@ -45,7 +44,7 @@ Feature: Extra credit contributions are normalised when going out of bounds And I press "Save changes" And I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I give the grade "80.00" to the user "Student 1" for the grade item "Manual item 1" diff --git a/grade/tests/behat/grade_grade_minmax_change.feature b/grade/tests/behat/grade_grade_minmax_change.feature index 16d6abd0430..1ed3712b04f 100644 --- a/grade/tests/behat/grade_grade_minmax_change.feature +++ b/grade/tests/behat/grade_grade_minmax_change.feature @@ -20,8 +20,7 @@ Feature: We can change the maximum and minimum number of points for manual items | student1 | C1 | student | | student2 | C1 | student | And I log in as "teacher1" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Setup > Gradebook setup" in the course gradebook And I press "Add grade item" And I set the following fields to these values: @@ -35,7 +34,7 @@ Feature: We can change the maximum and minimum number of points for manual items And I press "Save changes" Scenario: Change maximum number of points on a graded item. - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I give the grade "10.00" to the user "Student 1" for the grade item "Manual item 1" diff --git a/grade/tests/behat/grade_hidden_items.feature b/grade/tests/behat/grade_hidden_items.feature index fd7aa450f2d..ca3b5180eb5 100644 --- a/grade/tests/behat/grade_hidden_items.feature +++ b/grade/tests/behat/grade_hidden_items.feature @@ -34,7 +34,7 @@ Feature: Student and teacher's view of aggregated grade items is consistent when And I press "Save changes" When I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I give the grade "50.00" to the user "Student 1" for the grade item "Test assignment one" @@ -43,7 +43,7 @@ Feature: Student and teacher's view of aggregated grade items is consistent when And I set the following settings for grade item "Test assignment four": | Hidden | 1 | And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > User report" in the course gradebook And I select "Myself" from the "View report as" singleselect And I select "Student 1" from the "Select all or one user" singleselect @@ -55,7 +55,7 @@ Feature: Student and teacher's view of aggregated grade items is consistent when | Course total | - | 100.00 | 0–200 | 50.00 % | - | When I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "User report" in the course gradebook Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | diff --git a/grade/tests/behat/grade_item_validation.feature b/grade/tests/behat/grade_item_validation.feature index ef381aad6ae..4f3d6cb3663 100644 --- a/grade/tests/behat/grade_item_validation.feature +++ b/grade/tests/behat/grade_item_validation.feature @@ -28,8 +28,7 @@ Feature: Grade item validation | Name | Letter scale | | Scale | Disappointing, Good, Very good, Excellent | And I press "Save changes" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Setup > Gradebook setup" in the course gradebook And I press "Add grade item" And I set the following fields to these values: diff --git a/grade/tests/behat/grade_letter_boundary.feature b/grade/tests/behat/grade_letter_boundary.feature index 33466731920..d9d951c3ff8 100644 --- a/grade/tests/behat/grade_letter_boundary.feature +++ b/grade/tests/behat/grade_letter_boundary.feature @@ -21,7 +21,7 @@ Feature: We can customise the letter boundary of a course. | activity | course | idnumber | name | intro | grade | | assign | C1 | a1 | Test assignment one | Submit something! | 100 | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Setup > Course grade settings" in the course gradebook And I set the following fields to these values: | Grade display type | Letter | diff --git a/grade/tests/behat/grade_letter_boundary_20160518.feature b/grade/tests/behat/grade_letter_boundary_20160518.feature index 8ec6dafd571..0f5dbc18dcd 100644 --- a/grade/tests/behat/grade_letter_boundary_20160518.feature +++ b/grade/tests/behat/grade_letter_boundary_20160518.feature @@ -22,7 +22,7 @@ Feature: We can customise the letter boundary of a course in gradebook version 2 | activity | course | idnumber | name | intro | grade | | assign | C1 | a1 | Test assignment one | Submit something! | 100 | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Setup > Course grade settings" in the course gradebook And I set the following fields to these values: | Grade display type | Letter | diff --git a/grade/tests/behat/grade_mingrade.feature b/grade/tests/behat/grade_mingrade.feature index 0011f2086e4..b385b3d27ae 100644 --- a/grade/tests/behat/grade_mingrade.feature +++ b/grade/tests/behat/grade_mingrade.feature @@ -25,8 +25,7 @@ Feature: We can use a minimum grade different than zero And I log in as "admin" And I set the following administration settings values: | grade_aggregations_visible | Mean of grades,Weighted mean of grades,Simple weighted mean of grades,Mean of grades (with extra credits),Median of grades,Lowest grade,Highest grade,Mode of grades,Natural | - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Setup > Gradebook setup" in the course gradebook And I press "Add grade item" And I set the following fields to these values: @@ -84,7 +83,7 @@ Feature: We can use a minimum grade different than zero | Exclude empty grades | 0 | And I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on When I give the grade "-25.00" to the user "Student 1" for the grade item "Manual item 1" diff --git a/grade/tests/behat/grade_natural_exclude_empty.feature b/grade/tests/behat/grade_natural_exclude_empty.feature index 37f411d4e56..b23be1f4533 100644 --- a/grade/tests/behat/grade_natural_exclude_empty.feature +++ b/grade/tests/behat/grade_natural_exclude_empty.feature @@ -24,7 +24,7 @@ Feature: Weights in natural aggregation are adjusted if the items are excluded f | assign | C1 | a4 | Test assignment four (extra) | x | 20 | | assign | C1 | a5 | Test assignment five (extra) | x | 10 | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Setup > Gradebook setup" in the course gradebook And I set the following settings for grade item "Test assignment four (extra)": | Extra credit | 1 | diff --git a/grade/tests/behat/grade_natural_exclude_empty_20150619.feature b/grade/tests/behat/grade_natural_exclude_empty_20150619.feature index 54dfc00d25d..39578ff9a38 100644 --- a/grade/tests/behat/grade_natural_exclude_empty_20150619.feature +++ b/grade/tests/behat/grade_natural_exclude_empty_20150619.feature @@ -25,7 +25,7 @@ Feature: Gradebook calculations for extra credit items before the fix 20150619 | assign | C1 | a4 | Test assignment four (extra) | x | 20 | | assign | C1 | a5 | Test assignment five (extra) | x | 10 | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Setup > Gradebook setup" in the course gradebook And I set the following settings for grade item "Test assignment four (extra)": | Extra credit | 1 | diff --git a/grade/tests/behat/grade_natural_normalisation.feature b/grade/tests/behat/grade_natural_normalisation.feature index 82dbd8d05b7..4706e101d1e 100644 --- a/grade/tests/behat/grade_natural_normalisation.feature +++ b/grade/tests/behat/grade_natural_normalisation.feature @@ -31,7 +31,7 @@ Feature: We can use natural aggregation and weights will be normalised to a tota | assign | C1 | a6 | Test assignment six | Submit something! | Sub category 1 | 10 | | assign | C1 | a7 | Test assignment seven | Submit nothing! | Sub category 1 | 15 | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Setup > Gradebook setup" in the course gradebook @javascript diff --git a/grade/tests/behat/grade_natural_normalisation_20150619.feature b/grade/tests/behat/grade_natural_normalisation_20150619.feature index b302102589a..887a5c0000e 100644 --- a/grade/tests/behat/grade_natural_normalisation_20150619.feature +++ b/grade/tests/behat/grade_natural_normalisation_20150619.feature @@ -32,7 +32,7 @@ Feature: Gradebook calculations for natural weights normalisation before the fix | assign | C1 | a6 | Test assignment six | Submit something! | Sub category 1 | 10 | | assign | C1 | a7 | Test assignment seven | Submit nothing! | Sub category 1 | 15 | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Setup > Gradebook setup" in the course gradebook @javascript diff --git a/grade/tests/behat/grade_override_letter.feature b/grade/tests/behat/grade_override_letter.feature index be0ff3dd907..da25498442b 100644 --- a/grade/tests/behat/grade_override_letter.feature +++ b/grade/tests/behat/grade_override_letter.feature @@ -15,7 +15,7 @@ Feature: Grade letters can be overridden | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Letters > Edit" in the course gradebook Scenario Outline: Grade letters can be completely overridden diff --git a/grade/tests/behat/grade_point_maximum.feature b/grade/tests/behat/grade_point_maximum.feature index 347b9860c3a..5e172374629 100644 --- a/grade/tests/behat/grade_point_maximum.feature +++ b/grade/tests/behat/grade_point_maximum.feature @@ -19,8 +19,7 @@ Feature: We can change the grading type and maximum grade point values | Grade point maximum | 900 | | Grade point default | 800 | And I press "Save changes" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage @javascript Scenario: Validate that switching the type of grading used correctly disables input form elements @@ -97,8 +96,7 @@ Feature: We can change the grading type and maximum grade point values And I set the following fields to these values: | Grade point maximum | 100 | And I press "Save changes" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test Assignment 1" And I navigate to "Edit settings" in current page administration And I press "Save and display" diff --git a/grade/tests/behat/grade_scales.feature b/grade/tests/behat/grade_scales.feature index 400e96fb43d..d673eabf7cb 100644 --- a/grade/tests/behat/grade_scales.feature +++ b/grade/tests/behat/grade_scales.feature @@ -42,7 +42,7 @@ Feature: View gradebook when scales are used | activity | course | idnumber | name | intro | gradecategory | | assign | C1 | a1 | Test assignment one | Submit something! | Sub category 1 | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment one" And I navigate to "Edit settings" in current page administration And I expand all fieldsets @@ -70,7 +70,7 @@ Feature: View gradebook when scales are used And I set the field "Grade" to "F" And I press "Save changes" And I press "Ok" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Setup > Course grade settings" in the course gradebook And I set the field "Show weightings" to "Show" And I set the field "Show contribution to course total" to "Show" diff --git a/grade/tests/behat/grade_scales_aggregation.feature b/grade/tests/behat/grade_scales_aggregation.feature index dc2092b9d78..8a26b40519a 100644 --- a/grade/tests/behat/grade_scales_aggregation.feature +++ b/grade/tests/behat/grade_scales_aggregation.feature @@ -38,7 +38,7 @@ Feature: Control the aggregation of the scales Scenario Outline: Scales can be excluded from aggregation Given I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on When I give the grade "10" to the user "Student 1" for the grade item "Grade me" @@ -59,7 +59,7 @@ Feature: Control the aggregation of the scales | grade_includescalesinaggregation | 1 | And I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > User report" in the course gradebook And I select "Student 1" from the "Select all or one user" singleselect And the following should exist in the "user-grade" table: @@ -83,7 +83,7 @@ Feature: Control the aggregation of the scales @javascript Scenario: Weights of scales cannot be edited when they are not aggregated Given I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on When I set the following settings for grade item "Course 1": @@ -98,7 +98,7 @@ Feature: Control the aggregation of the scales And I should not see "Weight" And the following config values are set as admin: | grade_includescalesinaggregation | 1 | - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Setup > Gradebook setup" in the course gradebook And I set the field "Override weight of Grade me" to "1" And the field "Override weight of Grade me" matches value "95.238" diff --git a/grade/tests/behat/grade_single_item_scales.feature b/grade/tests/behat/grade_single_item_scales.feature index 2f4a7cc158a..1b319e328d0 100644 --- a/grade/tests/behat/grade_single_item_scales.feature +++ b/grade/tests/behat/grade_single_item_scales.feature @@ -36,7 +36,7 @@ Feature: View gradebook when single item scales are used | activity | course | idnumber | name | intro | gradecategory | | assign | C1 | a1 | Test assignment one | Submit something! | Sub category 1 | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment one" And I navigate to "Edit settings" in current page administration And I expand all fieldsets @@ -48,7 +48,7 @@ Feature: View gradebook when single item scales are used And I set the field "Grade" to "A" And I press "Save changes" And I press "Ok" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Setup > Course grade settings" in the course gradebook And I set the field "Show weightings" to "Show" And I set the field "Show contribution to course total" to "Show" diff --git a/grade/tests/behat/grade_to_pass.feature b/grade/tests/behat/grade_to_pass.feature index b108e06b107..a59f0fc396f 100644 --- a/grade/tests/behat/grade_to_pass.feature +++ b/grade/tests/behat/grade_to_pass.feature @@ -19,7 +19,7 @@ Feature: We can set the grade to pass value | name | scale | | Test Scale 1 | Disappointing, Good, Very good, Excellent | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage @javascript Scenario: Validate that switching the type of grading used correctly disables grade to pass @@ -58,7 +58,7 @@ Feature: We can set the grade to pass value And I turn editing mode on And I click on "Edit assign Test Assignment 1" "link" Then the field "Grade to pass" matches value "25" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test Assignment 1" And I follow "Edit settings" And I expand all fieldsets @@ -83,7 +83,7 @@ Feature: We can set the grade to pass value Then the field "Grade to pass" matches value "3" And I set the field "Grade to pass" to "4" And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test Assignment 1" And I follow "Edit settings" And the field "Grade to pass" matches value "4" @@ -119,7 +119,7 @@ Feature: We can set the grade to pass value And the field "Grade to pass" matches value "10" And I set the field "Grade to pass" to "15" And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test Workshop 1" And I follow "Edit settings" And the field "Submission grade to pass" matches value "45" @@ -149,7 +149,7 @@ Feature: We can set the grade to pass value Then the field "Grade to pass" matches value "9.5" And I set the field "Grade to pass" to "8" And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test Quiz 1" And I follow "Edit settings" And the field "Grade to pass" matches value "8.00" @@ -167,7 +167,7 @@ Feature: We can set the grade to pass value Then the field "Grade to pass" matches value "90" And I set the field "Grade to pass" to "80" And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test Lesson 1" And I follow "Edit settings" And the field "Grade to pass" matches value "80" @@ -186,7 +186,7 @@ Feature: We can set the grade to pass value Then the field "Grade to pass" matches value "90" And I set the field "Grade to pass" to "80" And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage And I click on "Edit settings" "link" in the "Test Database 1" activity And the field "Grade to pass" matches value "80" @@ -214,7 +214,7 @@ Feature: We can set the grade to pass value Then the field "Grade to pass" matches value "90" And I set the field "Grade to pass" to "80" And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test Forum 1" And I follow "Edit settings" And the field "Grade to pass" matches value "80" @@ -233,7 +233,7 @@ Feature: We can set the grade to pass value Then the field "Grade to pass" matches value "90" And I set the field "Grade to pass" to "80" And I press "Save changes" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test Glossary 1" And I follow "Edit settings" And the field "Grade to pass" matches value "80" diff --git a/grade/tests/behat/grade_view.feature b/grade/tests/behat/grade_view.feature index f2c85c47bcd..1700dc0efed 100644 --- a/grade/tests/behat/grade_view.feature +++ b/grade/tests/behat/grade_view.feature @@ -22,8 +22,7 @@ Feature: We can enter in grades and view reports from the gradebook | grade_aggregations_visible | Mean of grades,Weighted mean of grades,Simple weighted mean of grades,Mean of grades (with extra credits),Median of grades,Lowest grade,Highest grade,Mode of grades,Natural | And I log out And I log in as "teacher1" - And I follow "Course 1" - And I turn editing mode on + And I am on "Course 1" course homepage with editing mode on And I add a "Assignment" to section "1" and I fill the form with: | Assignment name | Test assignment name 1 | | Description | Submit your online text | @@ -34,14 +33,14 @@ Feature: We can enter in grades and view reports from the gradebook | assignsubmission_onlinetext_enabled | 1 | And I log out And I log in as "student1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment name 1" When I press "Add submission" And I set the following fields to these values: | Online text | This is a submission for assignment 1 | And I press "Save changes" Then I should see "Submitted for grading" - And I follow "Course 1" + And I am on "Course 1" course homepage And I follow "Test assignment name 2" When I press "Add submission" And I set the following fields to these values: @@ -50,7 +49,7 @@ Feature: We can enter in grades and view reports from the gradebook Then I should see "Submitted for grading" And I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I give the grade "80.00" to the user "Student 1" for the grade item "Test assignment name 1" diff --git a/group/tests/behat/auto_creation.feature b/group/tests/behat/auto_creation.feature index e41b32fc9d8..8610678a60a 100644 --- a/group/tests/behat/auto_creation.feature +++ b/group/tests/behat/auto_creation.feature @@ -37,7 +37,7 @@ Feature: Automatic creation of groups | student10 | C1 | student | 0 | | suspendedstudent11 | C1 | student | 1 | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Users > Groups" in current page administration When I press "Auto-create groups" And I expand all fieldsets @@ -163,7 +163,7 @@ Feature: Automatic creation of groups | moodle/course:viewsuspendedusers | Prevent | And I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Users > Groups" in current page administration When I press "Auto-create groups" Then I should not see "Include only active enrolments" diff --git a/group/tests/behat/create_groups.feature b/group/tests/behat/create_groups.feature index b2cdd223dff..4642d4d5f36 100644 --- a/group/tests/behat/create_groups.feature +++ b/group/tests/behat/create_groups.feature @@ -24,7 +24,7 @@ Feature: Organize students into groups | student2 | C1 | student | | student3 | C1 | student | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Users > Groups" in current page administration And I press "Create group" And I set the following fields to these values: @@ -71,7 +71,7 @@ Feature: Organize students into groups | moodle/course:changeidnumber | Prevent | And I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Users > Groups" in current page administration When I press "Create group" Then the "idnumber" "field" should be readonly @@ -93,8 +93,7 @@ Feature: Organize students into groups | Course 1 | C1 | 0 | 1 | | Course 2 | C2 | 0 | 1 | And I log in as "admin" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Users > Groups" in current page administration When I press "Create group" And I set the following fields to these values: @@ -116,8 +115,7 @@ Feature: Organize students into groups | Enrolment key | Abcdef-2 | And I press "Save changes" And the "groups" select box should contain "Group B (0)" - And I am on site homepage - And I follow "Course 2" + And I am on "Course 2" course homepage And I navigate to "Users > Groups" in current page administration And I press "Create group" And I set the following fields to these values: diff --git a/group/tests/behat/delete_groups.feature b/group/tests/behat/delete_groups.feature index b77d0007c0b..9c89d407d5e 100644 --- a/group/tests/behat/delete_groups.feature +++ b/group/tests/behat/delete_groups.feature @@ -15,7 +15,7 @@ Feature: Automatic deletion of groups and groupings | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Users > Groups" in current page administration And I press "Create group" And I set the following fields to these values: @@ -64,7 +64,7 @@ Feature: Automatic deletion of groups and groupings | moodle/course:changeidnumber | Prevent | And I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Users > Groups" in current page administration When I set the field "groups" to "Group (with ID) (0)" Then the "Delete selected group" "button" should be disabled diff --git a/group/tests/behat/groups_import.feature b/group/tests/behat/groups_import.feature index eaea7d4c439..0bec1e371be 100644 --- a/group/tests/behat/groups_import.feature +++ b/group/tests/behat/groups_import.feature @@ -18,7 +18,7 @@ Feature: Importing of groups and groupings @javascript Scenario: Import groups and groupings as teacher Given I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Users > Groups" in current page administration And I press "Import groups" When I upload "group/tests/fixtures/groups_import.csv" file to "Import" filemanager @@ -42,7 +42,7 @@ Feature: Importing of groups and groupings @javascript Scenario: Import groups with idnumber when the user has proper permissions for the idnumber field Given I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Users > Groups" in current page administration And I press "Import groups" When I upload "group/tests/fixtures/groups_import.csv" file to "Import" filemanager @@ -78,14 +78,13 @@ Feature: Importing of groups and groupings @javascript Scenario: Import groups with idnumber when the user does not have proper permissions for the idnumber field Given I log in as "admin" - And I am on site homepage - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Users > Permissions" in current page administration And I override the system permissions of "Teacher" role with: | moodle/course:changeidnumber | Prevent | And I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Users > Groups" in current page administration And I press "Import groups" When I upload "group/tests/fixtures/groups_import.csv" file to "Import" filemanager diff --git a/group/tests/behat/id_uniqueness.feature b/group/tests/behat/id_uniqueness.feature index 14c18c46190..779b1da8eaf 100644 --- a/group/tests/behat/id_uniqueness.feature +++ b/group/tests/behat/id_uniqueness.feature @@ -15,7 +15,7 @@ Feature: Uniqueness of Group ID number | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Users > Groups" in current page administration Scenario: Group ID number uniqueness diff --git a/group/tests/behat/overview.feature b/group/tests/behat/overview.feature index 422c0738174..c2ef16edf02 100644 --- a/group/tests/behat/overview.feature +++ b/group/tests/behat/overview.feature @@ -57,7 +57,7 @@ Feature: Group overview Scenario: Filter the overview in various different ways Given I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Groups" node in "Course administration > Users" And I follow "Overview" diff --git a/group/tests/behat/update_groups.feature b/group/tests/behat/update_groups.feature index 6da490d0c7f..9d6c53066b2 100644 --- a/group/tests/behat/update_groups.feature +++ b/group/tests/behat/update_groups.feature @@ -15,7 +15,7 @@ Feature: Automatic updating of groups and groupings | user | course | role | | teacher1 | C1 | editingteacher | And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Users > Groups" in current page administration And I press "Create group" And I set the following fields to these values: @@ -71,7 +71,7 @@ Feature: Automatic updating of groups and groupings | moodle/course:changeidnumber | Prevent | And I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Users > Groups" in current page administration And I set the field "groups" to "Group (with ID)" When I press "Edit group settings" @@ -108,7 +108,7 @@ Feature: Automatic updating of groups and groupings | teacher1 | C2 | editingteacher | And I log out And I log in as "teacher1" - And I follow "Course 1" + And I am on "Course 1" course homepage And I navigate to "Users > Groups" in current page administration And I set the field "groups" to "Group (with ID)" And I press "Edit group settings" @@ -133,8 +133,7 @@ Feature: Automatic updating of groups and groupings | Enrolment key | Abcdef-2 | And I press "Save changes" And I should not see "This enrolment key is already used for another group." - And I am on homepage - And I follow "Course 2" + And I am on "Course 2" course homepage And I navigate to "Users > Groups" in current page administration And I press "Create group" And I set the following fields to these values: diff --git a/lang/en/calendar.php b/lang/en/calendar.php index a24c0f32cb6..d36accc45ea 100644 --- a/lang/en/calendar.php +++ b/lang/en/calendar.php @@ -26,6 +26,7 @@ $string['advancedoptions'] = 'Advanced options'; $string['allday'] = 'All day'; $string['addevent'] = 'Add events'; $string['annually'] = 'Annually'; +$string['activityevent'] = 'Activity event'; $string['calendar'] = 'Calendar'; $string['calendarheading'] = '{$a} Calendar'; $string['calendarpreferences'] = 'Calendar preferences'; diff --git a/lang/en/completion.php b/lang/en/completion.php index c19e306bb8b..41bc6c2656e 100644 --- a/lang/en/completion.php +++ b/lang/en/completion.php @@ -67,7 +67,8 @@ $string['completiondefault'] = 'Default completion tracking'; $string['completiondisabled'] = 'Disabled, not shown in activity settings'; $string['completionenabled'] = 'Enabled, control via completion and activity settings'; $string['completionexpected'] = 'Expect completed on'; -$string['completionexpected_help'] = 'This setting specifies the date when the activity is expected to be completed. The date is not shown to students and is only displayed in the activity completion report.'; +$string['completionexpected_help'] = 'This setting specifies the date when the activity is expected to be completed.'; +$string['completionexpectedfor'] = 'Expected completion for \'{$a->modulename}\' activity \'{$a->instancename}\''; $string['completionicons'] = 'Completion tick boxes'; $string['completionicons_help'] = 'A tick next to an activity name may be used to indicate when the activity is complete. @@ -114,7 +115,7 @@ $string['dependencies'] = 'Dependencies'; $string['dependenciescompleted'] = 'Completion of other courses'; $string['editcoursecompletionsettings'] = 'Edit course completion settings'; $string['enablecompletion'] = 'Enable completion tracking'; -$string['enablecompletion_help'] = 'If enabled, activity completion conditions may be set in the activity settings and/or course completion conditions may be set.'; +$string['enablecompletion_help'] = 'If enabled, activity completion conditions may be set in the activity settings and/or course completion conditions may be set. It is recommended to have this enabled in order for the course progress dashboard to display meaningful data.'; $string['enrolmentduration'] = 'Enrolment duration'; $string['enrolmentdurationlength'] = 'User must remain enrolled for'; $string['err_noactivities'] = 'Completion information is not enabled for any activity, so none can be displayed. You can enable completion information by editing the settings for an activity.'; diff --git a/lib/amd/build/templates.min.js b/lib/amd/build/templates.min.js index 87d6ada6701..c465c9abf72 100644 --- a/lib/amd/build/templates.min.js +++ b/lib/amd/build/templates.min.js @@ -1 +1 @@ -define(["core/mustache","jquery","core/ajax","core/str","core/notification","core/url","core/config","core/localstorage","core/icon_system","core/event","core/yui","core/log","core/truncate","core/user_date"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n){var o=0,p={},q={},r={},s=function(){this.requiredStrings=[],this.requiredJS=[],this.requiredDates=[],this.currentThemeName=""};s.prototype.requiredStrings=null,s.prototype.requiredDates=[],s.prototype.requiredJS=null,s.prototype.currentThemeName="",s.prototype.getTemplate=function(a){var d=a.split("/"),e=d.shift(),f=d.shift(),g=this.currentThemeName+"/"+a;if(g in q)return q[g];var i=h.get("core_template/"+g);if(i)return p[g]=i,q[g]=b.Deferred().resolve(i).promise(),q[g];var j=c.call([{methodname:"core_output_load_template",args:{component:e,template:f,themename:this.currentThemeName}}],!0,!1);return q[g]=j[0].then(function(a){return p[g]=a,h.set("core_template/"+g,a),a}),q[g]},s.prototype.partialHelper=function(a){var b=this.currentThemeName+"/"+a;return b in p||e.exception(new Error("Failed to pre-fetch the template: "+a)),p[b]},s.prototype.renderIcon=function(a,c,d){var e=g.iconsystemmodule,f=b.Deferred();return require([e],function(a){var b=new a;b instanceof i?(r=b,b.init().then(f.resolve)):f.reject("Invalid icon system specified"+g.iconsystemmodule)}),f.then(function(a){return this.getTemplate(a.getTemplateName())}.bind(this)).then(function(b){return r.renderIcon(a,c,d,b)})},s.prototype.pixHelper=function(a,b,c){var d=b.split(","),e="",f="",g="";d.length>0&&(e=c(d.shift().trim())),d.length>0&&(f=c(d.shift().trim())),d.length>0&&(g=c(d.join(",").trim()));var h=r.getTemplateName(),i=this.currentThemeName+"/"+h,j=p[i];return r.renderIcon(e,f,g,j)},s.prototype.jsHelper=function(a,b,c){return this.requiredJS.push(c(b,a)),""},s.prototype.stringHelper=function(a,b,c){var d=b.split(","),e="",f="",g="";d.length>0&&(e=d.shift().trim()),d.length>0&&(f=d.shift().trim()),d.length>0&&(g=d.join(",").trim()),""!==g&&(g=c(g,a)),0===g.indexOf("{")&&0!==g.indexOf("{{")&&(g=JSON.parse(g));var h=this.requiredStrings.length;return this.requiredStrings.push({key:e,component:f,param:g}),"[[_s"+h+"]]"},s.prototype.quoteHelper=function(a,b,c){var d=c(b.trim(),a);return d=d.replace('"','\\"').replace(/([\{\}]{2,3})/g,"{{=<% %>=}}$1<%={{ }}=%>"),'"'+d+'"'},s.prototype.shortenTextHelper=function(a,b,c){var d=/(.*?),(.*)/,e=b.match(d),f=e[1].trim(),g=e[2].trim(),h=c(g,a);return m.truncate(h,{length:f,words:!0,ellipsis:"..."})},s.prototype.userDateHelper=function(a,b,c){var d=/(.*?),(.*)/,e=b.match(d),f=c(e[1].trim(),a),g=c(e[2].trim(),a),h=this.requiredDates.length;return this.requiredDates.push({timestamp:f,format:g}),"[[_t_"+h+"]]"},s.prototype.addHelpers=function(a,b){this.currentThemeName=b,this.requiredStrings=[],this.requiredJS=[],a.uniqid=o++,a.str=function(){return this.stringHelper.bind(this,a)}.bind(this),a.pix=function(){return this.pixHelper.bind(this,a)}.bind(this),a.js=function(){return this.jsHelper.bind(this,a)}.bind(this),a.quote=function(){return this.quoteHelper.bind(this,a)}.bind(this),a.shortentext=function(){return this.shortenTextHelper.bind(this,a)}.bind(this),a.userdate=function(){return this.userDateHelper.bind(this,a)}.bind(this),a.globals={config:g},a.currentTheme=b},s.prototype.getJS=function(){var a="";return this.requiredJS.length>0&&(a=this.requiredJS.join(";\n")),a},s.prototype.treatStringsInContent=function(a,b){var c,d,e,f,g,h,i=/\[\[_s\d+\]\]/;do{for(c="",d=a.search(i);d>-1;){c+=a.substring(0,d),a=a.substr(d),e="",f=4,g=a.substr(f,1);do e+=g,f++,g=a.substr(f,1);while("]"!=g);h=b[parseInt(e,10)],"undefined"==typeof h&&(l.debug("Could not find string for pattern [[_s"+e+"]]."),h=""),c+=h,a=a.substr(6+e.length),d=a.search(i)}a=c+a,d=a.search(i)}while(d>-1);return a},s.prototype.treatDatesInContent=function(a,b){return b.forEach(function(b,c){var d="\\[\\[_t_"+c+"\\]\\]",e=new RegExp(d,"g");a=a.replace(e,b)}),a},s.prototype.doRender=function(c,e,f){this.currentThemeName=f;var g=r.getTemplateName();return this.getTemplate(g).then(function(){this.addHelpers(e,f);var d=a.render(c,e,this.partialHelper.bind(this));return b.Deferred().resolve(d.trim(),this.getJS()).promise()}.bind(this)).then(function(a,c){return this.requiredStrings.length>0?d.get_strings(this.requiredStrings).then(function(d){return this.requiredDates=this.requiredDates.map(function(a){return{timestamp:this.treatStringsInContent(a.timestamp,d),format:this.treatStringsInContent(a.format,d)}}.bind(this)),a=this.treatStringsInContent(a,d),c=this.treatStringsInContent(c,d),b.Deferred().resolve(a,c).promise()}.bind(this)):b.Deferred().resolve(a,c).promise()}.bind(this)).then(function(a,c){return this.requiredDates.length>0?n.get(this.requiredDates).then(function(d){return a=this.treatDatesInContent(a,d),c=this.treatDatesInContent(c,d),b.Deferred().resolve(a,c).promise()}.bind(this)):b.Deferred().resolve(a,c).promise()}.bind(this))};var t=function(a){if(""!==a.trim()){var c=b("