Merge branch 'MDL-50704-master' of git://github.com/FMCorz/moodle
authorDan Poltawski <dan@moodle.com>
Tue, 10 May 2016 14:59:41 +0000 (15:59 +0100)
committerDan Poltawski <dan@moodle.com>
Tue, 10 May 2016 14:59:41 +0000 (15:59 +0100)
49 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/tests/api_test.php
competency/tests/external_test.php
composer.json
composer.lock
course/externallib.php
enrol/lti/lang/en/enrol_lti.php
enrol/tests/enrollib_test.php
install/lang/oc_lnc/error.php
lib/adminlib.php
lib/ajax/service.php
lib/amd/build/tree.min.js
lib/amd/src/tree.js
lib/enrollib.php
lib/externallib.php
lib/filelib.php
lib/filestorage/file_storage.php
lib/tests/externallib_test.php
lib/tests/unoconv_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
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 d0f86eb..1d322f5 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
                                         ));
 
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": [],
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)) {
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 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 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 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 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 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 77b410e..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.
      */
index a32b155..27988f2 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2016050600.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2016051000.00;              // 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.