Merge branch 'MDL-54040_master' of git://github.com/dmonllao/moodle
authorDan Poltawski <dan@moodle.com>
Mon, 9 May 2016 09:29:25 +0000 (10:29 +0100)
committerDan Poltawski <dan@moodle.com>
Mon, 9 May 2016 09:29:25 +0000 (10:29 +0100)
81 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/build/user_competency_course_navigation.min.js
admin/tool/lp/amd/src/tree.js
admin/tool/lp/amd/src/user_competency_course_navigation.js
admin/tool/lp/classes/external/user_competency_summary_in_course_exporter.php
admin/tool/lp/db/services.php
admin/tool/lp/styles.css
admin/tool/lp/version.php
admin/tool/recyclebin/tests/course_bin_test.php
auth/shibboleth/logout.php
backup/moodle2/backup_root_task.class.php
backup/moodle2/backup_settingslib.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_root_task.class.php
backup/moodle2/restore_settingslib.php
backup/moodle2/restore_stepslib.php
badges/lib.php
blocks/badges/tests/behat/block_badges.feature [new file with mode: 0644]
blocks/badges/tests/behat/block_badges_course.feature [new file with mode: 0644]
blocks/badges/tests/behat/block_badges_dashboard.feature [new file with mode: 0644]
blocks/badges/tests/behat/block_badges_frontpage.feature [new file with mode: 0644]
blocks/badges/tests/fixtures/badge.png [new file with mode: 0644]
composer.json
composer.lock
config-dist.php
course/externallib.php
course/tests/externallib_test.php
course/tests/restore_test.php [new file with mode: 0644]
enrol/editinstance.php
enrol/lti/classes/helper.php
enrol/lti/lib.php
enrol/tests/enrollib_test.php
grade/grading/form/rubric/tests/behat/behat_gradingform_rubric.php
install/lang/fr/admin.php
lang/en/backup.php
lib/adminlib.php
lib/ajax/service.php
lib/amd/build/form-autocomplete.min.js
lib/amd/build/form-course-selector.min.js
lib/amd/build/tree.min.js
lib/amd/src/form-autocomplete.js
lib/amd/src/form-course-selector.js
lib/amd/src/tree.js
lib/classes/session/manager.php
lib/classes/session/memcached.php
lib/classes/session/redis.php [deleted file]
lib/db/services.php
lib/dml/mysqli_native_moodle_database.php
lib/dml/tests/dml_test.php
lib/enrollib.php
lib/externallib.php
lib/filelib.php
lib/filestorage/file_storage.php
lib/form/course.php
lib/form/url.js
lib/formslib.php
lib/tests/externallib_test.php
lib/tests/redis_session_test.php [deleted file]
lib/tests/unoconv_test.php
lib/upgrade.txt
mod/assign/styles.css
mod/assign/tests/externallib_test.php
mod/assign/tests/locallib_test.php
mod/feedback/classes/course_map_form.php
mod/feedback/item/info/info_form.php
mod/feedback/item/info/lib.php
mod/forum/index.php
mod/forum/lib.php
mod/forum/markposts.php
mod/forum/subscribe.php
mod/forum/subscribe_ajax.php
mod/scorm/player.js
mod/scorm/view.js
mod/wiki/classes/search/collaborative_page.php
theme/bootstrapbase/less/moodle/yui_fixes.less [deleted file]
user/edit_form.php
user/profile/lib.php
user/tests/profilelib_test.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 2168ebd..e11b466 100644 (file)
Binary files a/admin/tool/lp/amd/build/user_competency_course_navigation.min.js and b/admin/tool/lp/amd/build/user_competency_course_navigation.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 7cfa93d..a7a43ba 100644 (file)
@@ -38,8 +38,6 @@ define(['jquery'], function($) {
         this._userId = userId + '';
         this._competencyId = competencyId + '';
         this._courseId = courseId;
-        this._ignoreFirstUser = true;
-        this._ignoreFirstCompetency = true;
 
         $(userSelector).on('change', this._userChanged.bind(this));
         $(competencySelector).on('change', this._competencyChanged.bind(this));
@@ -52,11 +50,6 @@ define(['jquery'], function($) {
      * @param {Event} e
      */
     UserCompetencyCourseNavigation.prototype._userChanged = function(e) {
-        if (this._ignoreFirstUser) {
-            this._ignoreFirstUser = false;
-            return;
-        }
-
         var newUserId = $(e.target).val();
         var queryStr = '?userid=' + newUserId + '&courseid=' + this._courseId + '&competencyid=' + this._competencyId;
         document.location = this._baseUrl + queryStr;
@@ -69,10 +62,6 @@ define(['jquery'], function($) {
      * @param {Event} e
      */
     UserCompetencyCourseNavigation.prototype._competencyChanged = function(e) {
-        if (this._ignoreFirstCompetency) {
-            this._ignoreFirstCompetency = false;
-            return;
-        }
         var newCompetencyId = $(e.target).val();
         var queryStr = '?userid=' + this._userId + '&courseid=' + this._courseId + '&competencyid=' + newCompetencyId;
         document.location = this._baseUrl + queryStr;
index 0055734..f3be693 100644 (file)
@@ -24,6 +24,7 @@
 namespace tool_lp\external;
 
 use core_competency\api;
+use core_competency\user_competency;
 use context_course;
 use renderer_base;
 use stdClass;
@@ -72,6 +73,8 @@ class user_competency_summary_in_course_exporter extends \core_competency\extern
         $related['usercompetency'] = null;
         $exporter = new user_competency_summary_exporter(null, $related);
         $result->usercompetencysummary = $exporter->export($output);
+        $result->usercompetencysummary->cangrade = user_competency::can_grade_user_in_course($this->related['user']->id,
+            $this->related['course']->id);
 
         $context = context_course::instance($this->related['course']->id);
         $exporter = new course_summary_exporter($this->related['course'], array('context' => $context));
index 209db55..258f382 100644 (file)
@@ -73,6 +73,7 @@ $functions = array(
         'type'         => 'read',
         'capabilities' => 'moodle/competency:coursecompetencyview',
         'ajax'         => true,
+        'services'     => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'tool_lp_data_for_template_competencies_page' => array(
         'classname'    => 'tool_lp\external',
@@ -100,6 +101,7 @@ $functions = array(
         'type'         => 'read',
         'capabilities' => 'moodle/competency:planviewown',
         'ajax'         => true,
+        'services'     => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'tool_lp_data_for_plan_page' => array(
         'classname'    => 'tool_lp\external',
@@ -109,6 +111,7 @@ $functions = array(
         'type'         => 'read',
         'capabilities' => 'moodle/competency:planview',
         'ajax'         => true,
+        'services'     => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'tool_lp_data_for_related_competencies_section' => array(
         'classname'    => 'tool_lp\external',
@@ -147,6 +150,7 @@ $functions = array(
         'type'         => 'read',
         'capabilities' => 'moodle/competency:userevidenceview',
         'ajax'         => true,
+        'services'     => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'tool_lp_data_for_user_evidence_page' => array(
         'classname'    => 'tool_lp\external',
@@ -156,6 +160,7 @@ $functions = array(
         'type'         => 'read',
         'capabilities' => 'moodle/competency:userevidenceview',
         'ajax'         => true,
+        'services'     => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
 
     // User competency.
@@ -167,6 +172,7 @@ $functions = array(
         'type'         => 'read',
         'capabilities' => 'moodle/competency:planview',
         'ajax'         => true,
+        'services'     => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'tool_lp_data_for_user_competency_summary_in_plan' => array(
         'classname'    => 'tool_lp\external',
@@ -176,6 +182,7 @@ $functions = array(
         'type'         => 'read',
         'capabilities' => 'moodle/competency:planview',
         'ajax'         => true,
+        'services'     => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'tool_lp_data_for_user_competency_summary_in_course' => array(
         'classname'    => 'tool_lp\external',
@@ -185,6 +192,7 @@ $functions = array(
         'type'         => 'read',
         'capabilities' => 'moodle/competency:coursecompetencyview',
         'ajax'         => true,
+        'services'     => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
 );
 
index 75dd6b9..fab384d 100644 (file)
     display: table-cell;
 }
 .path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-outcome"] select,
-.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-type"] select {
+.path-admin-tool-lp [data-region="competencyruleconfig"] [data-region="rule-type"] select,
+.path-admin-tool-lp [data-region="competencylinktree"] select {
     width: 100%;
 }
 
index b0351dc..f409b47 100644 (file)
@@ -25,6 +25,6 @@
 defined('MOODLE_INTERNAL') || die();
 
 
-$plugin->version   = 2016020925; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2016050400; // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2014110400; // Requires this Moodle version.
 $plugin->component = 'tool_lp'; // Full name of the plugin (used for diagnostics).
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()));
     }
 
index 60f44c3..997f757 100644 (file)
@@ -23,6 +23,7 @@ if (!is_enabled_auth('shibboleth')) {
 }
 
 // Front channel logout.
+$inputstream = file_get_contents("php://input");
 if ($action == 'logout' && !empty($redirect)) {
 
     if ($USER->auth == 'shibboleth') {
@@ -32,7 +33,7 @@ if ($action == 'logout' && !empty($redirect)) {
         redirect($redirect);
     }
 
-} else if (!file_get_contents("php://input")) {
+} else if (!empty($inputstream)) {
 
     // Back channel logout.
     // Set SOAP header.
index 45e792d..6366c8f 100644 (file)
@@ -163,5 +163,10 @@ class backup_root_task extends backup_task {
         $groups = new backup_groups_setting('groups', base_setting::IS_BOOLEAN, true);
         $groups->set_ui(new backup_setting_ui_checkbox($groups, get_string('rootsettinggroups', 'backup')));
         $this->add_setting($groups);
+
+        // Define competencies inclusion setting if competencies are enabled.
+        $competencies = new backup_competencies_setting();
+        $competencies->set_ui(new backup_setting_ui_checkbox($competencies, get_string('rootsettingcompetencies', 'backup')));
+        $this->add_setting($competencies);
     }
 }
index f799e4a..d2e1e5f 100644 (file)
@@ -130,6 +130,27 @@ class backup_calendarevents_setting extends backup_anonymize_setting {}
  */
 class backup_userscompletion_setting extends backup_anonymize_setting {}
 
+/**
+ * root setting to control if backup will include competencies or not.
+ */
+class backup_competencies_setting extends backup_generic_setting {
+
+    /**
+     * backup_competencies_setting constructor.
+     */
+    public function __construct() {
+        $defaultvalue = false;
+        $visibility = base_setting::HIDDEN;
+        $status = base_setting::LOCKED_BY_CONFIG;
+        if (\core_competency\api::is_enabled()) {
+            $defaultvalue = true;
+            $visibility = base_setting::VISIBLE;
+            $status = base_setting::NOT_LOCKED;
+        }
+        parent::__construct('competencies', base_setting::IS_BOOLEAN, $defaultvalue, $visibility, $status);
+    }
+}
+
 // Section backup settings
 
 /**
index 2ace0f3..da219a1 100644 (file)
@@ -1585,6 +1585,21 @@ class backup_course_competencies_structure_step extends backup_structure_step {
 
         return $wrapper;
     }
+
+    /**
+     * Execute conditions.
+     *
+     * @return bool
+     */
+    protected function execute_condition() {
+
+        // Do not execute if competencies are not included.
+        if (!$this->get_setting_value('competencies')) {
+            return false;
+        }
+
+        return true;
+    }
 }
 
 /**
@@ -1612,6 +1627,21 @@ class backup_activity_competencies_structure_step extends backup_structure_step
 
         return $wrapper;
     }
+
+    /**
+     * Execute conditions.
+     *
+     * @return bool
+     */
+    protected function execute_condition() {
+
+        // Do not execute if competencies are not included.
+        if (!$this->get_setting_value('competencies')) {
+            return false;
+        }
+
+        return true;
+    }
 }
 
 /**
index 922e203..42b7c80 100644 (file)
@@ -266,5 +266,11 @@ class restore_root_task extends restore_task {
         $groups->set_ui(new backup_setting_ui_checkbox($groups, get_string('rootsettinggroups', 'backup')));
         $groups->get_ui()->set_changeable($changeable);
         $this->add_setting($groups);
+
+        // Competencies restore setting. Show when competencies is enabled and the setting is available.
+        $hascompetencies = !empty($rootsettings['competencies']);
+        $competencies = new restore_competencies_setting($hascompetencies);
+        $competencies->set_ui(new backup_setting_ui_checkbox($competencies, get_string('rootsettingcompetencies', 'backup')));
+        $this->add_setting($competencies);
     }
 }
index 3ac0e2b..005449f 100644 (file)
@@ -78,6 +78,30 @@ class restore_comments_setting extends restore_role_assignments_setting {}
  */
 class restore_badges_setting extends restore_generic_setting {}
 
+/**
+ * root setting to control if competencies will also be restored.
+ */
+class restore_competencies_setting extends restore_generic_setting {
+
+    /**
+     * restore_competencies_setting constructor.
+     * @param bool $hascompetencies Flag whether to set the restore setting as checked and unlocked.
+     */
+    public function __construct($hascompetencies) {
+        $defaultvalue = false;
+        $visibility = base_setting::HIDDEN;
+        $status = base_setting::LOCKED_BY_CONFIG;
+        if (\core_competency\api::is_enabled()) {
+            $visibility = base_setting::VISIBLE;
+            if ($hascompetencies) {
+                $defaultvalue = true;
+                $status = base_setting::NOT_LOCKED;
+            }
+        }
+        parent::__construct('competencies', base_setting::IS_BOOLEAN, $defaultvalue, $visibility, $status);
+    }
+}
+
 /**
  * root setting to control if restore will create
  * events or no, depends of @restore_users_setting
index 861dcae..3c1f547 100644 (file)
@@ -1703,27 +1703,54 @@ class restore_course_structure_step extends restore_structure_step {
      */
     public function process_course($data) {
         global $CFG, $DB;
+        $context = context::instance_by_id($this->task->get_contextid());
+        $userid = $this->task->get_userid();
+        $target = $this->get_task()->get_target();
+        $isnewcourse = $target != backup::TARGET_CURRENT_ADDING && $target != backup::TARGET_EXISTING_ADDING;
+
+        // When restoring to a new course we can set all the things except for the ID number.
+        $canchangeidnumber = $isnewcourse || has_capability('moodle/course:changeidnumber', $context, $userid);
+        $canchangeshortname = $isnewcourse || has_capability('moodle/course:changeshortname', $context, $userid);
+        $canchangefullname = $isnewcourse || has_capability('moodle/course:changefullname', $context, $userid);
+        $canchangesummary = $isnewcourse || has_capability('moodle/course:changesummary', $context, $userid);
 
         $data = (object)$data;
+        $data->id = $this->get_courseid();
 
         $fullname  = $this->get_setting_value('course_fullname');
         $shortname = $this->get_setting_value('course_shortname');
         $startdate = $this->get_setting_value('course_startdate');
 
-        // Calculate final course names, to avoid dupes
+        // Calculate final course names, to avoid dupes.
         list($fullname, $shortname) = restore_dbops::calculate_course_names($this->get_courseid(), $fullname, $shortname);
 
-        // Need to change some fields before updating the course record
-        $data->id = $this->get_courseid();
-        $data->fullname = $fullname;
-        $data->shortname= $shortname;
+        if ($canchangefullname) {
+            $data->fullname = $fullname;
+        } else {
+            unset($data->fullname);
+        }
+
+        if ($canchangeshortname) {
+            $data->shortname = $shortname;
+        } else {
+            unset($data->shortname);
+        }
+
+        if (!$canchangesummary) {
+            unset($data->summary);
+            unset($data->summaryformat);
+        }
 
         // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
         // another course on this site.
-        $context = context::instance_by_id($this->task->get_contextid());
-        if (!empty($data->idnumber) && has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid()) &&
-                $this->task->is_samesite() && !$DB->record_exists('course', array('idnumber' => $data->idnumber))) {
+        if (!empty($data->idnumber) && $canchangeidnumber && $this->task->is_samesite()
+                && !$DB->record_exists('course', array('idnumber' => $data->idnumber))) {
             // Do not reset idnumber.
+
+        } else if (!$isnewcourse) {
+            // Prevent override when restoring as merge.
+            unset($data->idnumber);
+
         } else {
             $data->idnumber = '';
         }
@@ -3186,8 +3213,8 @@ class restore_course_competencies_structure_step extends restore_structure_step
      */
     protected function execute_condition() {
 
-        // Do not restore when competencies are disabled.
-        if (!\core_competency\api::is_enabled()) {
+        // Do not execute if competencies are not included.
+        if (!$this->get_setting_value('competencies')) {
             return false;
         }
 
@@ -3261,8 +3288,8 @@ class restore_activity_competencies_structure_step extends restore_structure_ste
      */
     protected function execute_condition() {
 
-        // Do not restore when competencies are disabled.
-        if (!\core_competency\api::is_enabled()) {
+        // Do not execute if competencies are not included.
+        if (!$this->get_setting_value('competencies')) {
             return false;
         }
 
index 6e1be22..4c2a0ff 100644 (file)
@@ -45,13 +45,7 @@ function core_badges_myprofile_navigation(\core_user\output\myprofile\tree $tree
     // Add category. This node should appear after 'contact' so that administration block appears towards the end. Refer MDL-49928.
     $category = new core_user\output\myprofile\category('badges', get_string('badges', 'badges'), 'contact');
     $tree->add_category($category);
-
-    // Determine context.
-    if (isloggedin()) {
-        $context = context_user::instance($USER->id);
-    } else {
-        $context = context_system::instance();
-    }
+    $context = context_user::instance($user->id);
     $courseid = empty($course) ? 0 : $course->id;
 
     if ($USER->id == $user->id || has_capability('moodle/badges:viewotherbadges', $context)) {
@@ -78,4 +72,4 @@ function core_badges_myprofile_navigation(\core_user\output\myprofile\tree $tree
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/blocks/badges/tests/behat/block_badges.feature b/blocks/badges/tests/behat/block_badges.feature
new file mode 100644 (file)
index 0000000..e9da78a
--- /dev/null
@@ -0,0 +1,34 @@
+@block @block_badges
+Feature: Enable Block Badges in a course without badges
+  In order to view the badges block in a course
+  As a teacher
+  I can add badges block to a course and view the contents
+
+  Background:
+    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 |
+
+  Scenario: Add the block to a the course when badges are disabled
+    Given I log in as "admin"
+    And the following config values are set as admin:
+      | enablebadges | 0 |
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    When I add the "Latest badges" block
+    Then I should see "Badges are not enabled on this site." in the "Latest badges" "block"
+
+  Scenario: Add the block to a the course when badges are enabled
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    When I add the "Latest badges" block
+    Then I should see "You have no badges to display" in the "Latest badges" "block"
diff --git a/blocks/badges/tests/behat/block_badges_course.feature b/blocks/badges/tests/behat/block_badges_course.feature
new file mode 100644 (file)
index 0000000..75942ff
--- /dev/null
@@ -0,0 +1,73 @@
+@block @block_badges @core_badges @_file_upload @javascript
+Feature: Enable Block Badges in a course
+  In order to enable the badges block in a course
+  As a teacher
+  I can add badges block to a course
+
+  Background:
+    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 |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    # Issue badge 1 of 2
+    And I navigate to "Add a new badge" node in "Badges"
+    And I set the following fields to these values:
+      | id_name | Badge 1 |
+      | id_description | Badge 1 |
+      | id_issuername | Teacher 1 |
+    And I upload "blocks/badges/tests/fixtures/badge.png" file to "Image" filemanager
+    And I press "Create badge"
+    And I select "Manual issue by role" from the "Add badge criteria" singleselect
+    And I set the field "Teacher" to "1"
+    And I press "Save"
+    And I press "Enable access"
+    And I press "Continue"
+    And I follow "Recipients (0)"
+    And I press "Award badge"
+    And I set the field "potentialrecipients[]" to "Teacher 1 (teacher1@example.com)"
+    And I press "Award badge"
+    # Issue Badge 2 of 2
+    And I navigate to "Add a new badge" node in "Badges"
+    And I set the following fields to these values:
+      | id_name | Badge 2 |
+      | id_description | Badge 2 |
+      | id_issuername | Teacher 1 |
+    And I upload "blocks/badges/tests/fixtures/badge.png" file to "Image" filemanager
+    And I press "Create badge"
+    And I select "Manual issue by role" from the "Add badge criteria" singleselect
+    And I set the field "Teacher" to "1"
+    And I press "Save"
+    And I press "Enable access"
+    And I press "Continue"
+    And I follow "Recipients (0)"
+    And I press "Award badge"
+    And I set the field "potentialrecipients[]" to "Teacher 1 (teacher1@example.com)"
+    And I press "Award badge"
+    And I log out
+
+  Scenario: Add the recent badges block to a course.
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    When I add the "Latest badges" block
+    Then I should see "Badge 1" in the "Latest badges" "block"
+    And I should see "Badge 2" in the "Latest badges" "block"
+
+  Scenario: Add the recent badges block to a course and limit it to only display 1 badge.
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    When I add the "Latest badges" block
+    And I configure the "Latest badges" block
+    And I set the following fields to these values:
+      | id_config_numberofbadges | 1 |
+    And I press "Save changes"
+    Then I should see "Badge 2" in the "Latest badges" "block"
+    And I should not see "Badge 1" in the "Latest badges" "block"
diff --git a/blocks/badges/tests/behat/block_badges_dashboard.feature b/blocks/badges/tests/behat/block_badges_dashboard.feature
new file mode 100644 (file)
index 0000000..6bc3d1f
--- /dev/null
@@ -0,0 +1,39 @@
+@block @block_badges @core_badges @_file_upload @javascript
+Feature: Enable Block Badges on the dashboard and view awarded badges
+  In order to view recent badges on the dashboard
+  As a teacher
+  I can add badges block to the dashboard
+
+  Scenario: Add the recent badges block to 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 |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    # Issue badge 1 of 2
+    And I navigate to "Add a new badge" node in "Badges"
+    And I set the following fields to these values:
+      | id_name | Badge 1 |
+      | id_description | Badge 1 |
+      | id_issuername | Teacher 1 |
+    And I upload "blocks/badges/tests/fixtures/badge.png" file to "Image" filemanager
+    And I press "Create badge"
+    And I select "Manual issue by role" from the "Add badge criteria" singleselect
+    And I set the field "Teacher" to "1"
+    And I press "Save"
+    And I press "Enable access"
+    And I press "Continue"
+    And I follow "Recipients (0)"
+    And I press "Award badge"
+    And I set the field "potentialrecipients[]" to "Teacher 1 (teacher1@example.com)"
+    And I press "Award badge"
+    And I log out
+    When I log in as "teacher1"
+    And I click on "Dashboard" "link" in the "Navigation" "block"
+    Then I should see "Badge 1" in the "Latest badges" "block"
diff --git a/blocks/badges/tests/behat/block_badges_frontpage.feature b/blocks/badges/tests/behat/block_badges_frontpage.feature
new file mode 100644 (file)
index 0000000..8b40357
--- /dev/null
@@ -0,0 +1,44 @@
+@block @block_badges @core_badges @_file_upload @javascript
+Feature: Enable Block Badges on the frontpage and view awarded badges
+  In order to enable the badges block on the frontpage
+  As a admin
+  I can add badges block to the frontpage
+
+  Scenario: Add the recent badges block on the frontpage and view recent badges
+    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 |
+    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 "Latest badges" block
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    # Issue badge 1 of 2
+    And I navigate to "Add a new badge" node in "Badges"
+    And I set the following fields to these values:
+      | id_name | Badge 1 |
+      | id_description | Badge 1 |
+      | id_issuername | Teacher 1 |
+    And I upload "blocks/badges/tests/fixtures/badge.png" file to "Image" filemanager
+    And I press "Create badge"
+    And I select "Manual issue by role" from the "Add badge criteria" singleselect
+    And I set the field "Teacher" to "1"
+    And I press "Save"
+    And I press "Enable access"
+    And I press "Continue"
+    And I follow "Recipients (0)"
+    And I press "Award badge"
+    And I set the field "potentialrecipients[]" to "Teacher 1 (teacher1@example.com)"
+    And I press "Award badge"
+    And I log out
+    When I log in as "teacher1"
+    And I am on site homepage
+    Then I should see "Badge 1" in the "Latest badges" "block"
diff --git a/blocks/badges/tests/fixtures/badge.png b/blocks/badges/tests/fixtures/badge.png
new file mode 100644 (file)
index 0000000..73f2c07
Binary files /dev/null and b/blocks/badges/tests/fixtures/badge.png differ
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 262bcf9..30d8c15 100644 (file)
@@ -242,10 +242,6 @@ $CFG->admin = 'admin';
 //      $CFG->session_handler_class = '\core\session\file';
 //      $CFG->session_file_save_path = $CFG->dataroot.'/sessions';
 //
-//   Redis session handler (requires redis server and redis extension):
-//      $CFG->session_handler_class = '\core\session\redis';
-//      $CFG->session_redis_save_path = 'tcp://127.0.0.1'
-//
 //   Memcached session handler (requires memcached server and extension):
 //      $CFG->session_handler_class = '\core\session\memcached';
 //      $CFG->session_memcached_save_path = '127.0.0.1:11211';
index da654a2..ea687c6 100644 (file)
@@ -431,6 +431,7 @@ class core_course_external extends external_api {
             $courseinfo['id'] = $course->id;
             $courseinfo['fullname'] = $course->fullname;
             $courseinfo['shortname'] = $course->shortname;
+            $courseinfo['displayname'] = external_format_string(get_course_display_name_for_list($course), $context->id);
             $courseinfo['categoryid'] = $course->category;
             list($courseinfo['summary'], $courseinfo['summaryformat']) =
                 external_format_text($course->summary, $course->summaryformat, $context->id, 'course', 'summary', 0);
@@ -498,6 +499,7 @@ class core_course_external extends external_api {
                             'categorysortorder' => new external_value(PARAM_INT,
                                     'sort order into the category', VALUE_OPTIONAL),
                             'fullname' => new external_value(PARAM_TEXT, 'full name'),
+                            'displayname' => new external_value(PARAM_TEXT, 'course display name'),
                             'idnumber' => new external_value(PARAM_RAW, 'id number', VALUE_OPTIONAL),
                             'summary' => new external_value(PARAM_RAW, 'summary'),
                             'summaryformat' => new external_format_value('summary'),
@@ -2177,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 81af78e..9ca911b 100644 (file)
@@ -561,6 +561,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
             $dbcourse = $generatedcourses[$course['id']];
             $this->assertEquals($course['idnumber'], $dbcourse->idnumber);
             $this->assertEquals($course['fullname'], $dbcourse->fullname);
+            $this->assertEquals($course['displayname'], get_course_display_name_for_list($dbcourse));
             // Summary was converted to the HTML format.
             $this->assertEquals($course['summary'], format_text($dbcourse->summary, FORMAT_MOODLE, array('para' => false)));
             $this->assertEquals($course['summaryformat'], FORMAT_HTML);
diff --git a/course/tests/restore_test.php b/course/tests/restore_test.php
new file mode 100644 (file)
index 0000000..f794e66
--- /dev/null
@@ -0,0 +1,324 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Course restore tests.
+ *
+ * @package    core_course
+ * @copyright  2016 Frédéric Massart - FMCorz.net
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+global $CFG;
+
+require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
+require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
+
+/**
+ * Course restore testcase.
+ *
+ * @package    core_course
+ * @copyright  2016 Frédéric Massart - FMCorz.net
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_restore_backup_testcase extends advanced_testcase {
+
+    /**
+     * Backup a course and return its backup ID.
+     *
+     * @param int $courseid The course ID.
+     * @param int $userid The user doing the backup.
+     * @return string
+     */
+    protected function backup_course($courseid, $userid = 2) {
+        globaL $CFG;
+        $packer = get_file_packer('application/vnd.moodle.backup');
+
+        $bc = new backup_controller(backup::TYPE_1COURSE, $courseid, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO,
+            backup::MODE_GENERAL, $userid);
+        $bc->execute_plan();
+
+        $results = $bc->get_results();
+        $results['backup_destination']->extract_to_pathname($packer, "$CFG->tempdir/backup/core_course_testcase");
+
+        $bc->destroy();
+        unset($bc);
+        return 'core_course_testcase';
+    }
+
+    /**
+     * Create a role with capabilities and permissions.
+     *
+     * @param string|array $caps Capability names.
+     * @param int $perm Constant CAP_* to apply to the capabilities.
+     * @return int The new role ID.
+     */
+    protected function create_role_with_caps($caps, $perm) {
+        $caps = (array) $caps;
+        $dg = $this->getDataGenerator();
+        $roleid = $dg->create_role();
+        foreach ($caps as $cap) {
+            assign_capability($cap, $perm, $roleid, context_system::instance()->id, true);
+        }
+        accesslib_clear_all_caches_for_unit_testing();
+        return $roleid;
+    }
+
+    /**
+     * Restore a course.
+     *
+     * @param int $backupid The backup ID.
+     * @param int $courseid The course ID to restore in, or 0.
+     * @param int $userid The ID of the user performing the restore.
+     * @return stdClass The updated course object.
+     */
+    protected function restore_course($backupid, $courseid, $userid) {
+        global $DB;
+
+        $target = backup::TARGET_CURRENT_ADDING;
+        if (!$courseid) {
+            $target = backup::TARGET_NEW_COURSE;
+            $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
+            $courseid = restore_dbops::create_new_course('Tmp', 'tmp', $categoryid);
+        }
+
+        $rc = new restore_controller($backupid, $courseid, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $userid, $target);
+        $target == backup::TARGET_NEW_COURSE ?: $rc->get_plan()->get_setting('overwrite_conf')->set_value(true);
+        $rc->execute_precheck();
+        $rc->execute_plan();
+
+        $course = $DB->get_record('course', array('id' => $rc->get_courseid()));
+
+        $rc->destroy();
+        unset($rc);
+        return $course;
+    }
+
+    /**
+     * Restore a course to an existing course.
+     *
+     * @param int $backupid The backup ID.
+     * @param int $courseid The course ID to restore in.
+     * @param int $userid The ID of the user performing the restore.
+     * @return stdClass The updated course object.
+     */
+    protected function restore_to_existing_course($backupid, $courseid, $userid = 2) {
+        return $this->restore_course($backupid, $courseid, $userid);
+    }
+
+    /**
+     * Restore a course to a new course.
+     *
+     * @param int $backupid The backup ID.
+     * @param int $userid The ID of the user performing the restore.
+     * @return stdClass The new course object.
+     */
+    protected function restore_to_new_course($backupid, $userid = 2) {
+        return $this->restore_course($backupid, 0, $userid);
+    }
+
+    public function test_restore_existing_idnumber_in_new_course() {
+        $this->resetAfterTest();
+
+        $dg = $this->getDataGenerator();
+        $c1 = $dg->create_course(['idnumber' => 'ABC']);
+        $backupid = $this->backup_course($c1->id);
+        $c2 = $this->restore_to_new_course($backupid);
+
+        // The ID number is set empty.
+        $this->assertEquals('', $c2->idnumber);
+    }
+
+    public function test_restore_non_existing_idnumber_in_new_course() {
+        global $DB;
+        $this->resetAfterTest();
+
+        $dg = $this->getDataGenerator();
+        $c1 = $dg->create_course(['idnumber' => 'ABC']);
+        $backupid = $this->backup_course($c1->id);
+
+        $c1->idnumber = 'BCD';
+        $DB->update_record('course', $c1);
+
+        // The ID number changed.
+        $c2 = $this->restore_to_new_course($backupid);
+        $this->assertEquals('ABC', $c2->idnumber);
+    }
+
+    public function test_restore_existing_idnumber_in_existing_course() {
+        global $DB;
+        $this->resetAfterTest();
+
+        $dg = $this->getDataGenerator();
+        $c1 = $dg->create_course(['idnumber' => 'ABC']);
+        $c2 = $dg->create_course(['idnumber' => 'DEF']);
+        $backupid = $this->backup_course($c1->id);
+
+        // The ID number does not change.
+        $c2 = $this->restore_to_existing_course($backupid, $c2->id);
+        $this->assertEquals('DEF', $c2->idnumber);
+
+        $c1 = $DB->get_record('course', array('id' => $c1->id));
+        $this->assertEquals('ABC', $c1->idnumber);
+    }
+
+    public function test_restore_non_existing_idnumber_in_existing_course() {
+        global $DB;
+        $this->resetAfterTest();
+
+        $dg = $this->getDataGenerator();
+        $c1 = $dg->create_course(['idnumber' => 'ABC']);
+        $c2 = $dg->create_course(['idnumber' => 'DEF']);
+        $backupid = $this->backup_course($c1->id);
+
+        $c1->idnumber = 'XXX';
+        $DB->update_record('course', $c1);
+
+        // The ID number has changed.
+        $c2 = $this->restore_to_existing_course($backupid, $c2->id);
+        $this->assertEquals('ABC', $c2->idnumber);
+    }
+
+    public function test_restore_idnumber_in_existing_course_without_permissions() {
+        global $DB;
+        $this->resetAfterTest();
+        $dg = $this->getDataGenerator();
+        $u1 = $dg->create_user();
+
+        $managers = get_archetype_roles('manager');
+        $manager = array_shift($managers);
+        $roleid = $this->create_role_with_caps('moodle/course:changeidnumber', CAP_PROHIBIT);
+        $dg->role_assign($manager->id, $u1->id);
+        $dg->role_assign($roleid, $u1->id);
+
+        $c1 = $dg->create_course(['idnumber' => 'ABC']);
+        $c2 = $dg->create_course(['idnumber' => 'DEF']);
+        $backupid = $this->backup_course($c1->id);
+
+        $c1->idnumber = 'XXX';
+        $DB->update_record('course', $c1);
+
+        // The ID number does not change.
+        $c2 = $this->restore_to_existing_course($backupid, $c2->id, $u1->id);
+        $this->assertEquals('DEF', $c2->idnumber);
+    }
+
+    public function test_restore_course_info_in_new_course() {
+        global $DB;
+        $this->resetAfterTest();
+        $dg = $this->getDataGenerator();
+
+        $c1 = $dg->create_course(['shortname' => 'SN', 'fullname' => 'FN', 'summary' => 'DESC', 'summaryformat' => FORMAT_MOODLE]);
+        $backupid = $this->backup_course($c1->id);
+
+        // The information is restored but adapted because names are already taken.
+        $c2 = $this->restore_to_new_course($backupid);
+        $this->assertEquals('SN_1', $c2->shortname);
+        $this->assertEquals('FN copy 1', $c2->fullname);
+        $this->assertEquals('DESC', $c2->summary);
+        $this->assertEquals(FORMAT_MOODLE, $c2->summaryformat);
+    }
+
+    public function test_restore_course_info_in_existing_course() {
+        global $DB;
+        $this->resetAfterTest();
+        $dg = $this->getDataGenerator();
+
+        $c1 = $dg->create_course(['shortname' => 'SN', 'fullname' => 'FN', 'summary' => 'DESC', 'summaryformat' => FORMAT_MOODLE]);
+        $c2 = $dg->create_course(['shortname' => 'A', 'fullname' => 'B', 'summary' => 'C', 'summaryformat' => FORMAT_PLAIN]);
+        $backupid = $this->backup_course($c1->id);
+
+        // The information is restored but adapted because names are already taken.
+        $c2 = $this->restore_to_existing_course($backupid, $c2->id);
+        $this->assertEquals('SN_1', $c2->shortname);
+        $this->assertEquals('FN copy 1', $c2->fullname);
+        $this->assertEquals('DESC', $c2->summary);
+        $this->assertEquals(FORMAT_MOODLE, $c2->summaryformat);
+    }
+
+    public function test_restore_course_shortname_in_existing_course_without_permissions() {
+        global $DB;
+        $this->resetAfterTest();
+        $dg = $this->getDataGenerator();
+        $u1 = $dg->create_user();
+
+        $managers = get_archetype_roles('manager');
+        $manager = array_shift($managers);
+        $roleid = $this->create_role_with_caps('moodle/course:changeshortname', CAP_PROHIBIT);
+        $dg->role_assign($manager->id, $u1->id);
+        $dg->role_assign($roleid, $u1->id);
+
+        $c1 = $dg->create_course(['shortname' => 'SN', 'fullname' => 'FN', 'summary' => 'DESC', 'summaryformat' => FORMAT_MOODLE]);
+        $c2 = $dg->create_course(['shortname' => 'A1', 'fullname' => 'B1', 'summary' => 'C1', 'summaryformat' => FORMAT_PLAIN]);
+
+        // The shortname does not change.
+        $backupid = $this->backup_course($c1->id);
+        $restored = $this->restore_to_existing_course($backupid, $c2->id, $u1->id);
+        $this->assertEquals($c2->shortname, $restored->shortname);
+        $this->assertEquals('FN copy 1', $restored->fullname);
+        $this->assertEquals('DESC', $restored->summary);
+        $this->assertEquals(FORMAT_MOODLE, $restored->summaryformat);
+    }
+
+    public function test_restore_course_fullname_in_existing_course_without_permissions() {
+        global $DB;
+        $this->resetAfterTest();
+        $dg = $this->getDataGenerator();
+        $u1 = $dg->create_user();
+
+        $managers = get_archetype_roles('manager');
+        $manager = array_shift($managers);
+        $roleid = $this->create_role_with_caps('moodle/course:changefullname', CAP_PROHIBIT);
+        $dg->role_assign($manager->id, $u1->id);
+        $dg->role_assign($roleid, $u1->id);
+
+        $c1 = $dg->create_course(['shortname' => 'SN', 'fullname' => 'FN', 'summary' => 'DESC', 'summaryformat' => FORMAT_MOODLE]);
+        $c2 = $dg->create_course(['shortname' => 'A1', 'fullname' => 'B1', 'summary' => 'C1', 'summaryformat' => FORMAT_PLAIN]);
+
+        // The fullname does not change.
+        $backupid = $this->backup_course($c1->id);
+        $restored = $this->restore_to_existing_course($backupid, $c2->id, $u1->id);
+        $this->assertEquals('SN_1', $restored->shortname);
+        $this->assertEquals($c2->fullname, $restored->fullname);
+        $this->assertEquals('DESC', $restored->summary);
+        $this->assertEquals(FORMAT_MOODLE, $restored->summaryformat);
+    }
+
+    public function test_restore_course_summary_in_existing_course_without_permissions() {
+        global $DB;
+        $this->resetAfterTest();
+        $dg = $this->getDataGenerator();
+        $u1 = $dg->create_user();
+
+        $managers = get_archetype_roles('manager');
+        $manager = array_shift($managers);
+        $roleid = $this->create_role_with_caps('moodle/course:changesummary', CAP_PROHIBIT);
+        $dg->role_assign($manager->id, $u1->id);
+        $dg->role_assign($roleid, $u1->id);
+
+        $c1 = $dg->create_course(['shortname' => 'SN', 'fullname' => 'FN', 'summary' => 'DESC', 'summaryformat' => FORMAT_MOODLE]);
+        $c2 = $dg->create_course(['shortname' => 'A1', 'fullname' => 'B1', 'summary' => 'C1', 'summaryformat' => FORMAT_PLAIN]);
+
+        // The summary and format do not change.
+        $backupid = $this->backup_course($c1->id);
+        $restored = $this->restore_to_existing_course($backupid, $c2->id, $u1->id);
+        $this->assertEquals('SN_1', $restored->shortname);
+        $this->assertEquals('FN copy 1', $restored->fullname);
+        $this->assertEquals($c2->summary, $restored->summary);
+        $this->assertEquals($c2->summaryformat, $restored->summaryformat);
+    }
+}
index 12cea2e..8fcc61c 100644 (file)
@@ -42,6 +42,7 @@ require_capability('enrol/' . $type . ':config', $context);
 
 $PAGE->set_url('/enrol/editinstance.php', array('courseid' => $course->id, 'id' => $instanceid, 'type' => $type));
 $PAGE->set_pagelayout('admin');
+$PAGE->set_docs_path('enrol/' . $type . '/edit');
 
 if (empty($return)) {
     $return = new moodle_url('/enrol/instances.php', array('id' => $course->id));
index f6a006a..d2d6336 100644 (file)
@@ -267,7 +267,10 @@ class helper {
             $instance->enrol = 'lti';
             $instance->status = $tool->status;
             $ltienrol = enrol_get_plugin('lti');
-            $ltienrol->enrol_user($instance, $userid, null, time(), $timeend);
+
+            // Hack - need to do this to workaround DB caching hack. See MDL-53977.
+            $timestart = intval(substr(time(), 0, 8) . '00') - 1;
+            $ltienrol->enrol_user($instance, $userid, null, $timestart, $timeend);
         }
 
         return self::ENROLMENT_SUCCESSFUL;
index 372e287..1df4193 100644 (file)
@@ -174,11 +174,12 @@ class enrol_lti_plugin extends enrol_plugin {
     public function unenrol_user(stdClass $instance, $userid) {
         global $DB;
 
-        // Get the tool associated with this instance.
-        $tool = $DB->get_record('enrol_lti_tools', array('enrolid' => $instance->id), 'id', MUST_EXIST);
-
-        // Need to remove the user from the users table.
-        $DB->delete_records('enrol_lti_users', array('userid' => $userid, 'toolid' => $tool->id));
+        // Get the tool associated with this instance. Note - it may not exist if we have deleted
+        // the tool. This is fine because we have already cleaned the 'enrol_lti_users' table.
+        if ($tool = $DB->get_record('enrol_lti_tools', array('enrolid' => $instance->id), 'id')) {
+            // Need to remove the user from the users table.
+            $DB->delete_records('enrol_lti_users', array('userid' => $userid, 'toolid' => $tool->id));
+        }
 
         parent::unenrol_user($instance, $userid);
     }
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 e0d3299..4c31b28 100644 (file)
@@ -281,6 +281,11 @@ class behat_gradingform_rubric extends behat_base {
         $stepusage = '"I grade by filling the rubric with:" step needs you to provide a table where each row is a criterion' .
             ' and each criterion has 3 different values: | Criterion name | Number of points | Remark text |';
 
+        // If running Javascript, ensure we zoom in before filling the grades.
+        if ($this->running_javascript()) {
+            $this->execute('behat_general::click_link', get_string('togglezoom', 'mod_assign'));
+        }
+
         // First element -> name, second -> points, third -> Remark.
         foreach ($criteria as $name => $criterion) {
 
@@ -326,6 +331,11 @@ class behat_gradingform_rubric extends behat_base {
             $textarea = $this->get_node_in_container('css_element', 'textarea', 'table_row', $name);
             $this->execute('behat_forms::i_set_the_field_to', array($textarea->getAttribute('name'), $criterion[1]));
         }
+
+        // If running Javascript, then ensure to close zoomed rubric.
+        if ($this->running_javascript()) {
+            $this->execute('behat_general::click_link', get_string('togglezoom', 'mod_assign'));
+        }
     }
 
     /**
index 21687c4..31e96a8 100644 (file)
@@ -40,6 +40,6 @@ $string['cliunknowoption'] = 'Options non reconnues :
  {$a}.
 Utilisez l\'option --help.';
 $string['cliyesnoprompt'] = 'Tapez y (pour oui) ou n (pour non)';
-$string['environmentrequireinstall'] = 'doit être installé et activé';
-$string['environmentrequireversion'] = 'la version {$a->needed} est requise ; vous utilisez actuellement la version {$a->current}';
+$string['environmentrequireinstall'] = 'L\'extension doit être installée et activée';
+$string['environmentrequireversion'] = 'La version {$a->needed} est requise ; vous utilisez actuellement la version {$a->current}';
 $string['upgradekeyset'] = 'Mettre à jour la clef (laisser vide pour ne pas la définir)';
index dc12e73..a63c3e6 100644 (file)
@@ -237,6 +237,7 @@ $string['rootsettingroleassignments'] = 'Include user role assignments';
 $string['rootsettingactivities'] = 'Include activities and resources';
 $string['rootsettingbadges'] = 'Include badges';
 $string['rootsettingblocks'] = 'Include blocks';
+$string['rootsettingcompetencies'] = 'Include competencies';
 $string['rootsettingfilters'] = 'Include filters';
 $string['rootsettingcomments'] = 'Include comments';
 $string['rootsettingcalendarevents'] = 'Include calendar events';
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 1186663..73fe814 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js and b/lib/amd/build/form-autocomplete.min.js differ
index 0417989..be46978 100644 (file)
Binary files a/lib/amd/build/form-course-selector.min.js and b/lib/amd/build/form-course-selector.min.js differ
index 837d4dc..c3695e9 100644 (file)
Binary files a/lib/amd/build/tree.min.js and b/lib/amd/build/tree.min.js differ
index d436d5e..3090b1f 100644 (file)
@@ -109,15 +109,21 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                 });
             }
         }).fail(notification.exception);
-        // Because this function get's called after changing the selection, this is a good place
-        // to trigger a change notification.
-        if (typeof M.core_formchangechecker != 'undefined') {
+    };
+
+    /**
+     * Notify of a change in the selection.
+     *
+     * @param {jQuery} originalSelect The jQuery object matching the hidden select list.
+     * @return {Void}
+     */
+    var notifyChange = function(originalSelect) {
+        if (typeof M.core_formchangechecker !== 'undefined') {
             M.core_formchangechecker.set_form_changed();
         }
         originalSelect.change();
     };
 
-
     /**
      * Remove the given item from the list of selected things.
      *
@@ -146,6 +152,8 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
         }
         // Rerender the selection list.
         updateSelectionList(options, state, originalSelect);
+        // Notifiy that the selection changed.
+        notifyChange(originalSelect);
     };
 
     /**
@@ -402,7 +410,8 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
         });
 
         updateSelectionList(options, state, originalSelect);
-
+        // Notifiy that the selection changed.
+        notifyChange(originalSelect);
         // Clear the input field.
         inputElement.val('');
         // Close the suggestions list.
@@ -440,6 +449,8 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
         });
         // Rerender the selection list.
         updateSelectionList(options, state, originalSelect);
+        // Notifiy that the selection changed.
+        notifyChange(originalSelect);
         // Clear the input element.
         inputElement.val('');
         // Close the list of suggestions.
index 78c71af..4e1b8c7 100644 (file)
@@ -41,18 +41,21 @@ define(['core/ajax', 'jquery'], function(ajax, $) {
         },
 
         transport: function(selector, query, success, failure) {
+            var el = $(selector);
+
             // Parse some data-attributes from the form element.
-            var requiredcapabilities = $(selector).data('requiredcapabilities');
+            var requiredcapabilities = el.data('requiredcapabilities');
             if (requiredcapabilities.trim() !== "") {
                 requiredcapabilities = requiredcapabilities.split(',');
             } else {
                 requiredcapabilities = [];
             }
 
-            var limittoenrolled = $(selector).data('limittoenrolled');
+            var limittoenrolled = el.data('limittoenrolled');
+            var includefrontpage = el.data('includefrontpage');
 
             // Build the query.
-            var promise = null;
+            var promises = null;
 
             if (typeof query === "undefined") {
                 query = '';
@@ -66,15 +69,35 @@ define(['core/ajax', 'jquery'], function(ajax, $) {
                 requiredcapabilities: requiredcapabilities,
                 limittoenrolled: limittoenrolled
             };
-            // Go go go!
-            promise = ajax.call([{
-                methodname: 'core_course_search_courses', args: searchargs
-            }]);
 
-            promise[0].done(success);
-            promise[0].fail(failure);
+            var calls = [{
+                methodname: 'core_course_search_courses', args: searchargs
+            }];
+            if (includefrontpage) {
+                calls.push({
+                    methodname: 'core_course_get_courses',
+                    args: {
+                        options: {
+                            ids: [includefrontpage]
+                        }
+                    }
+                });
+            }
 
-            return promise;
+            // Go go go!
+            promises = ajax.call(calls);
+            $.when.apply($.when, promises).done(function(data, site) {
+                if (site && site.length == 1) {
+                    var frontpage = site.pop();
+                    var matches = query === ''
+                        || frontpage.fullname.toUpperCase().indexOf(query.toUpperCase()) > -1
+                        || frontpage.shortname.toUpperCase().indexOf(query.toUpperCase()) > -1;
+                    if (matches) {
+                        data.courses.splice(0, 0, frontpage);
+                    }
+                }
+                success(data);
+            }).fail(failure);
         }
     };
 });
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 331e722..85499a0 100644 (file)
@@ -72,11 +72,14 @@ class manager {
         try {
             self::$handler->init();
             self::prepare_cookies();
-            $newsid = empty($_COOKIE[session_name()]);
+            $isnewsession = empty($_COOKIE[session_name()]);
 
-            self::$handler->start();
+            if (!self::$handler->start()) {
+                // Could not successfully start/recover session.
+                throw new \core\session\exception(get_string('servererror'));
+            }
 
-            self::initialise_user_session($newsid);
+            self::initialise_user_session($isnewsession);
             self::check_security();
 
             // Link global $USER and $SESSION,
@@ -90,7 +93,6 @@ class manager {
             $_SESSION['SESSION'] =& $GLOBALS['SESSION'];
 
         } catch (\Exception $ex) {
-            @session_write_close();
             self::init_empty_session();
             self::$sessionactive = false;
             throw $ex;
@@ -438,6 +440,7 @@ class manager {
      * Do various session security checks.
      *
      * WARNING: $USER and $SESSION are set up later, do not use them yet!
+     * @throws \core\session\exception
      */
     protected static function check_security() {
         global $CFG;
@@ -521,11 +524,23 @@ class manager {
      * Unblocks the sessions, other scripts may start executing in parallel.
      */
     public static function write_close() {
-        if (self::$sessionactive) {
-            session_write_close();
+        if (version_compare(PHP_VERSION, '5.6.0', '>=')) {
+            // More control over whether session data
+            // is persisted or not.
+            if (self::$sessionactive && session_id()) {
+                // Write session and release lock only if
+                // indication session start was clean.
+                session_write_close();
+            } else {
+                // Otherwise, if possibile lock exists want
+                // to clear it, but do not write session.
+                @session_abort();
+            }
         } else {
-            if (session_id()) {
-                @session_write_close();
+            // Any indication session was started, attempt
+            // to close it.
+            if (self::$sessionactive || session_id()) {
+                session_write_close();
             }
         }
         self::$sessionactive = false;
index 8be9a2d..bf1678b 100644 (file)
@@ -93,8 +93,24 @@ class memcached extends handler {
         $default = ini_get('max_execution_time');
         set_time_limit($this->acquiretimeout);
 
+        $isnewsession = empty($_COOKIE[session_name()]);
+        $starttimer = microtime(true);
+
         $result = parent::start();
 
+        // If session_start returned TRUE, but it took as long
+        // as the timeout value, and the $_SESSION returned is
+        // empty when should not have been (isnewsession false)
+        // then assume it did timeout and is invalid.
+        // Add 1 second to elapsed time to account for inexact
+        // timings in php_memcached_session.c.
+        // @TODO Remove this check when php-memcached is fixed
+        // to return false after key lock acquisition timeout.
+        if (!$isnewsession && $result && count($_SESSION) == 0
+            && (microtime(true) - $starttimer + 1) >= floatval($this->acquiretimeout)) {
+            $result = false;
+        }
+
         set_time_limit($default);
         return $result;
     }
diff --git a/lib/classes/session/redis.php b/lib/classes/session/redis.php
deleted file mode 100644 (file)
index faee370..0000000
+++ /dev/null
@@ -1,351 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Redis based session handler.
- *
- * @package    core
- * @copyright  2016 Nicholas Hoobin <nicholashoobin@catalyst-au.net>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace core\session;
-
-defined('MOODLE_INTERNAL') || die();
-
-/**
- * Redis based session handler.
- *
- * @package    core
- * @copyright  2016 Nicholas Hoobin <nicholashoobin@catalyst-au.net>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class redis extends handler {
-    /** @var string $savepath save_path string */
-    protected $savepath;
-
-    /** @var array $servers list of servers parsed from save_path */
-    protected $servers;
-
-    /** @var int $acquiretimeout how long to wait for session lock */
-    protected $acquiretimeout = 120;
-
-    /**
-     * Create new instance of handler.
-     */
-    public function __construct() {
-        global $CFG;
-
-        if (!empty($CFG->session_redis_acquire_lock_timeout)) {
-            $this->acquiretimeout = $CFG->session_redis_acquire_lock_timeout;
-        }
-
-        if (empty($CFG->session_redis_save_path)) {
-            $this->savepath = '';
-        } else {
-            $this->savepath = $CFG->session_redis_save_path;
-        }
-
-        if (empty($this->savepath)) {
-            $this->servers = array();
-        } else {
-            $this->servers = $this->connection_string_to_redis_servers($this->savepath);
-        }
-
-    }
-
-    /**
-     * Start the session.
-     * @return bool success
-     */
-    public function start() {
-        $default = ini_get('max_execution_time');
-        set_time_limit($this->acquiretimeout);
-
-        $result = parent::start();
-
-        set_time_limit($default);
-
-        return $result;
-    }
-
-    /**
-     * Init session handler.
-     */
-    public function init() {
-        if (!extension_loaded('Redis')) {
-            throw new exception('sessionhandlerproblem', 'error', '', null, 'redis extension is not loaded');
-        }
-
-        // The session handler requires a version of Redis with the SETEX command (at least 2.0).
-        $version = phpversion('Redis');
-        if (!$version or version_compare($version, '2.0') <= 0) {
-            throw new exception('sessionhandlerproblem', 'error', '', null, 'redis extension version must be at least 2.0');
-        }
-
-        if (empty($this->savepath)) {
-            throw new exception('sessionhandlerproblem', 'error', '', null,
-                '$CFG->session_redis_save_path must be specified in config.php');
-        }
-
-        ini_set('session.save_handler', 'redis');
-        ini_set('session.save_path', $this->savepath);
-    }
-
-    /**
-     * Check the backend contains data for this session id.
-     *
-     * Note: this is intended to be called from manager::session_exists() only.
-     *
-     * @param string $sid
-     * @return bool true if session found.
-     */
-    public function session_exists($sid) {
-        if (!$this->servers) {
-            return false;
-        }
-
-        foreach ($this->servers as $server) {
-            if ($redis = $this->redis_connect($server)) {
-                $value = $redis->get($server['prefix'] . $sid);
-                $redis->close();
-            }
-
-            if ($value !== false) {
-                return true;
-            }
-        }
-
-        return false;
-    }
-
-    /**
-     * Kill all active sessions, the core sessions table is
-     * purged afterwards.
-     */
-    public function kill_all_sessions() {
-        global $DB;
-        if (!$this->servers) {
-            return false;
-        }
-
-        $serverlist = array();
-        foreach ($this->servers as $server) {
-            if ($redis = $this->redis_connect($server)) {
-                $serverlist[] = array($redis, $server['prefix']);
-            }
-        }
-
-        $rs = $DB->get_recordset('sessions', array(), 'id DESC', 'id, sid');
-        foreach ($rs as $record) {
-            foreach ($serverlist as $arr) {
-                list($server, $prefix) = $arr;
-                $server->delete($prefix . $sid);
-            }
-        }
-
-        foreach ($serverlist as $arr) {
-            list($server, $prefix) = $arr;
-            $server->close();
-        }
-    }
-
-    /**
-     * Kill one session, the session record is removed afterwards.
-     * @param string $sid
-     */
-    public function kill_session($sid) {
-        if (!$this->servers) {
-            return false;
-        }
-
-        // Go through the list of all servers because
-        // we do not know where the session handler put the
-        // data.
-
-        foreach ($this->servers as $server) {
-            if ($redis = $this->redis_connect($server)) {
-                $redis->delete($server['prefix'] . $sid);
-                $redis->close();
-            }
-        }
-    }
-
-    /**
-     * Convert a connection string to an array of servers
-     *
-     * Example conversion,
-     * "tcp://host1:123?database=0, unix:///var/run/redis/redis.sock?database=0" to
-     *
-     *  array(
-     *      (
-     *          [scheme]   => 'tcp',
-     *          [host]     => 'host1',
-     *          [port]     => 123,
-     *          [database] => 0,
-     *          [prefix]   => 'PHPREDIS_SESSION:'
-     *      ),
-     *      (
-     *          [scheme]   => 'unix',
-     *          [path]     => '/var/run/redis/redis.sock',
-     *          [database] => 0,
-     *          [prefix]   => 'PHPREDIS_SESSION:'
-     *      )
-     *  )
-     *
-     * @copyright  2016 Nicholas Hoobin <nicholashoobin@catalyst-au.net>
-     * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
-     * @author     Nicholas Hoobin
-     *
-     * @param string $str save_path value containing redis connection string
-     * @return array
-     */
-    public function connection_string_to_redis_servers($str) {
-        $servers     = array();
-        $connections = array_map('trim', explode(',', $str));
-
-        foreach ($connections as $con) {
-            if (strpos($con, "unix:///") !== false) {
-                $fields = $this->parse_unix_sock($con);
-
-            } else if (strpos($con, "tcp://") !== false) {
-                $fields = $this->parse_url_tcp($con);
-
-            } else {
-                $fields = false;
-                debugging("Invalid Redis schema in connection savepath");
-
-            }
-
-            // Parsing failed.
-            if ($fields === false) {
-                continue;
-            }
-
-            // Setting the default prefix.
-            if (!isset($fields['prefix'])) {
-                $fields['prefix'] = 'PHPREDIS_SESSION:';
-            }
-
-            // Setting the default database.
-            if (!isset($fields['database'])) {
-                $fields['database'] = 0;
-            }
-
-            // Setting the default timeout.
-            if (!isset($fields['timeout'])) {
-                $fields['timeout'] = 86400;
-            }
-
-            $servers[] = $fields;
-        }
-
-        return $servers;
-    }
-
-    /**
-     * Parses the tcp connection string and returns an object.
-     * @param string $con connection string
-     * @return object $con connection data object
-     */
-    private function parse_url_tcp($con) {
-        $con = parse_url($con);
-
-        // Seriously wrong url, parsing failed.
-        if ($con === false) {
-            return false;
-        }
-
-        // Parsing the query string.
-        if (isset($con['query'])) {
-            $query = $con['query'];
-            $parts = explode('&', $query);
-
-            foreach ($parts as $part) {
-                list($key, $value) = explode('=', $part);
-                $con[$key] = $value;
-            }
-        }
-
-        // Setting the default port.
-        if (!isset($con['port'])) {
-            $con['port'] = 6379;
-        }
-
-        return $con;
-    }
-
-    /**
-     * Parses the unix domain socket connection string and returns an object.
-     * @param string $con connection string
-     * @return object $con connection data object
-     */
-    private function parse_unix_sock($con) {
-        // Lets use parse_url to get the bits we need.
-        // To use this, replace the three slashes with two slashes.
-        $con = str_replace(":///", "://", $con);
-        $con = parse_url($con);
-
-        // Seriously wrong url, parsing failed.
-        if ($con === false) {
-            return false;
-        }
-
-        /* Eg. host = var
-               path = run/redis/redis.sock
-               new path = /var/run/redis/redis.sock
-        */
-        $con['path'] = '/' . $con['host'] . $con['path'];
-        unset($con['host']);
-
-        // Parsing the query string.
-        if (isset($con['query'])) {
-            $query = $con['query'];
-            $parts = explode('&', $query);
-
-            foreach ($parts as $part) {
-                list($key, $value) = explode('=', $part);
-                $con[$key] = $value;
-            }
-        }
-
-        return $con;
-    }
-
-    /**
-     * Connects to the Redis server with the details from the connection object.
-     * @param object $con connection details object
-     * @return redis $redis redis connection
-     */
-    private function redis_connect($con) {
-        $redis = new \Redis();
-
-        $func = isset($con['persistent']) ? 'pconnect' : 'connect';
-
-        if ($con['scheme'] === 'tcp') {
-            // Only TCP connections will have a port, default 6379.
-            $result = $redis->$func($con['host'], $con['port'], $con['timeout']);
-        } else if ($con['scheme'] === 'unix') {
-            // Unix domain socket.
-            $result = $redis->$func($con['path']);
-        }
-
-        $result = true ? $redis->select($con['database']) : false;
-
-        return $redis;
-    }
-}
-
index c0dc3b2..5bec76c 100644 (file)
@@ -962,6 +962,7 @@ $functions = array(
         'type'         => 'read',
         'capabilities' => 'moodle/competency:competencyview',
         'ajax'         => true,
+        'services'     => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'core_competency_delete_competency' => array(
         'classname'    => 'core_competency\external',
@@ -1061,6 +1062,7 @@ $functions = array(
         'type'         => 'read',
         'capabilities' => 'moodle/competency:coursecompetencyview',
         'ajax'         => true,
+        'services'     => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'core_competency_count_competencies_in_course' => array(
         'classname'    => 'core_competency\external',
@@ -1394,6 +1396,7 @@ $functions = array(
         'type'         => 'read',
         'capabilities' => 'moodle/competency:competencymanage',
         'ajax'         => true,
+        'services'     => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'core_competency_add_related_competency' => array(
         'classname'    => 'core_competency\external',
@@ -1493,6 +1496,7 @@ $functions = array(
         'type'         => 'read',
         'capabilities' => 'moodle/competency:usercompetencyview',
         'ajax'         => true,
+        'services'     => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'core_competency_user_competency_viewed_in_plan' => array(
         'classname'    => 'core_competency\external',
@@ -1502,6 +1506,7 @@ $functions = array(
         'type'         => 'read',
         'capabilities' => 'moodle/competency:usercompetencyview',
         'ajax'         => true,
+        'services'     => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'core_competency_user_competency_viewed_in_course' => array(
         'classname'    => 'core_competency\external',
@@ -1511,6 +1516,7 @@ $functions = array(
         'type'         => 'read',
         'capabilities' => 'moodle/competency:usercompetencyview',
         'ajax'         => true,
+        'services'     => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'core_competency_user_competency_plan_viewed' => array(
         'classname'    => 'core_competency\external',
@@ -1520,6 +1526,7 @@ $functions = array(
         'type'         => 'read',
         'capabilities' => 'moodle/competency:usercompetencyview',
         'ajax'         => true,
+        'services'     => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'core_competency_grade_competency' => array(
         'classname'    => 'core_competency\external',
@@ -1547,6 +1554,7 @@ $functions = array(
         'type'         => 'write',
         'capabilities' => 'moodle/competency:competencygrade',
         'ajax'         => true,
+        'services'     => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'core_competency_unlink_plan_from_template' => array(
         'classname'    => 'core_competency\external',
@@ -1592,6 +1600,7 @@ $functions = array(
         'type'          => 'write',
         'capabilities'  => 'moodle/competency:evidencedelete',
         'ajax'          => true,
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
 
     'core_webservice_get_site_info' => array(
index e8015a2..663bd6a 100644 (file)
@@ -1504,7 +1504,11 @@ class mysqli_native_moodle_database extends moodle_database {
     }
 
     public function sql_cast_char2real($fieldname, $text=false) {
-        return ' CAST(' . $fieldname . ' AS DECIMAL) ';
+        // Set to 65 (max mysql 5.5 precision) with 7 as scale
+        // because we must ensure at least 6 decimal positions
+        // per casting given that postgres is casting to that scale (::real::).
+        // Can be raised easily but that must be done in all DBs and tests.
+        return ' CAST(' . $fieldname . ' AS DECIMAL(65,7)) ';
     }
 
     /**
index b2bb25e..7df8222 100644 (file)
@@ -3670,7 +3670,7 @@ class core_dml_testcase extends database_driver_testcase {
 
         $DB->insert_record($tablename, array('name'=>'10.10', 'nametext'=>'10.10', 'res'=>5.1));
         $DB->insert_record($tablename, array('name'=>'91.10', 'nametext'=>'91.10', 'res'=>666));
-        $DB->insert_record($tablename, array('name'=>'011.10', 'nametext'=>'011.10', 'res'=>10.1));
+        $DB->insert_record($tablename, array('name'=>'011.13333333', 'nametext'=>'011.13333333', 'res'=>10.1));
 
         // Casting varchar field.
         $sql = "SELECT * FROM {{$tablename}} WHERE ".$DB->sql_cast_char2real('name')." > res";
@@ -3681,8 +3681,11 @@ class core_dml_testcase extends database_driver_testcase {
         $records = $DB->get_records_sql($sql);
         $this->assertCount(3, $records);
         $this->assertSame('10.10', reset($records)->name);
-        $this->assertSame('011.10', next($records)->name);
+        $this->assertSame('011.13333333', next($records)->name);
         $this->assertSame('91.10', next($records)->name);
+        // And verify we can operate with them without too much problem with at least 6 decimals scale accuracy.
+        $sql = "SELECT AVG(" . $DB->sql_cast_char2real('name') . ") FROM {{$tablename}}";
+        $this->assertEquals(37.44444443333333, (float)$DB->get_field_sql($sql), '', 1.0E-6);
 
         // Casting text field.
         $sql = "SELECT * FROM {{$tablename}} WHERE ".$DB->sql_cast_char2real('nametext', true)." > res";
@@ -3693,8 +3696,19 @@ class core_dml_testcase extends database_driver_testcase {
         $records = $DB->get_records_sql($sql);
         $this->assertCount(3, $records);
         $this->assertSame('10.10', reset($records)->nametext);
-        $this->assertSame('011.10', next($records)->nametext);
+        $this->assertSame('011.13333333', next($records)->nametext);
         $this->assertSame('91.10', next($records)->nametext);
+        // And verify we can operate with them without too much problem with at least 6 decimals scale accuracy.
+        $sql = "SELECT AVG(" . $DB->sql_cast_char2real('nametext', true) . ") FROM {{$tablename}}";
+        $this->assertEquals(37.44444443333333, (float)$DB->get_field_sql($sql), '', 1.0E-6);
+
+        // Check it works with values passed as param.
+        $sql = "SELECT name FROM {{$tablename}} WHERE FLOOR(res - " . $DB->sql_cast_char2real(':param') . ") = 0";
+        $this->assertEquals('011.13333333', $DB->get_field_sql($sql, array('param' => '10.09999')));
+
+        // And also, although not recommended, with directly passed values.
+        $sql = "SELECT name FROM {{$tablename}} WHERE FLOOR(res - " . $DB->sql_cast_char2real('10.09999') . ") = 0";
+        $this->assertEquals('011.13333333', $DB->get_field_sql($sql));
     }
 
     public function test_sql_compare_text() {
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 72e76db..d3f4a5c 100644 (file)
@@ -69,6 +69,8 @@ class MoodleQuickForm_course extends MoodleQuickForm_autocomplete {
      *                       'multiple' - boolean multi select
      *                       'exclude' - array or int, list of course ids to never show
      *                       'requiredcapabilities' - array of capabilities. Uses ANY to combine them.
+     *                       'limittoenrolled' - boolean Limits to enrolled courses.
+     *                       'includefrontpage' - boolean Enables the frontpage to be selected.
      */
     public function __construct($elementname = null, $elementlabel = null, $options = array()) {
         if (isset($options['multiple'])) {
@@ -102,6 +104,9 @@ class MoodleQuickForm_course extends MoodleQuickForm_autocomplete {
         if (isset($options['placeholder'])) {
             $validattributes['placeholder'] = $options['placeholder'];
         }
+        if (!empty($options['includefrontpage'])) {
+            $validattributes['data-includefrontpage'] = SITEID;
+        }
 
         parent::__construct($elementname, $elementlabel, array(), $validattributes);
     }
@@ -125,7 +130,7 @@ class MoodleQuickForm_course extends MoodleQuickForm_autocomplete {
         }
 
         if (empty($coursestofetch)) {
-            return $this->setSelected(array());
+            return $this->setSelected($values);
         }
 
         // There is no API function to load a list of course from a list of ids.
@@ -144,14 +149,16 @@ class MoodleQuickForm_course extends MoodleQuickForm_autocomplete {
         $coursestoselect = array();
         foreach ($list as $course) {
             context_helper::preload_from_record($course);
+            $context = context_course::instance($course->id);
             // Make sure we can see the course.
-            if (!$course->visible && !has_capability('moodle/course:viewhiddencourses', context_course::instance($course->id))) {
+            if (!$course->visible && !has_capability('moodle/course:viewhiddencourses', $context)) {
                 continue;
             }
-            $label = get_course_display_name_for_list($course);
+            $label = format_string(get_course_display_name_for_list($course), true, ['context' => $context]);
             $this->addOption($label, $course->id);
             array_push($coursestoselect, $course->id);
         }
-        return $this->setSelected($coursestoselect);
+
+        return $this->setSelected($values);
     }
 }
index b37f36c..c85326a 100644 (file)
@@ -8,8 +8,7 @@ M.form_url.init = function(Y, options) {
     Y.on('click', function(e, client_id) {
         e.preventDefault();
         M.core_filepicker.instances[client_id].show();
-    }, '#filepicker-button-'+options.client_id, null, options.client_id);
-
+    }, '#filepicker-button-js-'+options.client_id, null, options.client_id);
 };
 
 M.form_url.callback = function (params) {
index f62a277..e531486 100644 (file)
@@ -80,27 +80,29 @@ function form_init_date_js() {
         $calendar = \core_calendar\type_factory::get_calendar_instance();
         $module   = 'moodle-form-dateselector';
         $function = 'M.form.dateselector.init_date_selectors';
+        $defaulttimezone = date_default_timezone_get();
+
         $config = array(array(
             'firstdayofweek'    => $calendar->get_starting_weekday(),
-            'mon'               => strftime('%a', strtotime("Monday")),
-            'tue'               => strftime('%a', strtotime("Tuesday")),
-            'wed'               => strftime('%a', strtotime("Wednesday")),
-            'thu'               => strftime('%a', strtotime("Thursday")),
-            'fri'               => strftime('%a', strtotime("Friday")),
-            'sat'               => strftime('%a', strtotime("Saturday")),
-            'sun'               => strftime('%a', strtotime("Sunday")),
-            'january'           => strftime('%B', strtotime("January 1")),
-            'february'          => strftime('%B', strtotime("February 1")),
-            'march'             => strftime('%B', strtotime("March 1")),
-            'april'             => strftime('%B', strtotime("April 1")),
-            'may'               => strftime('%B', strtotime("May 1")),
-            'june'              => strftime('%B', strtotime("June 1")),
-            'july'              => strftime('%B', strtotime("July 1")),
-            'august'            => strftime('%B', strtotime("August 1")),
-            'september'         => strftime('%B', strtotime("September 1")),
-            'october'           => strftime('%B', strtotime("October 1")),
-            'november'          => strftime('%B', strtotime("November 1")),
-            'december'          => strftime('%B', strtotime("December 1"))
+            'mon'               => date_format_string(strtotime("Monday"), '%a', $defaulttimezone),
+            'tue'               => date_format_string(strtotime("Tuesday"), '%a', $defaulttimezone),
+            'wed'               => date_format_string(strtotime("Wednesday"), '%a', $defaulttimezone),
+            'thu'               => date_format_string(strtotime("Thursday"), '%a', $defaulttimezone),
+            'fri'               => date_format_string(strtotime("Friday"), '%a', $defaulttimezone),
+            'sat'               => date_format_string(strtotime("Saturday"), '%a', $defaulttimezone),
+            'sun'               => date_format_string(strtotime("Sunday"), '%a', $defaulttimezone),
+            'january'           => date_format_string(strtotime("January 1"), '%B', $defaulttimezone),
+            'february'          => date_format_string(strtotime("February 1"), '%B', $defaulttimezone),
+            'march'             => date_format_string(strtotime("March 1"), '%B', $defaulttimezone),
+            'april'             => date_format_string(strtotime("April 1"), '%B', $defaulttimezone),
+            'may'               => date_format_string(strtotime("May 1"), '%B', $defaulttimezone),
+            'june'              => date_format_string(strtotime("June 1"), '%B', $defaulttimezone),
+            'july'              => date_format_string(strtotime("July 1"), '%B', $defaulttimezone),
+            'august'            => date_format_string(strtotime("August 1"), '%B', $defaulttimezone),
+            'september'         => date_format_string(strtotime("September 1"), '%B', $defaulttimezone),
+            'october'           => date_format_string(strtotime("October 1"), '%B', $defaulttimezone),
+            'november'          => date_format_string(strtotime("November 1"), '%B', $defaulttimezone),
+            'december'          => date_format_string(strtotime("December 1"), '%B', $defaulttimezone)
         ));
         $PAGE->requires->yui_module($module, $function, $config);
         $done = true;
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);
diff --git a/lib/tests/redis_session_test.php b/lib/tests/redis_session_test.php
deleted file mode 100644 (file)
index 14ecfd8..0000000
+++ /dev/null
@@ -1,112 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Tests redis session handler
- *
- * @package    core
- * @copyright  2016 Nicholas Hoobin (nicholashoobin@catalyst-au.net)
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-/**
- * Tests redis session handler class
- *
- * @package    core
- * @copyright  2016 Nicholas Hoobin (nicholashoobin@catalyst-au.net)
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class redis_session_testcase extends advanced_testcase {
-
-    /**
-     * Test test_redis_connection_parser()
-     * @param string $constring The connection string, 'savepath'.
-     * @param array $expected An array of expected results.
-     * @param int $count Number of valid connections.
-     * @dataProvider connectionprovider
-     */
-    public function test_redis_connection_parser($constring, $expected, $debug) {
-        $handler = new \core\session\redis;
-
-        $servers = $handler->connection_string_to_redis_servers($constring);
-
-        if ($debug == true) {
-            $this->assertDebuggingCalled();
-        }
-
-        $this->assertEquals($expected, $servers);
-    }
-
-    /**
-     * Provides data for test_redis_connection_parser().
-     * @return array array of connection results
-     */
-    public function connectionprovider() {
-        return array(
-            array(
-                "tcp://127.0.0.1, unix:///var/run/redis/redis.sock",
-                array(
-                    array(
-                        'database' => 0,
-                        'timeout' => 86400,
-                        'port' => 6379,
-                        'scheme' => 'tcp',
-                        'prefix' => 'PHPREDIS_SESSION:',
-                        'host' => '127.0.0.1'
-                    ),
-                    array(
-                        'database' => 0,
-                        'timeout' => 86400,
-                        'scheme' => 'unix',
-                        'prefix' => 'PHPREDIS_SESSION:',
-                        'path' => '/var/run/redis/redis.sock'
-                    )
-                ),
-                false
-            ),
-            array(
-                "tcp://127.0.0.1?database=2&timeout=2.5&port=54428",
-                array(
-                    array(
-                        'database' => '2',
-                        'timeout' => '2.5',
-                        'port' => '54428',
-                        'scheme' => 'tcp',
-                        'prefix' => 'PHPREDIS_SESSION:',
-                        'host' => '127.0.0.1',
-                        'query' => 'database=2&timeout=2.5&port=54428'
-                    ),
-                ),
-                false
-            ),
-            array(
-                "127.0.0.1",
-                array(),
-                true
-            ),
-            array(
-                "tcp:sdgf243@Q#t23",
-                array(),
-                true
-            ),
-            array(
-                "/var/run/redis/redis.sock",
-                array(),
-                true
-            )
-        );
-    }
-}
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..a703923 100644 (file)
@@ -119,6 +119,15 @@ 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.
+  * pre_course_category_delete($category)
+  * pre_course_delete($course)
+  * pre_course_module_delete($cm)
+  * pre_block_delete($instance)
+  * pre_user_delete($user)
+  These hooks allow developers to use the item in question before it is deleted by core.
 
 === 3.0 ===
 
index afb0973..c3a8505 100644 (file)
 
 .path-mod-assign [data-region="grade-panel"] #id_gradeheader {
     display: table-cell;
+    min-width: 0;
 }
 
 .path-mod-assign [data-region="grade-panel"] #id_gradeheader > legend {
 
 .path-mod-assign [data-region="grade-panel"] .gradingform_rubric {
     padding-bottom: 0;
+    max-width: none;
 }
 
 .path-mod-assign [data-region="grade-panel"] .gradingform_rubric .criterion .description {
     left: 20%;
     right: 20%;
     top: 20%;
+    bottom: 20%;
     z-index: 1000;
     border: 1px solid rgba(0, 0, 0, 0.3);
     border-radius: 6px;
     text-align: center;
     padding-left: 15px;
     padding-right: 15px;
+    height: 45px;
 }
 
 .path-mod-assign #page-content [data-region="grade-panel"] .mform:not(.unresponsive) .fcontainer .fitem.popout .fitemtitle label {
 
 .path-mod-assign #page-content [data-region="grade-panel"] .mform:not(.unresponsive) .fcontainer .fitem.popout .felement {
     padding: 10px 15px 15px;
+    height: calc(100% - 54px);
+    overflow: auto;
+}
+
+.path-mod-assign #page-content [data-region="grade-panel"] .mform:not(.unresponsive) .fcontainer .fitem.popout .felement .gradingform_rubric {
+    overflow: visible;
 }
 
 /***** End popout dialogue *****/
index 7d1dbc6..47e1953 100644 (file)
@@ -1869,8 +1869,15 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals(0, $result['lastattempt']['submission']['groupid']);
         $this->assertEquals($assign->get_instance()->id, $result['lastattempt']['submission']['assignment']);
         $this->assertEquals(1, $result['lastattempt']['submission']['latest']);
-        $this->assertEquals('Submission text', $result['lastattempt']['submission']['plugins'][0]['editorfields'][0]['text']);
-        $this->assertEquals('/t.txt', $result['lastattempt']['submission']['plugins'][1]['fileareas'][0]['files'][0]['filepath']);
+
+        // Map plugins based on their type - we can't rely on them being in a
+        // particular order, especially if 3rd party plugins are installed.
+        $submissionplugins = array();
+        foreach ($result['lastattempt']['submission']['plugins'] as $plugin) {
+            $submissionplugins[$plugin['type']] = $plugin;
+        }
+        $this->assertEquals('Submission text', $submissionplugins['onlinetext']['editorfields'][0]['text']);
+        $this->assertEquals('/t.txt', $submissionplugins['file']['fileareas'][0]['files'][0]['filepath']);
     }
 
     /**
@@ -1998,10 +2005,21 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals(50, $result['previousattempts'][0]['grade']['grade']);
         $this->assertEquals($teacher->id, $result['previousattempts'][0]['grade']['grader']);
         $this->assertEquals($student1->id, $result['previousattempts'][0]['grade']['userid']);
-        $this->assertEquals('Yeeha!', $result['previousattempts'][0]['feedbackplugins'][0]['editorfields'][0]['text']);
-        $submissionplugins = $result['previousattempts'][0]['submission']['plugins'];
-        $this->assertEquals('Submission text', $submissionplugins[0]['editorfields'][0]['text']);
-        $this->assertEquals('/t.txt', $submissionplugins[1]['fileareas'][0]['files'][0]['filepath']);
+
+        // Map plugins based on their type - we can't rely on them being in a
+        // particular order, especially if 3rd party plugins are installed.
+        $feedbackplugins = array();
+        foreach ($result['previousattempts'][0]['feedbackplugins'] as $plugin) {
+            $feedbackplugins[$plugin['type']] = $plugin;
+        }
+        $this->assertEquals('Yeeha!', $feedbackplugins['comments']['editorfields'][0]['text']);
+
+        $submissionplugins = array();
+        foreach ($result['previousattempts'][0]['submission']['plugins'] as $plugin) {
+            $submissionplugins[$plugin['type']] = $plugin;
+        }
+        $this->assertEquals('Submission text', $submissionplugins['onlinetext']['editorfields'][0]['text']);
+        $this->assertEquals('/t.txt', $submissionplugins['file']['fileareas'][0]['files'][0]['filepath']);
     }
 
     /**
index f691574..49583e7 100644 (file)
@@ -2430,8 +2430,12 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
 
         // Test that all the submission and feedback plugins are returning the expected file aras.
         $usingfilearea = 0;
+        $coreplugins = core_plugin_manager::standard_plugins_list('assignsubmission');
         foreach ($assign->get_submission_plugins() as $plugin) {
             $type = $plugin->get_type();
+            if (!in_array($type, $coreplugins)) {
+                continue;
+            }
             $fileareas = $plugin->get_file_areas();
 
             if ($type == 'onlinetext') {
@@ -2447,8 +2451,12 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
         $this->assertEquals(2, $usingfilearea);
 
         $usingfilearea = 0;
+        $coreplugins = core_plugin_manager::standard_plugins_list('assignfeedback');
         foreach ($assign->get_feedback_plugins() as $plugin) {
             $type = $plugin->get_type();
+            if (!in_array($type, $coreplugins)) {
+                continue;
+            }
             $fileareas = $plugin->get_file_areas();
 
             if ($type == 'editpdf') {
index fe92115..71ed1a9 100644 (file)
@@ -40,7 +40,7 @@ class mod_feedback_course_map_form extends moodleform {
         $mform->addElement('hidden', 'id');
         $mform->setType('id', PARAM_INT);
 
-        $options = array('multiple' => true);
+        $options = array('multiple' => true, 'includefrontpage' => true);
         $mform->addElement('course', 'mappedcourses', get_string('courses'), $options);
 
         $this->add_action_buttons();
index daa61a2..52b9c5d 100644 (file)
@@ -25,6 +25,7 @@ class feedback_info_form extends feedback_item_form {
         $common = $this->_customdata['common'];
         $positionlist = $this->_customdata['positionlist'];
         $position = $this->_customdata['position'];
+        $presentationoptions = $this->_customdata['presentationoptions'];
 
         $mform =& $this->_form;
 
@@ -41,14 +42,10 @@ class feedback_info_form extends feedback_item_form {
                             get_string('item_label', 'feedback'),
                             array('size'=>FEEDBACK_ITEM_LABEL_TEXTBOX_SIZE, 'maxlength'=>255));
 
-        $options=array();
-        $options[1]  = get_string('responsetime', 'feedback');
-        $options[2]  = get_string('course');
-        $options[3]  = get_string('coursecategory');
         $this->infotype = &$mform->addElement('select',
                                               'presentation',
                                               get_string('infotype', 'feedback'),
-                                              $options);
+                                              $presentationoptions);
 
         parent::definition();
         $this->set_data($item);
index 20cc371..2e47674 100644 (file)
@@ -48,7 +48,7 @@ class feedback_item_info extends feedback_item_base {
         //the elements for position dropdownlist
         $positionlist = array_slice(range(0, $i_formselect_last), 1, $i_formselect_last, true);
 
-        $item->presentation = empty($item->presentation) ? 1 : $item->presentation;
+        $item->presentation = empty($item->presentation) ? self::MODE_COURSE : $item->presentation;
         $item->required = 0;
 
         //all items for dependitem
@@ -59,12 +59,23 @@ class feedback_item_info extends feedback_item_base {
                              'items'=>$feedbackitems,
                              'feedback'=>$feedback->id);
 
+        // Options for the 'presentation' select element.
+        $presentationoptions = array();
+        if ($feedback->anonymous == FEEDBACK_ANONYMOUS_NO || $item->presentation == self::MODE_RESPONSETIME) {
+            // "Response time" is hidden anyway in case of anonymous feedback, no reason to offer this option.
+            // However if it was already selected leave it in the dropdown.
+            $presentationoptions[self::MODE_RESPONSETIME] = get_string('responsetime', 'feedback');
+        }
+        $presentationoptions[self::MODE_COURSE]  = get_string('course');
+        $presentationoptions[self::MODE_CATEGORY]  = get_string('coursecategory');
+
         //build the form
         $this->item_form = new feedback_info_form('edit_item.php',
                                                   array('item'=>$item,
                                                   'common'=>$commonparams,
                                                   'positionlist'=>$positionlist,
-                                                  'position' => $position));
+                                                  'position' => $position,
+                                                  'presentationoptions' => $presentationoptions));
     }
 
     public function save_item() {
@@ -112,7 +123,7 @@ class feedback_item_info extends feedback_item_base {
                 switch($presentation) {
                     case self::MODE_RESPONSETIME:
                         $datavalue->value = $value->value;
-                        $datavalue->show = userdate($datavalue->value);
+                        $datavalue->show = $value->value ? userdate($datavalue->value) : '';
                         break;
                     case self::MODE_COURSE:
                         $datavalue->value = $value->value;
@@ -153,7 +164,8 @@ class feedback_item_info extends feedback_item_base {
             echo '</th></tr>';
             $sizeofdata = count($data);
             for ($i = 0; $i < $sizeofdata; $i++) {
-                echo '<tr><td colspan="2" class="singlevalue">';
+                $class = strlen(trim($data[$i]->show)) ? '' : ' class="isempty"';
+                echo '<tr'.$class.'><td colspan="2" class="singlevalue">';
                 echo str_replace("\n", '<br />', $data[$i]->show);
                 echo '</td></tr>';
             }
index 1e9d1fd..a8a3827 100644 (file)
@@ -245,7 +245,7 @@ if ($generalforums) {
                 } else if ($unread = forum_tp_count_forum_unread_posts($cm, $course)) {
                         $unreadlink = '<span class="unread"><a href="view.php?f='.$forum->id.'">'.$unread.'</a>';
                     $unreadlink .= '<a title="'.$strmarkallread.'" href="markposts.php?f='.
-                                   $forum->id.'&amp;mark=read"><img src="'.$OUTPUT->pix_url('t/markasread') . '" alt="'.$strmarkallread.'" class="iconsmall" /></a></span>';
+                                   $forum->id.'&amp;mark=read&amp;sesskey=' . sesskey() . '"><img src="'.$OUTPUT->pix_url('t/markasread') . '" alt="'.$strmarkallread.'" class="iconsmall" /></a></span>';
                 } else {
                     $unreadlink = '<span class="read">0</span>';
                 }
@@ -383,7 +383,7 @@ if ($course->id != SITEID) {    // Only real courses have learning forums
                     } else if ($unread = forum_tp_count_forum_unread_posts($cm, $course)) {
                         $unreadlink = '<span class="unread"><a href="view.php?f='.$forum->id.'">'.$unread.'</a>';
                         $unreadlink .= '<a title="'.$strmarkallread.'" href="markposts.php?f='.
-                                       $forum->id.'&amp;mark=read"><img src="'.$OUTPUT->pix_url('t/markasread') . '" alt="'.$strmarkallread.'" class="iconsmall" /></a></span>';
+                                       $forum->id.'&amp;mark=read&sesskey=' . sesskey() . '"><img src="'.$OUTPUT->pix_url('t/markasread') . '" alt="'.$strmarkallread.'" class="iconsmall" /></a></span>';
                     } else {
                         $unreadlink = '<span class="read">0</span>';
                     }
index 58b9e0a..fe08813 100644 (file)
@@ -3741,7 +3741,7 @@ function forum_print_discussion_header(&$post, $forum, $group = -1, $datestring
                     echo $post->unread;
                     echo '</a>';
                     echo '<a title="'.$strmarkalldread.'" href="'.$CFG->wwwroot.'/mod/forum/markposts.php?f='.
-                         $forum->id.'&amp;d='.$post->discussion.'&amp;mark=read&amp;returnpage=view.php">' .
+                         $forum->id.'&amp;d='.$post->discussion.'&amp;mark=read&amp;returnpage=view.php&amp;sesskey=' . sesskey() . '">' .
                          '<img src="'.$OUTPUT->pix_url('t/markasread') . '" class="iconsmall" alt="'.$strmarkalldread.'" /></a>';
                     echo '</span>';
                 } else {
@@ -5433,7 +5433,7 @@ function forum_print_latest_discussions($course, $forum, $maxdiscussions = -1, $
                 if ($forumtracked) {
                     echo '<a title="'.get_string('markallread', 'forum').
                          '" href="'.$CFG->wwwroot.'/mod/forum/markposts.php?f='.
-                         $forum->id.'&amp;mark=read&amp;returnpage=view.php">'.
+                         $forum->id.'&amp;mark=read&amp;returnpage=view.php&amp;sesskey=' . sesskey() . '">'.
                          '<img src="'.$OUTPUT->pix_url('t/markasread') . '" class="iconsmall" alt="'.get_string('markallread', 'forum').'" /></a>';
                 }
                 echo '</th>';
index e6b9442..52fda29 100644 (file)
@@ -55,6 +55,7 @@ if (!$cm = get_coursemodule_from_instance("forum", $forum->id, $course->id)) {
 $user = $USER;
 
 require_login($course, false, $cm);
+require_sesskey();
 
 if ($returnpage == 'index.php') {
     $returnto = new moodle_url("/mod/forum/$returnpage", array('id' => $course->id));
index ab209b0..74e9c30 100644 (file)
@@ -53,7 +53,9 @@ if (!is_null($sesskey)) {
 }
 if (!is_null($discussionid)) {
     $url->param('d', $discussionid);
-    $discussion = $DB->get_record('forum_discussions', array('id' => $discussionid), '*', MUST_EXIST);
+    if (!$discussion = $DB->get_record('forum_discussions', array('id' => $discussionid, 'forum' => $id))) {
+        print_error('invaliddiscussionid', 'forum');
+    }
 }
 $PAGE->set_url($url);
 
index fe7b43e..cf6bb70 100644 (file)
@@ -32,7 +32,9 @@ $includetext    = optional_param('includetext', false, PARAM_BOOL);
 
 $forum          = $DB->get_record('forum', array('id' => $forumid), '*', MUST_EXIST);
 $course         = $DB->get_record('course', array('id' => $forum->course), '*', MUST_EXIST);
-$discussion     = $DB->get_record('forum_discussions', array('id' => $discussionid), '*', MUST_EXIST);
+if (!$discussion = $DB->get_record('forum_discussions', array('id' => $discussionid, 'forum' => $forumid))) {
+    print_error('invaliddiscussionid', 'forum');
+}
 $cm             = get_coursemodule_from_instance('forum', $forum->id, $course->id, false, MUST_EXIST);
 $context        = context_module::instance($cm->id);
 
index 47923c3..20dd6c0 100644 (file)
@@ -23,6 +23,7 @@ function scorm_openpopup(url,name,options,width,height) {
     options += ",width=" + width + ",height=" + height;
 
     windowobj = window.open(url,name,options);
+    windowobj.opener = null;
     if (!windowobj) {
         return;
     }
index b2c3d9c..22ab9c6 100644 (file)
@@ -83,6 +83,7 @@ M.mod_scormform.init = function(Y) {
         winobj = window.open(launch_url,'Popup', poptions);
         this.target = 'Popup';
         scormredirect(winobj);
+        winobj.opener = null;
     }
     // Listen for view form submit and generate popup on user interaction.
     if (scormform) {
@@ -91,6 +92,7 @@ M.mod_scormform.init = function(Y) {
             winobj = window.open(launch_url, 'Popup', poptions);
             this.target = 'Popup';
             scormredirect(winobj);
+            winobj.opener = null;
             e.preventDefault();
         }, scormform);
     }
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);
diff --git a/theme/bootstrapbase/less/moodle/yui_fixes.less b/theme/bootstrapbase/less/moodle/yui_fixes.less
deleted file mode 100644 (file)
index c71797e..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-// we might be able to just turn these off for 2.5 see bug MDL-????
-input[type=text],input[type=password],textarea {
-    width:auto;
-}
-th,
-td {
-    border: 0 solid #fff;
-}
index 3eb12c8..5bf35a9 100644 (file)
@@ -131,6 +131,7 @@ class user_edit_form extends moodleform {
             $fields = get_user_fieldnames();
             $authplugin = get_auth_plugin($user->auth);
             $customfields = $authplugin->get_custom_user_profile_fields();
+            $customfieldsdata = profile_user_record($userid, false);
             $fields = array_merge($fields, $customfields);
             foreach ($fields as $field) {
                 if ($field === 'description') {
@@ -142,7 +143,15 @@ class user_edit_form extends moodleform {
                 if (!$mform->elementExists($formfield)) {
                     continue;
                 }
-                $value = $mform->getElement($formfield)->exportValue($mform->getElementValue($formfield)) ?: '';
+
+                // Get the original value for the field.
+                if (in_array($field, $customfields)) {
+                    $key = str_replace('profile_field_', '', $field);
+                    $value = isset($customfieldsdata->{$key}) ? $customfieldsdata->{$key} : '';
+                } else {
+                    $value = $user->{$field};
+                }
+
                 $configvariable = 'field_lock_' . $field;
                 if (isset($authplugin->config->{$configvariable})) {
                     if ($authplugin->config->{$configvariable} === 'locked') {
index 41470bb..d38bbe8 100644 (file)
@@ -561,9 +561,10 @@ function profile_signup_fields($mform) {
 /**
  * Returns an object with the custom profile fields set for the given user
  * @param integer $userid
+ * @param bool $onlyinuserobject True if you only want the ones in $USER.
  * @return stdClass
  */
-function profile_user_record($userid) {
+function profile_user_record($userid, $onlyinuserobject = true) {
     global $CFG, $DB;
 
     $usercustomfields = new stdClass();
@@ -573,7 +574,7 @@ function profile_user_record($userid) {
             require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php');
             $newfield = 'profile_field_'.$field->datatype;
             $formfield = new $newfield($field->id, $userid);
-            if ($formfield->is_user_object_data()) {
+            if (!$onlyinuserobject || $formfield->is_user_object_data()) {
                 $usercustomfields->{$field->shortname} = $formfield->data;
             }
         }
index 7e3eab3..6e24e0e 100644 (file)
@@ -62,6 +62,9 @@ class core_user_profilelib_testcase extends advanced_testcase {
         // Check that profile_user_record returns same (no) fields.
         $this->assertObjectNotHasAttribute('frogdesc', profile_user_record($user->id));
 
+        // Check that profile_user_record returns all the fields when requested.
+        $this->assertObjectHasAttribute('frogdesc', profile_user_record($user->id, false));
+
         // Add another custom field, this time of normal text type.
         $id2 = $DB->insert_record('user_info_field', array(
                 'shortname' => 'frogname', 'name' => 'Name of frog', 'categoryid' => 1,
@@ -77,6 +80,9 @@ class core_user_profilelib_testcase extends advanced_testcase {
 
         // Check profile_user_record returns same field.
         $this->assertObjectHasAttribute('frogname', profile_user_record($user->id));
+
+        // Check that profile_user_record returns all the fields when requested.
+        $this->assertObjectHasAttribute('frogname', profile_user_record($user->id, false));
     }
 
     /**
index edf2273..0a8bd77 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 545e964..a32b155 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2016050400.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2016050600.00;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
-$release  = '3.1beta (Build: 20160504)'; // Human-friendly version name
+$release  = '3.1beta+ (Build: 20160506)'; // Human-friendly version name
 
 $branch   = '31';                       // This version's branch.
 $maturity = MATURITY_BETA;             // This version's maturity level.