Merge branch 'MDL-54044-master' of git://github.com/FMCorz/moodle
authorDavid Monllao <davidm@moodle.com>
Wed, 11 May 2016 01:09:11 +0000 (09:09 +0800)
committerDavid Monllao <davidm@moodle.com>
Wed, 11 May 2016 01:09:11 +0000 (09:09 +0800)
63 files changed:
admin/tests/behat/enable_multiple_accounts_use_same_email.feature [new file with mode: 0644]
admin/tool/lp/amd/build/tree.min.js
admin/tool/lp/amd/src/tree.js
admin/tool/recyclebin/tests/course_bin_test.php
blocks/calendar_month/tests/behat/block_calendar_month.feature [new file with mode: 0644]
blocks/calendar_month/tests/behat/block_calendar_month_course.feature [new file with mode: 0644]
blocks/calendar_month/tests/behat/block_calendar_month_dashboard.feature [new file with mode: 0644]
blocks/calendar_month/tests/behat/block_calendar_month_frontpage.feature [new file with mode: 0644]
blocks/calendar_upcoming/tests/behat/block_calendar_upcoming_course.feature [new file with mode: 0644]
blocks/calendar_upcoming/tests/behat/block_calendar_upcoming_dashboard.feature [new file with mode: 0644]
blocks/calendar_upcoming/tests/behat/block_calendar_upcoming_frontpage.feature [new file with mode: 0644]
blocks/comments/tests/behat/block_comment_activity.feature [new file with mode: 0644]
blocks/comments/tests/behat/block_comment_course.feature [new file with mode: 0644]
blocks/comments/tests/behat/block_comment_frontpage.feature [new file with mode: 0644]
blocks/globalsearch/styles.css [new file with mode: 0644]
competency/classes/plan_competency.php
competency/classes/template_competency.php
competency/classes/user_competency.php
competency/classes/user_competency_course.php
competency/classes/user_competency_plan.php
competency/tests/api_test.php
competency/tests/external_test.php
composer.json
composer.lock
course/classes/search/mycourse.php [new file with mode: 0644]
course/externallib.php
course/tests/search_test.php [new file with mode: 0644]
enrol/lti/lang/en/enrol_lti.php
enrol/tests/enrollib_test.php
install/lang/oc_lnc/error.php
lang/en/search.php
lib/adminlib.php
lib/ajax/service.php
lib/amd/build/tree.min.js
lib/amd/src/tree.js
lib/classes/date.php
lib/classes/user.php
lib/enrollib.php
lib/externallib.php
lib/filelib.php
lib/filestorage/file_storage.php
lib/tests/date_test.php
lib/tests/externallib_test.php
lib/tests/moodlelib_test.php
lib/tests/unoconv_test.php
lib/tests/user_test.php
lib/upgrade.txt
mod/assign/amd/build/grading_navigation_user_info.min.js
mod/assign/amd/src/grading_navigation_user_info.js
mod/assign/db/services.php
mod/assign/externallib.php
mod/assign/gradeform.php
mod/assign/gradingtable.php
mod/assign/locallib.php
mod/assign/tests/behat/online_submissions.feature
mod/assign/tests/externallib_test.php
mod/assign/tests/locallib_test.php
mod/assign/version.php
mod/lti/styles.css
mod/wiki/classes/search/collaborative_page.php
search/classes/area/base.php
user/tests/userlib_test.php
version.php

diff --git a/admin/tests/behat/enable_multiple_accounts_use_same_email.feature b/admin/tests/behat/enable_multiple_accounts_use_same_email.feature
new file mode 100644 (file)
index 0000000..30d21b9
--- /dev/null
@@ -0,0 +1,57 @@
+@core @core_admin
+Feature: Enable multiple accounts to have the same email address
+  In order to have multiple accounts registerd on the system with the same email address
+  As an admin
+  I need to enable multiple accounts to be registered with the same email address and verify it is applied
+
+  Background:
+    Given I log in as "admin"
+
+  Scenario: Enable registration of multiple accounts with the same email address
+    Given the following config values are set as admin:
+      | allowaccountssameemail | 1 |
+    When I navigate to "Add a new user" node in "Site administration>Users>Accounts"
+    And I set the following fields to these values:
+      | Username                        | testmultiemailuser1             |
+      | Choose an authentication method | Manual accounts                 |
+      | New password                    | test@User1                      |
+      | First name                      | Test                            |
+      | Surname                         | Multi1                          |
+      | Email address                   | testmultiemailuser@example.com  |
+    And I press "Create user"
+    And I should see "Test Multi1"
+    And I press "Add a new user"
+    And I set the following fields to these values:
+      | Username                        | testmultiemailuser2             |
+      | Choose an authentication method | Manual accounts                 |
+      | New password                    | test@User2                      |
+      | First name                      | Test                            |
+      | Surname                         | Multi2                          |
+      | Email address                   | testmultiemailuser@example.com  |
+    And I press "Create user"
+    Then I should see "Test Multi2"
+    And I should not see "This email address is already registered"
+
+  Scenario: Disable registration of multiple accounts with the same email address
+    Given the following config values are set as admin:
+      | allowaccountssameemail | 0 |
+    When I navigate to "Add a new user" node in "Site administration>Users>Accounts"
+    And I set the following fields to these values:
+      | Username                        | testmultiemailuser1             |
+      | Choose an authentication method | Manual accounts                 |
+      | New password                    | test@User1                      |
+      | First name                      | Test                            |
+      | Surname                         | Multi1                          |
+      | Email address                   | testmultiemailuser@example.com  |
+    And I press "Create user"
+    And I should see "Test Multi1"
+    And I press "Add a new user"
+    And I set the following fields to these values:
+      | Username                        | testmultiemailuser2             |
+      | Choose an authentication method | Manual accounts                 |
+      | New password                    | test@User2                      |
+      | First name                      | Test                            |
+      | Surname                         | Multi2                          |
+      | Email address                   | testmultiemailuser@example.com  |
+    And I press "Create user"
+    Then I should see "This email address is already registered"
\ No newline at end of file
index e03f666..4cc8611 100644 (file)
Binary files a/admin/tool/lp/amd/build/tree.min.js and b/admin/tool/lp/amd/build/tree.min.js differ
index a8ac21b..f52934d 100644 (file)
@@ -69,6 +69,7 @@ define(['jquery', 'core/url', 'core/log'], function($, url, log) {
             up:       38,
             right:    39,
             down:     40,
+            eight:    56,
             asterisk: 106
         };
 
@@ -274,6 +275,8 @@ define(['jquery', 'core/url', 'core/log'], function($, url, log) {
     Tree.prototype.handleKeyDown = function(item, e) {
         var currentIndex = this.visibleItems.index(item);
         var newItem = null;
+        var hasKeyModifier = e.shiftKey || e.ctrlKey || e.metaKey || e.altKey;
+        var thisObj = this;
 
         switch (e.keyCode) {
             case this.keys.home: {
@@ -282,7 +285,7 @@ define(['jquery', 'core/url', 'core/log'], function($, url, log) {
                 newItem.focus();
                 if (e.shiftKey) {
                     this.multiSelectItem(newItem);
-                } else if (!e.ctrlKey) {
+                } else if (!hasKeyModifier) {
                     this.selectItem(newItem);
                 }
 
@@ -295,7 +298,7 @@ define(['jquery', 'core/url', 'core/log'], function($, url, log) {
                 newItem.focus();
                 if (e.shiftKey) {
                     this.multiSelectItem(newItem);
-                } else if (!e.ctrlKey) {
+                } else if (!hasKeyModifier) {
                     this.selectItem(newItem);
                 }
 
@@ -307,7 +310,7 @@ define(['jquery', 'core/url', 'core/log'], function($, url, log) {
 
                 if (e.shiftKey) {
                     this.multiSelectItem(item);
-                } else if (e.ctrlKey) {
+                } else if (e.metaKey || e.ctrlKey) {
                     this.toggleItem(item);
                 } else {
                     this.selectItem(item);
@@ -327,7 +330,7 @@ define(['jquery', 'core/url', 'core/log'], function($, url, log) {
                         itemParent.focus();
                         if (e.shiftKey) {
                             this.multiSelectItem(itemParent);
-                        } else if (!e.ctrlKey) {
+                        } else if (!hasKeyModifier) {
                             this.selectItem(itemParent);
                         }
                     }
@@ -346,7 +349,7 @@ define(['jquery', 'core/url', 'core/log'], function($, url, log) {
                         newItem.focus();
                         if (e.shiftKey) {
                             this.multiSelectItem(newItem);
-                        } else if (!e.ctrlKey) {
+                        } else if (!hasKeyModifier) {
                             this.selectItem(newItem);
                         }
                     }
@@ -362,7 +365,7 @@ define(['jquery', 'core/url', 'core/log'], function($, url, log) {
                     prev.focus();
                     if (e.shiftKey) {
                         this.multiSelectItem(prev);
-                    } else if (!e.ctrlKey) {
+                    } else if (!hasKeyModifier) {
                         this.selectItem(prev);
                     }
                 }
@@ -377,7 +380,7 @@ define(['jquery', 'core/url', 'core/log'], function($, url, log) {
                     next.focus();
                     if (e.shiftKey) {
                         this.multiSelectItem(next);
-                    } else if (!e.ctrlKey) {
+                    } else if (!hasKeyModifier) {
                         this.selectItem(next);
                     }
                 }
@@ -386,9 +389,6 @@ define(['jquery', 'core/url', 'core/log'], function($, url, log) {
             }
             case this.keys.asterisk: {
                 // Expand all groups.
-
-                var thisObj = this;
-
                 this.parents.each(function() {
                     thisObj.expandGroup($(this));
                 });
@@ -396,6 +396,18 @@ define(['jquery', 'core/url', 'core/log'], function($, url, log) {
                 e.stopPropagation();
                 return false;
             }
+            case this.keys.eight: {
+                if (e.shiftKey) {
+                    // Expand all groups.
+                    this.parents.each(function() {
+                        thisObj.expandGroup($(this));
+                    });
+
+                    e.stopPropagation();
+                }
+
+                return false;
+            }
         }
 
         return true;
@@ -409,7 +421,7 @@ define(['jquery', 'core/url', 'core/log'], function($, url, log) {
      * @param {Event} e The event.
      */
     Tree.prototype.handleKeyPress = function(item, e) {
-        if (e.altKey || e.ctrlKey || e.shiftKey) {
+        if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
             // Do nothing.
             return true;
         }
@@ -498,7 +510,7 @@ define(['jquery', 'core/url', 'core/log'], function($, url, log) {
      */
     Tree.prototype.handleDblClick = function(item, e) {
 
-        if (e.altKey || e.ctrlKey || e.shiftKey) {
+        if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
             // Do nothing.
             return true;
         }
@@ -540,7 +552,7 @@ define(['jquery', 'core/url', 'core/log'], function($, url, log) {
 
         if (e.shiftKey) {
             this.multiSelectItem(item);
-        } else if (e.ctrlKey) {
+        } else if (e.metaKey || e.ctrlKey) {
             this.toggleItem(item);
         } else {
             this.selectItem(item);
index edfa1a2..1f83de3 100644 (file)
@@ -85,6 +85,8 @@ class tool_recyclebin_course_bin_tests extends advanced_testcase {
     public function test_restore() {
         global $DB;
 
+        $startcount = $DB->count_records('course_modules');
+
         // Delete the course module.
         course_delete_module($this->quiz->cmid);
 
@@ -95,7 +97,7 @@ class tool_recyclebin_course_bin_tests extends advanced_testcase {
         }
 
         // Check that it was restored and removed from the recycle bin.
-        $this->assertEquals(1, $DB->count_records('course_modules'));
+        $this->assertEquals($startcount, $DB->count_records('course_modules'));
         $this->assertEquals(0, count($recyclebin->get_items()));
     }
 
@@ -105,6 +107,8 @@ class tool_recyclebin_course_bin_tests extends advanced_testcase {
     public function test_delete() {
         global $DB;
 
+        $startcount = $DB->count_records('course_modules');
+
         // Delete the course module.
         course_delete_module($this->quiz->cmid);
 
@@ -115,7 +119,7 @@ class tool_recyclebin_course_bin_tests extends advanced_testcase {
         }
 
         // Item was deleted, so no course module was restored.
-        $this->assertEquals(0, $DB->count_records('course_modules'));
+        $this->assertEquals($startcount - 1, $DB->count_records('course_modules'));
         $this->assertEquals(0, count($recyclebin->get_items()));
     }
 
diff --git a/blocks/calendar_month/tests/behat/block_calendar_month.feature b/blocks/calendar_month/tests/behat/block_calendar_month.feature
new file mode 100644 (file)
index 0000000..1dd28e9
--- /dev/null
@@ -0,0 +1,197 @@
+@block @block_calendar_month
+Feature: Enable the calendar block in a course and test it's functionality
+  In order to enable the calendar block in a course
+  As a teacher
+  I can add the calendar block to a course
+
+  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 |
+      | student2 | Student | 2 | student2@example.com | S2 |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+      | student2 | C1 | student |
+
+  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
+    When I add the "Calendar" block
+    Then I should see "Events key" in the "Calendar" "block"
+
+  @javascript
+  Scenario: View a global event in the calendar block
+    Given I log in as "admin"
+    And I create a calendar event with form data:
+      | id_eventtype | Site |
+      | 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 add the "Calendar" block
+    And I hover over today in the calendar
+    Then I should see "Site Event"
+
+  @javascript
+  Scenario: Filter site events in the calendar block
+    Given I log in as "admin"
+    And I create a calendar event with form data:
+      | id_eventtype | Site |
+      | 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 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 follow "Hide global events"
+    And I hover over today in the calendar
+    Then I should not see "Site Event"
+    And I should see "Course Event"
+
+  @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 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"
+    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 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 create a calendar event with form data:
+      | id_eventtype | User |
+      | id_name | User Event |
+    When I click on "Dashboard" "link" in the "Navigation" "block"
+    And I follow "Course 1"
+    And I follow "Hide course events"
+    And I hover over today in the calendar
+    Then I should not see "Course Event"
+    And I should see "User Event"
+
+  @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 add the "Calendar" block
+    And I create a calendar event with form data:
+      | id_eventtype | User |
+      | id_name | User Event |
+    When I click on "Dashboard" "link" in the "Navigation" "block"
+    And I follow "Course 1"
+    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 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 create a calendar event with form data:
+      | id_eventtype | User |
+      | id_name | User Event |
+    When I click on "Dashboard" "link" in the "Navigation" "block"
+    And I follow "Course 1"
+    And I follow "Hide user events"
+    And I hover over today in the calendar
+    Then I should not see "User Event"
+    And I should see "Course Event"
+
+  @javascript
+  Scenario: View a group event in the calendar block
+    Given the following "groups" exist:
+      | name    | course | idnumber |
+      | Group 1 | C1     | G1       |
+      | Group 2 | C1     | G2       |
+    And the following "group members" exist:
+      | user     | group   |
+      | student1 | G1 |
+      | student2 | G2 |
+    When I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Edit settings" node in "Course administration"
+    And I set the following fields to these values:
+      | id_groupmode | Separate groups |
+      | id_groupmodeforce | Yes |
+    And I press "Save and display"
+    And I turn editing mode on
+    And I add the "Calendar" block
+    And I create a calendar event with form data:
+      | id_eventtype | Group |
+      | id_groupid | Group 1 |
+      | id_name | Group Event |
+    And I log out
+    Then I log in as "student1"
+    And I follow "Course 1"
+    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 hover over today in the calendar
+    And I should not see "Group Event"
+
+  @javascript
+  Scenario: Filter group events in the calendar block
+    Given the following "groups" exist:
+      | name    | course | idnumber |
+      | Group 1 | C1     | G1       |
+      | Group 2 | C1     | G2       |
+    And the following "group members" exist:
+      | user     | group   |
+      | student1 | G1 |
+      | student2 | G2 |
+    When I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Edit settings" node in "Course administration"
+    And I set the following fields to these values:
+      | id_groupmode | Separate groups |
+      | id_groupmodeforce | Yes |
+    And I press "Save and display"
+    And I turn 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 1 |
+    And I follow "Course 1"
+    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 follow "Hide group events"
+    And I hover over today in the calendar
+    And I should not see "Group Event 1"
+    And I should see "Course 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
new file mode 100644 (file)
index 0000000..1c59a60
--- /dev/null
@@ -0,0 +1,28 @@
+@block @block_calendar_month
+Feature: Enable the calendar block in a course
+  In order to enable the calendar block in a course
+  As a teacher
+  I can add the calendar block to a course
+
+  @javascript
+  Scenario: View a global event in the calendar block in a course
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    When I log in as "admin"
+    And I create a calendar event with form data:
+      | id_eventtype | Site |
+      | 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 add the "Calendar" block
+    And I hover over today in the calendar
+    And I should see "Site Event"
diff --git a/blocks/calendar_month/tests/behat/block_calendar_month_dashboard.feature b/blocks/calendar_month/tests/behat/block_calendar_month_dashboard.feature
new file mode 100644 (file)
index 0000000..aa74c43
--- /dev/null
@@ -0,0 +1,19 @@
+@block @block_calendar_month
+Feature: View a site event on the dashboard
+  In order to view a site event
+  As a student
+  I can view the event in the calendar
+
+  @javascript
+  Scenario: View a global event in the calendar block on the dashboard
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | student1 | Student | 1 | student1@example.com | S1 |
+    And I log in as "admin"
+    And I create a calendar event with form data:
+      | id_eventtype | Site |
+      | id_name | Site Event |
+    And I log out
+    When I log in as "student1"
+    And I hover over today in the calendar
+    Then I should see "Site Event"
diff --git a/blocks/calendar_month/tests/behat/block_calendar_month_frontpage.feature b/blocks/calendar_month/tests/behat/block_calendar_month_frontpage.feature
new file mode 100644 (file)
index 0000000..ae09aa8
--- /dev/null
@@ -0,0 +1,21 @@
+@block @block_calendar_month
+Feature: Enable the calendar block on the site front page
+  In order to enable the calendar block on the site front page
+  As an admin
+  I can add the calendar block on the site front page
+
+  @javascript
+  Scenario: View a global event in the calendar block on the front page
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | student1 | Student | 1 | student1@example.com | S1 |
+    And I log in as "admin"
+    And I am on site homepage
+    And I create a calendar event with form data:
+      | id_eventtype | Site |
+      | id_name | Site Event |
+    And I log out
+    When I log in as "student1"
+    And I am on site homepage
+    And I hover over today in the calendar
+    Then I should see "Site Event"
diff --git a/blocks/calendar_upcoming/tests/behat/block_calendar_upcoming_course.feature b/blocks/calendar_upcoming/tests/behat/block_calendar_upcoming_course.feature
new file mode 100644 (file)
index 0000000..d517c5f
--- /dev/null
@@ -0,0 +1,23 @@
+@block  @block_calendar_upcoming
+Feature: Enable the upcoming events block in a course
+  In order to enable the calendar block in a course
+  As a teacher
+  I can view the event in the upcoming events block
+
+  Scenario: View a global event in the upcoming events block in a course
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    When I log in as "admin"
+    And I create a calendar event with form data:
+      | id_eventtype | Site |
+      | id_name | My Site Event |
+    And I log out
+    When I log in as "teacher1"
+    Then I should see "My Site Event" in the "Upcoming events" "block"
diff --git a/blocks/calendar_upcoming/tests/behat/block_calendar_upcoming_dashboard.feature b/blocks/calendar_upcoming/tests/behat/block_calendar_upcoming_dashboard.feature
new file mode 100644 (file)
index 0000000..ea2bcd1
--- /dev/null
@@ -0,0 +1,17 @@
+@block @block_calendar_upcoming
+Feature: View a site event on the dashboard
+  In order to view a site event
+  As a student
+  I can view the event in the upcoming events block
+
+  Scenario: View a global event in the upcoming events block on the dashboard
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | student1 | Student | 1 | student1@example.com | S1 |
+    And I log in as "admin"
+    And I create a calendar event with form data:
+      | id_eventtype | Site |
+      | id_name | My Site Event |
+    And I log out
+    When I log in as "student1"
+    Then I should see "My Site Event"
diff --git a/blocks/calendar_upcoming/tests/behat/block_calendar_upcoming_frontpage.feature b/blocks/calendar_upcoming/tests/behat/block_calendar_upcoming_frontpage.feature
new file mode 100644 (file)
index 0000000..a101952
--- /dev/null
@@ -0,0 +1,21 @@
+@block @block_calendar_upcoming
+Feature: View a site event on the frontpage
+  In order to view a site event
+  As a teacher
+  I can view the event in the upcoming events block
+
+  Scenario: View a global event in the upcoming events block on the frontpage
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+    And I log in as "admin"
+    And I create a calendar event with form data:
+      | id_eventtype | Site |
+      | id_name | My Site Event |
+    And I am on site homepage
+    And I navigate to "Turn editing on" node in "Front page settings"
+    And I add the "Upcoming events" block
+    And I log out
+    When I log in as "teacher1"
+    And I am on site homepage
+    Then I should see "My Site Event" in the "Upcoming events" "block"
diff --git a/blocks/comments/tests/behat/block_comment_activity.feature b/blocks/comments/tests/behat/block_comment_activity.feature
new file mode 100644 (file)
index 0000000..d2a3180
--- /dev/null
@@ -0,0 +1,34 @@
+@block @block_comments
+Feature: Enable Block comments on an activity page and view comments
+  In order to enable the comments block on an activity page
+  As a teacher
+  I can add the comments block to an activity page
+
+  Scenario: Add the comments block on an activity page and add comments
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | Frist | teacher1@example.com |
+      | student1 | Student | First | student1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And the following "activities" exist:
+      | 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 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 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
new file mode 100644 (file)
index 0000000..c3acbc4
--- /dev/null
@@ -0,0 +1,29 @@
+@block @block_comments
+Feature: Enable Block comments on a course page and view comments
+  In order to enable the comments block on a course page
+  As a teacher
+  I can add the comments block to the course page
+
+  Scenario: Add the comments block on the course page and add comments
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | Frist | teacher1@example.com |
+      | student1 | Student | First | student1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | 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 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 follow "Show comments"
+    Then I should see "I'm a comment from the teacher"
diff --git a/blocks/comments/tests/behat/block_comment_frontpage.feature b/blocks/comments/tests/behat/block_comment_frontpage.feature
new file mode 100644 (file)
index 0000000..5d949ab
--- /dev/null
@@ -0,0 +1,21 @@
+@block @block_comments
+Feature: Enable Block comments on the frontpage and view comments
+  In order to enable the comments block on the frontpage
+  As a admin
+  I can add the comments block to the frontpage
+
+  Scenario: Add the comments block on the frontpage and add comments
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+    And I log in as "admin"
+    And I am on site homepage
+    And I navigate to "Turn editing on" node in "Front page settings"
+    And I add the "Comments" block
+    And I follow "Show comments"
+    And I add "I'm a comment from admin" comment to comments block
+    And I log out
+    When I log in as "teacher1"
+    And I am on site homepage
+    And I follow "Show comments"
+    Then I should see "I'm a comment from admin"
diff --git a/blocks/globalsearch/styles.css b/blocks/globalsearch/styles.css
new file mode 100644 (file)
index 0000000..b8b5c69
--- /dev/null
@@ -0,0 +1,2 @@
+.block_globalsearch .searchform {text-align: center;}
+.block_globalsearch .footer {text-align: center;}
index d45ec01..57a72ed 100644 (file)
@@ -71,7 +71,8 @@ class plan_competency extends persistent {
                   JOIN {' . self::TABLE . '} plancomp
                     ON plancomp.competencyid = comp.id
                  WHERE plancomp.planid = ?
-              ORDER BY plancomp.sortorder ASC';
+              ORDER BY plancomp.sortorder ASC,
+                       plancomp.id ASC';
         $params = array($planid);
 
         // TODO MDL-52229 Handle hidden competencies.
index 41019e2..62a1ca7 100644 (file)
@@ -202,7 +202,8 @@ class template_competency extends persistent {
                   JOIN {' . self::TABLE . '} tplcomp
                     ON tplcomp.competencyid = comp.id
                  WHERE tplcomp.templateid = ?
-              ORDER BY tplcomp.sortorder ASC';
+              ORDER BY tplcomp.sortorder ASC,
+                       tplcomp.id ASC';
         $params = array($templateid);
 
         $results = $DB->get_records_sql($sql, $params);
index f003d38..1cf1272 100644 (file)
@@ -508,7 +508,8 @@ class user_competency extends persistent {
             $sql = "competencyid $insql";
         }
 
-        return self::get_records_select("userid = :userid AND $sql", $params);
+        // Order by ID to prevent random ordering.
+        return self::get_records_select("userid = :userid AND $sql", $params, 'id ASC');
     }
 
     /**
index e731ca6..c139061 100644 (file)
@@ -227,7 +227,8 @@ class user_competency_course extends persistent {
             $sql = "competencyid $insql";
         }
 
-        return self::get_records_select("userid = :userid AND courseid = :courseid AND $sql", $params);
+        // Order by ID to prevent random ordering.
+        return self::get_records_select("userid = :userid AND courseid = :courseid AND $sql", $params, 'id ASC');
     }
 
     /**
index 81b7791..e493e5b 100644 (file)
@@ -189,7 +189,8 @@ class user_competency_plan extends persistent {
                     ON ucp.competencyid = c.id
                    AND ucp.userid = :userid
                  WHERE ucp.planid = :planid
-              ORDER BY ucp.sortorder ASC';
+              ORDER BY ucp.sortorder ASC,
+                       ucp.id ASC';
         $params = array('userid' => $userid, 'planid' => $planid);
 
         $results = $DB->get_recordset_sql($sql, $params);
@@ -258,7 +259,8 @@ class user_competency_plan extends persistent {
             $sql = "competencyid $insql";
         }
 
-        return static::get_records_select("userid = :userid AND planid = :planid AND $sql", $params);
+        // Order by ID to prevent random ordering.
+        return static::get_records_select("userid = :userid AND planid = :planid AND $sql", $params, 'id ASC');
     }
 
     /**
index d0f86eb..4ede634 100644 (file)
@@ -2686,17 +2686,17 @@ class core_competency_api_testcase extends advanced_testcase {
         $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency');
         $user = $dg->create_user();
 
-        $dg->create_scale(array("id" => "1", "scale" => "value1, value2"));
-        $dg->create_scale(array("id" => "2", "scale" => "value3, value4, value5, value6"));
+        $s1 = $dg->create_scale(array("scale" => "value1, value2"));
+        $s2 = $dg->create_scale(array("scale" => "value3, value4, value5, value6"));
 
-        $scaleconfiguration1 = '[{"scaleid":"1"},{"name":"value1","id":1,"scaledefault":1,"proficient":0},' .
+        $scaleconfiguration1 = '[{"scaleid":"'.$s1->id.'"},{"name":"value1","id":1,"scaledefault":1,"proficient":0},' .
                 '{"name":"value2","id":2,"scaledefault":0,"proficient":1}]';
-        $scaleconfiguration2 = '[{"scaleid":"2"},{"name":"value3","id":1,"scaledefault":1,"proficient":0},'
+        $scaleconfiguration2 = '[{"scaleid":"'.$s2->id.'"},{"name":"value3","id":1,"scaledefault":1,"proficient":0},'
                 . '{"name":"value4","id":2,"scaledefault":0,"proficient":1}]';
 
         // Create a framework with scale configuration1.
         $frm = array(
-            'scaleid' => 1,
+            'scaleid' => $s1->id,
             'scaleconfiguration' => $scaleconfiguration1
         );
         $framework = $lpg->create_framework($frm);
@@ -2704,7 +2704,7 @@ class core_competency_api_testcase extends advanced_testcase {
 
         // Create competency with its own scale configuration.
         $c2 = $lpg->create_competency(array('competencyframeworkid' => $framework->get_id(),
-                                            'scaleid' => 2,
+                                            'scaleid' => $s2->id,
                                             'scaleconfiguration' => $scaleconfiguration2
                                         ));
 
@@ -3961,7 +3961,6 @@ class core_competency_api_testcase extends advanced_testcase {
         $dg = $this->getDataGenerator();
         $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency');
 
-        $currenttime = time();
         $syscontext = context_system::instance();
 
         // Create users.
@@ -3979,10 +3978,8 @@ class core_competency_api_testcase extends advanced_testcase {
         $pc1 = $lpg->create_plan_competency(array('planid' => $p1->get_id(), 'competencyid' => $c1->get_id()));
         $pc2 = $lpg->create_plan_competency(array('planid' => $p2->get_id(), 'competencyid' => $c1->get_id()));
 
-        // Create user competency. Add user_evidence and associate it to the user competency.
+        // Create user competency and add an evidence.
         $uc = $lpg->create_user_competency(array('userid' => $user->id, 'competencyid' => $c1->get_id()));
-        $ue = $lpg->create_user_evidence(array('userid' => $user->id));
-        $uec = $lpg->create_user_evidence_competency(array('userevidenceid' => $ue->get_id(), 'competencyid' => $c1->get_id()));
         $e1 = $lpg->create_evidence(array('usercompetencyid' => $uc->get_id()));
 
         // Check both plans as one evidence.
@@ -3990,24 +3987,18 @@ class core_competency_api_testcase extends advanced_testcase {
         $this->assertEquals(1, count(api::list_evidence($user->id, $c1->get_id(), $p2->get_id())));
 
         // Complete second plan.
-        $currenttime += 1;
         $p2->set_status(plan::STATUS_COMPLETE);
         $p2->update();
-        $plansql = "UPDATE {" . plan::TABLE . "} SET timemodified = :currenttime WHERE id = :planid";
-        $DB->execute($plansql, array('currenttime' => $currenttime, 'planid' => $p2->get_id()));
 
-        // Add an other user evidence for the same competency.
-        $currenttime += 1;
-        $ue2 = $lpg->create_user_evidence(array('userid' => $user->id));
-        $uec2 = $lpg->create_user_evidence_competency(array('userevidenceid' => $ue2->get_id(), 'competencyid' => $c1->get_id()));
+        // Add another evidence for the same competency, but in the future (time + 1).
         $e2 = $lpg->create_evidence(array('usercompetencyid' => $uc->get_id()));
         $evidencesql = "UPDATE {" . evidence::TABLE . "} SET timecreated = :currenttime WHERE id = :evidenceid";
-        $DB->execute($evidencesql, array('currenttime' => $currenttime, 'evidenceid' => $e2->get_id()));
+        $DB->execute($evidencesql, array('currenttime' => time() + 1, 'evidenceid' => $e2->get_id()));
 
-        // Check first plan which is not completed as all evidences.
+        // Check that the first plan, which is not completed, has all the evidence.
         $this->assertEquals(2, count(api::list_evidence($user->id, $c1->get_id(), $p1->get_id())));
 
-        // Check second plan completed before the new evidence as only the first evidence.
+        // Check that the second plan, completed before the new evidence, only has the first piece of evidence.
         $listevidences = api::list_evidence($user->id, $c1->get_id(), $p2->get_id());
         $this->assertEquals(1, count($listevidences));
         $this->assertEquals($e1->get_id(), $listevidences[$e1->get_id()]->get_id());
index f280e5f..6be6bf0 100644 (file)
@@ -69,6 +69,18 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
     /** @var int User role id */
     protected $userrole = null;
 
+    /** @var stdClass $scale1 Scale */
+    protected $scale1 = null;
+
+    /** @var stdClass $scale2 Scale */
+    protected $scale2 = null;
+
+    /** @var stdClass $scale3 Scale */
+    protected $scale3 = null;
+
+    /** @var stdClass $scale4 Scale */
+    protected $scale4 = null;
+
     /** @var string scaleconfiguration */
     protected $scaleconfiguration1 = null;
 
@@ -153,31 +165,36 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
         $this->category = $category;
         $this->othercategory = $othercategory;
 
-        $this->getDataGenerator()->create_scale(array("id" => "1", "scale" => "value1, value2"));
-        $this->getDataGenerator()->create_scale(array("id" => "2", "scale" => "value3, value4"));
-        $this->getDataGenerator()->create_scale(array("id" => "3", "scale" => "value5, value6"));
-        $this->getDataGenerator()->create_scale(array("id" => "4", "scale" => "value7, value8"));
+        $this->scale1 = $this->getDataGenerator()->create_scale(array("scale" => "value1, value2"));
+        $this->scale2 = $this->getDataGenerator()->create_scale(array("scale" => "value3, value4"));
+        $this->scale3 = $this->getDataGenerator()->create_scale(array("scale" => "value5, value6"));
+        $this->scale4 = $this->getDataGenerator()->create_scale(array("scale" => "value7, value8"));
 
-        $this->scaleconfiguration1 = '[{"scaleid":"1"},{"name":"value1","id":1,"scaledefault":1,"proficient":0},' .
+        $this->scaleconfiguration1 = '[{"scaleid":"'.$this->scale1->id.'"},' .
+                '{"name":"value1","id":1,"scaledefault":1,"proficient":0},' .
                 '{"name":"value2","id":2,"scaledefault":0,"proficient":1}]';
-        $this->scaleconfiguration2 = '[{"scaleid":"2"},{"name":"value3","id":1,"scaledefault":1,"proficient":0},' .
+        $this->scaleconfiguration2 = '[{"scaleid":"'.$this->scale2->id.'"},' .
+                '{"name":"value3","id":1,"scaledefault":1,"proficient":0},' .
                 '{"name":"value4","id":2,"scaledefault":0,"proficient":1}]';
-        $this->scaleconfiguration3 = '[{"scaleid":"3"},{"name":"value5","id":1,"scaledefault":1,"proficient":0},' .
+        $this->scaleconfiguration3 = '[{"scaleid":"'.$this->scale3->id.'"},' .
+                '{"name":"value5","id":1,"scaledefault":1,"proficient":0},' .
                 '{"name":"value6","id":2,"scaledefault":0,"proficient":1}]';
-        $this->scaleconfiguration4 = '[{"scaleid":"4"},{"name":"value8","id":1,"scaledefault":1,"proficient":0},' .
+        $this->scaleconfiguration4 = '[{"scaleid":"'.$this->scale4->id.'"},'.
+                '{"name":"value8","id":1,"scaledefault":1,"proficient":0},' .
                 '{"name":"value8","id":2,"scaledefault":0,"proficient":1}]';
         accesslib_clear_all_caches_for_unit_testing();
     }
 
 
     protected function create_competency_framework($number = 1, $system = true) {
+        $scalename = 'scale' . $number;
         $scalepropname = 'scaleconfiguration' . $number;
         $framework = array(
             'shortname' => 'shortname' . $number,
             'idnumber' => 'idnumber' . $number,
             'description' => 'description' . $number,
             'descriptionformat' => FORMAT_HTML,
-            'scaleid' => $number,
+            'scaleid' => $this->$scalename->id,
             'scaleconfiguration' => $this->$scalepropname,
             'visible' => true,
             'contextid' => $system ? context_system::instance()->id : context_coursecat::instance($this->category->id)->id
@@ -241,6 +258,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
     }
 
     protected function update_competency_framework($id, $number = 1, $system = true) {
+        $scalename = 'scale' . $number;
         $scalepropname = 'scaleconfiguration' . $number;
         $framework = array(
             'id' => $id,
@@ -248,7 +266,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
             'idnumber' => 'idnumber' . $number,
             'description' => 'description' . $number,
             'descriptionformat' => FORMAT_HTML,
-            'scaleid' => $number,
+            'scaleid' => $this->$scalename->id,
             'scaleconfiguration' => $this->$scalepropname,
             'visible' => true,
             'contextid' => $system ? context_system::instance()->id : context_coursecat::instance($this->category->id)->id
@@ -316,7 +334,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals('idnumber1', $result->idnumber);
         $this->assertEquals('description1', $result->description);
         $this->assertEquals(FORMAT_HTML, $result->descriptionformat);
-        $this->assertEquals(1, $result->scaleid);
+        $this->assertEquals($this->scale1->id, $result->scaleid);
         $this->assertEquals($this->scaleconfiguration1, $result->scaleconfiguration);
         $this->assertEquals(true, $result->visible);
     }
@@ -335,7 +353,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals('idnumber1', $result->idnumber);
         $this->assertEquals('description1', $result->description);
         $this->assertEquals(FORMAT_HTML, $result->descriptionformat);
-        $this->assertEquals(1, $result->scaleid);
+        $this->assertEquals($this->scale1->id, $result->scaleid);
         $this->assertEquals($this->scaleconfiguration1, $result->scaleconfiguration);
         $this->assertEquals(true, $result->visible);
 
@@ -358,7 +376,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
             'idnumber' => 'id;"number',
             'description' => 'de<>\\..scription',
             'descriptionformat' => FORMAT_HTML,
-            'scaleid' => 1,
+            'scaleid' => $this->scale1->id,
             'scaleconfiguration' => $this->scaleconfiguration1,
             'visible' => true,
             'contextid' => context_system::instance()->id
@@ -384,7 +402,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals('idnumber1', $result->idnumber);
         $this->assertEquals('description1', $result->description);
         $this->assertEquals(FORMAT_HTML, $result->descriptionformat);
-        $this->assertEquals(1, $result->scaleid);
+        $this->assertEquals($this->scale1->id, $result->scaleid);
         $this->assertEquals($this->scaleconfiguration1, $result->scaleconfiguration);
         $this->assertEquals(true, $result->visible);
     }
@@ -410,7 +428,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals('idnumber2', $result->idnumber);
         $this->assertEquals('description2', $result->description);
         $this->assertEquals(FORMAT_HTML, $result->descriptionformat);
-        $this->assertEquals(2, $result->scaleid);
+        $this->assertEquals($this->scale2->id, $result->scaleid);
         $this->assertEquals($this->scaleconfiguration2, $result->scaleconfiguration);
         $this->assertEquals(true, $result->visible);
 
@@ -444,7 +462,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals('idnumber1', $result->idnumber);
         $this->assertEquals('description1', $result->description);
         $this->assertEquals(FORMAT_HTML, $result->descriptionformat);
-        $this->assertEquals(1, $result->scaleid);
+        $this->assertEquals($this->scale1->id, $result->scaleid);
         $this->assertEquals($this->scaleconfiguration1, $result->scaleconfiguration);
         $this->assertEquals(true, $result->visible);
     }
@@ -470,7 +488,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals('idnumber2', $result->idnumber);
         $this->assertEquals('description2', $result->description);
         $this->assertEquals(FORMAT_HTML, $result->descriptionformat);
-        $this->assertEquals(2, $result->scaleid);
+        $this->assertEquals($this->scale2->id, $result->scaleid);
         $this->assertEquals($this->scaleconfiguration2, $result->scaleconfiguration);
         $this->assertEquals(true, $result->visible);
 
@@ -579,12 +597,12 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
 
         $s1 = $this->getDataGenerator()->create_scale();
 
-        $f1 = $lpg->create_framework(array('scaleid' => 1));
-        $f2 = $lpg->create_framework(array('scaleid' => 1));
+        $f1 = $lpg->create_framework(array('scaleid' => $s1->id));
+        $f2 = $lpg->create_framework(array('scaleid' => $s1->id));
         $c1 = $lpg->create_competency(array('competencyframeworkid' => $f1->get_id()));
         $c2 = $lpg->create_competency(array('competencyframeworkid' => $f2->get_id()));
 
-        $this->assertEquals(1, $f1->get_scaleid());
+        $this->assertEquals($s1->id, $f1->get_scaleid());
 
         // Make the scale of f2 being used.
         $lpg->create_user_competency(array('userid' => $this->user->id, 'competencyid' => $c2->get_id()));
@@ -593,7 +611,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
         $result = $this->update_competency_framework($f1->get_id(), 3, true);
 
         $f1 = new \core_competency\competency_framework($f1->get_id());
-        $this->assertEquals(3, $f1->get_scaleid());
+        $this->assertEquals($this->scale3->id, $f1->get_scaleid());
 
         // Changing the framework where the scale is used.
         try {
@@ -645,7 +663,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals('idnumber1', $result->idnumber);
         $this->assertEquals('description1', $result->description);
         $this->assertEquals(FORMAT_HTML, $result->descriptionformat);
-        $this->assertEquals(1, $result->scaleid);
+        $this->assertEquals($this->scale1->id, $result->scaleid);
         $this->assertEquals($this->scaleconfiguration1, $result->scaleconfiguration);
         $this->assertEquals(true, $result->visible);
     }
@@ -732,7 +750,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals('idnumber1', $result->idnumber);
         $this->assertEquals('description1', $result->description);
         $this->assertEquals(FORMAT_HTML, $result->descriptionformat);
-        $this->assertEquals(1, $result->scaleid);
+        $this->assertEquals($this->scale1->id, $result->scaleid);
         $this->assertEquals($this->scaleconfiguration1, $result->scaleconfiguration);
         $this->assertEquals(true, $result->visible);
     }
index 35dc8d7..b47b573 100644 (file)
@@ -2,6 +2,6 @@
     "require-dev": {
         "phpunit/phpunit": "4.8.*",
         "phpunit/dbUnit": "1.4.*",
-        "moodlehq/behat-extension": "3.31.1"
+        "moodlehq/behat-extension": "3.31.2"
     }
 }
index 8a46951..fccd56f 100644 (file)
@@ -4,8 +4,8 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "hash": "08ee36172d6de7fe083e753b44255ed7",
-    "content-hash": "2bc89ce1a925ac037c899ae6f02eaa26",
+    "hash": "ccba8f24cd70bd4ca9b78873fc4be17f",
+    "content-hash": "cf7a848add8e3de854561718a0d18986",
     "packages": [],
     "packages-dev": [
         {
         },
         {
             "name": "moodlehq/behat-extension",
-            "version": "v3.31.1",
+            "version": "v3.31.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/moodlehq/moodle-behat-extension.git",
-                "reference": "d876ea5940e7ad115318140ae37f228c70450225"
+                "reference": "f0b6a44de9111fd4fa82796aca712b9e9772d07e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/d876ea5940e7ad115318140ae37f228c70450225",
-                "reference": "d876ea5940e7ad115318140ae37f228c70450225",
+                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/f0b6a44de9111fd4fa82796aca712b9e9772d07e",
+                "reference": "f0b6a44de9111fd4fa82796aca712b9e9772d07e",
                 "shasum": ""
             },
             "require": {
                 "Behat",
                 "moodle"
             ],
-            "time": "2016-04-01 01:57:33"
+            "time": "2016-05-09 03:32:06"
         },
         {
             "name": "phpdocumentor/reflection-docblock",
         },
         {
             "name": "react/promise",
-            "version": "v2.4.0",
+            "version": "v2.4.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/reactphp/promise.git",
-                "reference": "f942da7b505d1a294284ab343d05df42d02ad6d9"
+                "reference": "8025426794f1944de806618671d4fa476dc7626f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/reactphp/promise/zipball/f942da7b505d1a294284ab343d05df42d02ad6d9",
-                "reference": "f942da7b505d1a294284ab343d05df42d02ad6d9",
+                "url": "https://api.github.com/repos/reactphp/promise/zipball/8025426794f1944de806618671d4fa476dc7626f",
+                "reference": "8025426794f1944de806618671d4fa476dc7626f",
                 "shasum": ""
             },
             "require": {
                 }
             ],
             "description": "A lightweight implementation of CommonJS Promises/A for PHP",
-            "time": "2016-03-31 13:10:33"
+            "time": "2016-05-03 17:50:52"
         },
         {
             "name": "sebastian/comparator",
         },
         {
             "name": "sebastian/environment",
-            "version": "1.3.5",
+            "version": "1.3.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/environment.git",
-                "reference": "dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf"
+                "reference": "2292b116f43c272ff4328083096114f84ea46a56"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf",
-                "reference": "dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf",
+                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/2292b116f43c272ff4328083096114f84ea46a56",
+                "reference": "2292b116f43c272ff4328083096114f84ea46a56",
                 "shasum": ""
             },
             "require": {
                 "environment",
                 "hhvm"
             ],
-            "time": "2016-02-26 18:40:46"
+            "time": "2016-05-04 07:59:13"
         },
         {
             "name": "sebastian/exporter",
         },
         {
             "name": "symfony/browser-kit",
-            "version": "v2.8.4",
+            "version": "v2.8.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/browser-kit.git",
         },
         {
             "name": "symfony/class-loader",
-            "version": "v2.8.4",
+            "version": "v2.8.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/class-loader.git",
-                "reference": "7d362c22710980730d46a5d039e788946a2938cb"
+                "reference": "f1cf312c81c7b4f0f11431e6fd37b66890f5e27b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/class-loader/zipball/7d362c22710980730d46a5d039e788946a2938cb",
-                "reference": "7d362c22710980730d46a5d039e788946a2938cb",
+                "url": "https://api.github.com/repos/symfony/class-loader/zipball/f1cf312c81c7b4f0f11431e6fd37b66890f5e27b",
+                "reference": "f1cf312c81c7b4f0f11431e6fd37b66890f5e27b",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony ClassLoader Component",
             "homepage": "https://symfony.com",
-            "time": "2016-03-10 19:33:53"
+            "time": "2016-03-30 10:37:34"
         },
         {
             "name": "symfony/config",
-            "version": "v2.8.4",
+            "version": "v2.8.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/config.git",
-                "reference": "5273f4724dc5288fe7a33cb08077ab9852621f2c"
+                "reference": "edbbcf33cffa2a85104fc80de8dc052cc51596bb"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/config/zipball/5273f4724dc5288fe7a33cb08077ab9852621f2c",
-                "reference": "5273f4724dc5288fe7a33cb08077ab9852621f2c",
+                "url": "https://api.github.com/repos/symfony/config/zipball/edbbcf33cffa2a85104fc80de8dc052cc51596bb",
+                "reference": "edbbcf33cffa2a85104fc80de8dc052cc51596bb",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Config Component",
             "homepage": "https://symfony.com",
-            "time": "2016-03-04 07:54:35"
+            "time": "2016-04-20 18:52:26"
         },
         {
             "name": "symfony/console",
-            "version": "v2.8.4",
+            "version": "v2.8.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "9a5aef5fc0d4eff86853d44202b02be8d5a20154"
+                "reference": "48221d3de4dc22d2cd57c97e8b9361821da86609"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/9a5aef5fc0d4eff86853d44202b02be8d5a20154",
-                "reference": "9a5aef5fc0d4eff86853d44202b02be8d5a20154",
+                "url": "https://api.github.com/repos/symfony/console/zipball/48221d3de4dc22d2cd57c97e8b9361821da86609",
+                "reference": "48221d3de4dc22d2cd57c97e8b9361821da86609",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Console Component",
             "homepage": "https://symfony.com",
-            "time": "2016-03-17 09:19:04"
+            "time": "2016-04-26 12:00:47"
         },
         {
             "name": "symfony/css-selector",
-            "version": "v2.8.4",
+            "version": "v2.8.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
         },
         {
             "name": "symfony/dependency-injection",
-            "version": "v2.8.4",
+            "version": "v2.8.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dependency-injection.git",
-                "reference": "f7b4a498e679fa440b16facb934680a1527ed48c"
+                "reference": "35ac8cd26e4477d79e5cbd4f11d41dc92fed4d8d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f7b4a498e679fa440b16facb934680a1527ed48c",
-                "reference": "f7b4a498e679fa440b16facb934680a1527ed48c",
+                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/35ac8cd26e4477d79e5cbd4f11d41dc92fed4d8d",
+                "reference": "35ac8cd26e4477d79e5cbd4f11d41dc92fed4d8d",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony DependencyInjection Component",
             "homepage": "https://symfony.com",
-            "time": "2016-03-21 07:27:21"
+            "time": "2016-04-20 14:12:37"
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v2.8.4",
+            "version": "v2.8.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "aae5c37d243c6ec11db62221aaff37e7f8005926"
+                "reference": "f282b08f6bbbc72e7af2e9e0c2f896221053f791"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/aae5c37d243c6ec11db62221aaff37e7f8005926",
-                "reference": "aae5c37d243c6ec11db62221aaff37e7f8005926",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/f282b08f6bbbc72e7af2e9e0c2f896221053f791",
+                "reference": "f282b08f6bbbc72e7af2e9e0c2f896221053f791",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony DomCrawler Component",
             "homepage": "https://symfony.com",
-            "time": "2016-03-23 13:11:46"
+            "time": "2016-04-12 18:01:21"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v2.8.4",
+            "version": "v2.8.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "47d2d8cade9b1c3987573d2943bb9352536cdb87"
+                "reference": "81c4c51f7fd6d0d40961bd53dd60cade32db6ed6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/47d2d8cade9b1c3987573d2943bb9352536cdb87",
-                "reference": "47d2d8cade9b1c3987573d2943bb9352536cdb87",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/81c4c51f7fd6d0d40961bd53dd60cade32db6ed6",
+                "reference": "81c4c51f7fd6d0d40961bd53dd60cade32db6ed6",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony EventDispatcher Component",
             "homepage": "https://symfony.com",
-            "time": "2016-03-07 14:04:32"
+            "time": "2016-04-05 16:36:54"
         },
         {
             "name": "symfony/filesystem",
-            "version": "v2.8.4",
+            "version": "v2.8.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "f08ffdf229252cd2745558cb2112df43903bcae4"
+                "reference": "dee379131dceed90a429e951546b33edfe7dccbb"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/f08ffdf229252cd2745558cb2112df43903bcae4",
-                "reference": "f08ffdf229252cd2745558cb2112df43903bcae4",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/dee379131dceed90a429e951546b33edfe7dccbb",
+                "reference": "dee379131dceed90a429e951546b33edfe7dccbb",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "time": "2016-03-27 10:20:16"
+            "time": "2016-04-12 18:01:21"
         },
         {
             "name": "symfony/polyfill-apcu",
         },
         {
             "name": "symfony/process",
-            "version": "v2.8.4",
+            "version": "v2.8.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/process.git",
-                "reference": "fb467471952ef5cf8497c029980e556b47545333"
+                "reference": "1276bd9be89be039748cf753a2137f4ef149cd74"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/process/zipball/fb467471952ef5cf8497c029980e556b47545333",
-                "reference": "fb467471952ef5cf8497c029980e556b47545333",
+                "url": "https://api.github.com/repos/symfony/process/zipball/1276bd9be89be039748cf753a2137f4ef149cd74",
+                "reference": "1276bd9be89be039748cf753a2137f4ef149cd74",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Process Component",
             "homepage": "https://symfony.com",
-            "time": "2016-03-23 13:11:46"
+            "time": "2016-04-14 15:22:22"
         },
         {
             "name": "symfony/translation",
-            "version": "v2.8.4",
+            "version": "v2.8.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
         },
         {
             "name": "symfony/yaml",
-            "version": "v2.8.4",
+            "version": "v2.8.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/yaml.git",
-                "reference": "584e52cb8f788a887553ba82db6caacb1d6260bb"
+                "reference": "e4fbcc65f90909c999ac3b4dfa699ee6563a9940"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/yaml/zipball/584e52cb8f788a887553ba82db6caacb1d6260bb",
-                "reference": "584e52cb8f788a887553ba82db6caacb1d6260bb",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/e4fbcc65f90909c999ac3b4dfa699ee6563a9940",
+                "reference": "e4fbcc65f90909c999ac3b4dfa699ee6563a9940",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Yaml Component",
             "homepage": "https://symfony.com",
-            "time": "2016-03-04 07:54:35"
+            "time": "2016-03-29 19:00:15"
         }
     ],
     "aliases": [],
diff --git a/course/classes/search/mycourse.php b/course/classes/search/mycourse.php
new file mode 100644 (file)
index 0000000..a2161f6
--- /dev/null
@@ -0,0 +1,120 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Search area for Moodle courses I can access.
+ *
+ * @package    core_course
+ * @copyright  2016 Skylar Kelty <S.Kelty@kent.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_course\search;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Search area for Moodle courses I can access.
+ *
+ * @package    core_course
+ * @copyright  2016 Skylar Kelty <S.Kelty@kent.ac.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mycourse extends \core_search\area\base {
+
+    /**
+     * Returns recordset containing required data for indexing courses.
+     *
+     * @param int $modifiedfrom timestamp
+     * @return \moodle_recordset
+     */
+    public function get_recordset_by_timestamp($modifiedfrom = 0) {
+        global $DB;
+        return $DB->get_recordset_select('course', 'timemodified >= ?', array($modifiedfrom));
+    }
+
+    /**
+     * Returns the document associated with this course.
+     *
+     * @param stdClass $record
+     * @param array    $options
+     * @return \core_search\document
+     */
+    public function get_document($record, $options = array()) {
+        try {
+            $context = \context_course::instance($record->id);
+        } catch (\moodle_exception $ex) {
+            // Notify it as we run here as admin, we should see everything.
+            debugging('Error retrieving ' . $this->areaid . ' ' . $record->id . ' document, not all required data is available: ' .
+                $ex->getMessage(), DEBUG_DEVELOPER);
+            return false;
+        }
+        // Prepare associative array with data from DB.
+        $doc = \core_search\document_factory::instance($record->id, $this->componentname, $this->areaname);
+        $doc->set('title', $record->fullname);
+        $doc->set('content', content_to_text($record->summary, $record->summaryformat));
+        $doc->set('contextid', $context->id);
+        $doc->set('courseid', $record->id);
+        $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID);
+        $doc->set('modified', $record->timemodified);
+        $doc->set('description1', $record->shortname);
+
+        // Check if this document should be considered new.
+        if (isset($options['lastindexedtime']) && $options['lastindexedtime'] < $record->timecreated) {
+            // If the document was created after the last index time, it must be new.
+            $doc->set_is_new(true);
+        }
+
+        return $doc;
+    }
+
+    /**
+     * Whether the user can access the document or not.
+     *
+     * @param int $id The course instance id.
+     * @return int
+     */
+    public function check_access($id) {
+        global $DB;
+        $course = $DB->get_record('course', array('id' => $id));
+        if (!$course) {
+            return \core_search\manager::ACCESS_DELETED;
+        }
+        if (can_access_course($course)) {
+            return \core_search\manager::ACCESS_GRANTED;
+        }
+        return \core_search\manager::ACCESS_DENIED;
+    }
+
+    /**
+     * Link to the course.
+     *
+     * @param \core_search\document $doc
+     * @return \moodle_url
+     */
+    public function get_doc_url(\core_search\document $doc) {
+        return $this->get_context_url($doc);
+    }
+
+    /**
+     * Link to the course.
+     *
+     * @param \core_search\document $doc
+     * @return \moodle_url
+     */
+    public function get_context_url(\core_search\document $doc) {
+        return new \moodle_url('/course/view.php', array('id' => $doc->get('courseid')));
+    }
+}
index 7054241..ea687c6 100644 (file)
@@ -2179,6 +2179,7 @@ class core_course_external extends external_api {
             'requiredcapabilities' => $requiredcapabilities
         );
         $params = self::validate_parameters(self::search_courses_parameters(), $parameters);
+        self::validate_context(context_system::instance());
 
         $allowedcriterianames = array('search', 'modulelist', 'blocklist', 'tagid');
         if (!in_array($params['criterianame'], $allowedcriterianames)) {
diff --git a/course/tests/search_test.php b/course/tests/search_test.php
new file mode 100644 (file)
index 0000000..6750fbf
--- /dev/null
@@ -0,0 +1,164 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Course global search unit tests.
+ *
+ * @package     core
+ * @category    phpunit
+ * @copyright   2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php');
+
+/**
+ * Provides the unit tests for course global search.
+ *
+ * @package     core
+ * @category    phpunit
+ * @copyright   2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class course_search_testcase extends advanced_testcase {
+
+    /**
+     * @var string Area id
+     */
+    protected $mycoursesareaid = null;
+
+    public function setUp() {
+        $this->resetAfterTest(true);
+        set_config('enableglobalsearch', true);
+
+        $this->mycoursesareaid = \core_search\manager::generate_areaid('core_course', 'mycourse');
+
+        // Set \core_search::instance to the mock_search_engine as we don't require the search engine to be working to test this.
+        $search = testable_core_search::instance();
+    }
+
+    /**
+     * Indexing my courses contents.
+     *
+     * @return void
+     */
+    public function test_mycourses_indexing() {
+
+        // Returns the instance as long as the area is supported.
+        $searcharea = \core_search\manager::get_search_area($this->mycoursesareaid);
+        $this->assertInstanceOf('\core_course\search\mycourse', $searcharea);
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $course1 = self::getDataGenerator()->create_course();
+        $course2 = self::getDataGenerator()->create_course();
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'student');
+        $this->getDataGenerator()->enrol_user($user2->id, $course1->id, 'student');
+
+        $record = new stdClass();
+        $record->course = $course1->id;
+
+        // All records.
+        $recordset = $searcharea->get_recordset_by_timestamp(0);
+        $this->assertTrue($recordset->valid());
+        $nrecords = 0;
+        foreach ($recordset as $record) {
+            $this->assertInstanceOf('stdClass', $record);
+            $doc = $searcharea->get_document($record);
+            $this->assertInstanceOf('\core_search\document', $doc);
+            $nrecords++;
+        }
+        // If there would be an error/failure in the foreach above the recordset would be closed on shutdown.
+        $recordset->close();
+        $this->assertEquals(3, $nrecords);
+
+        // The +2 is to prevent race conditions.
+        $recordset = $searcharea->get_recordset_by_timestamp(time() + 2);
+
+        // No new records.
+        $this->assertFalse($recordset->valid());
+        $recordset->close();
+    }
+
+    /**
+     * Document contents.
+     *
+     * @return void
+     */
+    public function test_mycourses_document() {
+
+        // Returns the instance as long as the area is supported.
+        $searcharea = \core_search\manager::get_search_area($this->mycoursesareaid);
+        $this->assertInstanceOf('\core_course\search\mycourse', $searcharea);
+
+        $user = self::getDataGenerator()->create_user();
+        $course = self::getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'teacher');
+
+        $doc = $searcharea->get_document($course);
+        $this->assertInstanceOf('\core_search\document', $doc);
+        $this->assertEquals($course->id, $doc->get('itemid'));
+        $this->assertEquals($this->mycoursesareaid . '-' . $course->id, $doc->get('id'));
+        $this->assertEquals($course->id, $doc->get('courseid'));
+        $this->assertFalse($doc->is_set('userid'));
+        $this->assertEquals(\core_search\manager::NO_OWNER_ID, $doc->get('owneruserid'));
+        $this->assertEquals($course->fullname, $doc->get('title'));
+
+        // Not nice. Applying \core_search\document::set line breaks clean up.
+        $summary = preg_replace("/\s+/", ' ', trim(content_to_text($course->summary, $course->summaryformat), "\r\n"));
+        $this->assertEquals($summary, $doc->get('content'));
+        $this->assertEquals($course->shortname, $doc->get('description1'));
+    }
+
+    /**
+     * Document accesses.
+     *
+     * @return void
+     */
+    public function test_mycourses_access() {
+
+        // Returns the instance as long as the area is supported.
+        $searcharea = \core_search\manager::get_search_area($this->mycoursesareaid);
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $course1 = self::getDataGenerator()->create_course();
+        $course2 = self::getDataGenerator()->create_course(array('visible' => 0));
+        $course3 = self::getDataGenerator()->create_course();
+
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id, 'teacher');
+        $this->getDataGenerator()->enrol_user($user2->id, $course1->id, 'student');
+        $this->getDataGenerator()->enrol_user($user1->id, $course2->id, 'teacher');
+        $this->getDataGenerator()->enrol_user($user2->id, $course2->id, 'student');
+
+        $this->setUser($user1);
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($course1->id));
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($course2->id));
+        $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($course3->id));
+        $this->assertEquals(\core_search\manager::ACCESS_DELETED, $searcharea->check_access(-123));
+
+        $this->setUser($user2);
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED, $searcharea->check_access($course1->id));
+        $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($course2->id));
+        $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($course3->id));
+    }
+}
index efe55ff..122cbf1 100644 (file)
@@ -62,7 +62,7 @@ $string['sharedexternaltools'] = 'Shared external tools';
 $string['syncsettings'] = 'Synchronisation settings';
 $string['tooldoesnotexist'] = 'The requested tool does not exist.';
 $string['tasksyncgrades'] = 'Handles syncing grades with the consumer';
-$string['tasksyncmembers'] = 'handles syncing members with the consumer';
+$string['tasksyncmembers'] = 'Handles syncing members with the consumer';
 $string['toolsprovided'] = 'Tools provided';
 $string['tooltobeprovided'] = 'Tool to be provided';
 $string['userdefaultvalues'] = 'User default values';
index 1a88c50..0355f13 100644 (file)
@@ -136,7 +136,7 @@ class core_enrollib_testcase extends advanced_testcase {
         // Make sure sorting and columns work.
 
         $basefields = array('id', 'category', 'sortorder', 'shortname', 'fullname', 'idnumber',
-            'startdate', 'visible', 'groupmode', 'groupmodeforce');
+            'startdate', 'visible', 'groupmode', 'groupmodeforce', 'defaultgroupingid');
 
         $courses = enrol_get_all_users_courses($user2->id, true);
         $course = reset($courses);
index b2085da..5c1a07e 100644 (file)
@@ -42,12 +42,12 @@ $string['cannotsavemd5file'] = 'Enregistrament del fichièr md5 impossible';
 $string['cannotsavezipfile'] = 'Enregistrament del fichièr ZIP impossible';
 $string['cannotunzipfile'] = 'Descompression del fichièr ZIP impossibla';
 $string['componentisuptodate'] = 'Lo component es a jorn';
-$string['dmlexceptiononinstall'] = '<p>Una error de banca de donadas s\'es producha [{$a->errorcode}].<br />{$a->debuginfo}</p>';
+$string['dmlexceptiononinstall'] = '<p>Una error de banca de donadas s\'es produita [{$a->errorcode}].<br />{$a->debuginfo}</p>';
 $string['downloadedfilecheckfailed'] = 'La verificacion del fichièr telecargat a fracassat';
-$string['invalidmd5'] = 'Lo còde de contraròtle md5 es pas valid';
+$string['invalidmd5'] = 'Lo còdi de contraròtle md5 es pas valid';
 $string['missingrequiredfield'] = 'Un camp obligatòri es pas completat';
 $string['remotedownloaderror'] = '<p>Lo telecargament del component sus vòstre servidor a fracassat. Verificatz los reglatges de proxy. L\'extension cURL de PHP es bravament recomandada.</p>
-<p>Vos cal telecargar manualament lo fichièr <a href="{$a->url}">{$a->url}</a>, lo copiar sus vòstre servidor a l\'emplaçament « {$a->dest} » e lo descompressar a aqueste endrech.</p>';
+<p>Vos cal telecargar manualament lo fichièr <a href="{$a->url}">{$a->url}</a>, lo copiar sus vòstre servidor a l\'emplaçament « {$a->dest} » e lo descompressar a aqueste endreit.</p>';
 $string['wrongdestpath'] = 'Camin de destinacion incorrècte';
 $string['wrongsourcebase'] = 'Adreça URL de basa de la font incorrècta';
 $string['wrongzipfilename'] = 'Nom de fichièr ZIP incorrècte';
index be993f3..c7219ec 100644 (file)
@@ -80,6 +80,7 @@ $string['runindexer'] = 'Run indexer (real)';
 $string['runindexertest'] = 'Run indexer test';
 $string['score'] = 'Score';
 $string['search'] = 'Search';
+$string['search:mycourse'] = 'My courses';
 $string['searcharea'] = 'Search area';
 $string['searching'] = 'Searching in ...';
 $string['searchnotpermitted'] = 'You are not allowed to do a search';
index c12e545..72dd645 100644 (file)
@@ -2543,9 +2543,10 @@ class admin_setting_configexecutable extends admin_setting_configfile {
     public function output_html($data, $query='') {
         global $CFG;
         $default = $this->get_defaultsetting();
+        require_once("$CFG->libdir/filelib.php");
 
         if ($data) {
-            if (file_exists($data) and !is_dir($data) and is_executable($data)) {
+            if (file_exists($data) and !is_dir($data) and file_is_executable($data)) {
                 $executable = '<span class="pathok">&#x2714;</span>';
             } else {
                 $executable = '<span class="patherror">&#x2718;</span>';
index d3d23ab..19a43b6 100644 (file)
@@ -41,6 +41,13 @@ if ($requests === null) {
 }
 $responses = array();
 
+// Defines the external settings required for Ajax processing.
+$settings = external_settings::get_instance();
+$settings->set_file('pluginfile.php');
+$settings->set_fileurl(true);
+$settings->set_filter(true);
+$settings->set_raw(false);
+
 foreach ($requests as $request) {
     $response = array();
     $methodname = clean_param($request['methodname'], PARAM_ALPHANUMEXT);
index 837d4dc..c3695e9 100644 (file)
Binary files a/lib/amd/build/tree.min.js and b/lib/amd/build/tree.min.js differ
index 195258f..0efa999 100644 (file)
@@ -326,7 +326,7 @@ define(['jquery'], function($) {
     Tree.prototype.handleKeyDown = function(item, e) {
         var currentIndex = this.getVisibleItems().index(item);
 
-        if ((e.altKey || e.ctrlKey) || (e.shiftKey && e.keyCode != this.keys.tab)) {
+        if ((e.altKey || e.ctrlKey || e.metaKey) || (e.shiftKey && e.keyCode != this.keys.tab)) {
             // Do nothing.
             return true;
         }
@@ -443,7 +443,7 @@ define(['jquery'], function($) {
      */
     Tree.prototype.handleClick = function(item, e) {
 
-        if (e.altKey || e.ctrlKey || e.shiftKey) {
+        if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
             // Do nothing.
             return true;
         }
index 3685809..a2b4a6b 100644 (file)
@@ -72,8 +72,12 @@ class core_date {
 
         if (is_numeric($currentvalue)) {
             // UTC offset.
-            $modifier = ($currentvalue > 0) ? '+' : '';
-            $a = 'UTC' . $modifier . number_format($currentvalue, 1);
+            if ($currentvalue == 0) {
+                $a = 'UTC';
+            } else {
+                $modifier = ($currentvalue > 0) ? '+' : '';
+                $a = 'UTC' . $modifier . number_format($currentvalue, 1);
+            }
             $timezones[$currentvalue] = get_string('timezoneinvalid', 'core_admin', $a);
         } else {
             // Some string we don't recognise.
@@ -508,7 +512,7 @@ class core_date {
         );
 
         // Legacy GMT fallback.
-        for ($i = -14; $i <= 13; $i++) {
+        for ($i = -12; $i <= 14; $i++) {
             $off = abs($i);
             if ($i < 0) {
                 $mapto = 'Etc/GMT+' . $off;
index c06a84c..a730cc0 100644 (file)
@@ -337,8 +337,8 @@ class core_user {
                 'choices' => array_merge(array('' => ''), \core_calendar\type_factory::get_list_of_calendar_types()));
         $fields['theme'] = array('type' => PARAM_THEME, 'null' => NULL_NOT_ALLOWED,
                 'default' => theme_config::DEFAULT_THEME, 'choices' => array_merge(array('' => ''), get_list_of_themes()));
-        $fields['timezone'] = array('type' => PARAM_TIMEZONE, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->timezone,
-                'choices' => core_date::get_list_of_timezones(null, true));
+        $fields['timezone'] = array('type' => PARAM_TIMEZONE, 'null' => NULL_NOT_ALLOWED,
+                'default' => core_date::get_server_timezone()); // Must not use choices here: timezones can come and go.
         $fields['firstaccess'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
         $fields['lastaccess'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
         $fields['lastlogin'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
@@ -528,7 +528,7 @@ class core_user {
      * Get the choices of the property.
      *
      * This is a helper method to validate a value against a list of acceptable choices.
-     * For instance: country, timezone, language, themes and etc.
+     * For instance: country, language, themes and etc.
      *
      * @param string $property property name to be retrieved.
      * @throws coding_exception if the requested property name is invalid or if it does not has a list of choices.
index 764dc9e..f42e639 100644 (file)
@@ -789,6 +789,7 @@ function enrol_get_all_users_courses($userid, $onlyactive = false, $fields = NUL
     $basefields = array('id', 'category', 'sortorder',
             'shortname', 'fullname', 'idnumber',
             'startdate', 'visible',
+            'defaultgroupingid',
             'groupmode', 'groupmodeforce');
 
     if (empty($fields)) {
index 7c0e838..befa4e6 100644 (file)
@@ -853,6 +853,11 @@ function external_validate_format($format) {
  * The caller can change the format (raw) with the external_settings singleton
  * All web service servers must set this singleton when parsing the $_GET and $_POST.
  *
+ * <pre>
+ * Options are the same that in {@link format_string()} with some changes:
+ *      filter      : Can be set to false to force filters off, else observes {@link external_settings}.
+ * </pre>
+ *
  * @param string $str The string to be filtered. Should be plain text, expect
  * possibly for multilang tags.
  * @param boolean $striplinks To strip any link in the result text. Moodle 1.8 default changed from false to true! MDL-8713
@@ -872,7 +877,7 @@ function external_format_string($str, $contextid, $striplinks = true, $options =
     if (!$settings->get_raw()) {
         $context = context::instance_by_id($contextid);
         $options['context'] = $context;
-        $options['filter'] = $settings->get_filter();
+        $options['filter'] = isset($options['filter']) && !$options['filter'] ? false : $settings->get_filter();
         $str = format_string($str, $striplinks, $options);
     }
 
@@ -890,8 +895,7 @@ function external_format_string($str, $contextid, $striplinks = true, $options =
  *      trusted     :   If true the string won't be cleaned. Default false.
  *      noclean     :   If true the string won't be cleaned only if trusted is also true. Default false.
  *      nocache     :   If true the string will not be cached and will be formatted every call. Default false.
- *      filter      :   If true the string will be run through applicable filters as well. Default (different from format_text)
- *                      got form settings.
+ *      filter      :   Can be set to false to force filters off, else observes {@link external_settings}.
  *      para        :   If true then the returned string will be wrapped in div tags. Default (different from format_text) false.
  *                      Default changed because div tags are not commonly needed.
  *      newlines    :   If true then lines newline breaks will be converted to HTML newline breaks. Default true.
@@ -935,7 +939,7 @@ function external_format_text($text, $textformat, $contextid, $component, $filea
             }
         }
 
-        $options['filter'] = isset($options['filter']) ? $options['filter'] : $settings->get_filter();
+        $options['filter'] = isset($options['filter']) && !$options['filter'] ? false : $settings->get_filter();
         $options['para'] = isset($options['para']) ? $options['para'] : false;
         $options['context'] = context::instance_by_id($contextid);
         $options['allowid'] = isset($options['allowid']) ? $options['allowid'] : true;
index 246aaad..c26865d 100644 (file)
@@ -2659,6 +2659,35 @@ function file_modify_html_header($text) {
     return $text;
 }
 
+/**
+ * Tells whether the filename is executable.
+ *
+ * @link http://php.net/manual/en/function.is-executable.php
+ * @link https://bugs.php.net/bug.php?id=41062
+ * @param string $filename Path to the file.
+ * @return bool True if the filename exists and is executable; otherwise, false.
+ */
+function file_is_executable($filename) {
+    if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
+        if (is_executable($filename)) {
+            return true;
+        } else {
+            $fileext = strrchr($filename, '.');
+            // If we have an extension we can check if it is listed as executable.
+            if ($fileext && file_exists($filename) && !is_dir($filename)) {
+                $winpathext = strtolower(getenv('PATHEXT'));
+                $winpathexts = explode(';', $winpathext);
+
+                return in_array(strtolower($fileext), $winpathexts);
+            }
+
+            return false;
+        }
+    } else {
+        return is_executable($filename);
+    }
+}
+
 /**
  * RESTful cURL class
  *
index 6ccb84e..e4b1dde 100644 (file)
@@ -224,7 +224,7 @@ class file_storage {
     protected function create_converted_document(stored_file $file, $format) {
         global $CFG;
 
-        if (empty($CFG->pathtounoconv) || !is_executable(trim($CFG->pathtounoconv))) {
+        if (empty($CFG->pathtounoconv) || !file_is_executable(trim($CFG->pathtounoconv))) {
             // No conversions are possible, sorry.
             return false;
         }
index 3e4d908..2cd0d9f 100644 (file)
@@ -77,7 +77,7 @@ class core_date_testcase extends advanced_testcase {
         $this->assertSame('Pacific/Auckland', core_date::normalise_timezone(-14));
         $this->assertSame('Etc/GMT-12', core_date::normalise_timezone(12));
         $this->assertSame('Etc/GMT-13', core_date::normalise_timezone(13));
-        $this->assertSame('Pacific/Auckland', core_date::normalise_timezone(14));
+        $this->assertSame('Etc/GMT-14', core_date::normalise_timezone(14));
 
         $this->assertSame('Asia/Kabul', core_date::normalise_timezone(4.5));
         $this->assertSame('Asia/Kolkata', core_date::normalise_timezone(5.5));
@@ -107,7 +107,7 @@ class core_date_testcase extends advanced_testcase {
         $this->assertSame('Pacific/Auckland', core_date::normalise_timezone(-14));
         $this->assertSame('Etc/GMT-12', core_date::normalise_timezone(12));
         $this->assertSame('Etc/GMT-13', core_date::normalise_timezone(13));
-        $this->assertSame('Pacific/Auckland', core_date::normalise_timezone(14));
+        $this->assertSame('Etc/GMT-14', core_date::normalise_timezone(14));
 
         $this->setTimezone('Pacific/Auckland', 'Pacific/Auckland');
         $tz = new DateTimeZone('Pacific/Auckland');
@@ -334,6 +334,76 @@ class core_date_testcase extends advanced_testcase {
         $this->assertSame('Etc/GMT-1', date_default_timezone_get());
     }
 
+    public function legacyUserTimezoneProvider() {
+        return [
+            ['', 'Australia/Perth'],            // Fallback on default timezone.
+            ['-13.0', 'Australia/Perth'],       // Fallback on default timezone.
+            ['-12.5', 'Etc/GMT+12'],
+            ['-12.0', 'Etc/GMT+12'],
+            ['-11.5', 'Etc/GMT+11'],
+            ['-11.0', 'Etc/GMT+11'],
+            ['-10.5', 'Etc/GMT+10'],
+            ['-10.0', 'Etc/GMT+10'],
+            ['-9.5', 'Etc/GMT+9'],
+            ['-9.0', 'Etc/GMT+9'],
+            ['-8.5', 'Etc/GMT+8'],
+            ['-8.0', 'Etc/GMT+8'],
+            ['-7.5', 'Etc/GMT+7'],
+            ['-7.0', 'Etc/GMT+7'],
+            ['-6.5', 'Etc/GMT+6'],
+            ['-6.0', 'Etc/GMT+6'],
+            ['-5.5', 'Etc/GMT+5'],
+            ['-5.0', 'Etc/GMT+5'],
+            ['-4.5', 'Etc/GMT+4'],
+            ['-4.0', 'Etc/GMT+4'],
+            ['-3.5', 'Etc/GMT+3'],
+            ['-3.0', 'Etc/GMT+3'],
+            ['-2.5', 'Etc/GMT+2'],
+            ['-2.0', 'Etc/GMT+2'],
+            ['-1.5', 'Etc/GMT+1'],
+            ['-1.0', 'Etc/GMT+1'],
+            ['-0.5', 'Etc/GMT'],
+            ['0', 'Etc/GMT'],
+            ['0.0', 'Etc/GMT'],
+            ['0.5', 'Etc/GMT'],
+            ['1.0', 'Etc/GMT-1'],
+            ['1.5', 'Etc/GMT-1'],
+            ['2.0', 'Etc/GMT-2'],
+            ['2.5', 'Etc/GMT-2'],
+            ['3.0', 'Etc/GMT-3'],
+            ['3.5', 'Etc/GMT-3'],
+            ['4.0', 'Etc/GMT-4'],
+            ['4.5', 'Asia/Kabul'],
+            ['5.0', 'Etc/GMT-5'],
+            ['5.5', 'Asia/Kolkata'],
+            ['6.0', 'Etc/GMT-6'],
+            ['6.5', 'Asia/Rangoon'],
+            ['7.0', 'Etc/GMT-7'],
+            ['7.5', 'Etc/GMT-7'],
+            ['8.0', 'Etc/GMT-8'],
+            ['8.5', 'Etc/GMT-8'],
+            ['9.0', 'Etc/GMT-9'],
+            ['9.5', 'Australia/Darwin'],
+            ['10.0', 'Etc/GMT-10'],
+            ['10.5', 'Etc/GMT-10'],
+            ['11.0', 'Etc/GMT-11'],
+            ['11.5', 'Etc/GMT-11'],
+            ['12.0', 'Etc/GMT-12'],
+            ['12.5', 'Etc/GMT-12'],
+            ['13.0', 'Etc/GMT-13'],
+        ];
+    }
+
+    /**
+     * @dataProvider legacyUserTimezoneProvider
+     * @param string $tz The legacy timezone.
+     * @param string $expected The expected converted timezone.
+     */
+    public function test_get_legacy_user_timezone($tz, $expected) {
+        $this->setTimezone('Australia/Perth', 'Australia/Perth');
+        $this->assertEquals($expected, core_date::get_user_timezone($tz));
+    }
+
     public function test_get_user_timezone() {
         global $CFG, $USER;
         $this->resetAfterTest();
index c018617..39796cc 100644 (file)
@@ -96,6 +96,13 @@ class core_externallib_testcase extends advanced_testcase {
 </span></span>', FORMAT_HTML);
         $this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0), $correct);
 
+        // Filters can be opted out from by the developer.
+        $test = '$$ \pi $$';
+        $testformat = FORMAT_MARKDOWN;
+        $correct = array('<p>$$ \pi $$</p>
+', FORMAT_HTML);
+        $this->assertSame(external_format_text($test, $testformat, $context->id, 'core', '', 0, ['filter' => false]), $correct);
+
         $test = '<p><a id="test"></a><a href="#test">Text</a></p>';
         $testformat = FORMAT_HTML;
         $correct = array($test, FORMAT_HTML);
@@ -135,23 +142,47 @@ class core_externallib_testcase extends advanced_testcase {
     }
 
     public function test_external_format_string() {
+        $this->resetAfterTest();
         $settings = external_settings::get_instance();
-
         $currentraw = $settings->get_raw();
         $currentfilter = $settings->get_filter();
 
+        // Enable multilang filter to on content and heading.
+        filter_set_global_state('multilang', TEXTFILTER_ON);
+        filter_set_applies_to_strings('multilang', 1);
+        $filtermanager = filter_manager::instance();
+        $filtermanager->reset_caches();
+
         $settings->set_raw(true);
+        $settings->set_filter(true);
         $context = context_system::instance();
 
-        $test = '$$ \pi $$ <script>hi</script> <h3>there</h3>';
+        $test = '<span lang="en" class="multilang">EN</span><span lang="fr" class="multilang">FR</span> ' .
+            '<script>hi</script> <h3>there</h3>!';
         $correct = $test;
-        $this->assertSame(external_format_string($test, $context->id), $correct);
+        $this->assertSame($correct, external_format_string($test, $context->id));
 
         $settings->set_raw(false);
+        $settings->set_filter(false);
+
+        $test = '<span lang="en" class="multilang">EN</span><span lang="fr" class="multilang">FR</span> ' .
+            '<script>hi</script> <h3>there</h3>?';
+        $correct = 'ENFR hi there?';
+        $this->assertSame($correct, external_format_string($test, $context->id));
+
+        $settings->set_filter(true);
+
+        $test = '<span lang="en" class="multilang">EN</span><span lang="fr" class="multilang">FR</span> ' .
+            '<script>hi</script> <h3>there</h3>@';
+        $correct = 'EN hi there@';
+        $this->assertSame($correct, external_format_string($test, $context->id));
+
+        // Filters can be opted out.
+        $test = '<span lang="en" class="multilang">EN</span><span lang="fr" class="multilang">FR</span> ' .
+            '<script>hi</script> <h3>there</h3>%';
+        $correct = 'ENFR hi there%';
+        $this->assertSame($correct, external_format_string($test, $context->id, false, ['filter' => false]));
 
-        $test = '$$ \pi $$<script>hi</script> <h3>there</h3>';
-        $correct = '$$ \pi $$hi there';
-        $this->assertSame(external_format_string($test, $context->id), $correct);
 
         $settings->set_raw($currentraw);
         $settings->set_filter($currentfilter);
index fb9d5ae..34e05bc 100644 (file)
@@ -1582,7 +1582,7 @@ class core_moodlelib_testcase extends advanced_testcase {
                 'expectedoutput' => '1309485600'
             ),
             array(
-                'usertimezone' => '14', // Server time.
+                'usertimezone' => '-14', // Server time.
                 'year' => '2011',
                 'month' => '7',
                 'day' => '1',
index 3038abb..19817df 100644 (file)
@@ -71,7 +71,7 @@ class core_unoconv_testcase extends advanced_testcase {
     public function test_generate_pdf() {
         global $CFG;
 
-        if (empty($CFG->pathtounoconv) || !is_executable(trim($CFG->pathtounoconv))) {
+        if (empty($CFG->pathtounoconv) || !file_is_executable(trim($CFG->pathtounoconv))) {
             // No conversions are possible, sorry.
             return $this->markTestSkipped();
         }
@@ -90,7 +90,7 @@ class core_unoconv_testcase extends advanced_testcase {
     public function test_generate_markdown() {
         global $CFG;
 
-        if (empty($CFG->pathtounoconv) || !is_executable(trim($CFG->pathtounoconv))) {
+        if (empty($CFG->pathtounoconv) || !file_is_executable(trim($CFG->pathtounoconv))) {
             // No conversions are possible, sorry.
             return $this->markTestSkipped();
         }
index cc64ba0..03192a5 100644 (file)
@@ -363,15 +363,6 @@ class core_user_testcase extends advanced_testcase {
         $this->assertArrayNotHasKey('unknowntheme', $choices);
         $this->assertArrayNotHasKey('wrongtheme', $choices);
 
-        // Test against timezone property choices.
-        $choices = core_user::get_property_choices('timezone');
-        $this->assertArrayHasKey('America/Sao_Paulo', $choices);
-        $this->assertArrayHasKey('Australia/Perth', $choices);
-        $this->assertArrayHasKey('99', $choices);
-        $this->assertArrayHasKey('UTC', $choices);
-        $this->assertArrayNotHasKey('North Korea', $choices);
-        $this->assertArrayNotHasKey('New york', $choices);
-
         // Try to fetch type of a non-existent properties.
         $nonexistingproperty = 'language';
         $this->setExpectedException('coding_exception', 'Invalid property requested: ' . $nonexistingproperty);
@@ -386,6 +377,7 @@ class core_user_testcase extends advanced_testcase {
      */
     public function test_get_property_default() {
         global $CFG;
+        $this->resetAfterTest();
 
         $country = core_user::get_property_default('country');
         $this->assertEquals($CFG->country, $country);
@@ -400,12 +392,14 @@ class core_user_testcase extends advanced_testcase {
         $lang = core_user::get_property_default('lang');
         $this->assertEquals($CFG->lang, $lang);
 
+        $this->setTimezone('Europe/London', 'Pacific/Auckland');
+        core_user::reset_caches();
         $timezone = core_user::get_property_default('timezone');
-        $this->assertEquals($CFG->timezone, $timezone);
-        set_config('timezone', 99);
+        $this->assertEquals('Europe/London', $timezone);
+        $this->setTimezone('99', 'Pacific/Auckland');
         core_user::reset_caches();
         $timezone = core_user::get_property_default('timezone');
-        $this->assertEquals(99, $timezone);
+        $this->assertEquals('Pacific/Auckland', $timezone);
 
         $this->setExpectedException('coding_exception', 'Invalid property requested, or the property does not has a default value.');
         core_user::get_property_default('firstname');
index 8e80d16..3326bd0 100644 (file)
@@ -119,6 +119,17 @@ information provided here is intended especially for developers.
 * table_sql download process is using the new data formats plugin which you can't use if you are buffering any output
     * flexible_table::get_download_menu(), considered private, has been deleted. Use
       $OUTPUT->download_dataformat_selector() instead.
+  when building Xpath, or pass the unescaped value when using the named selector.
+* Add new file_is_executable(), to consistently check for executables even in Windows (PHP bug #41062).
+* Introduced new hooks for plugin developers.
+    - <component>_pre_course_category_delete($category)
+    - <component>_pre_course_delete($course)
+    - <component>_pre_course_module_delete($cm)
+    - <component>_pre_block_delete($instance)
+    - <component>_pre_user_delete($user)
+  These hooks allow developers to use the item in question before it is deleted by core. For example, if your plugin is
+  a module (plugins located in the mod folder) called 'xxx' and you wish to interact with the user object before it is
+  deleted then the function to create would be mod_xxx_pre_user_delete($user) in mod/xxx/lib.php.
 
 === 3.0 ===
 
index 5b5804a..6f235fd 100644 (file)
Binary files a/mod/assign/amd/build/grading_navigation_user_info.min.js and b/mod/assign/amd/build/grading_navigation_user_info.min.js differ
index 617af68..a58257f 100644 (file)
@@ -34,7 +34,7 @@ define(['jquery', 'core/notification', 'core/ajax', 'core/templates'], function(
     var UserInfo = function(selector) {
         this._regionSelector = selector;
         this._region = $(selector);
-        this._userCache = [];
+        this._userCache = {};
 
         $(document).on('user-changed', this._refreshUserInfo.bind(this));
     };
@@ -51,6 +51,17 @@ define(['jquery', 'core/notification', 'core/ajax', 'core/templates'], function(
     /** @type {Integer} Remember the last user id to prevent unnessecary reloads. */
     UserInfo.prototype._lastUserId = 0;
 
+    /**
+     * Get the assignment id
+     *
+     * @private
+     * @method _getAssignmentId
+     * @return int assignment id
+     */
+    UserInfo.prototype._getAssignmentId = function() {
+        return this._region.attr('data-assignmentid');
+    };
+
     /**
      * Get the user context - re-render the template in the page.
      *
@@ -92,18 +103,21 @@ define(['jquery', 'core/notification', 'core/ajax', 'core/templates'], function(
                 promise.resolve(this._userCache[userid]);
             } else {
                 // Load context from ajax.
+                var assignmentId = this._getAssignmentId();
                 var requests = ajax.call([{
-                    methodname: 'core_user_get_users_by_field',
-                    args: { field: 'id', values: [ userid ] }
+                    methodname: 'mod_assign_get_participant',
+                    args: {
+                        userid: userid,
+                        assignid: assignmentId,
+                        embeduser: true
+                    }
                 }]);
 
-                requests[0].done(function(result) {
-                    if (result.length < 1) {
+                requests[0].done(function(participant) {
+                    if (!participant.hasOwnProperty('id')) {
                         promise.reject('No users');
                     } else {
-                        $.each(result, function(index, user) {
-                            this._userCache[user.id] = user;
-                        }.bind(this));
+                        this._userCache[userid] = participant;
                         promise.resolve(this._userCache[userid]);
                     }
                 }.bind(this)).fail(notification.exception);
@@ -114,14 +128,22 @@ define(['jquery', 'core/notification', 'core/ajax', 'core/templates'], function(
                     identity = [];
                 // Render the template.
                 context.courseid = $('[data-region="grading-navigation-panel"]').attr('data-courseid');
-                // Build a string for the visible identity fields listed in showuseridentity config setting.
-                $.each(identityfields, function(i, k) {
-                    if (typeof context[k] !== 'undefined' && context[k] !== '') {
-                        context.hasidentity = true;
-                        identity.push(context[k]);
+
+                if (context.user) {
+                    // Build a string for the visible identity fields listed in showuseridentity config setting.
+                    $.each(identityfields, function(i, k) {
+                        if (typeof context.user[k] !== 'undefined' && context.user[k] !== '') {
+                            context.hasidentity = true;
+                            identity.push(context.user[k]);
+                        }
+                    });
+                    context.identity = identity.join(', ');
+
+                    // Add profile image url to context.
+                    if (context.user.profileimageurl) {
+                        context.profileimageurl = context.user.profileimageurl;
                     }
-                });
-                context.identity = identity.join(', ');
+                }
 
                 templates.render('mod_assign/grading_navigation_user_summary', context).done(function(html, js) {
                     // Update the page.
@@ -139,7 +161,8 @@ define(['jquery', 'core/notification', 'core/ajax', 'core/templates'], function(
                         this._region.fadeIn("fast");
                     }.bind(this));
                 }.bind(this)).fail(notification.exception);
-            });
+            }
+            .bind(this));
         }.bind(this)).fail(notification.exception);
     };
 
index 8d41225..79217a8 100644 (file)
@@ -210,4 +210,13 @@ $functions = array(
                 'ajax'          => true,
                 'capabilities'  => 'mod/assign:grade'
         ),
+        'mod_assign_get_participant' => array(
+                'classname'     => 'mod_assign_external',
+                'methodname'    => 'get_participant',
+                'classpath'     => 'mod/assign/externallib.php',
+                'description'   => 'Get a participant for an assignment, with some summary info about their submissions.',
+                'type'          => 'read',
+                'ajax'          => true,
+                'capabilities'  => 'mod/assign:view, mod/assign:viewgrades'
+        ),
 );
index 4523113..fa0b1a5 100644 (file)
@@ -26,6 +26,7 @@
 defined('MOODLE_INTERNAL') || die;
 
 require_once("$CFG->libdir/externallib.php");
+require_once("$CFG->dirroot/user/externallib.php");
 require_once("$CFG->dirroot/mod/assign/locallib.php");
 
 /**
@@ -2762,4 +2763,105 @@ class mod_assign_external extends external_api {
             ))
         );
     }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.1
+     */
+    public static function get_participant_parameters() {
+        return new external_function_parameters(
+            array(
+                'assignid' => new external_value(PARAM_INT, 'assign instance id'),
+                'userid' => new external_value(PARAM_INT, 'user id'),
+                'embeduser' => new external_value(PARAM_BOOL, 'user id', VALUE_DEFAULT, false),
+            )
+        );
+    }
+
+    /**
+     * Get the user participating in the given assignment. An error with code 'usernotincourse'
+     * is thrown is the user isn't a participant of the given assignment.
+     *
+     * @param int $assignid the assign instance id
+     * @param int $userid the user id
+     * @param bool $embeduser return user details (only applicable if not blind marking)
+     * @return array of warnings and status result
+     * @since Moodle 3.1
+     * @throws moodle_exception
+     */
+    public static function get_participant($assignid, $userid, $embeduser) {
+        global $DB, $CFG;
+        require_once($CFG->dirroot . "/mod/assign/locallib.php");
+        require_once($CFG->dirroot . "/user/lib.php");
+
+        $params = self::validate_parameters(self::get_participant_parameters(), array(
+            'assignid' => $assignid,
+            'userid' => $userid,
+            'embeduser' => $embeduser
+        ));
+
+        // Request and permission validation.
+        $assign = $DB->get_record('assign', array('id' => $params['assignid']), 'id', MUST_EXIST);
+        list($course, $cm) = get_course_and_cm_from_instance($assign, 'assign');
+
+        $context = context_module::instance($cm->id);
+        self::validate_context($context);
+
+        $assign = new assign($context, null, null);
+        $assign->require_view_grades();
+
+        $participant = $assign->get_participant($params['userid']);
+        if (!$participant) {
+            // No participant found so we can return early.
+            throw new moodle_exception('usernotincourse');
+        }
+
+        $return = array(
+            'id' => $participant->id,
+            'fullname' => $participant->fullname,
+            'submitted' => $participant->submitted,
+            'requiregrading' => $participant->requiregrading,
+            'blindmarking' => $assign->is_blind_marking(),
+        );
+
+        if (!empty($participant->groupid)) {
+            $return['groupid'] = $participant->groupid;
+        }
+        if (!empty($participant->groupname)) {
+            $return['groupname'] = $participant->groupname;
+        }
+
+        // Skip the expensive lookup of user detail if we're blind marking or the caller
+        // hasn't asked for user details to be embedded.
+        if (!$assign->is_blind_marking() && $embeduser) {
+            $return['user'] = user_get_user_details($participant, $course);
+        }
+
+        return $return;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 3.1
+     */
+    public static function get_participant_returns() {
+        $userdescription = core_user_external::user_description();
+        $userdescription->default = [];
+        $userdescription->required = VALUE_OPTIONAL;
+
+        return new external_single_structure(array(
+            'id' => new external_value(PARAM_INT, 'ID of the user'),
+            'fullname' => new external_value(PARAM_NOTAGS, 'The fullname of the user'),
+            'submitted' => new external_value(PARAM_BOOL, 'have they submitted their assignment'),
+            'requiregrading' => new external_value(PARAM_BOOL, 'is their submission waiting for grading'),
+            'blindmarking' => new external_value(PARAM_BOOL, 'is blind marking enabled for this assignment'),
+            'groupid' => new external_value(PARAM_INT, 'for group assignments this is the group id', VALUE_OPTIONAL),
+            'groupname' => new external_value(PARAM_NOTAGS, 'for group assignments this is the group name', VALUE_OPTIONAL),
+            'user' => $userdescription,
+        ));
+    }
 }
index 74a407b..e7dcbb8 100644 (file)
@@ -65,7 +65,7 @@ class mod_assign_grade_form extends moodleform {
      */
     protected function get_form_identifier() {
         $params = $this->_customdata[2];
-        return get_class($this) . '_' . $params['rownum'];
+        return get_class($this) . '_' . $params['userid'];
     }
 
     /**
index 2c5b5bd..8dfae96 100644 (file)
@@ -835,8 +835,14 @@ class assign_grading_table extends table_sql implements renderable {
         if (!$this->is_downloading() && $this->hasgrade) {
             $urlparams = array('id' => $this->assignment->get_course_module()->id,
                                'rownum' => 0,
-                               'action' => 'grader',
-                               'userid' => $row->userid);
+                               'action' => 'grader');
+
+            if ($this->assignment->is_blind_marking()) {
+                $urlparams['blindid'] = $this->assignment->get_uniqueid_for_user($row->userid);
+            } else {
+                $urlparams['userid'] = $row->userid;
+            }
+
             $url = new moodle_url('/mod/assign/view.php', $urlparams);
             $link = '<a href="' . $url . '" class="btn btn-primary">' . get_string('grade') . '</a>';
             $grade .= $link . $separator;
@@ -1004,10 +1010,15 @@ class assign_grading_table extends table_sql implements renderable {
 
         $actions = array();
 
-        $urlparams = array('id'=>$this->assignment->get_course_module()->id,
-                           'rownum'=>$this->rownum,
-                           'action' => 'grade',
-                           'useridlistid' => $this->assignment->get_useridlist_key_id());
+        $urlparams = array('id' => $this->assignment->get_course_module()->id,
+                               'rownum' => 0,
+                               'action' => 'grader');
+
+        if ($this->assignment->is_blind_marking()) {
+            $urlparams['blindid'] = $this->assignment->get_uniqueid_for_user($row->userid);
+        } else {
+            $urlparams['userid'] = $row->userid;
+        }
         $url = new moodle_url('/mod/assign/view.php', $urlparams);
         $noimage = null;
 
index de28b24..c5a2b1e 100644 (file)
@@ -1446,18 +1446,20 @@ class assign {
     }
 
     /**
-     * Get the submission status/grading status for all submissions in this assignment.
+     * Get the submission status/grading status for all submissions in this assignment for the
+     * given paticipants.
+     *
      * These statuses match the available filters (requiregrading, submitted, notsubmitted).
      * If this is a group assignment, group info is also returned.
      *
-     * @param int $currentgroup
-     * @return array List of user records with extra fields 'submitted', 'notsubmitted', 'requiregrading', 'groupid', 'groupname'
+     * @param array $participants an associative array where the key is the participant id and
+     *                            the value is the participant record.
+     * @return array an associative array where the key is the participant id and the value is
+     *               the participant record.
      */
-    public function list_participants_with_filter_status_and_group($currentgroup) {
+    private function get_submission_info_for_participants($participants) {
         global $DB;
 
-        $participants = $this->list_participants($currentgroup, false);
-
         if (empty($participants)) {
             return $participants;
         }
@@ -1523,6 +1525,24 @@ class assign {
         return $participants;
     }
 
+    /**
+     * Get the submission status/grading status for all submissions in this assignment.
+     * These statuses match the available filters (requiregrading, submitted, notsubmitted).
+     * If this is a group assignment, group info is also returned.
+     *
+     * @param int $currentgroup
+     * @return array List of user records with extra fields 'submitted', 'notsubmitted', 'requiregrading', 'groupid', 'groupname'
+     */
+    public function list_participants_with_filter_status_and_group($currentgroup) {
+        $participants = $this->list_participants($currentgroup, false);
+
+        if (empty($participants)) {
+            return $participants;
+        } else {
+            return $this->get_submission_info_for_participants($participants);
+        }
+    }
+
     /**
      * Load a list of users enrolled in the current course with the specified permission and group.
      * 0 for no group.
@@ -1564,6 +1584,29 @@ class assign {
         return $this->participants[$key];
     }
 
+    /**
+     * Load a user if they are enrolled in the current course. Populated with submission
+     * status for this assignment.
+     *
+     * @param int $userid
+     * @return null|stdClass user record
+     */
+    public function get_participant($userid) {
+        global $DB;
+
+        $participant = $DB->get_record('user', array('id' => $userid));
+        if (!$participant) {
+            return null;
+        }
+
+        if (!is_enrolled($this->context, $participant, 'mod/assign:submit', $this->show_only_active_users())) {
+            return null;
+        }
+
+        $result = $this->get_submission_info_for_participants(array($participant->id => $participant));
+        return $result[$participant->id];
+    }
+
     /**
      * Load a count of valid teams for this assignment.
      *
@@ -3347,7 +3390,7 @@ class assign {
         $o .= $this->get_renderer()->render($header);
 
         // If userid is passed - we are only grading a single student.
-        $rownum = required_param('rownum', PARAM_INT);
+        $rownum = optional_param('rownum', 0, PARAM_INT);
         $useridlistid = optional_param('useridlistid', $this->get_useridlist_key_id(), PARAM_ALPHANUM);
         $userid = optional_param('userid', 0, PARAM_INT);
         $attemptnumber = optional_param('attemptnumber', -1, PARAM_INT);
@@ -3360,6 +3403,7 @@ class assign {
             $useridlist = $SESSION->mod_assign_useridlist[$useridlistkey];
         } else {
             $rownum = 0;
+            $useridlistid = 0;
             $useridlist = array($userid);
         }
 
@@ -3479,11 +3523,11 @@ class assign {
 
         // Now show the grading form.
         if (!$mform) {
-            $pagination = array('rownum'=>$rownum,
-                                'useridlistid'=>$useridlistid,
-                                'last'=>$last,
-                                'userid'=>optional_param('userid', 0, PARAM_INT),
-                                'attemptnumber'=>$attemptnumber);
+            $pagination = array('rownum' => $rownum,
+                                'useridlistid' => $useridlistid,
+                                'last' => $last,
+                                'userid' => $userid,
+                                'attemptnumber' => $attemptnumber);
             $formparams = array($this, $data, $pagination);
             $mform = new mod_assign_grade_form(null,
                                                $formparams,
@@ -3779,6 +3823,11 @@ class assign {
         $o .= $this->get_renderer()->header();
 
         $userid = optional_param('userid', 0, PARAM_INT);
+        $blindid = optional_param('blindid', 0, PARAM_INT);
+
+        if (!$userid && $blindid) {
+            $userid = $this->get_user_id_for_uniqueid($blindid);
+        }
 
         $currentgroup = groups_get_activity_group($this->get_course_module(), true);
         $framegrader = new grading_app($userid, $currentgroup, $this);
@@ -3899,6 +3948,12 @@ class assign {
         // Need submit permission to submit an assignment.
         $userid = optional_param('userid', $USER->id, PARAM_INT);
         $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
+
+        // This variation on the url will link direct to this student.
+        // The benefit is the url will be the same every time for this student, so Atto autosave drafts can match up.
+        $returnparams = array('userid' => $userid, 'rownum' => 0, 'useridlistid' => 0);
+        $this->register_return_link('editsubmission', $returnparams);
+
         if ($userid == $USER->id) {
             if (!$this->can_edit_submission($userid, $USER->id)) {
                 print_error('nopermission');
@@ -6537,7 +6592,9 @@ class assign {
         $userid = isset($params['userid']) ? $params['userid'] : 0;
         $attemptnumber = isset($params['attemptnumber']) ? $params['attemptnumber'] : 0;
         $gradingpanel = !empty($params['gradingpanel']);
-        if (!$userid) {
+        $bothids = ($userid && $useridlistid);
+
+        if (!$userid || $bothids) {
             $useridlistkey = $this->get_useridlist_key($useridlistid);
             if (empty($SESSION->mod_assign_useridlist[$useridlistkey])) {
                 $SESSION->mod_assign_useridlist[$useridlistkey] = $this->get_grading_userid_list();
@@ -6703,6 +6760,8 @@ class assign {
         $mform->setType('attemptnumber', PARAM_INT);
         $mform->addElement('hidden', 'ajax', optional_param('ajax', 0, PARAM_INT));
         $mform->setType('ajax', PARAM_INT);
+        $mform->addElement('hidden', 'userid', optional_param('userid', 0, PARAM_INT));
+        $mform->setType('userid', PARAM_INT);
 
         if ($this->get_instance()->teamsubmission) {
             $mform->addElement('header', 'groupsubmissionsettings', get_string('groupsubmissionsettings', 'assign'));
@@ -7433,11 +7492,11 @@ class assign {
 
         $data = new stdClass();
 
-        $gradeformparams = array('rownum'=>$rownum,
-                                 'useridlistid'=>$useridlistid,
-                                 'last'=>false,
-                                 'attemptnumber'=>$attemptnumber,
-                                 'userid'=>optional_param('userid', 0, PARAM_INT));
+        $gradeformparams = array('rownum' => $rownum,
+                                 'useridlistid' => $useridlistid,
+                                 'last' => $last,
+                                 'attemptnumber' => $attemptnumber,
+                                 'userid' => $userid);
         $mform = new mod_assign_grade_form(null,
                                            array($this, $data, $gradeformparams),
                                            'post',
index 08f58b2..f2d154f 100644 (file)
@@ -49,3 +49,41 @@ Feature: In an assignment, students can add and edit text online
     Then I should see "Submitted for grading"
     And I should see "I'm the student second submission"
     And I should not see "I'm the student first submission"
+
+  @javascript
+  Scenario: Auto-draft save online text submission
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | student1 | Student | 1 | student1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And the following config values are set as admin:
+      | autosavefrequency | 1 | editor_atto |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn 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 |
+      | assignsubmission_onlinetext_enabled | 1 |
+      | assignsubmission_file_enabled | 0 |
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test assignment name"
+    When I press "Add submission"
+    And I set the following fields to these values:
+      | Online text | text submission |
+    # Wait for the draft auto save.
+    And I wait "2" seconds
+    And I follow "Course 1"
+    And I follow "Test assignment name"
+    When I press "Add submission"
+    # Confirm draft was restored.
+    Then I should see "text submission" in the "#id_onlinetext_editoreditable" "css_element"
index 47e1953..822d36e 100644 (file)
@@ -2037,4 +2037,276 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
         mod_assign_external::get_submission_status($assign->get_instance()->id, $student1->id);
 
     }
+
+    /**
+     * get_participant should throw an excaption if the requested assignment doesn't exist.
+     */
+    public function test_get_participant_no_assignment() {
+        $this->resetAfterTest(true);
+        $this->setExpectedException('moodle_exception');
+        mod_assign_external::get_participant('-1', '-1', false);
+    }
+
+    /**
+     * get_participant should throw a require_login_exception if the user doesn't have access
+     * to view assignments.
+     */
+    public function test_get_participant_no_view_capability() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $result = $this->create_assign_with_student_and_teacher();
+        $assign = $result['assign'];
+        $student = $result['student'];
+        $course = $result['course'];
+        $context = context_course::instance($course->id);
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+
+        $this->setUser($student);
+        assign_capability('mod/assign:view', CAP_PROHIBIT, $studentrole->id, $context->id, true);
+
+        $this->setExpectedException('require_login_exception');
+        mod_assign_external::get_participant($assign->id, $student->id, false);
+    }
+
+    /**
+     * get_participant should throw a required_capability_exception if the user doesn't have access
+     * to view assignment grades.
+     */
+    public function test_get_participant_no_grade_capability() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $result = $this->create_assign_with_student_and_teacher();
+        $assign = $result['assign'];
+        $student = $result['student'];
+        $teacher = $result['teacher'];
+        $course = $result['course'];
+        $context = context_course::instance($course->id);
+        $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
+
+        $this->setUser($teacher);
+        assign_capability('mod/assign:viewgrades', CAP_PROHIBIT, $teacherrole->id, $context->id, true);
+        assign_capability('mod/assign:grade', CAP_PROHIBIT, $teacherrole->id, $context->id, true);
+        accesslib_clear_all_caches_for_unit_testing();
+
+        $this->setExpectedException('required_capability_exception');
+        mod_assign_external::get_participant($assign->id, $student->id, false);
+    }
+
+    /**
+     * get_participant should throw an exception if the user isn't enrolled in the course.
+     */
+    public function test_get_participant_no_participant() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $result = $this->create_assign_with_student_and_teacher(array('blindmarking' => true));
+        $student = $this->getDataGenerator()->create_user();
+        $assign = $result['assign'];
+        $teacher = $result['teacher'];
+
+        $this->setUser($teacher);
+
+        $this->setExpectedException('moodle_exception');
+        $result = mod_assign_external::get_participant($assign->id, $student->id, false);
+    }
+
+    /**
+     * get_participant should return a summarised list of details with a different fullname if blind
+     * marking is on for the requested assignment.
+     */
+    public function test_get_participant_blind_marking() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $result = $this->create_assign_with_student_and_teacher(array('blindmarking' => true));
+        $assign = $result['assign'];
+        $student = $result['student'];
+        $teacher = $result['teacher'];
+        $course = $result['course'];
+        $context = context_course::instance($course->id);
+        $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
+
+        $this->setUser($teacher);
+
+        $result = mod_assign_external::get_participant($assign->id, $student->id, true);
+        $this->assertEquals($student->id, $result['id']);
+        $this->assertFalse(fullname($student) == $result['fullname']);
+        $this->assertFalse($result['submitted']);
+        $this->assertFalse($result['requiregrading']);
+        $this->assertTrue($result['blindmarking']);
+        // Make sure we don't get any additional info.
+        $this->assertTrue(empty($result['user']));
+    }
+
+    /**
+     * get_participant should return a summarised list of details if requested.
+     */
+    public function test_get_participant_no_user() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $result = $this->create_assign_with_student_and_teacher();
+        $assignmodule = $result['assign'];
+        $student = $result['student'];
+        $teacher = $result['teacher'];
+        $course = $result['course'];
+        $context = context_course::instance($course->id);
+        $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
+
+        // Create an assign instance to save a submission.
+        set_config('submissionreceipts', 0, 'assign');
+
+        $cm = get_coursemodule_from_instance('assign', $assignmodule->id);
+        $context = context_module::instance($cm->id);
+
+        $assign = new assign($context, $cm, $course);
+
+        $this->setUser($student);
+
+        // Simulate a submission.
+        $data = new stdClass();
+        $data->onlinetext_editor = array(
+            'itemid' => file_get_unused_draft_itemid(),
+            'text' => 'Student submission text',
+            'format' => FORMAT_MOODLE
+        );
+
+        $notices = array();
+        $assign->save_submission($data, $notices);
+
+        $data = new stdClass;
+        $data->userid = $student->id;
+        $assign->submit_for_grading($data, array());
+
+        $this->setUser($teacher);
+
+        $result = mod_assign_external::get_participant($assignmodule->id, $student->id, false);
+        $this->assertEquals($student->id, $result['id']);
+        $this->assertEquals(fullname($student), $result['fullname']);
+        $this->assertTrue($result['submitted']);
+        $this->assertTrue($result['requiregrading']);
+        $this->assertFalse($result['blindmarking']);
+        // Make sure we don't get any additional info.
+        $this->assertTrue(empty($result['user']));
+    }
+
+    /**
+     * get_participant should return user details if requested.
+     */
+    public function test_get_participant_full_details() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $result = $this->create_assign_with_student_and_teacher();
+        $assign = $result['assign'];
+        $student = $result['student'];
+        $teacher = $result['teacher'];
+        $course = $result['course'];
+        $context = context_course::instance($course->id);
+        $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
+
+        $this->setUser($teacher);
+
+        $result = mod_assign_external::get_participant($assign->id, $student->id, true);
+        // Check some of the extended properties we get when requesting the user.
+        $this->assertEquals($student->id, $result['id']);
+        // We should get user infomation back.
+        $user = $result['user'];
+        $this->assertFalse(empty($user));
+        $this->assertEquals($student->firstname, $user['firstname']);
+        $this->assertEquals($student->lastname, $user['lastname']);
+        $this->assertEquals($student->email, $user['email']);
+    }
+
+    /**
+     * get_participant should return group details if a group submission was
+     * submitted.
+     */
+    public function test_get_participant_group_submission() {
+        global $DB, $CFG;
+        require_once($CFG->dirroot . '/mod/assign/tests/base_test.php');
+
+        $this->resetAfterTest(true);
+
+        $result = $this->create_assign_with_student_and_teacher(array(
+            'assignsubmission_onlinetext_enabled' => 1,
+            'teamsubmission' => 1
+        ));
+        $assignmodule = $result['assign'];
+        $student = $result['student'];
+        $teacher = $result['teacher'];
+        $course = $result['course'];
+        $context = context_course::instance($course->id);
+        $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
+        $group = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+        $cm = get_coursemodule_from_instance('assign', $assignmodule->id);
+        $context = context_module::instance($cm->id);
+        $assign = new testable_assign($context, $cm, $course);
+
+        groups_add_member($group, $student);
+
+        $this->setUser($student);
+        $submission = $assign->get_group_submission($student->id, $group->id, true);
+        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
+        $assign->testable_update_submission($submission, $student->id, true, false);
+        $data = new stdClass();
+        $data->onlinetext_editor = array('itemid' => file_get_unused_draft_itemid(),
+                                         'text' => 'Submission text',
+                                         'format' => FORMAT_MOODLE);
+        $plugin = $assign->get_submission_plugin_by_type('onlinetext');
+        $plugin->save($submission, $data);
+
+        $this->setUser($teacher);
+
+        $result = mod_assign_external::get_participant($assignmodule->id, $student->id, false);
+        // Check some of the extended properties we get when not requesting a summary.
+        $this->assertEquals($student->id, $result['id']);
+        $this->assertEquals($group->id, $result['groupid']);
+        $this->assertEquals($group->name, $result['groupname']);
+    }
+
+    /**
+     * Create a a course, assignment module instance, student and teacher and enrol them in
+     * the course.
+     *
+     * @param array $params parameters to be provided to the assignment module creation
+     * @return array containing the course, assignment module, student and teacher
+     */
+    private function create_assign_with_student_and_teacher($params = array()) {
+        global $DB;
+
+        $course = $this->getDataGenerator()->create_course();
+        $params = array_merge(array(
+            'course' => $course->id,
+            'name' => 'assignment',
+            'intro' => 'assignment intro text',
+        ), $params);
+
+        // Create a course and assignment and users.
+        $assign = $this->getDataGenerator()->create_module('assign', $params);
+
+        $cm = get_coursemodule_from_instance('assign', $assign->id);
+        $context = context_module::instance($cm->id);
+
+        $student = $this->getDataGenerator()->create_user();
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
+        $teacher = $this->getDataGenerator()->create_user();
+        $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
+        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
+
+        assign_capability('mod/assign:view', CAP_ALLOW, $teacherrole->id, $context->id, true);
+        assign_capability('mod/assign:viewgrades', CAP_ALLOW, $teacherrole->id, $context->id, true);
+        assign_capability('mod/assign:grade', CAP_ALLOW, $teacherrole->id, $context->id, true);
+        accesslib_clear_all_caches_for_unit_testing();
+
+        return array(
+            'course' => $course,
+            'assign' => $assign,
+            'student' => $student,
+            'teacher' => $teacher
+        );
+    }
 }
index 49583e7..2062696 100644 (file)
@@ -721,6 +721,95 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
         $this->assertEquals(2, count($assign->list_participants(null, true)));
     }
 
+    public function test_get_participant_user_not_exist() {
+        $assign = $this->create_instance(array('grade' => 100));
+        $this->assertNull($assign->get_participant('-1'));
+    }
+
+    public function test_get_participant_not_enrolled() {
+        $assign = $this->create_instance(array('grade' => 100));
+        $user = $this->getDataGenerator()->create_user();
+        $this->assertNull($assign->get_participant($user->id));
+    }
+
+    public function test_get_participant_no_submission() {
+        $assign = $this->create_instance(array('grade' => 100));
+        $student = $this->students[0];
+        $participant = $assign->get_participant($student->id);
+
+        $this->assertEquals($student->id, $participant->id);
+        $this->assertFalse($participant->submitted);
+        $this->assertFalse($participant->requiregrading);
+    }
+
+    public function test_get_participant_with_ungraded_submission() {
+        $assign = $this->create_instance(array('grade' => 100));
+        $student = $this->students[0];
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+
+        $this->setUser($student);
+
+        // Simulate a submission.
+        $data = new stdClass();
+        $data->onlinetext_editor = array(
+            'itemid' => file_get_unused_draft_itemid(),
+            'text' => 'Student submission text',
+            'format' => FORMAT_MOODLE
+        );
+
+        $notices = array();
+        $assign->save_submission($data, $notices);
+
+        $data = new stdClass;
+        $data->userid = $student->id;
+        $assign->submit_for_grading($data, array());
+
+        $participant = $assign->get_participant($student->id);
+
+        $this->assertEquals($student->id, $participant->id);
+        $this->assertTrue($participant->submitted);
+        $this->assertTrue($participant->requiregrading);
+    }
+
+    public function test_get_participant_with_graded_submission() {
+        $assign = $this->create_instance(array('grade' => 100));
+        $student = $this->students[0];
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+
+        $this->setUser($student);
+
+        // Simulate a submission.
+        $data = new stdClass();
+        $data->onlinetext_editor = array(
+            'itemid' => file_get_unused_draft_itemid(),
+            'text' => 'Student submission text',
+            'format' => FORMAT_MOODLE
+        );
+
+        $notices = array();
+        $assign->save_submission($data, $notices);
+
+        $data = new stdClass;
+        $data->userid = $student->id;
+        $assign->submit_for_grading($data, array());
+
+        // This is to make sure the grade happens after the submission because
+        // we have no control over the timemodified values.
+        sleep(1);
+        // Grade the submission.
+        $this->setUser($this->teachers[0]);
+
+        $data = new stdClass();
+        $data->grade = '50.0';
+        $assign->testable_apply_grade_to_user($data, $student->id, 0);
+
+        $participant = $assign->get_participant($student->id);
+
+        $this->assertEquals($student->id, $participant->id);
+        $this->assertTrue($participant->submitted);
+        $this->assertFalse($participant->requiregrading);
+    }
+
     public function test_count_teams() {
         $this->create_extra_users();
         $this->setUser($this->editingteachers[0]);
index d302de3..2ed65ac 100644 (file)
@@ -25,6 +25,6 @@
 defined('MOODLE_INTERNAL') || die();
 
 $plugin->component = 'mod_assign'; // Full name of the plugin (used for diagnostics).
-$plugin->version  = 2016041300;    // The current module version (Date: YYYYMMDDXX).
+$plugin->version  = 2016041301;    // The current module version (Date: YYYYMMDDXX).
 $plugin->requires = 2015111000;    // Requires this Moodle version.
 $plugin->cron     = 60;
index 1b5025a..e15d26a 100644 (file)
     display: inline-block;
     vertical-align: middle;
 }
+
+/* Styling for LTI view */
+#contentframe {
+    border: 1px solid #ddd;
+    border-radius: 4px;
+}
index 8ed300e..cbf2d95 100644 (file)
@@ -93,9 +93,8 @@ class collaborative_page extends \core_search\area\base_mod {
             $content = wiki_refresh_cachedcontent($page);
             $page = $content['page'];
         }
-        // Convert to HTML, then to text. Makes sure content is cleaned.
-        $html = format_text($page->cachedcontent, FORMAT_MOODLE, array('overflowdiv' => true, 'allowid' => true));
-        $content = content_to_text($page->cachedcontent, FORMAT_HTML);
+        // Convert to text.
+        $content = content_to_text($page->cachedcontent, FORMAT_MOODLE);
 
         // Prepare associative array with data from DB.
         $doc = \core_search\document_factory::instance($record->id, $this->componentname, $this->areaname);
index 78aa3b8..e7caa34 100644 (file)
@@ -136,7 +136,14 @@ abstract class base {
      * @return string
      */
     public function get_visible_name($lazyload = false) {
-        return get_string('search:' . $this->areaname, $this->componentname, null, $lazyload);
+
+        $component = $this->componentname;
+
+        // Core subsystem strings go to lang/XX/search.php.
+        if ($this->componenttype === 'core') {
+            $component = 'search';
+        }
+        return get_string('search:' . $this->areaname, $component, null, $lazyload);
     }
 
     /**
@@ -150,8 +157,9 @@ abstract class base {
     public function get_config_var_name() {
 
         if ($this->componenttype === 'core') {
-            // Core subsystems config in search.
-            return array('search', $this->areaid);
+            // Core subsystems config in core_search and setting name using only [a-zA-Z0-9_]+.
+            $parts = \core_search\manager::extract_areaid_parts($this->areaid);
+            return array('core_search', $parts[0] . '_' . $parts[1]);
         }
 
         // Plugins config in the plugin scope.
index edf2273..72aebbb 100644 (file)
@@ -37,6 +37,43 @@ require_once($CFG->dirroot.'/user/lib.php');
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class core_userliblib_testcase extends advanced_testcase {
+    /**
+     * Test user_get_user_details_courses
+     */
+    public function test_user_get_user_details_courses() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Create user and modify user profile.
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course1->id);
+        $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
+        role_assign($teacherrole->id, $user1->id, $coursecontext->id);
+        role_assign($teacherrole->id, $user2->id, $coursecontext->id);
+
+        accesslib_clear_all_caches_for_unit_testing();
+
+        // Get user2 details as a user with super system capabilities.
+        $result = user_get_user_details_courses($user2);
+        $this->assertEquals($user2->id, $result['id']);
+        $this->assertEquals(fullname($user2), $result['fullname']);
+        $this->assertEquals($course1->id, $result['enrolledcourses'][0]['id']);
+
+        $this->setUser($user1);
+        // Get user2 details as a user who can only see this user in a course.
+        $result = user_get_user_details_courses($user2);
+        $this->assertEquals($user2->id, $result['id']);
+        $this->assertEquals(fullname($user2), $result['fullname']);
+        $this->assertEquals($course1->id, $result['enrolledcourses'][0]['id']);
+
+    }
+
     /**
      * Test user_update_user.
      */
@@ -99,7 +136,7 @@ class core_userliblib_testcase extends advanced_testcase {
         $user->country = 'WW';
         $user->lang = 'xy';
         $user->theme = 'somewrongthemename';
-        $user->timezone = 'Paris';
+        $user->timezone = '30.5';
         $user->url = 'wwww.somewrong@#$url.com.aus';
         $debugmessages = $this->getDebuggingMessages();
         user_update_user($user, true, false);
@@ -182,7 +219,7 @@ class core_userliblib_testcase extends advanced_testcase {
         $user['country'] = 'WW';
         $user['lang'] = 'xy';
         $user['theme'] = 'somewrongthemename';
-        $user['timezone'] = 'Paris';
+        $user['timezone'] = '-30.5';
         $user['url'] = 'wwww.somewrong@#$url.com.aus';
         $debugmessages = $this->getDebuggingMessages();
         $user['id'] = user_create_user($user, true, false);
@@ -190,7 +227,7 @@ class core_userliblib_testcase extends advanced_testcase {
         $dbuser = $DB->get_record('user', array('id' => $user['id']));
         $this->assertEquals($dbuser->country, 0);
         $this->assertEquals($dbuser->lang, 'en');
-        $this->assertEquals($dbuser->timezone, 'Australia/Perth');
+        $this->assertEquals($dbuser->timezone, '');
 
         // Now, with valid user data.
         $user['username'] = 'johndoe321';
index a32b155..aa11e66 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2016050600.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2016051000.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
-$release  = '3.1beta+ (Build: 20160506)'; // Human-friendly version name
+$release  = '3.1beta+ (Build: 20160510)'; // Human-friendly version name
 
 $branch   = '31';                       // This version's branch.
 $maturity = MATURITY_BETA;             // This version's maturity level.