diff --git a/admin/roles/tests/behat/override_roles_highlighting.feature b/admin/roles/tests/behat/override_roles_highlighting.feature index f7e358ffdc738..8fa6f0fd191e0 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 0f1d394f5dc67..5f83df78721a6 100644 --- a/admin/settings/courses.php +++ b/admin/settings/courses.php @@ -133,7 +133,7 @@ // 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 c0107b3e875a4..97b5b20b8e2cf 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 5560c833be80c..d8a325b2714ed 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 0b4e7e557445b..1e15acc262fc5 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 c0e7cc045f6e6..5de8e0a0c866b 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 4b467bc1529cf..151a59deb43b7 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 5f5efdf6f018f..85f52de84de5b 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 0a9f39f712e8c..5c464bacaa943 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 0d25c54be30f5..a38a75d309070 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 9acc86d30c4b5..2766705faaa84 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 31065d0ae9588..7df1cb2b8662b 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 6b4ea5bf2e564..26a009605141a 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 7ee6235889f17..e217cf1376581 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 33c7db7cc77d4..6a5f152d9f688 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 a0104d454bec5..46f226a9c06a8 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 9ac9f370c7aab..a57094fb26ade 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 b2d39004c5f7b..f0b80941f86d4 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 161080fe72532..b4ba14756583b 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 60f7920899656..a9845257a9cd7 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 31d65d21026e9..ceae232b52099 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 3c1ccb1ad38de..91f4d045d579d 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 a5b01b7cfa9aa..682f59f761fd2 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 0daeee26e5b40..60e7150b1e2c5 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 7193088a9b4b2..ce0392e715ade 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 4c252d096a734..1207eb77b15cb 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 111c339b60c0f..5adde7f3d8e1e 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 d5745421672ce..32ff5d1904e3c 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 32c1cd7e7f1a7..b02e25bd314a0 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 a1d2fb4a63ed0..13c040a17b975 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 462ef805ccb84..2f24252aa8d42 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 de4a14926fdff..3191e13411d40 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 f53d8eae84e89..0570dbddfeab9 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 d3f5c991ef8e3..5103f23804cec 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 ca45c84449a59..3af9d0e39fc48 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 620aac1cead3e..eb750a05aa002 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 d121c97b9da66..2a8bcf99fd03b 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 6b9c9727594fd..95008623b0237 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 0c1ccdd077d94..ccc223db26d20 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 0e64d0544fe42..11ee5a579a47b 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 e2c8729b9850a..f632d95e1db94 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 5068a7b364251..8885d70202df0 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 30602ff07468d..a896714616929 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 5d73692b9da1f..264eb8fa3f0b1 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 e137f929104d2..465250079981d 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 e9da78ac5ae0d..6586d5fa62d37 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 3f16fa421e0b6..33fe576425c70 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 3f87995725ef0..2928bf3508f5d 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 c6b0425861e32..d1daf80c63bc2 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 b93728f81be6b..419406b29fc35 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 6b8001f6c09be..9a342d231c4a8 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 76ceb37c78c3c..39821d2927679 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 6c3f73b773567..ccb4c64efb3b8 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 fae3f4a35dcb4..09112b7162c78 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 f06fad337e996..67ee460cfff74 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 60572e16a7f2b..8cf6b27804a3d 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 ace839e7b6698..cc3d6f69f6149 100644 --- a/blocks/calendar_month/block_calendar_month.php +++ b/blocks/calendar_month/block_calendar_month.php @@ -79,13 +79,16 @@ public function get_content() { 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 e4952e8829fa4..db6f058c71a23 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 1c59a6021f4ea..2360572154468 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 335bcc4e7e286..858a0e42baf45 100644 --- a/blocks/calendar_upcoming/block_calendar_upcoming.php +++ b/blocks/calendar_upcoming/block_calendar_upcoming.php @@ -90,7 +90,7 @@ public function get_content() { 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 @@ public function get_content() { 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 7aa437f546a1b..02f73f41047d7 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 d2a3180351f8d..4aa94bdded0d3 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 c3acbc43a62cc..3589da498e00b 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 73f78dd4670d3..f8939f42ae373 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 9fd829510aa6c..f5e24a149d831 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 0a31bb7bb36ad..a92c042bd1f7a 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 7d4c3f6c0f023..c02191487695e 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 81b02980442bc..d9b73c5eeb989 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 c4273008f28b4..3c03721f725f5 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 0441dfbb82555..8a14d9ef7bf3a 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 33f50594cf5e5..f31ae94807217 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 e13a38019bd65..b10cf27e1cc16 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 ae244ef108206..0000000000000 --- 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 d92f3dfdb0521..0000000000000 --- 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 06e6896ce3da7..0000000000000 --- 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 e900df806fe38..0000000000000 --- 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 b6f042dd7a196..0000000000000 --- 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 78c8b0ef33a32..0000000000000 --- 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 d1c427581355c..0000000000000 --- 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 2aab2a35d0667..0000000000000 --- 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 389b6c5353bd9..0000000000000 --- 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 c91c551fcf18f..0000000000000 --- 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 defb9f7f52772..3b4b32cdd0778 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 974a1ffafe101..423c1952faeb3 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 2eea426a972c4..3329702989366 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 0815badd43d2c..08e233e6416b1 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 43446c2e82a30..63dd4a511a310 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 0000000000000..5c6e35a14191b --- /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 0000000000000..7ebbaf32a39d4 --- /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 0000000000000..ee665491bc58a --- /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 0000000000000..db182eeb3adff --- /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 0000000000000..e153e2d57628a --- /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 0000000000000..1e33daef02d15 --- /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 0000000000000..f22ce15decb6b --- /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 0000000000000..5155930b45575 --- /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 0000000000000..86b0e49e239a6 --- /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 0000000000000..bdc92d8968e2b --- /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 0000000000000..606dd3bf16de2 --- /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 95abb630e64b6..d05b432ded9ee 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 @@ $capabilities = array( - 'block/course_overview:myaddinstance' => array( + 'block/myoverview:myaddinstance' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_SYSTEM, 'archetypes' => array( @@ -36,7 +36,7 @@ '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 0000000000000..02b0832b354e3 --- /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 0000000000000..3b5af3f0d007f --- /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 0000000000000..674b6192e7692 --- /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 0000000000000..af7d5ee350f1b --- /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 0000000000000..10a1c435dc0df --- /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 0000000000000..77cf57cbf9f46 --- /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 0000000000000..7d2250ba7e911 --- /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 0000000000000..c20a052f33f17 --- /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 0000000000000..21a55611de8c5 --- /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 0000000000000..dcde9a38389c8 --- /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 0000000000000..3360d55db46a7 --- /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 0000000000000..aa77887593cab --- /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 0000000000000..baeb696a067f1 --- /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 0000000000000..340fdcbb4a6ec --- /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 0000000000000..139152f23b6ea --- /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 0000000000000..2dc770bfc5e5f --- /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 0000000000000..83f62347ac4c5 --- /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 0000000000000..54fc37a634f10 --- /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 0000000000000..955ff02d991b3 --- /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 0000000000000..71ffecffcd49d --- /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 0000000000000..82e73e19bdeb3 --- /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 0000000000000..83a9cdd799bb9 --- /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 0000000000000..8c592e7c47229 --- /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 0000000000000..39b41e1a6fc4c --- /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 0000000000000..66cb8ea7b27b5 --- /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 0000000000000..cba411eb20a05 --- /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 0000000000000..7aec53c4f6a39 --- /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 0000000000000..90508bbc37337 --- /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 dec7d909c6707..682aeed95dc43 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 fb7cfd2d08465..bc0f704946e45 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 81d22767a6c0d..c0aa2db0ccfe0 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 56c85bf877df1..683c34ad41aeb 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 2093c763d26ae..dfc75eeadc858 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 f2022354fc368..22591be631e61 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 b50286e7734ac..aeb75be704cdf 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 d857871988233..fb8437712f1fb 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 b56c15b522b27..35a4e9629cc8f 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 973b10c1dfb31..62dc4904017c1 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 dedc8bcd1b4cb..e2e37d27dc66b 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 f43f6fd9287af..140a11d050968 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 a72fa87239071..ad6763592dd6b 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 9c7ee6b56335c..6a8bd4148958f 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 5d17e01ade67a..4fadd478b7904 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 a9d31d7af1b8d..40ae8db05ec23 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 6abadca1865e5..e41f9f2691767 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 c83bd02c4f3bd..db11e3f529da2 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 8cca1cf1b9a08..19b0fc6bcc885 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 8771a9fc78705..6a312cec5c242 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 6a5c2a11ff21e..581d0f84f775c 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 98226cdffaa5a..79c4b97a90413 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 e0de186a94740..6add035366f1b 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 e135681073142..f4bae300fc01c 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 0000000000000..118e3a2263205 --- /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 0000000000000..2002cc3a817c4 --- /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 0000000000000..ade78fa0423c3 --- /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 0000000000000..2895a409b85e4 --- /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 0000000000000..9198c4c9c9824 --- /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 0000000000000..349ff6094ac15 --- /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 0000000000000..b19fd60cc0aff --- /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 0000000000000..39f24df081564 --- /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 0000000000000..18682ff1170d5 --- /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 0000000000000..bb88a5ca6dc99 --- /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 0000000000000..8517e268f5d66 --- /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 0000000000000..705b8327de0a7 --- /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 0000000000000..1c76999e97a7d --- /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 0000000000000..41be943d656e4 --- /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 0000000000000..70c620227ee12 --- /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 0000000000000..cd5a1dff68bf1 --- /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 0000000000000..d1bccbcd45307 --- /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 0000000000000..a277901c3db42 --- /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 0000000000000..4289cb3ddcd41 --- /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 0000000000000..257a4c226a07e --- /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 0000000000000..93b831f357f77 --- /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 0000000000000..9f2b92c7e5861 --- /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 0000000000000..4efdb38d43779 --- /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 b4bb175bfa504..8acca90a0513c 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 0000000000000..ee034bbb86beb --- /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 0000000000000..89975d563c5dd --- /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 0000000000000..c3520f42b9b60 --- /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 0000000000000..9434f2a6c9c1c --- /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 0000000000000..74afeeaf2ee0e --- /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 0000000000000..ae50e7d361587 --- /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 0000000000000..6cf61bfad13bd --- /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 0000000000000..1ad48fbf1741d --- /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 0000000000000..6869fd85966c5 --- /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 0000000000000..e94746d65a290 --- /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 0000000000000..7d1836149e064 --- /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 0000000000000..cc670d3b05c58 --- /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 0000000000000..f7aa538b317dc --- /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 0000000000000..483eed3aa0c53 --- /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 0000000000000..3d4db691a3e97 --- /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 0000000000000..511b59c9f4989 --- /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 0000000000000..22cc8217e90ff --- /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 f11d89aaf70e9..78cee59681d5d 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 @@ public function parse_rrule() { /** * 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 @@ public function create_events($passedevent) { // 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 @@ protected function create_recurring_events($event, $eventtimes) { $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 da5f078e20277..0d0d75a28e6ba 100644 --- a/calendar/export_execute.php +++ b/calendar/export_execute.php @@ -60,7 +60,8 @@ 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 @@ $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 @@ $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 @@ $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 @@ $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 @@ 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 2383693de140b..418731e6f57ae 100644 --- a/calendar/externallib.php +++ b/calendar/externallib.php @@ -29,6 +29,11 @@ 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 @@ public static function get_calendar_events($events = array(), $options = array() } // 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 @@ public static function get_calendar_events_returns() { ); } + /** + * 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 199cd65aa4889..01abe8d38b7fb 100644 --- a/calendar/lib.php +++ b/calendar/lib.php @@ -126,722 +126,1062 @@ 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 + * Manage calendar events. * - * Used to be define('CALENDAR_STARTING_WEEKDAY', blah); + * 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. * - * @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 + * @package core_calendar + * @category calendar + * @copyright 2009 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * - * @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 + * @property int $id The id within the event table + * @property string $name The name of the event + * @property string $description The description of the event + * @property int $format The format of the description FORMAT_? + * @property int $courseid The course the event is associated with (0 if none) + * @property int $groupid The group the event is associated with (0 if none) + * @property int $userid The user the event is associated with (0 if none) + * @property int $repeatid If this is a repeated event this will be set to the + * id of the original + * @property string $modulename If added by a module this will be the module name + * @property int $instance If added by a module this will be the module instance + * @property string $eventtype The event type + * @property int $timestart The start time as a timestamp + * @property int $timeduration The duration of the event in seconds + * @property int $visible 1 if the event is visible + * @property int $uuid ? + * @property int $sequence ? + * @property int $timemodified The time last modified as a timestamp */ -function calendar_get_mini($courses, $groups, $users, $calmonth = false, $calyear = false, $placement = false, - $courseid = false, $time = 0) { - global $CFG, $OUTPUT, $PAGE; +class calendar_event { - // Get the calendar type we are using. - $calendartype = \core_calendar\type_factory::get_calendar_instance(); + /** @var array An object containing the event properties can be accessed via the magic __get/set methods */ + protected $properties = null; - $display = new stdClass; + /** @var string The converted event discription with file paths resolved. + * This gets populated when someone requests description for the first time */ + protected $_description = null; - // Assume we are not displaying this month for now. - $display->thismonth = false; + /** @var array The options to use with this description editor */ + protected $editoroptions = array( + 'subdirs' => false, + 'forcehttps' => false, + 'maxfiles' => -1, + 'maxbytes' => null, + 'trusttext' => false); - $content = ''; + /** @var object The context to use with the description editor */ + protected $editorcontext = null; - // 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; - } + /** + * Instantiates a new event and optionally populates its properties with the data provided. + * + * @param \stdClass $data Optional. An object containing the properties to for + * an event + */ + public function __construct($data = null) { + global $CFG, $USER; - list($d, $m, $y) = array($date['mday'], $date['mon'], $date['year']); // This is what we want to display. + // First convert to object if it is not already (should either be object or assoc array). + if (!is_object($data)) { + $data = (object) $data; + } - // Get Gregorian date for the start of the month. - $gregoriandate = $calendartype->convert_to_gregorian($date['year'], $date['mon'], 1); + $this->editoroptions['maxbytes'] = $CFG->maxbytes; - // 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']); + $data->eventrepeats = 0; - // 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(); + if (empty($data->id)) { + $data->id = null; + } - // Set the min and max weekday. - $display->minwday = calendar_get_starting_weekday(); - $display->maxwday = $display->minwday + ($numberofdaysinweek - 1); + if (!empty($data->subscriptionid)) { + $data->subscription = calendar_get_subscription($data->subscriptionid); + } - // 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; + // Default to a user event. + if (empty($data->eventtype)) { + $data->eventtype = 'user'; + } - // Align the starting weekday to fall in our display range - // This is simple, not foolproof. - if ($startwday < $display->minwday) { - $startwday += $numberofdaysinweek; - } + // Default to the current user. + if (empty($data->userid)) { + $data->userid = $USER->id; + } - // 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); + 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; + } - // 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]); - } - } + if (!empty($data->description) && is_array($data->description)) { + $data->format = $data->description['format']; + $data->description = $data->description['text']; + } else if (empty($data->description)) { + $data->description = ''; + $data->format = editors_get_preferred_format(); } - } - // 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); + // Ensure form is defaulted correctly. + if (empty($data->format)) { + $data->format = editors_get_preferred_format(); } - } - // 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); + $this->properties = $data; - // 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 .= ''; + if (empty($data->context)) { + $this->properties->context = $this->calculate_context(); + } } - $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"; + /** + * Magic set method. + * + * Attempts to call a set_$key method if one exists otherwise falls back + * to simply set the property. + * + * @param string $key property name + * @param mixed $value value of the property + */ + public function __set($key, $value) { + if (method_exists($this, 'set_'.$key)) { + $this->{'set_'.$key}($value); + } + $this->properties->{$key} = $value; } - $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"; + /** + * Magic get method. + * + * Attempts to call a get_$key method to return the property and ralls over + * 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 (!property_exists($this->properties, $key)) { + throw new \coding_exception('Undefined property requested'); + } + return $this->properties->{$key}; } - $weekend = CALENDAR_DEFAULT_WEEKEND; - if (isset($CFG->calendar_weekend)) { - $weekend = intval($CFG->calendar_weekend); + /** + * 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 + */ + public function __isset($key) { + return !empty($this->properties->{$key}); } - // 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; - } + /** + * 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 + * + * @return \stdClass The context object. + */ + protected function calculate_context() { + global $USER, $DB; - // Reset vars. - if ($weekend & (1 << ($dayweek % $numberofdaysinweek))) { - // Weekend. This is true no matter what the exact range is. - $class = 'weekend day'; + $context = null; + 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 { - // Normal working day. - $class = 'day'; + $context = \context_user::instance($this->properties->userid); } - $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'; - } + return $context; + } - 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; - } - } + /** + * Returns an array of editoroptions for this event. + * + * @return array event editor options + */ + protected function get_editoroptions() { + return $this->editoroptions; + } - $dayhref->set_anchor('event_'.$event->id); + /** + * Returns an event description: Called by __get + * Please use $blah = $event->description; + * + * @return string event description + */ + protected function get_description() { + global $CFG; - $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']; + require_once($CFG->libdir . '/filelib.php'); - 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); - } + if ($this->_description === null) { + // 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. + $this->editorcontext = $this->properties->context; + if ($this->properties->eventtype != 'user' && $this->properties->eventtype != 'course' + && $this->properties->eventtype != 'site' && $this->properties->eventtype != 'group') { + return clean_text($this->properties->description, $this->properties->format); } - $popupcontent .= html_writer::link($dayhref, $name); - $popupcontent .= html_writer::end_tag('div'); } - if ($display->thismonth && $day == $d) { - $popupdata = calendar_get_popup(true, $daytime, $popupcontent); + // 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 { - $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'; + $itemid = $this->properties->id; } - $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; + // 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); } - $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'; + // Finally return the description. + return $this->_description; + } + + /** + * 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--; } - if ($durationclass) { - $class .= ' duration '.$durationclass; + return $this->properties->eventrepeats; + } + + /** + * Update or create an event within the database + * + * Pass in a object containing the event properties and this function will + * insert it into the database and deal with any associated files + * + * @see self::create() + * @see self::update() + * + * @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) { + $this->properties->$key = $value; } - // If event has a class set then add it to the table day '; - } - $content .= ''; // Last row ends + $repeatedids = array(); - $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; + $this->properties->timemodified = time(); + $usingeditor = (!empty($this->properties->description) && is_array($this->properties->description)); + + // 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 + ) + ); + + if (empty($this->properties->id) || $this->properties->id < 1) { + + if ($checkcapability) { + if (!calendar_add_event_allowed($this->properties)) { + print_error('nopermissiontoupdatecalendar'); } - $event = $events[$eventid]; - if (!empty($event->class)) { - $class .= ' '.$event->class; + } + + if ($usingeditor) { + switch ($this->properties->eventtype) { + case 'user': + $this->properties->courseid = 0; + $this->properties->course = 0; + $this->properties->groupid = 0; + $this->properties->userid = $USER->id; + break; + case 'site': + $this->properties->courseid = SITEID; + $this->properties->course = SITEID; + $this->properties->groupid = 0; + $this->properties->userid = $USER->id; + break; + case 'course': + $this->properties->groupid = 0; + $this->properties->userid = $USER->id; + break; + case 'group': + $this->properties->userid = $USER->id; + break; + default: + // We should NEVER get here, but just incase we do lets fail gracefully. + $usingeditor = false; + break; } - break; + + // 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->editorcontext = $this->properties->context; + } + + $editor = $this->properties->description; + $this->properties->format = $this->properties->description['format']; + $this->properties->description = $this->properties->description['text']; } - } - 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')); + // Insert the event into the database. + $this->properties->id = $DB->insert_record('event', $this->properties); - 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); + 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)); } - $cell = get_accesshide($today . ' ') . $cell; - } - // Just display it - $cellattributes['class'] = $class; - $content .= html_writer::tag('td', $cell, $cellattributes); - } + // Log the event entry. + $eventargs['objectid'] = $this->properties->id; + $eventargs['context'] = $this->properties->context; + $event = \core\event\calendar_event_created::create($eventargs); + $event->trigger(); - // 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; -} + if (!empty($this->properties->repeat)) { + $this->properties->repeatid = $this->properties->id; + $DB->set_field('event', 'repeatid', $this->properties->repeatid, array('id' => $this->properties->id)); -/** - * 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; + $eventcopy = clone($this->properties); + unset($eventcopy->id); - $popupcaption = ''; - if ($today) { - $popupcaption = get_string('today', 'calendar') . ' '; - } + $timestart = new \DateTime('@' . $eventcopy->timestart); + $timestart->setTimezone(\core_date::get_user_timezone_object()); - if (false === $timestart) { - $popupcaption .= userdate(time(), get_string('strftimedayshort')); - $popupcontent = get_string('eventnone', 'calendar'); + for ($i = 1; $i < $eventcopy->repeats; $i++) { - } else { - $popupcaption .= get_string('eventsfor', 'calendar', userdate($timestart, get_string('strftimedayshort'))); - } + $timestart->add(new \DateInterval('P7D')); + $eventcopy->timestart = $timestart->getTimestamp(); - return array( - 'data-core_calendar-title' => $popupcaption, - 'data-core_calendar-popupcontent' => $popupcontent, - ); -} + // Get the event id for the log record. + $eventcopyid = $DB->insert_record('event', $eventcopy); -/** - * 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; + // 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); + foreach ($files as $file) { + $fs->create_file_from_storedfile(array('itemid' => $eventcopyid), $file); + } + } - $display = new stdClass; - $display->range = $daysinfuture; // How many days in the future we 'll look - $display->maxevents = $maxevents; + $repeatedids[] = $eventcopyid; - $output = array(); + // Trigger an event. + $eventargs['objectid'] = $eventcopyid; + $eventargs['other']['timestart'] = $eventcopy->timestart; + $event = \core\event\calendar_event_created::create($eventargs); + $event->trigger(); + } + } - // Prepare "course caching", since it may save us a lot of queries - $coursecache = array(); + return true; + } else { - $processed = 0; - $now = time(); // We 'll need this later - $usermidnighttoday = usergetmidnight($now); + if ($checkcapability) { + if (!calendar_edit_event_allowed($this->properties)) { + print_error('nopermissiontoupdatecalendar'); + } + } - if ($fromtime) { - $display->tstart = $fromtime; - } else { - $display->tstart = $usermidnighttoday; - } + 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']); + } else { + $this->properties->format = $this->properties->description['format']; + $this->properties->description = $this->properties->description['text']; + } + } - // 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; + $event = $DB->get_record('event', array('id' => $this->properties->id)); - // Get the events matching our criteria - $events = calendar_get_events($display->tstart, $display->tend, $users, $groups, $courses); + $updaterepeated = (!empty($this->properties->repeatid) && !empty($this->properties->repeateditall)); - // 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. + if ($updaterepeated) { + // Update all. + if ($this->properties->timestart != $event->timestart) { + $timestartoffset = $this->properties->timestart - $event->timestart; + $sql = "UPDATE {event} + SET name = ?, + description = ?, + timestart = timestart + ?, + timeduration = ?, + timemodified = ? + WHERE 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); + } + $DB->execute($sql, $params); - $hrefparams = array(); - if(!empty($courses)) { - $courses = array_diff($courses, array(SITEID)); - if(count($courses) == 1) { - $hrefparams['course'] = reset($courses); + // Trigger an update event for each of the calendar event. + $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 = 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(); + } + + return true; } } - if ($events !== false) { - - $modinfo = get_fast_modinfo($COURSE); + /** + * Deletes an event and if selected an repeated events in the same series + * + * This function deletes an event, any associated events if $deleterepeated=true, + * and cleans up any files associated with the events. + * + * @see self::delete() + * + * @param bool $deleterepeated delete event repeatedly + * @return bool succession of deleting event + */ + public function delete($deleterepeated = false) { + global $DB; - foreach($events as $event) { + // 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)); + // Trigger an event for the delete action. + $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_deleted::create($eventargs); + $event->add_record_snapshot('event', $calevent); + $event->trigger(); - 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 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); + 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. + $events = $DB->get_records('event', array('repeatid' => $newparent)); + // For each of the returned events trigger an update event. + foreach ($events as $calendarevent) { + // Trigger an event for the update. + $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(); } } + } - if ($processed >= $display->maxevents) { - break; + // 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 ($this->editorcontext !== null) { + $fs = get_file_storage(); + $files = $fs->get_area_files($this->editorcontext->id, 'calendar', 'event_description', $this->properties->id); + foreach ($files as $file) { + $file->delete(); } + } - $event->time = calendar_format_event_time($event, $now, $hrefparams); - $output[] = $event; - ++$processed; + // 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 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); + } } - } - return $output; -} + return true; + } -/** - * 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) { + /** + * 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 + * designed to work when the properties are going to be used to set the default + * values of a moodle forms form. + * + * @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 + */ + public function properties($prepareeditor = false) { + global $DB; - if (!$courseid) { - return ''; - } + // 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. + $properties = clone($this->properties); + // Clean the description here. + $properties->description = clean_text($properties->description, $properties->format); - 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); + // If set to true we need to prepare the properties for use with an editor + // and prepare the file area. + if ($prepareeditor) { - return $link; -} + // 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. + if (!empty($properties->id)) { + if ($properties->eventtype === 'site') { + // Site context. + $this->editorcontext = $this->properties->context; + } else if ($properties->eventtype === 'user') { + // 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)); + if (!$course) { + print_error('invalidcourse'); + } + // 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. + $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. + $this->editoroptions['maxbytes'] = 0; + $this->editoroptions['maxfiles'] = 0; + } -/** - * Add calendar event metadata - * - * @param stdClass $event event info - * @return stdClass $event metadata - */ -function calendar_add_event_metadata($event) { - global $CFG, $OUTPUT; + if (empty($this->editorcontext) || empty($this->editorcontext->id)) { + $contextid = false; + } else { + // Get the context id that is what we really want. + $contextid = $this->editorcontext->id; + } + } else { - //Support multilang in event->name - $event->name = format_string($event->name,true); + // If we get here then this is a new event in which case we don't need a + // context as there is no existing files to copy to the draft area. + $contextid = null; + } - 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 the contextid === false we don't support files so no preparing + // a draft area. + if ($contextid !== false) { + // 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); + } else { + $draftiddescription = 0; + } - if ($module === false) { - return; + // Structure the description field as the editor requires. + $properties->description = array('text' => $properties->description, 'format' => $properties->format, + 'itemid' => $draftiddescription); } - $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); + // Finally return the properties. + return $properties; + } - $event->referer = ''.$event->name.''; - $event->courselink = calendar_get_courselink($module->course); - $event->cmid = $module->id; + /** + * Toggles the visibility of an event + * + * @param null|bool $force If it is left null the events visibility is flipped, + * If it is false the event is made hidden, if it is true it + * is made visible. + * @return bool if event is successfully updated, toggle will be visible + */ + public function toggle_visibility($force = null) { + global $DB; - } 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 = ''; + // Set visible to the default if it is not already set. + if (empty($this->properties->visible)) { + $this->properties->visible = 1; } - $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; + if ($force === true || ($force !== false && $this->properties->visible == 0)) { + // Make this event visible. + $this->properties->visible = 1; + } else { + // Make this event hidden. + $this->properties->visible = 0; + } - $params = array(); - // Quick test. - if (empty($users) && empty($groups) && empty($courses)) { - return array(); - } + // 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); - // Array of filter conditions. To be concatenated by the OR operator. - $filters = []; + // 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(); - // 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)"; + return $success; } - // 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"; + /** + * 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. + * + * @param int|object $param event object or event id + * @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 = new calendar_event($event); + } + return $event; } - // 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)"; + /** + * Creates a new event and returns an event object + * + * @param \stdClass|array $properties An object containing event properties + * @param bool $checkcapability Check caps or not + * @throws \coding_exception + * + * @return calendar_event|bool The event object or false if it failed + */ + public static function create($properties, $checkcapability = true) { + if (is_array($properties)) { + $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'); + } + $event = new calendar_event($properties); + if ($event->update($properties, $checkcapability)) { + return $event; + } else { + return false; + } } - // 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(); - } + /** + * 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 + */ + public function format_external_text() { - // Build our clause for the filters. - $filterclause = implode(' OR ', $filters); + if ($this->editorcontext === null) { + // Switch on the event type to decide upon the appropriate context to use for this event. + $this->editorcontext = $this->properties->context; - // Array of where conditions for our query. To be concatenated by the AND operator. - $whereconditions = ["($filterclause)"]; + if ($this->properties->eventtype != 'user' && $this->properties->eventtype != 'course' + && $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); + } + } - // 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; + // 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; + } - // Show visible only. - if ($ignorehidden) { - $whereconditions[] = "(e.visible = 1)"; + return external_format_text($this->properties->description, $this->properties->format, $this->editorcontext->id, + 'calendar', 'event_description', $itemid); } +} - // Build the main query's WHERE clause. - $whereclause = implode(' AND ', $whereconditions); +/** + * Calendar information class + * + * This class is used simply to organise the information pertaining to a calendar + * and is used primarily to make information easily available. + * + * @package core_calendar + * @category calendar + * @copyright 2010 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class calendar_information { - // Build SQL subquery and conditions for filtered events based on priorities. - $subquerywhere = ''; - $subqueryconditions = []; + /** + * @var int The timestamp + * + * Rather than setting the day, month and year we will set a timestamp which will be able + * to be used by multiple calendars. + */ + public $time; - // Get the user's courses. Otherwise, get the default courses being shown by the calendar. - $usercourses = calendar_get_default_courses(); + /** @var int A course id */ + public $courseid = null; - // Set calendar filters. - list($usercourses, $usergroups, $user) = calendar_set_filters($usercourses, true); - $subqueryparams = []; + /** @var array An array of courses */ + public $courses = array(); - // Flag to indicate whether the query needs to exclude group overrides. - $viewgroupsonly = false; + /** @var array An array of groups */ + public $groups = array(); - 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; + /** @var array An array of users */ + public $users = array(); - 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; - } + /** + * Creates a new instance + * + * @param int $day the number of the day + * @param int $month the number of the month + * @param int $year the number of the year + * @param int $time the unixtimestamp representing the date we want to view, this is used instead of $calmonth + * and $calyear to support multiple calendars + */ + public function __construct($day = 0, $month = 0, $year = 0, $time = 0) { + // 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. This is done for BC. + if (!empty($day) || !empty($month) || !empty($year)) { + $date = usergetdate(time()); + if (empty($day)) { + $day = $date['mday']; + } + if (empty($month)) { + $month = $date['mon']; + } + if (empty($year)) { + $year = $date['year']; + } + if (checkdate($month, $day, $year)) { + $this->time = make_timestamp($year, $month, $day); + } else { + $this->time = time(); + } + } else if (!empty($time)) { + $this->time = $time; + } else { + $this->time = time(); + } + } + + /** + * Initialize calendar information + * + * @param stdClass $course object + * @param array $coursestoload An array of courses [$course->id => $course] + * @param bool $ignorefilters options to use filter + */ + public function prepare_for_view(stdClass $course, array $coursestoload, $ignorefilters = false) { + $this->courseid = $course->id; + $this->course = $course; + list($courses, $group, $user) = calendar_set_filters($coursestoload, $ignorefilters); + $this->courses = $courses; + $this->groups = $group; + $this->users = $user; + } + + /** + * Ensures the date for the calendar is correct and either sets it to now + * or throws a moodle_exception if not + * + * @param bool $defaultonow use current time + * @throws moodle_exception + * @return bool validation of checkdate + */ + public function checkdate($defaultonow = true) { + if (!checkdate($this->month, $this->day, $this->year)) { + if ($defaultonow) { + $now = usergetdate(time()); + $this->day = intval($now['mday']); + $this->month = intval($now['mon']); + $this->year = intval($now['year']); + return true; + } else { + throw new moodle_exception('invaliddate'); + } + } + return true; + } + + /** + * Gets todays timestamp for the calendar + * + * @return int today timestamp + */ + public function timestamp_today() { + return $this->time; + } + /** + * Gets tomorrows timestamp for the calendar + * + * @return int tomorrow timestamp + */ + public function timestamp_tomorrow() { + return strtotime('+1 day', $this->time); + } + /** + * Adds the pretend blocks for the calendar + * + * @param core_calendar_renderer $renderer + * @param bool $showfilters display filters, false is set as default + * @param string|null $view preference view options (eg: day, month, upcoming) + */ + public function add_sidecalendar_blocks(core_calendar_renderer $renderer, $showfilters=false, $view=null) { + if ($showfilters) { + $filters = new block_contents(); + $filters->content = $renderer->fake_block_filters($this->courseid, 0, 0, 0, $view, $this->courses); + $filters->footer = ''; + $filters->title = get_string('eventskey', 'calendar'); + $renderer->add_pretend_calendar_block($filters, BLOCK_POS_RIGHT); + } + $block = new block_contents; + $block->content = $renderer->fake_block_threemonths($this); + $block->footer = ''; + $block->title = get_string('monthlyview', 'calendar'); + $renderer->add_pretend_calendar_block($block, BLOCK_POS_RIGHT); + } +} + +/** + * 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; + } } } } @@ -864,6 +1204,7 @@ function calendar_get_events($tstart, $tend, $users, $groups, $courses, $withdur } else if (is_numeric($courses)) { $subquerycourses[] = $courses; } + // Merge with user courses, if necessary. if (!empty($usercourses)) { $subquerycourses = array_merge($subquerycourses, $usercourses); @@ -890,643 +1231,576 @@ function calendar_get_events($tstart, $tend, $users, $groups, $courses, $withdur // 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"; + 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"; + 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 +/** + * Return the days of the week. * - * @since Moodle 2.5 - * @param array $eventids list of event ids - * @return array Array of event entries, empty array if nothing found + * @return array array of days */ +function calendar_get_days() { + $calendartype = \core_calendar\type_factory::get_calendar_instance(); + return $calendartype->get_weekdays(); +} -function calendar_get_events_by_id($eventids) { +/** + * 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; - if (!is_array($eventids) || empty($eventids)) { - return array(); + $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); } - list($wheresql, $params) = $DB->get_in_or_equal($eventids); - $wheresql = "id $wheresql"; - return $DB->get_records_select('event', $wheresql, $params); + return $subscription; } /** - * Get control options for Calendar + * Gets the first day of the week. * - * @param string $type of calendar - * @param array $data calendar information - * @return string $content return available control for the calender in html + * Used to be define('CALENDAR_STARTING_WEEKDAY', blah); + * + * @return int */ -function calendar_top_controls($type, $data) { - global $PAGE, $OUTPUT; +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 = ''; - // 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(); + // 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 = make_timestamp($data['y'], $data['m'], $data['d']); + $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 if (!empty($data['time'])) { - $time = $data['time']; } else { + // Get the current date in the calendar type being used. $time = time(); + $date = $calendartype->timestamp_to_date_array($time); + $display->thismonth = true; } - // Get the date for the calendar type. - $date = $calendartype->timestamp_to_date_array($time); - - $urlbase = $PAGE->url; + list($d, $m, $y) = array($date['mday'], $date['mon'], $date['year']); // This is what we want to display. - // 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']); + // Get Gregorian date for the start of the month. + $gregoriandate = $calendartype->convert_to_gregorian($date['year'], $date['mon'], 1); - $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']); - } + // 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']); - 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); + // 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(); - if (!empty($data['id'])) { - $calendarlink->param('course', $data['id']); - } + // Set the min and max weekday. + $display->minwday = calendar_get_starting_weekday(); + $display->maxwday = $display->minwday + ($numberofdaysinweek - 1); - $prevlink = $prevlink; - $right = $nextlink; + // 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; - $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'); + // Align the starting weekday to fall in our display range. + // This is simple, not foolproof. + if ($startwday < $display->minwday) { + $startwday += $numberofdaysinweek; + } - 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); + // 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); - if (!empty($data['id'])) { - $calendarlink->param('course', $data['id']); + // 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]); + } } + } + } - $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); + // 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); + } + } - $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(); + // 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); - $prevtimestamp = strtotime('-1 day', $time); - $nexttimestamp = strtotime('+1 day', $time); + // 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. - $prevdate = $calendartype->timestamp_to_date_array($prevtimestamp); - $nextdate = $calendartype->timestamp_to_date_array($nexttimestamp); + // Print out the names of the weekdays. + for ($i = $display->minwday; $i <= $display->maxwday; $i++) { + $pos = $i % $numberofdaysinweek; + $content .= '\n"; + } - $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 .= ''; // End of day names; prepare for day numbers. - $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"; + // For the table display. $week is the row; $dayweek is the column. + $dayweek = $startwday; - break; + // Padding (the first week may have blank days in the beginning). + for ($i = $display->minwday; $i < $startwday; ++$i) { + $content .= '' ."\n"; } - 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; + $weekend = CALENDAR_DEFAULT_WEEKEND; + if (isset($CFG->calendar_weekend)) { + $weekend = intval($CFG->calendar_weekend); } - 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); + // 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; + } - 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); + // Reset vars. + if ($weekend & (1 << ($dayweek % $numberofdaysinweek))) { + // Weekend. This is true no matter what the exact range is. + $class = 'weekend day'; } else { - // This course CANNOT have group events, so lose the filter + // Normal working day. + $class = 'day'; } - $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'); - } + $eventids = array(); + if (!empty($eventsbyday[$day])) { + $eventids = $eventsbyday[$day]; + } - if($now === false) { - $now = time(); - } + if (!empty($durationbyday[$day])) { + $eventids = array_unique(array_merge($eventids, $durationbyday[$day])); + } - // To have it in one place, if a change is needed - $formal = userdate($tstamp, $shortformat); + $finishclass = false; - $datestamp = usergetdate($tstamp); - $datenow = usergetdate($now); + 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); - 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; - } -} + $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'; + } -/** - * 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); -} + 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; + } + } -/** - * 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); - } + $dayhref->set_anchor('event_' . $event->id); - // 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); + $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 { - $time = time(); + $cell = $day; } - } else if (empty($time)) { - $time = time(); - } - $linkbase->param('time', $time); + $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; + } - return $linkbase; -} + // If event has a class set then add it to the table day '; } - return link_arrow_right($text, (string)$href, $accesshide, 'next'); -} + $content .= ''; // Last row ends. -/** - * 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'); + $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; + } + } -/** - * 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; + 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); } - 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; + // 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; } /** - * Return the number of days in month + * Gets the calendar popup. * - * @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 + * It called at multiple points in from calendar_get_mini. + * Copied and modified from calendar_get_mini. * - * @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 + * @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_block_upcoming($events, $linkhref = NULL, $showcourselink = false) { - $content = ''; - $lines = count($events); - if (!$lines) { - return $content; +function calendar_get_popup($today = false, $timestart, $popupcontent = '') { + $popupcaption = ''; + if ($today) { + $popupcaption = get_string('today', 'calendar') . ' '; } - 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 .= '
    '; + 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 $content; + return array( + 'data-core_calendar-title' => $popupcaption, + 'data-core_calendar-popupcontent' => $popupcontent, + ); } /** - * Get the next following month + * Gets the calendar upcoming event. * - * @param int $month the number of the month. - * @param int $year the number of the year. - * @return array the following month + * @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_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); -} +function calendar_get_upcoming($courses, $groups, $users, $daysinfuture, $maxevents, $fromtime=0) { + global $COURSE; -/** - * 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); -} + $display = new \stdClass; + $display->range = $daysinfuture; // How many days in the future we 'll look. + $display->maxevents = $maxevents; -/** - * 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(); + $output = array(); - $eventsbyday = array(); - $typesbyday = array(); - $durationbyday = array(); + $processed = 0; + $now = time(); // We 'll need this later. + $usermidnighttoday = usergetmidnight($now); - if($events === false) { - return; + if ($fromtime) { + $display->tstart = $fromtime; + } else { + $display->tstart = $usermidnighttoday; } - 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; - } + // 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; - $eventdaystart = intval($startdate['mday']); + // Get the events matching our criteria. + $events = \core_calendar\local\api::get_legacy_events($display->tstart, $display->tend, $users, $groups, $courses); - if($startdate['mon'] == $month && $startdate['year'] == $year) { - // Give the event to its day - $eventsbyday[$eventdaystart][] = $event->id; + // 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); + } + } - // 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'; + 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; + } + } } - else if($event->userid) { - $typesbyday[$eventdaystart]['startuser'] = true; - // Set event class for user event - $events[$event->id]->class = 'calendar_event_user'; + + if ($processed >= $display->maxevents) { + break; } - } - if($event->timeduration == 0) { - // Proceed with the next - continue; + $event->time = calendar_format_event_time($event, $now, $hrefparams); + $output[] = $event; + $processed++; } + } - // The event starts on $month $year or before. So... - $lowerbound = $startdate['mon'] == $month && $startdate['year'] == $year ? intval($startdate['mday']) : 0; + return $output; +} - // Also, it ends on $month $year or later... - $upperbound = $enddate['mon'] == $month && $enddate['year'] == $year ? intval($enddate['mday']) : calendar_days_in_month($month, $year); +/** + * 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 ''; + } - // 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; - } - } + 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; + return $link; } /** - * Get current module cache + * Get current module cache. * - * @param array $coursecache list of course 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(&$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; +function calendar_get_module_cached(&$modulecache, $modulename, $instance) { + if (!isset($modulecache[$modulename . '_' . $instance])) { + $modulecache[$modulename . '_' . $instance] = get_coursemodule_from_instance($modulename, $instance); } - return $module; + + return $modulecache[$modulename . '_' . $instance]; } /** - * Get current course cache + * Get current course cache. * * @param array $coursecache list of course cache * @param int $courseid id of the course @@ -1554,1424 +1828,1057 @@ function calendar_get_group_cached($groupid) { } /** - * Returns the courses to load events for, the + * Add calendar event metadata * - * @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 + * @param stdClass $event event info + * @return stdClass $event metadata */ -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); +function calendar_add_event_metadata($event) { + global $CFG, $OUTPUT; - 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; - } + // Support multilang in event->name. + $event->name = format_string($event->name, true); - if ($ignorefilters || ($isloggedin && calendar_show_event_type(CALENDAR_EVENT_USER))) { - $user = $USER->id; - } + 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 (!empty($courseeventsfrom) && (calendar_show_event_type(CALENDAR_EVENT_GROUP) || $ignorefilters)) { + if ($module === false) { + return; + } - 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); - } + $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 = ''; } - if ($group === false) { - if (!empty($CFG->calendar_adminseesall) && has_any_capability($allgroupscaps, context_system::instance())) { - $group = true; - } else if ($isloggedin) { - $groupids = array(); + $icon = $OUTPUT->image_url('icon', $event->modulename) . ''; - // 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; - } - } + $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 = ''; } - } - if (empty($courses)) { - $courses = false; + $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 array($courses, $group, $user); + return $event; } /** - * Return the capability for editing calendar event + * Get calendar events by id. * - * @param calendar_event $event event object - * @return bool capability to edit event + * @since Moodle 2.5 + * @param array $eventids list of event ids + * @return array Array of event entries, empty array if nothing found */ -function calendar_edit_event_allowed($event) { - global $USER, $DB; +function calendar_get_events_by_id($eventids) { + global $DB; - // Must be logged in - if (!isloggedin()) { - return false; + if (!is_array($eventids) || empty($eventids)) { + return array(); } - // can not be using guest account - if (isguestuser()) { - return false; - } + list($wheresql, $params) = $DB->get_in_or_equal($eventids); + $wheresql = "id $wheresql"; - // 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; - } - } + return $DB->get_records_select('event', $wheresql, $params); +} - $sitecontext = context_system::instance(); - // if user has manageentries at site level, return true - if (has_capability('moodle/calendar:manageentries', $sitecontext)) { - return true; - } +/** + * 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; - // 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; -} + // Get the calendar type we are using. + $calendartype = \core_calendar\type_factory::get_calendar_instance(); -/** - * 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; + $content = ''; - if (!isloggedin()) { - return array(); + // Ensure course id passed if relevant. + $courseid = ''; + if (!empty($data['id'])) { + $courseid = '&course=' . $data['id']; } - $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); + // 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; } - return $courses; + 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(); } - $courses = enrol_get_my_courses(); + // Get the date for the calendar type. + $date = $calendartype->timestamp_to_date_array($time); - return $courses; -} + $urlbase = $PAGE->url; -/** - * 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; + // 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']); - // Guests have no preferences - if (!isloggedin() || isguestuser()) { - return ''; + $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']); } - 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")); -} + 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); -/** - * 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($data['id'])) { + $calendarlink->param('course', $data['id']); + } - if (empty($linkparams) || !is_array($linkparams)) { - $linkparams = array(); - } + $right = $nextlink; - $linkparams['view'] = 'day'; + $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'); - // 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; - } + 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); - // 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; + if (!empty($data['id'])) { + $calendarlink->param('course', $data['id']); } - } else { // It must spans two or more days. - $daystart = calendar_day_representation($event->timestart, $now, $usecommonwords) . ', '; - if ($showtime == $usermidnightstart) { - $daystart = ''; + + $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']); } - $timestart = calendar_time_representation($event->timestart); - $dayend = calendar_day_representation($event->timestart + $event->timeduration, $now, $usecommonwords) . ', '; - if ($showtime == $usermidnightend) { - $dayend = ''; + $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']); } - $timeend = calendar_time_representation($event->timestart + $event->timeduration); + $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); - // 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 . ' » '; + $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(); - $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; - } - } + $prevtimestamp = strtotime('-1 day', $time); + $nexttimestamp = strtotime('+1 day', $time); - // Check if It has expired. - if ($event->timestart + $event->timeduration < $now) { - $eventtime = '' . str_replace(' href=', ' class="dimmed" href=', $eventtime) . ''; + $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 $eventtime; + return $content; } /** - * Display month selector options + * Formats a filter control element. * - * @param string $name for the select element - * @param string|array $selected options for select elements + * @param moodle_url $url of the filter + * @param int $type constant defining the type filter + * @return string html content of the element */ -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'); +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'); } - echo html_writer::label(get_string('months'), 'menu'. $name, false, array('class' => 'accesshide')); - echo html_writer::select($months, $name, $selected, false); + $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; } /** - * Checks to see if the requested type of event should be shown for the given user. + * Get the controls filter for calendar. * - * @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 + * 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_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; +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); } - return $SESSION->calendarshoweventtype & $type; - } else { - return get_user_preferences('calendar_savedflt', $default, $user) & $type; + $seturl->param('var', 'showuser'); + $content .= calendar_filter_controls_element($seturl, CALENDAR_EVENT_USER); } + $content .= \html_writer::end_tag('ul'); + + return $content; } /** - * 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. + * Return the representation day. * - * @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; + * @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 ($display && !$current) { - $preference += $type; - } else if (!$display && $current) { - $preference -= $type; + + if ($now === false) { + $now = time(); } - if ($persist === 0) { - $SESSION->calendarshoweventtype = $preference; + + // 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 { - if ($preference == $default) { - unset_user_preference('calendar_savedflt', $user); - } else { - set_user_preference('calendar_savedflt', $preference, $user); - } + return $formal; } } /** - * Get calendar's allowed types + * return the formatted representation time. * - * @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); + * @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 (has_capability('moodle/calendar:manageentries', $coursecontext)) { - $allowed->courses = array($course->id => 1); + if ($langtimeformat === null) { + $langtimeformat = get_string('strftimetime'); + } - 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); - } - } - } - } + $timeformat = get_user_preferences('calendar_timeformat'); + if (empty($timeformat)) { + $timeformat = get_config(null, 'calendar_site_timeformat'); } + + return userdate($time, empty($timeformat) ? $langtimeformat : $timeformat); } /** - * See if user can add calendar entries at all - * used to print the "New Event" button + * Adds day, month, year arguments to a URL and returns a moodle_url object. * - * @param stdClass $course object of a course or course id - * @return bool has the capability to add at least one event type + * @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_user_can_add_event($course) { - if (!isloggedin() || isguestuser()) { - return false; +function calendar_get_link_href($linkbase, $d, $m, $y, $time = 0) { + if (empty($linkbase)) { + return null; } - calendar_get_allowed_types($allowed, $course); - return (bool)($allowed->user || $allowed->groups || $allowed->courses || $allowed->site); + + 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; } /** - * Check wether the current user is permitted to add events + * Build and return a previous month HTML link, with an arrow. * - * @param stdClass $event object of event - * @return bool has the capability to add event + * @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_add_event_allowed($event) { - global $USER, $DB; - - // can not be using guest account - if (!isloggedin() or isguestuser()) { - return false; - } +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); - $sitecontext = context_system::instance(); - // if user has manageentries at site level, always return true - if (has_capability('moodle/calendar:manageentries', $sitecontext)) { - return true; + if (empty($href)) { + return $text; } - 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 + return link_arrow_left($text, (string)$href, $accesshide, 'previous'); +} - case 'site': - return has_capability('moodle/calendar:manageentries', $event->context); +/** + * 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); - default: - return has_capability('moodle/calendar:manageentries', $event->context); + if (empty($href)) { + return $text; } + + return link_arrow_right($text, (string)$href, $accesshide, 'next'); } /** - * Manage calendar events + * Return the number of days in month. * - * 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 + * @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. * - * @package core_calendar - * @category calendar - * @copyright 2009 Sam Hemelryk - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @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. * - * @property int $id The id within the event table - * @property string $name The name of the event - * @property string $description The description of the event - * @property int $format The format of the description FORMAT_? - * @property int $courseid The course the event is associated with (0 if none) - * @property int $groupid The group the event is associated with (0 if none) - * @property int $userid The user the event is associated with (0 if none) - * @property int $repeatid If this is a repeated event this will be set to the - * id of the original - * @property string $modulename If added by a module this will be the module name - * @property int $instance If added by a module this will be the module instance - * @property string $eventtype The event type - * @property int $timestart The start time as a timestamp - * @property int $timeduration The duration of the event in seconds - * @property int $visible 1 if the event is visible - * @property int $uuid ? - * @property int $sequence ? - * @property int $timemodified The time last modified as a timestamp + * @param int $month the number of the month. + * @param int $year the number of the year. + * @return array previous month */ -class calendar_event { +function calendar_sub_month($month, $year) { + $calendartype = \core_calendar\type_factory::get_calendar_instance(); + return $calendartype->get_prev_month($year, $month); +} - /** @var array An object containing the event properties can be accessed via the magic __get/set methods */ - protected $properties = null; +/** + * 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(); - /** - * @var string The converted event discription with file paths resolved. This gets populated when someone requests description for the first time */ - protected $_description = null; + $eventsbyday = array(); + $typesbyday = array(); + $durationbyday = array(); - /** @var array The options to use with this description editor */ - protected $editoroptions = array( - 'subdirs'=>false, - 'forcehttps'=>false, - 'maxfiles'=>-1, - 'maxbytes'=>null, - 'trusttext'=>false); - - /** @var object The context to use with the description editor */ - protected $editorcontext = null; + if ($events === false) { + return; + } - /** - * Instantiates a new event and optionally populates its properties with the - * data provided - * - * @param stdClass $data Optional. An object containing the properties to for - * an event - */ - public function __construct($data=null) { - global $CFG, $USER; + 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; + } - // First convert to object if it is not already (should either be object or assoc array) - if (!is_object($data)) { - $data = (object)$data; + // 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; } - $this->editoroptions['maxbytes'] = $CFG->maxbytes; + $eventdaystart = intval($startdate['mday']); - $data->eventrepeats = 0; + if ($startdate['mon'] == $month && $startdate['year'] == $year) { + // Give the event to its day. + $eventsbyday[$eventdaystart][] = $event->id; - if (empty($data->id)) { - $data->id = null; + // 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 (!empty($data->subscriptionid)) { - $data->subscription = calendar_get_subscription($data->subscriptionid); + if ($event->timeduration == 0) { + // Proceed with the next. + continue; } - // Default to a user event - if (empty($data->eventtype)) { - $data->eventtype = 'user'; + // The event starts on $month $year or before. + if ($startdate['mon'] == $month && $startdate['year'] == $year) { + $lowerbound = intval($startdate['mday']); + } else { + $lowerbound = 0; } - // Default to the current user - if (empty($data->userid)) { - $data->userid = $USER->id; + // 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); } - 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; - } - if (!empty($data->description) && is_array($data->description)) { - $data->format = $data->description['format']; - $data->description = $data->description['text']; - } else if (empty($data->description)) { - $data->description = ''; - $data->format = editors_get_preferred_format(); - } - // Ensure form is defaulted correctly - if (empty($data->format)) { - $data->format = editors_get_preferred_format(); + // 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; + } } - if (empty($data->context)) { - $data->context = $this->calculate_context($data); - } - $this->properties = $data; } + return; +} - /** - * Magic property method - * - * Attempts to call a set_$key method if one exists otherwise falls back - * to simply set the property - * - * @param string $key property name - * @param mixed $value value of the property - */ - public function __set($key, $value) { - if (method_exists($this, 'set_'.$key)) { - $this->{'set_'.$key}($value); - } - $this->properties->{$key} = $value; - } +/** + * 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; - /** - * Magic get method - * - * Attempts to call a get_$key method to return the property and ralls over - * to return the raw property - * - * @param string $key property name - * @return mixed property value - */ - 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'); + // 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]); } - 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 ~! - * - * @param string $key $key property name - * @return bool|mixed property value, false if property is not exist - */ - public function __isset($key) { - return !empty($this->properties->{$key}); - } + $courses = array(); + $user = false; + $group = false; - /** - * Calculate the context value needed for calendar_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. - */ - protected function calculate_context(stdClass $data) { - global $USER, $DB; + // Get the capabilities that allow seeing group events from all groups. + $allgroupscaps = array('moodle/site:accessallgroups', 'moodle/calendar:manageentries'); - $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); - } else { - $context = context_user::instance($data->userid); - } + $isloggedin = isloggedin(); - return $context; + 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); - /** - * Returns an array of editoroptions for this event: Called by __get - * Please use $blah = $event->editoroptions; - * - * @return array event editor options - */ - protected function get_editoroptions() { - return $this->editoroptions; + 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; } - /** - * Returns an event description: Called by __get - * Please use $blah = $event->description; - * - * @return string event description - */ - protected function get_description() { - global $CFG; + if ($ignorefilters || ($isloggedin && calendar_show_event_type(CALENDAR_EVENT_USER))) { + $user = $USER->id; + } - require_once($CFG->libdir . '/filelib.php'); + if (!empty($courseeventsfrom) && (calendar_show_event_type(CALENDAR_EVENT_GROUP) || $ignorefilters)) { - if ($this->_description === null) { - // 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 - $this->editorcontext = $this->properties->context; - if ($this->properties->eventtype != 'user' && $this->properties->eventtype != 'course' - && $this->properties->eventtype != 'site' && $this->properties->eventtype != 'group') { - return clean_text($this->properties->description, $this->properties->format); - } + 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); } - - // 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; + } + 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; + } } - - // 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 - return $this->_description; + } + if (empty($courses)) { + $courses = false; } - /** - * 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--; - } - return $this->properties->eventrepeats; + 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; } - /** - * Update or create an event within the database - * - * Pass in a object containing the event properties and this function will - * insert it into the database and deal with any associated files - * - * @see self::create() - * @see self::update() - * - * @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; + // Can not be using guest account. + if (isguestuser()) { + return false; + } - foreach ($data as $key=>$value) { - $this->properties->$key = $value; + // 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; } + } - $this->properties->timemodified = time(); - $usingeditor = (!empty($this->properties->description) && is_array($this->properties->description)); - - // 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 - ) - ); + $sitecontext = \context_system::instance(); - if (empty($this->properties->id) || $this->properties->id < 1) { - - if ($checkcapability) { - if (!calendar_add_event_allowed($this->properties)) { - print_error('nopermissiontoupdatecalendar'); - } - } - - if ($usingeditor) { - switch ($this->properties->eventtype) { - case 'user': - $this->properties->courseid = 0; - $this->properties->course = 0; - $this->properties->groupid = 0; - $this->properties->userid = $USER->id; - break; - case 'site': - $this->properties->courseid = SITEID; - $this->properties->course = SITEID; - $this->properties->groupid = 0; - $this->properties->userid = $USER->id; - break; - case 'course': - $this->properties->groupid = 0; - $this->properties->userid = $USER->id; - break; - case 'group': - $this->properties->userid = $USER->id; - break; - default: - // Ewww we should NEVER get here, but just incase we do lets - // fail gracefully - $usingeditor = false; - break; - } - - // 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->editorcontext = $this->properties->context; - } - - $editor = $this->properties->description; - $this->properties->format = $this->properties->description['format']; - $this->properties->description = $this->properties->description['text']; - } - - // 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)); - } - - // Log the event entry. - $eventargs['objectid'] = $this->properties->id; - $eventargs['context'] = $this->properties->context; - $event = \core\event\calendar_event_created::create($eventargs); - $event->trigger(); - - $repeatedids = array(); - - if (!empty($this->properties->repeat)) { - $this->properties->repeatid = $this->properties->id; - $DB->set_field('event', 'repeatid', $this->properties->repeatid, array('id'=>$this->properties->id)); + // If user has manageentries at site level, return true. + if (has_capability('moodle/calendar:manageentries', $sitecontext)) { + return true; + } - $eventcopy = clone($this->properties); - unset($eventcopy->id); + // 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)); + } - $timestart = new DateTime('@' . $eventcopy->timestart); - $timestart->setTimezone(core_date::get_user_timezone_object()); + return false; +} - for($i = 1; $i < $eventcopy->repeats; $i++) { +/** + * 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; - $timestart->add(new DateInterval('P7D')); - $eventcopy->timestart = $timestart->getTimestamp(); + if (!isloggedin()) { + return array(); + } - // Get the event id for the log record. - $eventcopyid = $DB->insert_record('event', $eventcopy); + 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; + } - // 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); - foreach ($files as $file) { - $fs->create_file_from_storedfile(array('itemid'=>$eventcopyid), $file); - } - } + $courses = enrol_get_my_courses(); - $repeatedids[] = $eventcopyid; + return $courses; +} - // Trigger an event. - $eventargs['objectid'] = $eventcopyid; - $eventargs['other']['timestart'] = $eventcopy->timestart; - $event = \core\event\calendar_event_created::create($eventargs); - $event->trigger(); - } - } +/** + * 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; - // Hook for tracking added events - self::calendar_event_hook('add_event', array($this->properties, $repeatedids)); - return true; - } else { + if (empty($linkparams) || !is_array($linkparams)) { + $linkparams = array(); + } - if ($checkcapability) { - if(!calendar_edit_event_allowed($this->properties)) { - print_error('nopermissiontoupdatecalendar'); - } - } + $linkparams['view'] = 'day'; - 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']); - } else { - $this->properties->format = $this->properties->description['format']; - $this->properties->description = $this->properties->description['text']; - } + // 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; } - $event = $DB->get_record('event', array('id'=>$this->properties->id)); - - $updaterepeated = (!empty($this->properties->repeatid) && !empty($this->properties->repeateditall)); - - if ($updaterepeated) { - // Update all - if ($this->properties->timestart != $event->timestart) { - $timestartoffset = $this->properties->timestart - $event->timestart; - $sql = "UPDATE {event} - SET name = ?, - description = ?, - timestart = timestart + ?, - timeduration = ?, - timemodified = ? - WHERE 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); - } - $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; - $event = \core\event\calendar_event_updated::create($eventargs); - $event->trigger(); - } + // 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 { - $DB->update_record('event', $this->properties); - $event = calendar_event::load($this->properties->id); - $this->properties = $event->properties(); - - // Trigger an update event. - $event = \core\event\calendar_event_updated::create($eventargs); - $event->trigger(); - } - - // Hook for tracking event updates - self::calendar_event_hook('update_event', array($this->properties, $updaterepeated)); - return true; - } - } - - /** - * Deletes an event and if selected an repeated events in the same series - * - * This function deletes an event, any associated events if $deleterepeated=true, - * and cleans up any files associated with the events. - * - * @see self::delete() - * - * @param bool $deleterepeated delete event repeatedly - * @return bool succession of deleting event - */ - public function delete($deleterepeated=false) { - global $DB; - - // 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)); - - // Trigger an event for the delete action. - $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_deleted::create($eventargs); - $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 (($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); - 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 - $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) { - // Trigger an event for the update. - $eventargs['objectid'] = $event->id; - $eventargs['other']['timestart'] = $event->timestart; - $event = \core\event\calendar_event_updated::create($eventargs); - $event->trigger(); - - self::calendar_event_hook('update_event', array($event, false)); - } + $eventtime = $time; } - } - - // 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 ($this->editorcontext !== null) { - $fs = get_file_storage(); - $files = $fs->get_area_files($this->editorcontext->id, 'calendar', 'event_description', $this->properties->id); - foreach ($files as $file) { - $file->delete(); + } else { // It must spans two or more days. + $daystart = calendar_day_representation($event->timestart, $now, $usecommonwords) . ', '; + if ($showtime == $usermidnightstart) { + $daystart = ''; } - } - - // 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 ($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 - foreach ($events as $event) { - $event = new calendar_event($event); - $event->delete(false); + $timestart = calendar_time_representation($event->timestart); + $dayend = calendar_day_representation($event->timestart + $event->timeduration, $now, $usecommonwords) . ', '; + if ($showtime == $usermidnightend) { + $dayend = ''; } - } - - return true; - } - - /** - * 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 - * designed to work when the properties are going to be used to set the default - * values of a moodle forms form. - * - * @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 - */ - public function properties($prepareeditor=false) { - global $USER, $CFG, $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 - $properties = clone($this->properties); - // 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 - 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 - if (!empty($properties->id)) { - - if ($properties->eventtype === 'site') { - // Site context - $this->editorcontext = $this->properties->context; - } else if ($properties->eventtype === 'user') { - // 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)); - if (!$course) { - print_error('invalidcourse'); - } - // 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 - $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 - $this->editoroptions['maxbytes'] = 0; - $this->editoroptions['maxfiles'] = 0; - } + $timeend = calendar_time_representation($event->timestart + $event->timeduration); - if (empty($this->editorcontext) || empty($this->editorcontext->id)) { - $contextid = false; - } else { - // Get the context id that is what we really want - $contextid = $this->editorcontext->id; - } + // 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 . ' » '; - // If we get here then this is a new event in which case we don't need a - // context as there is no existing files to copy to the draft area. - $contextid = null; + $url = calendar_get_link_href(new \moodle_url(CALENDAR_URL . 'view.php', $linkparams), 0, 0, 0, $endtime); + $eventtime .= \html_writer::link($url, $dayend) . $timeend; } - - // If the contextid === false we don't support files so no preparing - // a draft area - if ($contextid !== false) { - // 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); - } else { - $draftiddescription = 0; - } - - // Structure the description field as the editor requires - $properties->description = array('text'=>$properties->description, 'format'=>$properties->format, 'itemid'=>$draftiddescription); } + } 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; + } + } - // Finally return the properties - return $properties; + // Check if It has expired. + if ($event->timestart + $event->timeduration < $now) { + $eventtime = '' . str_replace(' href=', ' class="dimmed" href=', $eventtime) . ''; } - /** - * Toggles the visibility of an event - * - * @param null|bool $force If it is left null the events visibility is flipped, - * If it is false the event is made hidden, if it is true it - * is made visible. - * @return bool if event is successfully updated, toggle will be visible - */ - public function toggle_visibility($force=null) { - global $CFG, $DB; + return $eventtime; +} - // Set visible to the default if it is not already set - if (empty($this->properties->visible)) { - $this->properties->visible = 1; - } +/** + * 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 ($force === true || ($force !== false && $this->properties->visible == 0)) { - // 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 - $this->properties->visible = 0; - // Fire the hook - self::calendar_event_hook('hide_event', array($this->properties)); + if (get_user_preferences('calendar_persistflt', 0, $user) === 0) { + global $SESSION; + if (!isset($SESSION->calendarshoweventtype)) { + $SESSION->calendarshoweventtype = $default; } - - // Update the database to reflect this change - return $DB->set_field('event', 'visible', $this->properties->visible, array('id'=>$this->properties->id)); + return $SESSION->calendarshoweventtype & $type; + } else { + return get_user_preferences('calendar_savedflt', $default, $user) & $type; } +} - /** - * 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; +/** + * 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; } - return false; + $preference = $SESSION->calendarshoweventtype; + } else { + $preference = get_user_preferences('calendar_savedflt', $default, $user); } - - /** - * Returns a calendar_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 - * - * @param int|object $param event object or event id - * @return calendar_event|false status for loading 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 = new calendar_event($event); - } - return $event; + $current = $preference & $type; + if ($display === null) { + $display = !$current; } - - /** - * Creates a new event and returns a calendar_event object - * - * @param stdClass|array $properties An object containing event properties - * @param bool $checkcapability Check caps or not - * @throws coding_exception - * - * @return calendar_event|bool The event object or false if it failed - */ - public static function create($properties, $checkcapability = true) { - if (is_array($properties)) { - $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'); - } - $event = new calendar_event($properties); - if ($event->update($properties, $checkcapability)) { - return $event; - } else { - return false; - } + if ($display && !$current) { + $preference += $type; + } else if (!$display && $current) { + $preference -= $type; } - - /** - * 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 - */ - public function format_external_text() { - - if ($this->editorcontext === null) { - // 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') { - // 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); - } - } - - // 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; + if ($persist === 0) { + $SESSION->calendarshoweventtype = $preference; + } else { + if ($preference == $default) { + unset_user_preference('calendar_savedflt', $user); } else { - $itemid = $this->properties->id; + set_user_preference('calendar_savedflt', $preference, $user); } - - return external_format_text($this->properties->description, $this->properties->format, $this->editorcontext->id, - 'calendar', 'event_description', $itemid); } } /** - * Calendar information class - * - * This class is used simply to organise the information pertaining to a calendar - * and is used primarily to make information easily available. + * Get calendar's allowed types. * - * @package core_calendar - * @category calendar - * @copyright 2010 Sam Hemelryk - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @param stdClass $allowed list of allowed edit for event type + * @param stdClass|int $course object of a course or course id */ -class calendar_information { - - /** - * @var int The timestamp - * - * Rather than setting the day, month and year we will set a timestamp which will be able - * to be used by multiple calendars. - */ - public $time; - - /** @var int A course id */ - public $courseid = null; +function calendar_get_allowed_types(&$allowed, $course = null) { + global $USER, $DB; - /** @var array An array of courses */ - public $courses = array(); + $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)); - /** @var array An array of groups */ - public $groups = array(); + 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); - /** @var array An array of users */ - public $users = array(); + if (has_capability('moodle/calendar:manageentries', $coursecontext)) { + $allowed->courses = array($course->id => 1); - /** - * Creates a new instance - * - * @param int $day the number of the day - * @param int $month the number of the month - * @param int $year the number of the year - * @param int $time the unixtimestamp representing the date we want to view, this is used instead of $calmonth - * and $calyear to support multiple calendars - */ - public function __construct($day = 0, $month = 0, $year = 0, $time = 0) { - // 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. This is done for BC. - if (!empty($day) || !empty($month) || !empty($year)) { - $date = usergetdate(time()); - if (empty($day)) { - $day = $date['mday']; - } - if (empty($month)) { - $month = $date['mon']; - } - if (empty($year)) { - $year = $date['year']; - } - if (checkdate($month, $day, $year)) { - $this->time = make_timestamp($year, $month, $day); - } else { - $this->time = time(); + 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); + } + } } - } else if (!empty($time)) { - $this->time = $time; - } else { - $this->time = time(); } } +} - /** - * Initialize calendar information - * - * @param stdClass $course object - * @param array $coursestoload An array of courses [$course->id => $course] - * @param bool $ignorefilters options to use filter - */ - public function prepare_for_view(stdClass $course, array $coursestoload, $ignorefilters = false) { - $this->courseid = $course->id; - $this->course = $course; - list($courses, $group, $user) = calendar_set_filters($coursestoload, $ignorefilters); - $this->courses = $courses; - $this->groups = $group; - $this->users = $user; +/** + * 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; } - /** - * Ensures the date for the calendar is correct and either sets it to now - * or throws a moodle_exception if not - * - * @param bool $defaultonow use current time - * @throws moodle_exception - * @return bool validation of checkdate - */ - public function checkdate($defaultonow = true) { - if (!checkdate($this->month, $this->day, $this->year)) { - if ($defaultonow) { - $now = usergetdate(time()); - $this->day = intval($now['mday']); - $this->month = intval($now['mon']); - $this->year = intval($now['year']); - return true; - } else { - throw new moodle_exception('invaliddate'); - } - } - return true; - } + calendar_get_allowed_types($allowed, $course); - /** - * Gets todays timestamp for the calendar - * - * @return int today timestamp - */ - public function timestamp_today() { - return $this->time; + 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; } - /** - * Gets tomorrows timestamp for the calendar - * - * @return int tomorrow timestamp - */ - public function timestamp_tomorrow() { - return strtotime('+1 day', $this->time); + + $sitecontext = \context_system::instance(); + + // If user has manageentries at site level, always return true. + if (has_capability('moodle/calendar:manageentries', $sitecontext)) { + return true; } - /** - * Adds the pretend blocks for the calendar - * - * @param core_calendar_renderer $renderer - * @param bool $showfilters display filters, false is set as default - * @param string|null $view preference view options (eg: day, month, upcoming) - */ - public function add_sidecalendar_blocks(core_calendar_renderer $renderer, $showfilters=false, $view=null) { - if ($showfilters) { - $filters = new block_contents(); - $filters->content = $renderer->fake_block_filters($this->courseid, 0, 0, 0, $view, $this->courses); - $filters->footer = ''; - $filters->title = get_string('eventskey', 'calendar'); - $renderer->add_pretend_calendar_block($filters, BLOCK_POS_RIGHT); - } - $block = new block_contents; - $block->content = $renderer->fake_block_threemonths($this); - $block->footer = ''; - $block->title = get_string('monthlyview', 'calendar'); - $renderer->add_pretend_calendar_block($block, BLOCK_POS_RIGHT); + + 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); } } @@ -2982,12 +2889,12 @@ public function add_sidecalendar_blocks(core_calendar_renderer $renderer, $showf */ 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 c68ca73608b56..014262c15a571 100644 --- a/calendar/renderer.php +++ b/calendar/renderer.php @@ -64,7 +64,8 @@ public function complete_layout() { 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 @@ public function fake_block_threemonths(calendar_information $calendar) { $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 @@ public function show_day(calendar_information $calendar, moodle_url $returnurl = $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 @@ public function show_day(calendar_information $calendar, moodle_url $returnurl = } $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 @@ public function show_day(calendar_information $calendar, moodle_url $returnurl = $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 @@ public function show_day(calendar_information $calendar, moodle_url $returnurl = $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 @@ public function show_month_detailed(calendar_information $calendar, moodle_url $ } // 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 @@ public function show_month_detailed(calendar_information $calendar, moodle_url $ } // 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 @@ public function show_month_detailed(calendar_information $calendar, moodle_url $ } $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 @@ public function show_month_detailed(calendar_information $calendar, moodle_url $ // 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 @@ public function show_upcoming_events(calendar_information $calendar, $futuredays $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 @@ public function show_upcoming_events(calendar_information $calendar, $futuredays 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 fab8aa54c02f1..c69de934a1a03 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 0000000000000..c655f84e01ef2 --- /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 0000000000000..d58a4775b94b6 --- /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 0000000000000..cc4dd6a3ccdd0 --- /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 c026cfe4360f6..81689dbef7ca8 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 9c6c046fd0dbe..819d03e2b30f5 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 c097cfd0759c9..5d9af035811d4 100644 --- a/calendar/tests/calendartype_test.php +++ b/calendar/tests/calendartype_test.php @@ -33,9 +33,6 @@ 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 @@ public function test_calendar_type_core_functions() { * 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 0000000000000..8f4f5a7f47f9f --- /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 0000000000000..b7c0e40ea4a23 --- /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 0000000000000..b2f3039f764ab --- /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 0000000000000..67738f4f19049 --- /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 0000000000000..c1ddaf96f27d2 --- /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 0000000000000..9a6c55cf4b3cf --- /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 0000000000000..1d57d1db9d217 --- /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 207d66a6a4fc0..6f78848a05965 100644 --- a/calendar/tests/events_test.php +++ b/calendar/tests/events_test.php @@ -219,6 +219,41 @@ public function test_calendar_event_updated() { } } + /** + * 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 9baf24918bea8..62f9336310444 100644 --- a/calendar/tests/externallib_test.php +++ b/calendar/tests/externallib_test.php @@ -85,6 +85,12 @@ public static function create_calendar_event($name, $userid = 0, $type = 'user', 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 @@ public function test_core_create_calendar_events() { $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 0000000000000..c990d4de37d7f --- /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 d2223b538c29d..0000000000000 --- 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 3e3bb3948241b..797dc891c42c7 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 { - 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); - } - /** - * Test calendar cron with a working subscription URL. + * Tests set up */ - 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(); + protected function setUp() { + $this->resetAfterTest(); } /** - * Test the calendar_get_events() function only returns activity - * events that are enabled. + * Test that the 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 @@ public function test_calendar_get_events_with_disabled_module() { '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 @@ public function test_calendar_get_events_with_disabled_module() { '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 @@ public function test_calendar_get_events_with_disabled_module() { */ 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 @@ public function test_calendar_get_events_with_overrides() { $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 @@ public function test_calendar_get_events_with_overrides() { '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 @@ public function test_calendar_get_events_with_overrides() { 'courseid' => SITEID, 'groupid' => 0, 'userid' => 2, - 'repeatid' => 1, + 'repeatid' => $event->id, 'modulename' => '0', 'instance' => 0, 'eventtype' => 'site', @@ -343,7 +254,7 @@ public function test_calendar_get_events_with_overrides() { 'courseid' => SITEID, 'groupid' => 0, 'userid' => 2, - 'repeatid' => 1, + 'repeatid' => $event->id, 'modulename' => '0', 'instance' => 0, 'eventtype' => 'site', @@ -355,9 +266,130 @@ public function test_calendar_get_events_with_overrides() { 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 0000000000000..599a605204869 --- /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 0000000000000..c279eec806bd3 --- /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 0000000000000..cbf8d706d40ba --- /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 0000000000000..f59fdd9cb0035 --- /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 0000000000000..cc912b3b7cfb5 --- /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 4a37e2c205bfd..e99279e8d1397 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 e9131933857a3..946bd04b84935 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 68adda85f835a..1b1edb4086a62 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 0000000000000..418d849886612 --- /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 0000000000000..803f2ef044e50 --- /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 0000000000000..6762484208691 --- /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 848b3155566c9..6ba1be434059e 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 3f94122bc0f99..551243d8e5d2a 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 6c970e87da2a0..3d6a6cdd11d3e 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 f00c39f496046..63d8422abeab5 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 ca0c35d90f03e..13810f3754603 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 0000000000000..b2e5b97e56f57 --- /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 afc96f3441166..531ed8c5cfcd3 100644 --- a/course/classes/external/course_summary_exporter.php +++ b/course/classes/external/course_summary_exporter.php @@ -42,6 +42,7 @@ protected static function define_related() { 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 @@ public static function define_properties() { ), '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 73370acc2db33..802cfd2fee5f4 100644 --- a/course/externallib.php +++ b/course/externallib.php @@ -2654,6 +2654,8 @@ public static function get_course_module_by_instance_returns() { /** * Returns description of method parameters * + * @deprecated since 3.3 + * * @return external_function_parameters * @since Moodle 3.2 */ @@ -2668,6 +2670,8 @@ public static function get_activities_overview_parameters() { /** * 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 @@ public static function get_activities_overview($courseids) { /** * Returns description of method result value * + * @deprecated since 3.3 + * * @return external_description * @since Moodle 3.2 */ @@ -2751,6 +2757,15 @@ public static function get_activities_overview_returns() { ); } + /** + * 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 4c83c5a320cb1..f7216c7f32b54 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 1c836478c7b03..ee86fb452d1a2 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 6269da09f5ae8..74d25ce4f2007 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 1886a28785f1b..46ed50affdbc4 100644 --- a/course/moodleform_mod.php +++ b/course/moodleform_mod.php @@ -636,7 +636,6 @@ function standard_coursemodule_elements(){ } 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 @@ function standard_coursemodule_elements(){ $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 @@ function standard_coursemodule_elements(){ $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 c4f44b7b95948..ebca3b4f1f3a0 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 2d53b7cc8fc3b..29e8911821c8a 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 735288ec59caf..4ed3330593b14 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 4429b2a104071..a863ff0c9e34f 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 ded5b0ec55134..8ef74131f60e9 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 6df1c37a828d6..4a45cec23aae7 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 b318d8046d04b..547162d4ae63f 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 fda72f91b5a7f..f28f75e9e0e26 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 8b6c46c58bc32..140fd3fcc18e6 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 fe1d331bc0ba8..62ccbeda6b23b 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 1ef2c67cb6468..e2ffa84e3cfe1 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 284a59dd1716d..1230ae95a6de7 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 ba900d446bf42..b06c7f6a73ba0 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 d1efdde9dd1d3..0e9e097346cb4 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 8be6ae11c5794..0f929bdcf7a3f 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 253aebdb49c1b..c16ead45273f4 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 dc240de804827..b31933fdf820c 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 3101265035787..d4b7f3d081708 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 af8c941d2a84f..07b54235bf56e 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 b9f781a0d7853..f11dc6dc6ccd9 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 14194c068b328..d447b2bb31466 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 c726278b3967b..52edef8b1f990 100644 --- a/course/tests/courselib_test.php +++ b/course/tests/courselib_test.php @@ -97,6 +97,7 @@ private function assign_create_set_values(&$moduleinfo) { $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 9616ca2bf15df..83f56997cf44b 100644 --- a/course/tests/externallib_test.php +++ b/course/tests/externallib_test.php @@ -1823,6 +1823,7 @@ public function test_get_activities_overview() { $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 711cc69ceb8ad..588fbf96f69b4 100644 --- a/enrol/externallib.php +++ b/enrol/externallib.php @@ -326,6 +326,11 @@ public static function get_users_courses($userid) { $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 @@ public static function get_users_courses($userid) { 'showgrades' => $course->showgrades, 'lang' => $course->lang, 'enablecompletion' => $course->enablecompletion, - 'category' => $course->category + 'category' => $course->category, + 'progress' => $progress, ); } @@ -369,6 +375,7 @@ public static function get_users_courses_returns() { '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 621482a358ae7..1b05008bb5071 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 eeb24b129d461..c2031792280d9 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 46b553fd09ade..0f7d81c44d6d2 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 e28b005e25a64..f8ba17310445c 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 31f4a8214ebab..c9cd9d46a49bf 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 a0c53dd7dde51..b38fb108ae440 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 a57e441cff9a6..79f4b4c39c7c3 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 94a80c5519643..aa88075b324e5 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 d284305671068..8edf4584e34ef 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 d50bde399d887..e2e77451afd13 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 b3dcfc1df17f0..b9c5d7f3118c2 100644 --- a/enrol/tests/enrollib_test.php +++ b/enrol/tests/enrollib_test.php @@ -476,4 +476,49 @@ public function test_enrollment_update_timemodified() { $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 0ed9a61ced732..59f1773cfb3b9 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 af9f65ab038c4..c44e15436faa4 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 ea92bf6ae29d5..1965c9acdf510 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 9e6450a01817e..d1ca5aacdee8b 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 37a256317d48b..db90d9499b2c4 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 6500d05d1698e..4fbde356c76c2 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 3e647147b311b..687f1a49c7e2b 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 83ee60b9f8e7c..33d0e1e0deb91 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 e755151d403e5..c1a5cbdf1e191 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 5effb3d0e32cd..f649d1129d6cf 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 dddab82eb2285..b7cbe9e6fb842 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 54bed2bbf64ce..7cd066a0a9856 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 3704c3ad4202e..119bd083f92de 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 fe94e1fcf2f8d..c99aefe1a2ab6 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 cd1774fd9a2c1..bfd7d7fc2db26 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 e54ac6f187ea8..eb9bef2d33405 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 3fc9563afe551..3aaf02aefbcfc 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 5661e329b05da..b354dae3e4f1d 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 df80dd3d24fae..68a9104123439 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 89db8d5680ef5..68835bb361ee4 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 f60cfa1582149..abbfe4fb3cec4 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 57046899f7b84..e3cca8d185fb3 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 e1eb3fbf9699c..88558150a3092 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 47a30400de87e..88b20c35f9661 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 74cff6208a596..ad270465406f1 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 05311952deca4..cb53856f38c4d 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 16d6abd043099..1ed3712b04f9a 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 fd7aa450f2d8d..ca3b5180eb5c6 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 ef381aad6aec5..4f3d6cb3663f9 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 33466731920ee..d9d951c3ff866 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 8ec6dafd5714f..0f5dbc18dcdf7 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 0011f2086e43b..b385b3d27ae30 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 37f411d4e56de..b23be1f4533c2 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 54dfc00d25dba..39578ff9a3842 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 82dbd8d05b716..4706e101d1e96 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 b302102589a7a..887a5c0000e88 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 be0ff3dd9079f..da25498442b56 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 347b9860c3a8a..5e1723746297f 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 400e96fb43d8a..d673eabf7cbf2 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 dc2092b9d7892..8a26b40519a9e 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 2f4a7cc158a1d..1b319e328d0e9 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 b108e06b107ef..a59f0fc396f86 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 f2c85c47bcda6..1700dc0efedd3 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 e41b32fc9d8a3..8610678a60aaa 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 b2cdd223dffda..4642d4d5f36ed 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 b77d0007c0bae..9c89d407d5e00 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 eaea7d4c4396e..0bec1e371bee9 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 14c18c461901b..779b1da8eaf76 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 422c0738174b7..c2ef16edf02f2 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 6da490d0c7fa6..9d6c53066b27f 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 a24c0f32cb6c3..d36accc45ea48 100644 --- a/lang/en/calendar.php +++ b/lang/en/calendar.php @@ -26,6 +26,7 @@ $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 c19e306bb8bc0..41bc6c2656efc 100644 --- a/lang/en/completion.php +++ b/lang/en/completion.php @@ -67,7 +67,8 @@ $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['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 87d6ada670121..c465c9abf725a 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("