Merge branch 'MDL-60249-master' of git://github.com/damyon/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 27 Nov 2017 23:48:41 +0000 (00:48 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 27 Nov 2017 23:48:41 +0000 (00:48 +0100)
67 files changed:
admin/tool/oauth2/classes/form/issuer.php
admin/tool/oauth2/lang/en/tool_oauth2.php
analytics/classes/course.php
analytics/tests/dataset_manager_test.php
auth/db/lang/en/auth_db.php
backup/util/settings/base_setting.class.php
backup/util/settings/setting_dependency.class.php
backup/util/ui/backup_ui_setting.class.php
backup/util/ui/base_moodleform.class.php
backup/util/ui/tests/behat/restore_moodle2_courses_settings.feature [new file with mode: 0644]
blocks/globalsearch/block_globalsearch.php
calendar/classes/external/month_exporter.php
calendar/classes/local/event/mappers/event_mapper.php
calendar/externallib.php
calendar/lib.php
calendar/tests/container_test.php
calendar/tests/event_factory_test.php
calendar/tests/event_mapper_test.php
calendar/tests/externallib_test.php
calendar/tests/helpers.php
calendar/tests/repeat_event_collection_test.php
completion/classes/external.php
completion/tests/externallib_test.php
composer.json
composer.lock
enrol/externallib.php
grade/grading/lib.php
group/externallib.php
group/import.php
group/lib.php
group/tests/behat/groups_import.feature
group/tests/externallib_test.php
group/tests/fixtures/groups_import_multicourse.csv [new file with mode: 0644]
lang/en/cache.php
lib/amd/build/form-autocomplete.min.js
lib/amd/src/form-autocomplete.js
lib/blocklib.php
lib/classes/oauth2/client.php
lib/classes/oauth2/issuer.php
lib/db/caches.php
lib/db/install.xml
lib/db/upgrade.php
lib/enrollib.php
lib/filelib.php
lib/form/templatable_form_element.php
lib/grouplib.php
lib/oauthlib.php
lib/outputcomponents.php
lib/testing/generator/data_generator.php
lib/tests/blocklib_test.php
lib/tests/formslib_test.php
mod/choice/amd/build/select_all_choices.min.js [new file with mode: 0644]
mod/choice/amd/src/select_all_choices.js [new file with mode: 0644]
mod/choice/renderer.php
mod/glossary/lib.php
mod/lti/db/upgrade.php
mod/lti/lib.php
mod/lti/locallib.php
mod/lti/version.php
mod/quiz/lib.php
mod/workshop/classes/external.php
mod/workshop/tests/external_test.php
theme/boost/templates/core_form/element-button-inline.mustache
theme/boost/templates/core_form/element-button.mustache
user/profile/lib.php
version.php
webservice/xmlrpc/lib.php

index a57b4c2..23874bb 100644 (file)
@@ -78,6 +78,10 @@ class issuer extends persistent {
         $mform->addRule('clientsecret', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
         $mform->addHelpButton('clientsecret', 'issuerclientsecret', 'tool_oauth2');
 
+        // Use basic authentication.
+        $mform->addElement('checkbox', 'basicauth', get_string('usebasicauth', 'tool_oauth2'));
+        $mform->addHelpButton('basicauth', 'usebasicauth', 'tool_oauth2');
+
         // Login scopes.
         $mform->addElement('text', 'loginscopes', get_string('issuerloginscopes', 'tool_oauth2'));
         $mform->addRule('loginscopes', null, 'required', null, 'client');
index 8c20c6a..b8fe75b 100644 (file)
@@ -95,6 +95,8 @@ $string['systemaccountconnected_help'] = 'System accounts are used to provide ad
 $string['systemaccountconnected'] = 'System account connected';
 $string['systemaccountnotconnected'] = 'System account not connected';
 $string['systemauthstatus'] = 'System account connected';
+$string['usebasicauth'] = 'Authenticate token requests via HTTP headers';
+$string['usebasicauth_help'] = 'Utilize the HTTP Basic authentication scheme when sending client ID and password with a refresh token request. Recommended by the OAuth 2 standard, but may not be available with some issuers.';
 $string['userfieldexternalfield'] = 'External field name';
 $string['userfieldexternalfield_help'] = 'Name of the field provided by the external OAuth system.';
 $string['userfieldinternalfield_help'] = 'Name of the Moodle user field that should be mapped from the external field.';
index eb87b75..919f36b 100644 (file)
@@ -134,7 +134,7 @@ class course implements \core_analytics\analysable {
      *
      * Lazy load of course data, students and teachers.
      *
-     * @param int|stdClass $course Course id
+     * @param int|\stdClass $course Course id
      * @return void
      */
     public function __construct($course) {
@@ -152,7 +152,7 @@ class course implements \core_analytics\analysable {
      *
      * Lazy load of course data, students and teachers.
      *
-     * @param int|stdClass $course Course object or course id
+     * @param int|\stdClass $course Course object or course id
      * @return \core_analytics\course
      */
     public static function instance($course) {
@@ -184,7 +184,7 @@ class course implements \core_analytics\analysable {
     /**
      * Loads the analytics course object.
      *
-     * @return null
+     * @return void
      */
     protected function load() {
 
@@ -451,7 +451,7 @@ class course implements \core_analytics\analysable {
     /**
      * Returns the course students.
      *
-     * @return stdClass[]
+     * @return int[]
      */
     public function get_students() {
 
@@ -595,7 +595,7 @@ class course implements \core_analytics\analysable {
      *
      * Keys are ignored.
      *
-     * @param int|float $values Sorted array of values
+     * @param int[]|float[] $values Sorted array of values
      * @return int
      */
     protected function median($values) {
@@ -605,7 +605,7 @@ class course implements \core_analytics\analysable {
             return reset($values);
         }
 
-        $middlevalue = floor(($count - 1) / 2);
+        $middlevalue = (int)floor(($count - 1) / 2);
 
         if ($count % 2) {
             // Odd number, middle is the median.
index 3856c5f..760e667 100644 (file)
@@ -138,6 +138,7 @@ class dataset_manager_testcase extends advanced_testcase {
         // Training and prediction files are not mixed up.
         $trainingfile1 = \core_analytics\dataset_manager::merge_datasets(array($file), $fakemodelid,
             '\core\analytics\time_splitting\quarters', \core_analytics\dataset_manager::LABELLED_FILEAREA, false);
+        $this->waitForSecond();
         $trainingfile2 = \core_analytics\dataset_manager::merge_datasets(array($file), $fakemodelid,
             '\core\analytics\time_splitting\quarters', \core_analytics\dataset_manager::LABELLED_FILEAREA, false);
 
index e2daf57..3b21d60 100644 (file)
@@ -45,7 +45,7 @@ $string['auth_dbname'] = 'Name of the database itself. Leave empty if using an O
 $string['auth_dbname_key'] = 'DB name';
 $string['auth_dbpass'] = 'Password matching the above username';
 $string['auth_dbpass_key'] = 'Password';
-$string['auth_dbpasstype'] = '<p>Specify the format that the password field is using. MD5 hashing is useful for connecting to other common web applications like PostNuke.</p> <p>Use \'internal\' if you want the external database to manage usernames and email addresses, but Moodle to manage passwords. If you use \'internal\', you <i>must</i> provide a populated email address field in the external database, and you must execute both admin/cron.php and auth/db/cli/sync_users.php regularly. Moodle will send an email to new users with a temporary password.</p>';
+$string['auth_dbpasstype'] = '<p>Specify the format that the password field is using.</p> <p>Use \'internal\' if you want the external database to manage usernames and email addresses, but Moodle to manage passwords. If you use \'internal\', you <i>must</i> provide a populated email address field in the external database, and you must execute both admin/cron.php and auth/db/cli/sync_users.php regularly. Moodle will send an email to new users with a temporary password.</p>';
 $string['auth_dbpasstype_key'] = 'Password format';
 $string['auth_dbreviveduser'] = 'Revived user {$a->name} id {$a->id}';
 $string['auth_dbrevivedusererror'] = 'Error reviving user {$a}';
index 4988d52..c8717fa 100644 (file)
@@ -65,6 +65,7 @@ abstract class base_setting {
 
     protected $name;  // name of the setting
     protected $value; // value of the setting
+    protected $unlockedvalue; // Value to set after the setting is unlocked.
     protected $vtype; // type of value (setting_base::IS_BOOLEAN/setting_base::IS_INTEGER...)
 
     protected $visibility; // visibility of the setting (setting_base::VISIBLE/setting_base::HIDDEN)
@@ -118,6 +119,7 @@ abstract class base_setting {
         $this->value       = $value;
         $this->visibility  = $visibility;
         $this->status      = $status;
+        $this->unlockedvalue = $this->value;
 
         // Generate a default ui
         $this->uisetting = new base_setting_ui($this);
@@ -225,6 +227,11 @@ abstract class base_setting {
         $this->status = $status;
         if ($status !== $oldstatus) { // Status has changed, let's inform dependencies
             $this->inform_dependencies(self::CHANGED_STATUS, $oldstatus);
+
+            if ($status == base_setting::NOT_LOCKED) {
+                // When setting gets unlocked set it to the original value.
+                $this->set_value($this->unlockedvalue);
+            }
         }
     }
 
index 425a328..079537f 100644 (file)
@@ -153,7 +153,7 @@ abstract class setting_dependency {
      */
     abstract public function get_moodleform_properties();
     /**
-     * Returns true if the dependent setting is locked.
+     * Returns true if the dependent setting is locked by this setting_dependency.
      * @return bool
      */
     abstract public function is_locked();
@@ -185,7 +185,7 @@ class setting_dependency_disabledif_equals extends setting_dependency {
         $this->value = ($value)?(string)$value:0;
     }
     /**
-     * Returns true if the dependent setting is locked.
+     * Returns true if the dependent setting is locked by this setting_dependency.
      * @return bool
      */
     public function is_locked() {
@@ -193,8 +193,8 @@ class setting_dependency_disabledif_equals extends setting_dependency {
         if ($this->setting->get_status() !== base_setting::NOT_LOCKED || $this->setting->get_value() == $this->value) {
             return true;
         }
-        // Else return based upon the dependent settings status
-        return ($this->dependentsetting->get_status() !== base_setting::NOT_LOCKED);
+        // Else the dependent setting is not locked by this setting_dependency.
+        return false;
     }
     /**
      * Processes a value change in the primary setting
@@ -343,7 +343,7 @@ class setting_dependency_disabledif_equals2 extends setting_dependency {
         $this->value = $value;
     }
     /**
-     * Returns true if the dependent setting is locked.
+     * Returns true if the dependent setting is locked by this setting_dependency.
      * @return bool
      */
     public function is_locked() {
@@ -351,8 +351,8 @@ class setting_dependency_disabledif_equals2 extends setting_dependency {
         if ($this->setting->get_status() !== base_setting::NOT_LOCKED || in_array($this->setting->get_value(), $this->value)) {
             return true;
         }
-        // Else return based upon the dependent settings status
-        return ($this->dependentsetting->get_status() !== base_setting::NOT_LOCKED);
+        // Else the dependent setting is not locked by this setting_dependency.
+        return false;
     }
     /**
      * Processes a value change in the primary setting
@@ -537,7 +537,7 @@ class setting_dependency_disabledif_not_empty extends setting_dependency_disable
     }
 
     /**
-     * Returns true if the dependent setting is locked.
+     * Returns true if the dependent setting is locked by this setting_dependency.
      * @return bool
      */
     public function is_locked() {
@@ -545,8 +545,8 @@ class setting_dependency_disabledif_not_empty extends setting_dependency_disable
         if ($this->setting->get_status() !== base_setting::NOT_LOCKED || !empty($value)) {
             return true;
         }
-        // Else return based upon the dependent settings status
-        return ($this->dependentsetting->get_status() !== base_setting::NOT_LOCKED);
+        // Else the dependent setting is not locked by this setting_dependency.
+        return false;
     }
 }
 
@@ -601,7 +601,7 @@ class setting_dependency_disabledif_empty extends setting_dependency_disabledif_
         return ($prevalue != $this->dependentsetting->get_value());
     }
     /**
-     * Returns true if the dependent setting is locked.
+     * Returns true if the dependent setting is locked by this setting_dependency.
      * @return bool
      */
     public function is_locked() {
@@ -609,7 +609,7 @@ class setting_dependency_disabledif_empty extends setting_dependency_disabledif_
         if ($this->setting->get_status() !== base_setting::NOT_LOCKED || empty($value)) {
             return true;
         }
-        // Else return based upon the dependent settings status
-        return ($this->dependentsetting->get_status() !== base_setting::NOT_LOCKED);
+        // Else the dependent setting is not locked by this setting_dependency.
+        return false;
     }
 }
index d928743..7109162 100644 (file)
@@ -307,10 +307,12 @@ abstract class backup_setting_ui extends base_setting_ui {
      * 2. The setting is locked but only by settings that are of the same level (same page)
      *
      * Condition 2 is really why we have this function
-     *
+     * @param int $level Optional, if provided only depedency_settings below or equal to this level are considered,
+     *          when checking if the ui_setting is changeable. Although dependencies might cause a lock on this setting,
+     *          they could be changeable in the same view.
      * @return bool
      */
-    public function is_changeable() {
+    public function is_changeable($level = null) {
         if ($this->setting->get_status() === backup_setting::NOT_LOCKED) {
             // Its not locked so its chanegable.
             return true;
@@ -319,6 +321,9 @@ abstract class backup_setting_ui extends base_setting_ui {
             return false;
         } else if ($this->setting->has_dependencies_on_settings()) {
             foreach ($this->setting->get_settings_depended_on() as $dependency) {
+                if ($level && $dependency->get_setting()->get_level() >= $level) {
+                    continue;
+                }
                 if ($dependency->is_locked() && $dependency->get_setting()->get_level() !== $this->setting->get_level()) {
                     // Its not changeable because one or more dependancies arn't changeable.
                     return false;
@@ -458,13 +463,16 @@ class backup_setting_ui_checkbox extends backup_setting_ui {
 
     /**
      * Returns true if the setting is changeable
+     * @param int $level Optional, if provided only depedency_settings below or equal to this level are considered,
+     *          when checking if the ui_setting is changeable. Although dependencies might cause a lock on this setting,
+     *          they could be changeable in the same view.
      * @return bool
      */
-    public function is_changeable() {
+    public function is_changeable($level = null) {
         if ($this->changeable === false) {
             return false;
         } else {
-            return parent::is_changeable();
+            return parent::is_changeable($level);
         }
     }
 
@@ -639,13 +647,16 @@ class backup_setting_ui_select extends backup_setting_ui {
     /**
      * Returns true if the setting is changeable, false otherwise
      *
+     * @param int $level Optional, if provided only depedency_settings below or equal to this level are considered,
+     *          when checking if the ui_setting is changeable. Although dependencies might cause a lock on this setting,
+     *          they could be changeable in the same view.
      * @return bool
      */
-    public function is_changeable() {
+    public function is_changeable($level = null) {
         if (count($this->values) == 1) {
             return false;
         } else {
-            return parent::is_changeable();
+            return parent::is_changeable($level);
         }
     }
 
index 4f8928e..34820e4 100644 (file)
@@ -183,11 +183,22 @@ abstract class base_moodleform extends moodleform {
     public function add_settings(array $settingstasks) {
         global $OUTPUT;
 
+        // Determine highest setting level, which is displayed in this stage. This is relevant for considering only
+        // locks of dependency settings for parent settings, which are not displayed in this stage.
+        $highestlevel = backup_setting::ACTIVITY_LEVEL;
+        foreach ($settingstasks as $st) {
+            list($setting, $task) = $st;
+            if ($setting->get_level() < $highestlevel) {
+                $highestlevel = $setting->get_level();
+            }
+        }
+
         $defaults = array();
         foreach ($settingstasks as $st) {
             list($setting, $task) = $st;
             // If the setting cant be changed or isn't visible then add it as a fixed setting.
-            if (!$setting->get_ui()->is_changeable() || $setting->get_visibility() != backup_setting::VISIBLE) {
+            if (!$setting->get_ui()->is_changeable($highestlevel) ||
+                $setting->get_visibility() != backup_setting::VISIBLE) {
                 $this->add_fixed_setting($setting, $task);
                 continue;
             }
diff --git a/backup/util/ui/tests/behat/restore_moodle2_courses_settings.feature b/backup/util/ui/tests/behat/restore_moodle2_courses_settings.feature
new file mode 100644 (file)
index 0000000..b851957
--- /dev/null
@@ -0,0 +1,125 @@
+@core @core_backup
+Feature: Restore Moodle 2 course backups with different user data settings
+  In order to decide upon including user data during backup and restore of courses
+  As a teacher and an admin
+  I need to be able to set and override backup and restore settings
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | student1 | Student | 1 | student1@example.com |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And the following "activities" exist:
+      | activity | name               | intro | course | idnumber |
+      | data     | Test database name | n     | C1     | data1    |
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I add a "Text input" field to "Test database name" database and I fill the form with:
+      | Field name | Test field name |
+      | Field description | Test field description |
+    And I follow "Templates"
+    And I wait until the page is ready
+    And I log out
+    And I log in as "student1"
+    And I am on "Course 1" course homepage
+    And I add an entry to "Test database name" database with:
+      | Test field name | Student entry |
+    And I press "Save and view"
+    And I log out
+    And I log in as "admin"
+    And I backup "Course 1" course using this options:
+      | Initial |  Include enrolled users | 1 |
+      | Confirmation | Filename | test_backup.mbz |
+
+  @javascript
+  Scenario: Restore a backup with user data
+    # "User data" marks the user data field for the section
+    # "-" marks the user data field for the data activity
+    When I restore "test_backup.mbz" backup into a new course using this options:
+      | Settings |  Include enrolled users | 1 |
+      | Schema | User data | 1 |
+      | Schema | - | 1 |
+    Then I should see "Test database name"
+    When I follow "Test database name"
+    Then I should see "Student entry"
+
+  @javascript
+  Scenario: Restore a backup without user data for data activity
+    # "User data" marks the user data field for the section
+    # "-" marks the user data field for the data activity
+    When I restore "test_backup.mbz" backup into a new course using this options:
+      | Settings |  Include enrolled users | 1 |
+      | Schema | User data | 1 |
+      | Schema | - | 0 |
+    Then I should see "Test database name"
+    When I follow "Test database name"
+    Then I should not see "Student entry"
+
+  @javascript
+  Scenario: Restore a backup without user data for section and data activity
+    # "User data" marks the user data field for the section
+    # "-" marks the user data field for the data activity
+    When I restore "test_backup.mbz" backup into a new course using this options:
+      | Settings |  Include enrolled users | 1 |
+      | Schema | User data | 0 |
+      | Schema | - | 0 |
+    Then I should see "Test database name"
+    When I follow "Test database name"
+    Then I should not see "Student entry"
+
+  @javascript
+  Scenario: Restore a backup without user data for section
+    # "User data" marks the user data field for the section
+    # "-" marks the user data field for the data activity
+    When I restore "test_backup.mbz" backup into a new course using this options:
+      | Settings |  Include enrolled users | 1 |
+      | Schema | - | 1 |
+      | Schema | User data | 0 |
+    Then I should see "Test database name"
+    When I follow "Test database name"
+    Then I should not see "Student entry"
+
+  @javascript
+  Scenario: Restore a backup with user data with local config for including users set to 0
+    And I restore "test_backup.mbz" backup into a new course using this options:
+      | Settings |  Include enrolled users | 0 |
+    Then I should see "Test database name"
+    When I follow "Test database name"
+    Then I should not see "Student entry"
+
+  @javascript
+  Scenario: Restore a backup with user data with site config for including users set to 0
+    Given I navigate to "General restore defaults" node in "Site administration > Courses > Backups"
+    And I set the field "s_restore_restore_general_users" to ""
+    And I press "Save changes"
+    And I am on "Course 1" course homepage
+    And I navigate to "Restore" node in "Course administration"
+    # "User data" marks the user data field for the section
+    # "-" marks the user data field for the data activity
+    And I restore "test_backup.mbz" backup into a new course using this options:
+      | Settings |  Include enrolled users | 1 |
+      | Schema | User data | 1 |
+      | Schema | - | 1 |
+    Then I should see "Test database name"
+    When I follow "Test database name"
+    Then I should see "Student entry"
+
+  @javascript
+  Scenario: Restore a backup with user data with local and site config config for including users set to 0
+    Given I navigate to "General restore defaults" node in "Site administration > Courses > Backups"
+    And I set the field "s_restore_restore_general_users" to ""
+    And I press "Save changes"
+    And I am on "Course 1" course homepage
+    And I navigate to "Restore" node in "Course administration"
+    When I restore "test_backup.mbz" backup into a new course using this options:
+      | Settings |  Include enrolled users | 0 |
+    Then I should see "Test database name"
+    When I follow "Test database name"
+    Then I should not see "Student entry"
\ No newline at end of file
index 5761d4c..7dd3057 100644 (file)
@@ -74,12 +74,13 @@ class block_globalsearch extends block_base {
         // Input.
         $this->content->text .= html_writer::tag('label', get_string('search', 'search'),
             array('for' => 'searchform_search', 'class' => 'accesshide'));
-        $inputoptions = array('id' => 'searchform_search', 'name' => 'q', 'type' => 'text', 'size' => '15');
+        $inputoptions = array('id' => 'searchform_search', 'name' => 'q', 'class' => 'form-control',
+            'type' => 'text', 'size' => '15');
         $this->content->text .= html_writer::empty_tag('input', $inputoptions);
 
         // Search button.
         $this->content->text .= html_writer::tag('button', get_string('search', 'search'),
-            array('id' => 'searchform_button', 'type' => 'submit', 'title' => 'globalsearch'));
+            array('id' => 'searchform_button', 'type' => 'submit', 'title' => 'globalsearch', 'class' => 'btn btn-secondary'));
         $this->content->text .= html_writer::end_tag('fieldset');
         $this->content->text .= html_writer::end_tag('form');
         $this->content->text .= html_writer::end_tag('div');
index 12b910e..b735d36 100644 (file)
@@ -274,13 +274,12 @@ class month_exporter extends exporter {
 
         // Calculate which day number is the first, and last day of the week.
         $firstdayofweek = $this->firstdayofweek;
-        $lastdayofweek = ($firstdayofweek + $daysinweek - 1) % $daysinweek;
 
         // The first week is special as it may have padding at the beginning.
         $day = reset($alldays);
         $firstdayno = $day['wday'];
 
-        $prepadding = ($firstdayno + $daysinweek - 1) % $daysinweek;
+        $prepadding = ($firstdayno + $daysinweek - $firstdayofweek) % $daysinweek;
         $daysinfirstweek = $daysinweek - $prepadding;
         $days = array_slice($alldays, 0, $daysinfirstweek);
         $week = new week_exporter($this->calendar, $days, $prepadding, ($daysinweek - count($days) - $prepadding), $this->related);
index 2e33869..80ef5ed 100644 (file)
@@ -119,6 +119,7 @@ class event_mapper implements event_mapper_interface {
             'description'      => $event->get_description()->get_value(),
             'format'           => $event->get_description()->get_format(),
             'courseid'         => $event->get_course() ? $event->get_course()->get('id') : null,
+            'categoryid'       => $event->get_category() ? $event->get_category()->get('id') : null,
             'groupid'          => $event->get_group() ? $event->get_group()->get('id') : null,
             'userid'           => $event->get_user() ? $event->get_user()->get('id') : null,
             'repeatid'         => $event->get_repeats()->get_id(),
index 213930f..049f13d 100644 (file)
@@ -127,23 +127,19 @@ class core_calendar_external extends external_api {
                                     'eventids' => new external_multiple_structure(
                                             new external_value(PARAM_INT, 'event ids')
                                             , 'List of event ids',
-                                            VALUE_DEFAULT, array(), NULL_ALLOWED
-                                                ),
+                                            VALUE_DEFAULT, array()),
                                     'courseids' => new external_multiple_structure(
                                             new external_value(PARAM_INT, 'course ids')
                                             , 'List of course ids for which events will be returned',
-                                            VALUE_DEFAULT, array(), NULL_ALLOWED
-                                                ),
+                                            VALUE_DEFAULT, array()),
                                     'groupids' => new external_multiple_structure(
                                             new external_value(PARAM_INT, 'group ids')
                                             , 'List of group ids for which events should be returned',
-                                            VALUE_DEFAULT, array(), NULL_ALLOWED
-                                                ),
+                                            VALUE_DEFAULT, array()),
                                     'categoryids' => new external_multiple_structure(
                                             new external_value(PARAM_INT, 'Category ids'),
                                             'List of category ids for which events will be returned',
-                                            VALUE_DEFAULT, array()
-                                ),
+                                            VALUE_DEFAULT, array()),
                             ), 'Event details', VALUE_DEFAULT, array()),
                     'options' => new external_single_structure(
                             array(
@@ -226,25 +222,36 @@ class core_calendar_external extends external_api {
         }
 
         $categories = array();
-        if (empty($params['events']['categoryids']) && !empty($courses)) {
-            list($wheresql, $sqlparams) = $DB->get_in_or_equal($courses);
-            $wheresql = "id $wheresql";
-            $courseswithcategory = $DB->get_records_select('course', $wheresql, $sqlparams);
+        if ($hassystemcap || !empty($courses)) {
 
-            // Grab the list of course categories for the requested course list.
             $coursecategories = array();
-            foreach ($courseswithcategory as $course) {
-                if (empty($course->visible)) {
-                    if (!has_capability('moodle/course:viewhidden', context_course::instance($course->id))) {
-                        continue;
+            if (!empty($courses)) {
+                list($wheresql, $sqlparams) = $DB->get_in_or_equal($courses);
+                $wheresql = "id $wheresql";
+                $courseswithcategory = $DB->get_records_select('course', $wheresql, $sqlparams);
+
+                // Grab the list of course categories for the requested course list.
+                foreach ($courseswithcategory as $course) {
+                    if (empty($course->visible)) {
+                        if (!has_capability('moodle/course:viewhidden', context_course::instance($course->id))) {
+                            continue;
+                        }
                     }
+                    $category = \coursecat::get($course->category);
+                    // Fetch parent categories.
+                    $coursecategories = array_merge($coursecategories, [$category->id], $category->get_parents());
                 }
-                $category = \coursecat::get($course->category);
-                $coursecategories[] = $category;
             }
 
             foreach (\coursecat::get_all() as $category) {
-                if (has_capability('moodle/category:manage', $category->get_context(), $USER, false)) {
+                // Skip categories not requested.
+                if (!empty($params['events']['categoryids'])) {
+                    if (!in_array($category->id, $params['events']['categoryids'])) {
+                        continue;
+                    }
+                }
+
+                if (has_capability('moodle/category:manage', $category->get_context())) {
                     // If a user can manage a category, then they can see all child categories. as well as all parent categories.
                     $categories[] = $category->id;
 
@@ -254,29 +261,14 @@ class core_calendar_external extends external_api {
                         }
                     }
                     $categories = array_merge($categories, $category->get_parents());
-                } else if (isset($coursecategories[$category->id])) {
+                } else if (in_array($category->id, $coursecategories)) {
+
                     // The user has access to a course in this category.
                     // Fetch all of the parents too.
                     $categories = array_merge($categories, [$category->id], $category->get_parents());
                     $categories[] = $category->id;
                 }
             }
-        } else {
-            // Build the category list.
-            // This includes the current category.
-            foreach ($params['events']['categoryids'] as $categoryid) {
-                $category = \coursecat::get($categoryid);
-                $categories = [$category->id];
-                // All of its descendants.
-                foreach (\coursecat::get_all() as $cat) {
-                    if (array_search($categoryid, $cat->get_parents()) !== false) {
-                        $categories[] = $cat->id;
-                    }
-                }
-
-                // And all of its parents.
-                $categories = array_merge($categories, $category->get_parents());
-            }
         }
 
         $funcparam['categories'] = array_unique($categories);
index 4790d70..45c1785 100644 (file)
@@ -1056,7 +1056,7 @@ class calendar_information {
             }
 
             $courses = [$course->id => $course];
-            $category = (\coursecat::get($course->category))->get_db_record();
+            $category = (\coursecat::get($course->category, MUST_EXIST, true))->get_db_record();
         } else if (!empty($categoryid)) {
             $course = get_site();
             $courses = calendar_get_default_courses();
@@ -1147,7 +1147,7 @@ class calendar_information {
             // A specific course was requested.
             // Fetch the category that this course is in, along with all parents.
             // Do not include child categories of this category, as the user many not have enrolments in those siblings or children.
-            $category = \coursecat::get($course->category);
+            $category = \coursecat::get($course->category, MUST_EXIST, true);
             $this->categoryid = $category->id;
 
             $this->categories = $category->get_parents();
@@ -2538,7 +2538,7 @@ function calendar_get_allowed_types(&$allowed, $course = null, $groups = null, $
 
     if (!empty($course)) {
         if (!is_object($course)) {
-            $course = $DB->get_record('course', array('id' => $course), '*', MUST_EXIST);
+            $course = $DB->get_record('course', array('id' => $course), 'id, groupmode, groupmodeforce', MUST_EXIST);
         }
         if ($course->id != SITEID) {
             $coursecontext = \context_course::instance($course->id);
@@ -2602,7 +2602,7 @@ function calendar_get_all_allowed_types() {
     // This function warms the context cache for the course so the calls
     // to load the course context in calendar_get_allowed_types don't result
     // in additional DB queries.
-    $courses = calendar_get_default_courses(null, '*', true);
+    $courses = calendar_get_default_courses(null, 'id, groupmode, groupmodeforce', true);
 
     // We want to pre-fetch all of the groups for each course in a single
     // query to avoid calendar_get_allowed_types from hitting the DB for
index 3298187..3323e1f 100644 (file)
@@ -533,6 +533,7 @@ class core_calendar_container_testcase extends advanced_testcase {
         $record->timesort = 0;
         $record->type = 1;
         $record->courseid = 0;
+        $record->categoryid = 0;
 
         foreach ($properties as $name => $value) {
             $record->$name = $value;
index 9375126..e2bf0bf 100644 (file)
@@ -465,6 +465,7 @@ class core_calendar_event_factory_testcase extends advanced_testcase {
         $record->timesort = 0;
         $record->type = 1;
         $record->courseid = 0;
+        $record->categoryid = 0;
 
         foreach ($properties as $name => $value) {
             $record->$name = $value;
index 8294651..47f3f4b 100644 (file)
@@ -147,6 +147,7 @@ class core_calendar_event_mapper_testcase extends advanced_testcase {
         $record->timesort = 0;
         $record->type = 1;
         $record->courseid = 0;
+        $record->categoryid = 0;
 
         foreach ($properties as $name => $value) {
             $record->$name = $value;
index 2fe4bbb..178bfff 100644 (file)
@@ -485,31 +485,58 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
         // Create some category events.
         $this->setAdminUser();
         $record = new stdClass();
+        $record->courseid = 0;
         $record->categoryid = $category->id;
-        $this->create_calendar_event('category a', $USER->id, 'category', 0, time(), $record);
+        $record->timestart = time() - DAYSECS;
+        $catevent1 = $this->create_calendar_event('category a', $USER->id, 'category', 0, time(), $record);
 
+        $record = new stdClass();
+        $record->courseid = 0;
         $record->categoryid = $category2->id;
-        $this->create_calendar_event('category b', $USER->id, 'category', 0, time(), $record);
+        $record->timestart = time() + DAYSECS;
+        $catevent2 = $this->create_calendar_event('category b', $USER->id, 'category', 0, time(), $record);
 
         // Now as student, make sure we get the events of the courses I am enrolled.
         $this->setUser($user2);
         $paramevents = array('categoryids' => array($category2b->id));
-        $options = array('timeend' => time() + 7 * WEEKSECS);
+        $options = array('timeend' => time() + 7 * WEEKSECS, 'userevents' => false, 'siteevents' => false);
         $events = core_calendar_external::get_calendar_events($paramevents, $options);
         $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
 
         // Should be just one, since there's just one category event of the course I am enrolled (course3 - cat2b).
         $this->assertEquals(1, count($events['events']));
+        $this->assertEquals($catevent2->id, $events['events'][0]['id']);
+        $this->assertEquals(0, count($events['warnings']));
+
+        // Now get category events but by course (there aren't course events in the course).
+        $paramevents = array('courseids' => array($course3->id));
+        $options = array('timeend' => time() + 7 * WEEKSECS, 'userevents' => false, 'siteevents' => false);
+        $events = core_calendar_external::get_calendar_events($paramevents, $options);
+        $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
+        $this->assertEquals(1, count($events['events']));
+        $this->assertEquals($catevent2->id, $events['events'][0]['id']);
+        $this->assertEquals(0, count($events['warnings']));
+
+        // Empty events in one where I'm not enrolled and one parent category
+        // (parent of a category where this is a course where the user is enrolled).
+        $paramevents = array('categoryids' => array($category2->id, $category->id));
+        $options = array('timeend' => time() + 7 * WEEKSECS, 'userevents' => false, 'siteevents' => false);
+        $events = core_calendar_external::get_calendar_events($paramevents, $options);
+        $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
+        $this->assertEquals(1, count($events['events']));
+        $this->assertEquals($catevent2->id, $events['events'][0]['id']);
         $this->assertEquals(0, count($events['warnings']));
 
         // Admin can see all category events.
         $this->setAdminUser();
         $paramevents = array('categoryids' => array($category->id, $category2->id, $category2b->id));
-        $options = array('timeend' => time() + 7 * WEEKSECS);
+        $options = array('timeend' => time() + 7 * WEEKSECS, 'userevents' => false, 'siteevents' => false);
         $events = core_calendar_external::get_calendar_events($paramevents, $options);
         $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
         $this->assertEquals(2, count($events['events']));
         $this->assertEquals(0, count($events['warnings']));
+        $this->assertEquals($catevent1->id, $events['events'][0]['id']);
+        $this->assertEquals($catevent2->id, $events['events'][1]['id']);
     }
 
     /**
index 2c3b461..014ae60 100644 (file)
@@ -56,6 +56,7 @@ function create_event($properties) {
     $record->timesort = 0;
     $record->type = CALENDAR_EVENT_TYPE_STANDARD;
     $record->courseid = 0;
+    $record->categoryid = 0;
 
     foreach ($properties as $name => $value) {
         $record->$name = $value;
index ec6320c..0b935af 100644 (file)
@@ -136,6 +136,7 @@ class core_calendar_repeat_event_collection_testcase extends advanced_testcase {
         $record->timesort = 0;
         $record->type = 1;
         $record->courseid = 0;
+        $record->categoryid = 0;
 
         foreach ($properties as $name => $value) {
             $record->$name = $value;
index 142496b..b610108 100644 (file)
@@ -265,9 +265,11 @@ class core_completion_external extends external_api {
                 $thisprogress  = $userprogress->progress[$activity->id];
                 $state         = $thisprogress->completionstate;
                 $timecompleted = $thisprogress->timemodified;
+                $overrideby    = $thisprogress->overrideby;
             } else {
                 $state = COMPLETION_INCOMPLETE;
                 $timecompleted = 0;
+                $overrideby = null;
             }
 
             $results[] = array(
@@ -276,7 +278,8 @@ class core_completion_external extends external_api {
                        'instance'      => $activity->instance,
                        'state'         => $state,
                        'timecompleted' => $timecompleted,
-                       'tracking'      => $activity->completion
+                       'tracking'      => $activity->completion,
+                       'overrideby'    => $overrideby
             );
         }
 
@@ -308,6 +311,8 @@ class core_completion_external extends external_api {
                             'timecompleted' => new external_value(PARAM_INT, 'timestamp for completed activity'),
                             'tracking'      => new external_value(PARAM_INT, 'type of tracking:
                                                                     0 means none, 1 manual, 2 automatic'),
+                            'overrideby' => new external_value(PARAM_INT, 'The user id who has overriden the status, or null',
+                                VALUE_OPTIONAL),
                         ), 'Activity'
                     ), 'List of activities status'
                 ),
index 0a5ec14..355ecc2 100644 (file)
@@ -164,7 +164,25 @@ class core_completion_externallib_testcase extends externallib_advanced_testcase
         // We added 4 activities, but only 3 with completion enabled and one of those is hidden.
         $this->assertCount(3, $result['statuses']);
 
-        // Change teacher role capabilities (disable access al goups).
+        // Override status by teacher.
+        $completion->update_state($cmforum, COMPLETION_INCOMPLETE, $student->id, true);
+
+        $result = core_completion_external::get_activities_completion_status($course->id, $student->id);
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $result = external_api::clean_returnvalue(
+            core_completion_external::get_activities_completion_status_returns(), $result);
+
+        // Check forum has been overriden by the teacher.
+        foreach ($result['statuses'] as $status) {
+            if ($status['cmid'] == $forum->cmid) {
+                $this->assertEquals(COMPLETION_INCOMPLETE, $status['state']);
+                $this->assertEquals(COMPLETION_TRACKING_MANUAL, $status['tracking']);
+                $this->assertEquals($teacher->id, $status['overrideby']);
+                break;
+            }
+        }
+
+        // Change teacher role capabilities (disable access all groups).
         $context = context_course::instance($course->id);
         assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $teacherrole->id, $context);
         accesslib_clear_all_caches_for_unit_testing();
index 6203b8a..3dd8de4 100644 (file)
@@ -7,7 +7,7 @@
     "require-dev": {
         "phpunit/phpunit": "6.4.*",
         "phpunit/dbUnit": "3.0.*",
-        "moodlehq/behat-extension": "3.34.1",
+        "moodlehq/behat-extension": "3.35.0",
         "mikey179/vfsStream": "^1.6"
     }
 }
index d69ba0b..f90ae23 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "content-hash": "b36746ace2486c033136c855a63f3793",
+    "content-hash": "7cd70172c941fb07f0a2d4173baef5f1",
     "packages": [],
     "packages-dev": [
         {
         },
         {
             "name": "behat/mink-extension",
-            "version": "v2.2",
+            "version": "2.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Behat/MinkExtension.git",
-                "reference": "5b4bda64ff456104564317e212c823e45cad9d59"
+                "reference": "badc565b7a1d05c4a4bf49c789045bcf7af6c6de"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Behat/MinkExtension/zipball/5b4bda64ff456104564317e212c823e45cad9d59",
-                "reference": "5b4bda64ff456104564317e212c823e45cad9d59",
+                "url": "https://api.github.com/repos/Behat/MinkExtension/zipball/badc565b7a1d05c4a4bf49c789045bcf7af6c6de",
+                "reference": "badc565b7a1d05c4a4bf49c789045bcf7af6c6de",
                 "shasum": ""
             },
             "require": {
-                "behat/behat": "~3.0,>=3.0.5",
-                "behat/mink": "~1.5",
+                "behat/behat": "^3.0.5",
+                "behat/mink": "^1.5",
                 "php": ">=5.3.2",
-                "symfony/config": "~2.2|~3.0"
+                "symfony/config": "^2.7|^3.0|^4.0"
             },
             "require-dev": {
-                "behat/mink-goutte-driver": "~1.1",
-                "phpspec/phpspec": "~2.0"
+                "behat/mink-goutte-driver": "^1.1",
+                "phpspec/phpspec": "^2.0"
             },
             "type": "behat-extension",
             "extra": {
                 "test",
                 "web"
             ],
-            "time": "2016-02-15T07:55:18+00:00"
+            "time": "2017-11-24T19:30:49+00:00"
         },
         {
             "name": "behat/mink-goutte-driver",
         },
         {
             "name": "fabpot/goutte",
-            "version": "v3.2.1",
+            "version": "v3.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/FriendsOfPHP/Goutte.git",
-                "reference": "db5c28f4a010b4161d507d5304e28a7ebf211638"
+                "reference": "395f61d7c2e15a813839769553a4de16fa3b3c96"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/db5c28f4a010b4161d507d5304e28a7ebf211638",
-                "reference": "db5c28f4a010b4161d507d5304e28a7ebf211638",
+                "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/395f61d7c2e15a813839769553a4de16fa3b3c96",
+                "reference": "395f61d7c2e15a813839769553a4de16fa3b3c96",
                 "shasum": ""
             },
             "require": {
                 "guzzlehttp/guzzle": "^6.0",
                 "php": ">=5.5.0",
-                "symfony/browser-kit": "~2.1|~3.0",
-                "symfony/css-selector": "~2.1|~3.0",
-                "symfony/dom-crawler": "~2.1|~3.0"
+                "symfony/browser-kit": "~2.1|~3.0|~4.0",
+                "symfony/css-selector": "~2.1|~3.0|~4.0",
+                "symfony/dom-crawler": "~2.1|~3.0|~4.0"
+            },
+            "require-dev": {
+                "symfony/phpunit-bridge": "^3.3 || ^4"
             },
             "type": "application",
             "extra": {
             "autoload": {
                 "psr-4": {
                     "Goutte\\": "Goutte"
-                }
+                },
+                "exclude-from-classmap": [
+                    "Goutte/Tests"
+                ]
             },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
             "keywords": [
                 "scraper"
             ],
-            "time": "2017-01-03T13:21:43+00:00"
+            "time": "2017-11-19T08:45:40+00:00"
         },
         {
             "name": "guzzlehttp/guzzle",
         },
         {
             "name": "moodlehq/behat-extension",
-            "version": "v3.34.1",
+            "version": "v3.35.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/moodlehq/moodle-behat-extension.git",
         },
         {
             "name": "phpspec/prophecy",
-            "version": "v1.7.2",
+            "version": "1.7.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpspec/prophecy.git",
-                "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6"
+                "reference": "e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6",
-                "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6",
+                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf",
+                "reference": "e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf",
                 "shasum": ""
             },
             "require": {
             },
             "require-dev": {
                 "phpspec/phpspec": "^2.5|^3.2",
-                "phpunit/phpunit": "^4.8 || ^5.6.5"
+                "phpunit/phpunit": "^4.8.35 || ^5.7"
             },
             "type": "library",
             "extra": {
                 "spy",
                 "stub"
             ],
-            "time": "2017-09-04T11:05:03+00:00"
+            "time": "2017-11-24T13:59:53+00:00"
         },
         {
             "name": "phpunit/dbunit",
-            "version": "3.0.1",
+            "version": "3.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/dbunit.git",
-                "reference": "6b9cec80dca8694243aade33bceb425ccafbbd0d"
+                "reference": "403350339b6aca748ee0067d027d85621992e21f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/dbunit/zipball/6b9cec80dca8694243aade33bceb425ccafbbd0d",
-                "reference": "6b9cec80dca8694243aade33bceb425ccafbbd0d",
+                "url": "https://api.github.com/repos/sebastianbergmann/dbunit/zipball/403350339b6aca748ee0067d027d85621992e21f",
+                "reference": "403350339b6aca748ee0067d027d85621992e21f",
                 "shasum": ""
             },
             "require": {
                 "ext-simplexml": "*",
                 "php": "^7.0",
                 "phpunit/phpunit": "^6.0",
-                "symfony/yaml": "^3.0"
+                "symfony/yaml": "^3.0 || ^4.0"
             },
             "type": "library",
             "extra": {
                 "testing",
                 "xunit"
             ],
-            "time": "2017-10-19T13:21:48+00:00"
+            "time": "2017-11-18T17:40:34+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "5.2.2",
+            "version": "5.2.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "8ed1902a57849e117b5651fc1a5c48110946c06b"
+                "reference": "8e1d2397d8adf59a3f12b2878a3aaa66d1ab189d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/8ed1902a57849e117b5651fc1a5c48110946c06b",
-                "reference": "8ed1902a57849e117b5651fc1a5c48110946c06b",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/8e1d2397d8adf59a3f12b2878a3aaa66d1ab189d",
+                "reference": "8e1d2397d8adf59a3f12b2878a3aaa66d1ab189d",
                 "shasum": ""
             },
             "require": {
                 "php": "^7.0",
                 "phpunit/php-file-iterator": "^1.4.2",
                 "phpunit/php-text-template": "^1.2.1",
-                "phpunit/php-token-stream": "^1.4.11 || ^2.0",
+                "phpunit/php-token-stream": "^2.0",
                 "sebastian/code-unit-reverse-lookup": "^1.0.1",
                 "sebastian/environment": "^3.0",
                 "sebastian/version": "^2.0.1",
                 "testing",
                 "xunit"
             ],
-            "time": "2017-08-03T12:40:43+00:00"
+            "time": "2017-11-03T13:47:33+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",
-            "version": "1.4.2",
+            "version": "1.4.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
-                "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5"
+                "reference": "8ebba84e5bd74fc5fdeb916b38749016c7232f93"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5",
-                "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/8ebba84e5bd74fc5fdeb916b38749016c7232f93",
+                "reference": "8ebba84e5bd74fc5fdeb916b38749016c7232f93",
                 "shasum": ""
             },
             "require": {
                 "filesystem",
                 "iterator"
             ],
-            "time": "2016-10-03T07:40:28+00:00"
+            "time": "2017-11-24T15:00:59+00:00"
         },
         {
             "name": "phpunit/php-text-template",
         },
         {
             "name": "phpunit/phpunit",
-            "version": "6.4.3",
+            "version": "6.4.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "06b28548fd2b4a20c3cd6e247dc86331a7d4db13"
+                "reference": "562f7dc75d46510a4ed5d16189ae57fbe45a9932"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/06b28548fd2b4a20c3cd6e247dc86331a7d4db13",
-                "reference": "06b28548fd2b4a20c3cd6e247dc86331a7d4db13",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/562f7dc75d46510a4ed5d16189ae57fbe45a9932",
+                "reference": "562f7dc75d46510a4ed5d16189ae57fbe45a9932",
                 "shasum": ""
             },
             "require": {
                 "testing",
                 "xunit"
             ],
-            "time": "2017-10-16T13:18:59+00:00"
+            "time": "2017-11-08T11:26:09+00:00"
         },
         {
             "name": "phpunit/phpunit-mock-objects",
         },
         {
             "name": "sebastian/comparator",
-            "version": "2.0.2",
+            "version": "2.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/comparator.git",
-                "reference": "ae068fede81d06e7bb9bb46a367210a3d3e1fe6a"
+                "reference": "1174d9018191e93cb9d719edec01257fc05f8158"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/ae068fede81d06e7bb9bb46a367210a3d3e1fe6a",
-                "reference": "ae068fede81d06e7bb9bb46a367210a3d3e1fe6a",
+                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1174d9018191e93cb9d719edec01257fc05f8158",
+                "reference": "1174d9018191e93cb9d719edec01257fc05f8158",
                 "shasum": ""
             },
             "require": {
                 "php": "^7.0",
                 "sebastian/diff": "^2.0",
-                "sebastian/exporter": "^3.0"
+                "sebastian/exporter": "^3.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.0"
+                "phpunit/phpunit": "^6.4"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.0.x-dev"
+                    "dev-master": "2.1.x-dev"
                 }
             },
             "autoload": {
                 }
             ],
             "description": "Provides the functionality to compare PHP values for equality",
-            "homepage": "http://www.github.com/sebastianbergmann/comparator",
+            "homepage": "https://github.com/sebastianbergmann/comparator",
             "keywords": [
                 "comparator",
                 "compare",
                 "equality"
             ],
-            "time": "2017-08-03T07:14:59+00:00"
+            "time": "2017-11-03T07:16:52+00:00"
         },
         {
             "name": "sebastian/diff",
         },
         {
             "name": "symfony/browser-kit",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/browser-kit.git",
-                "reference": "317d5bdf0127f06db7ea294186132b4f5b036839"
+                "reference": "03f957cd24bf939524f07b8b910c89cfcad722a8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/317d5bdf0127f06db7ea294186132b4f5b036839",
-                "reference": "317d5bdf0127f06db7ea294186132b4f5b036839",
+                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/03f957cd24bf939524f07b8b910c89cfcad722a8",
+                "reference": "03f957cd24bf939524f07b8b910c89cfcad722a8",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony BrowserKit Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-02T06:42:24+00:00"
+            "time": "2017-11-07T14:12:55+00:00"
         },
         {
             "name": "symfony/class-loader",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/class-loader.git",
-                "reference": "7572c904b209fa9907c69a6a9a68243c265a4d01"
+                "reference": "df173ac2af96ce202bf8bb5a3fc0bec8a4fdd4d1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/class-loader/zipball/7572c904b209fa9907c69a6a9a68243c265a4d01",
-                "reference": "7572c904b209fa9907c69a6a9a68243c265a4d01",
+                "url": "https://api.github.com/repos/symfony/class-loader/zipball/df173ac2af96ce202bf8bb5a3fc0bec8a4fdd4d1",
+                "reference": "df173ac2af96ce202bf8bb5a3fc0bec8a4fdd4d1",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony ClassLoader Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-02T06:42:24+00:00"
+            "time": "2017-11-05T15:47:03+00:00"
         },
         {
             "name": "symfony/config",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/config.git",
-                "reference": "4ab62407bff9cd97c410a7feaef04c375aaa5cfd"
+                "reference": "8d2649077dc54dfbaf521d31f217383d82303c5f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/config/zipball/4ab62407bff9cd97c410a7feaef04c375aaa5cfd",
-                "reference": "4ab62407bff9cd97c410a7feaef04c375aaa5cfd",
+                "url": "https://api.github.com/repos/symfony/config/zipball/8d2649077dc54dfbaf521d31f217383d82303c5f",
+                "reference": "8d2649077dc54dfbaf521d31f217383d82303c5f",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Config Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-04T18:56:58+00:00"
+            "time": "2017-11-07T14:16:22+00:00"
         },
         {
             "name": "symfony/console",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "116bc56e45a8e5572e51eb43ab58c769a352366c"
+                "reference": "63cd7960a0a522c3537f6326706d7f3b8de65805"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/116bc56e45a8e5572e51eb43ab58c769a352366c",
-                "reference": "116bc56e45a8e5572e51eb43ab58c769a352366c",
+                "url": "https://api.github.com/repos/symfony/console/zipball/63cd7960a0a522c3537f6326706d7f3b8de65805",
+                "reference": "63cd7960a0a522c3537f6326706d7f3b8de65805",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Console Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-02T06:42:24+00:00"
+            "time": "2017-11-16T15:24:32+00:00"
         },
         {
             "name": "symfony/css-selector",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
-                "reference": "07447650225ca9223bd5c97180fe7c8267f7d332"
+                "reference": "66e6e046032ebdf1f562c26928549f613d428bd1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/css-selector/zipball/07447650225ca9223bd5c97180fe7c8267f7d332",
-                "reference": "07447650225ca9223bd5c97180fe7c8267f7d332",
+                "url": "https://api.github.com/repos/symfony/css-selector/zipball/66e6e046032ebdf1f562c26928549f613d428bd1",
+                "reference": "66e6e046032ebdf1f562c26928549f613d428bd1",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony CssSelector Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-02T06:42:24+00:00"
+            "time": "2017-11-05T15:47:03+00:00"
         },
         {
             "name": "symfony/debug",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/debug.git",
-                "reference": "eb95d9ce8f18dcc1b3dfff00cb624c402be78ffd"
+                "reference": "74557880e2846b5c84029faa96b834da37e29810"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/debug/zipball/eb95d9ce8f18dcc1b3dfff00cb624c402be78ffd",
-                "reference": "eb95d9ce8f18dcc1b3dfff00cb624c402be78ffd",
+                "url": "https://api.github.com/repos/symfony/debug/zipball/74557880e2846b5c84029faa96b834da37e29810",
+                "reference": "74557880e2846b5c84029faa96b834da37e29810",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Debug Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-02T06:42:24+00:00"
+            "time": "2017-11-10T16:38:39+00:00"
         },
         {
             "name": "symfony/dependency-injection",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dependency-injection.git",
-                "reference": "8ebad929aee3ca185b05f55d9cc5521670821ad1"
+                "reference": "4e84f5af2c2d51ee3dee72df40b7fc08f49b4ab8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8ebad929aee3ca185b05f55d9cc5521670821ad1",
-                "reference": "8ebad929aee3ca185b05f55d9cc5521670821ad1",
+                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/4e84f5af2c2d51ee3dee72df40b7fc08f49b4ab8",
+                "reference": "4e84f5af2c2d51ee3dee72df40b7fc08f49b4ab8",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony DependencyInjection Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-04T17:15:30+00:00"
+            "time": "2017-11-13T18:10:32+00:00"
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "40dafd42d5dad7fe5ad4e958413d92a207522ac1"
+                "reference": "cebe3c068867956e012d9135282ba6a05d8a259e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/40dafd42d5dad7fe5ad4e958413d92a207522ac1",
-                "reference": "40dafd42d5dad7fe5ad4e958413d92a207522ac1",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/cebe3c068867956e012d9135282ba6a05d8a259e",
+                "reference": "cebe3c068867956e012d9135282ba6a05d8a259e",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony DomCrawler Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-02T06:42:24+00:00"
+            "time": "2017-11-05T15:47:03+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "d7ba037e4b8221956ab1e221c73c9e27e05dd423"
+                "reference": "271d8c27c3ec5ecee6e2ac06016232e249d638d9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d7ba037e4b8221956ab1e221c73c9e27e05dd423",
-                "reference": "d7ba037e4b8221956ab1e221c73c9e27e05dd423",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/271d8c27c3ec5ecee6e2ac06016232e249d638d9",
+                "reference": "271d8c27c3ec5ecee6e2ac06016232e249d638d9",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony EventDispatcher Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-02T06:42:24+00:00"
+            "time": "2017-11-05T15:47:03+00:00"
         },
         {
             "name": "symfony/filesystem",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "90bc45abf02ae6b7deb43895c1052cb0038506f1"
+                "reference": "77db266766b54db3ee982fe51868328b887ce15c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/90bc45abf02ae6b7deb43895c1052cb0038506f1",
-                "reference": "90bc45abf02ae6b7deb43895c1052cb0038506f1",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/77db266766b54db3ee982fe51868328b887ce15c",
+                "reference": "77db266766b54db3ee982fe51868328b887ce15c",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-03T13:33:10+00:00"
+            "time": "2017-11-07T14:12:55+00:00"
         },
         {
             "name": "symfony/polyfill-mbstring",
         },
         {
             "name": "symfony/process",
-            "version": "v2.8.28",
+            "version": "v2.8.31",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/process.git",
-                "reference": "26c9fb02bf06bd6b90f661a5bd17e510810d0176"
+                "reference": "d25449e031f600807949aab7cadbf267712f4eee"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/process/zipball/26c9fb02bf06bd6b90f661a5bd17e510810d0176",
-                "reference": "26c9fb02bf06bd6b90f661a5bd17e510810d0176",
+                "url": "https://api.github.com/repos/symfony/process/zipball/d25449e031f600807949aab7cadbf267712f4eee",
+                "reference": "d25449e031f600807949aab7cadbf267712f4eee",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Process Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-01T21:00:16+00:00"
+            "time": "2017-11-05T15:25:56+00:00"
         },
         {
             "name": "symfony/translation",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
-                "reference": "409bf229cd552bf7e3faa8ab7e3980b07672073f"
+                "reference": "373e553477e55cd08f8b86b74db766c75b987fdb"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation/zipball/409bf229cd552bf7e3faa8ab7e3980b07672073f",
-                "reference": "409bf229cd552bf7e3faa8ab7e3980b07672073f",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/373e553477e55cd08f8b86b74db766c75b987fdb",
+                "reference": "373e553477e55cd08f8b86b74db766c75b987fdb",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Translation Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-02T06:42:24+00:00"
+            "time": "2017-11-07T14:12:55+00:00"
         },
         {
             "name": "symfony/yaml",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/yaml.git",
-                "reference": "8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46"
+                "reference": "0938408c4faa518d95230deabb5f595bf0de31b9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/yaml/zipball/8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46",
-                "reference": "8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/0938408c4faa518d95230deabb5f595bf0de31b9",
+                "reference": "0938408c4faa518d95230deabb5f595bf0de31b9",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Yaml Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-05T14:43:42+00:00"
+            "time": "2017-11-10T18:26:04+00:00"
         },
         {
             "name": "theseer/tokenizer",
index 3a0c38f..8f30ad1 100644 (file)
@@ -453,7 +453,11 @@ class core_enrol_external extends external_api {
                                                $params['perpage']);
 
         $results = array();
-        $requiredfields = ['id', 'fullname', 'profileimageurl', 'profileimageurlsmall'];
+        // Add also extra user fields.
+        $requiredfields = array_merge(
+            ['id', 'fullname', 'profileimageurl', 'profileimageurlsmall'],
+            get_extra_user_fields($context)
+        );
         foreach ($users['users'] as $id => $user) {
             // Note: We pass the course here to validate that the current user can at least view user details in this course.
             // The user we are looking at is not in this course yet though - but we only fetch the minimal set of
index d7c8294..d2f57e9 100644 (file)
@@ -185,8 +185,8 @@ class grading_manager {
         } else if ($this->get_context()->contextlevel >= CONTEXT_COURSE) {
             list($context, $course, $cm) = get_context_info_array($this->get_context()->id);
 
-            if (strval($cm->name) !== '') {
-                $title = $cm->name;
+            if ($cm && strval($cm->name) !== '') {
+                $title = format_string($cm->name, true, array('context' => $context));
             } else {
                 debugging('Gradable areas are currently supported at the course module level only', DEBUG_DEVELOPER);
                 $title = $this->get_component();
index 53d2dfc..4c04acd 100644 (file)
@@ -90,9 +90,6 @@ class core_group_external extends external_api {
             if ($DB->get_record('groups', array('courseid'=>$group->courseid, 'name'=>$group->name))) {
                 throw new invalid_parameter_exception('Group with the same name already exists in the course');
             }
-            if (!empty($group->idnumber) && $DB->count_records('groups', array('idnumber' => $group->idnumber))) {
-                throw new invalid_parameter_exception('Group with the same idnumber already exists');
-            }
 
             // now security checks
             $context = context_course::instance($group->courseid, IGNORE_MISSING);
@@ -627,9 +624,6 @@ class core_group_external extends external_api {
             if ($DB->count_records('groupings', array('courseid'=>$grouping->courseid, 'name'=>$grouping->name))) {
                 throw new invalid_parameter_exception('Grouping with the same name already exists in the course');
             }
-            if (!empty($grouping->idnumber) && $DB->count_records('groupings', array('idnumber' => $grouping->idnumber))) {
-                throw new invalid_parameter_exception('Grouping with the same idnumber already exists');
-            }
 
             // Now security checks            .
             $context = context_course::instance($grouping->courseid);
@@ -731,11 +725,6 @@ class core_group_external extends external_api {
                     $DB->count_records('groupings', array('courseid'=>$currentgrouping->courseid, 'name'=>$grouping->name))) {
                 throw new invalid_parameter_exception('A different grouping with the same name already exists in the course');
             }
-            // Check if the new modified grouping idnumber already exists.
-            if (!empty($grouping->idnumber) && $grouping->idnumber != $currentgrouping->idnumber &&
-                    $DB->count_records('groupings', array('idnumber' => $grouping->idnumber))) {
-                throw new invalid_parameter_exception('A different grouping with the same idnumber already exists');
-            }
 
             $grouping->courseid = $currentgrouping->courseid;
 
index 2c09e6d..ae0923b 100644 (file)
@@ -118,7 +118,7 @@ if ($mform_post->is_cancelled()) {
             //decode encoded commas
             $record[$header[$key]] = preg_replace($csv_encode, $csv_delimiter, trim($value));
         }
-        if ($record[$header[0]]) {
+        if (trim($rawline) !== '') {
             // add a new group to the database
 
             // add fields to object $user
@@ -134,28 +134,32 @@ if ($mform_post->is_cancelled()) {
                 }
             }
 
-            if (isset($newgroup->idnumber)){
+            if (!empty($newgroup->idnumber)) {
                 //if idnumber is set, we use that.
                 //unset invalid courseid
                 if (!$mycourse = $DB->get_record('course', array('idnumber'=>$newgroup->idnumber))) {
                     echo $OUTPUT->notification(get_string('unknowncourseidnumber', 'error', $newgroup->idnumber));
                     unset($newgroup->courseid);//unset so 0 doesn't get written to database
+                } else {
+                    $newgroup->courseid = $mycourse->id;
                 }
-                $newgroup->courseid = $mycourse->id;
 
-            } else if (isset($newgroup->coursename)){
+            } else if (!empty($newgroup->coursename)) {
                 //else use course short name to look up
                 //unset invalid coursename (if no id)
-                if (!$mycourse = $DB->get_record('course', array('shortname', $newgroup->coursename))) {
+                if (!$mycourse = $DB->get_record('course', array('shortname' => $newgroup->coursename))) {
                     echo $OUTPUT->notification(get_string('unknowncourse', 'error', $newgroup->coursename));
                     unset($newgroup->courseid);//unset so 0 doesn't get written to database
+                } else {
+                    $newgroup->courseid = $mycourse->id;
                 }
-                $newgroup->courseid = $mycourse->id;
 
             } else {
                 //else use use current id
                 $newgroup->courseid = $id;
             }
+            unset($newgroup->idnumber);
+            unset($newgroup->coursename);
 
             //if courseid is set
             if (isset($newgroup->courseid)) {
@@ -196,7 +200,7 @@ if ($mform_post->is_cancelled()) {
                     }
 
                     // Add group to grouping
-                    if (!empty($newgroup->groupingname) || is_numeric($newgroup->groupingname)) {
+                    if (isset($newgroup->groupingname) && strlen($newgroup->groupingname)) {
                         $groupingname = $newgroup->groupingname;
                         if (! $groupingid = groups_get_grouping_by_name($newgroup->courseid, $groupingname)) {
                             $data = new stdClass();
index 5df273b..99a4c6e 100644 (file)
@@ -104,6 +104,9 @@ function groups_add_member($grouporid, $userorid, $component=null, $itemid=0) {
     $DB->set_field('groups', 'timemodified', $member->timeadded, array('id'=>$groupid));
     $group->timemodified = $member->timeadded;
 
+    // Invalidate the group and grouping cache for users.
+    cache_helper::invalidate_by_definition('core', 'user_group_groupings', array(), array($userid));
+
     // Trigger group event.
     $params = array(
         'context' => $context,
@@ -205,6 +208,9 @@ function groups_remove_member($grouporid, $userorid) {
     $DB->set_field('groups', 'timemodified', $time, array('id' => $groupid));
     $group->timemodified = $time;
 
+    // Invalidate the group and grouping cache for users.
+    cache_helper::invalidate_by_definition('core', 'user_group_groupings', array(), array($userid));
+
     // Trigger group event.
     $params = array(
         'context' => context_course::instance($group->courseid),
@@ -496,6 +502,8 @@ function groups_delete_group($grouporid) {
 
     // Invalidate the grouping cache for the course
     cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($group->courseid));
+    // Purge the group and grouping cache for users.
+    cache_helper::purge_by_definition('core', 'user_group_groupings');
 
     // Trigger group event.
     $params = array(
@@ -547,6 +555,8 @@ function groups_delete_grouping($groupingorid) {
 
     // Invalidate the grouping cache for the course
     cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($grouping->courseid));
+    // Purge the group and grouping cache for users.
+    cache_helper::purge_by_definition('core', 'user_group_groupings');
 
     // Trigger group event.
     $params = array(
@@ -621,6 +631,8 @@ function groups_delete_groupings_groups($courseid, $showfeedback=false) {
 
     // Invalidate the grouping cache for the course
     cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid));
+    // Purge the group and grouping cache for users.
+    cache_helper::purge_by_definition('core', 'user_group_groupings');
 
     // TODO MDL-41312 Remove events_trigger_legacy('groups_groupings_groups_removed').
     // This event is kept here for backwards compatibility, because it cannot be
@@ -649,6 +661,8 @@ function groups_delete_groups($courseid, $showfeedback=false) {
 
     // Invalidate the grouping cache for the course
     cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid));
+    // Purge the group and grouping cache for users.
+    cache_helper::purge_by_definition('core', 'user_group_groupings');
 
     // TODO MDL-41312 Remove events_trigger_legacy('groups_groups_deleted').
     // This event is kept here for backwards compatibility, because it cannot be
@@ -679,6 +693,8 @@ function groups_delete_groupings($courseid, $showfeedback=false) {
 
     // Invalidate the grouping cache for the course.
     cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid));
+    // Purge the group and grouping cache for users.
+    cache_helper::purge_by_definition('core', 'user_group_groupings');
 
     // TODO MDL-41312 Remove events_trigger_legacy('groups_groupings_deleted').
     // This event is kept here for backwards compatibility, because it cannot be
@@ -818,7 +834,7 @@ function groups_parse_name($format, $groupnumber) {
  * @param int groupingid
  * @param int groupid
  * @param int $timeadded  The time the group was added to the grouping.
- * @param bool $invalidatecache If set to true the course group cache will be invalidated as well.
+ * @param bool $invalidatecache If set to true the course group cache and the user group cache will be invalidated as well.
  * @return bool true or exception
  */
 function groups_assign_grouping($groupingid, $groupid, $timeadded = null, $invalidatecache = true) {
@@ -841,6 +857,8 @@ function groups_assign_grouping($groupingid, $groupid, $timeadded = null, $inval
     if ($invalidatecache) {
         // Invalidate the grouping cache for the course
         cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid));
+        // Purge the group and grouping cache for users.
+        cache_helper::purge_by_definition('core', 'user_group_groupings');
     }
 
     // Trigger event.
@@ -860,7 +878,7 @@ function groups_assign_grouping($groupingid, $groupid, $timeadded = null, $inval
  *
  * @param int groupingid
  * @param int groupid
- * @param bool $invalidatecache If set to true the course group cache will be invalidated as well.
+ * @param bool $invalidatecache If set to true the course group cache and the user group cache will be invalidated as well.
  * @return bool success
  */
 function groups_unassign_grouping($groupingid, $groupid, $invalidatecache = true) {
@@ -871,6 +889,8 @@ function groups_unassign_grouping($groupingid, $groupid, $invalidatecache = true
     if ($invalidatecache) {
         // Invalidate the grouping cache for the course
         cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid));
+        // Purge the group and grouping cache for users.
+        cache_helper::purge_by_definition('core', 'user_group_groupings');
     }
 
     // Trigger event.
index 0bec1e3..95019f1 100644 (file)
@@ -8,12 +8,14 @@ Feature: Importing of groups and groupings
     Given the following "courses" exist:
       | fullname | shortname | category |
       | Course 1 | C1 | 0 |
+      | Course 2 | C2 | 0 |
     And the following "users" exist:
       | username | firstname | lastname | email |
       | teacher1 | Teacher | 1 | teacher1@example.com |
     And the following "course enrolments" exist:
       | user | course | role |
       | teacher1 | C1 | editingteacher |
+      | teacher1 | C2 | editingteacher |
 
   @javascript
   Scenario: Import groups and groupings as teacher
@@ -110,3 +112,32 @@ Feature: Importing of groups and groupings
     And I press "Edit group settings"
     And the field "id_idnumber" matches value ""
     And I press "Cancel"
+
+  @javascript
+  Scenario: Import groups into multiple courses as a teacher
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I navigate to "Users > Groups" in current page administration
+    And I press "Import groups"
+    When I upload "group/tests/fixtures/groups_import_multicourse.csv" file to "Import" filemanager
+    And I press "Import groups"
+    Then I should see "Group group7 added successfully"
+    And I should see "Unknown course named \"C-non-existing\""
+    And I should see "Group group8 added successfully"
+    And I should not see "group-will-not-be-created"
+    And I should see "Group group9 added successfully"
+    And I should see "Group group10 added successfully"
+    And I press "Continue"
+    And I should see "group10"
+    And I should see "group7"
+    And I should see "group8"
+    And I should not see "group9"
+    And I should not see "group-will-not-be-created"
+    And I am on "Course 2" course homepage
+    And I navigate to "Users > Groups" in current page administration
+    And I should see "group9"
+    And I should not see "group-will-not-be-created"
+    And I should not see "group7"
+    And I should not see "group8"
+    And I should not see "group10"
+    And I log out
index dcbb0ef..fff4079 100644 (file)
@@ -106,9 +106,8 @@ class core_group_externallib_testcase extends externallib_advanced_testcase {
             $froups = core_group_external::create_groups(array($group3));
             $this->fail('Exception expected due to already existing idnumber.');
         } catch (moodle_exception $e) {
-            $this->assertInstanceOf('invalid_parameter_exception', $e);
-            $this->assertEquals('Invalid parameter value detected (Group with the same idnumber already exists)',
-                $e->getMessage());
+            $this->assertInstanceOf('moodle_exception', $e);
+            $this->assertEquals(get_string('idnumbertaken', 'error'), $e->getMessage());
         }
 
         // Call without required capability
@@ -258,9 +257,8 @@ class core_group_externallib_testcase extends externallib_advanced_testcase {
             $groupings = core_group_external::create_groupings(array($grouping1data));
             $this->fail('Exception expected due to already existing idnumber.');
         } catch (moodle_exception $e) {
-            $this->assertInstanceOf('invalid_parameter_exception', $e);
-            $this->assertEquals('Invalid parameter value detected (Grouping with the same idnumber already exists)',
-                $e->getMessage());
+            $this->assertInstanceOf('moodle_exception', $e);
+            $this->assertEquals(get_string('idnumbertaken', 'error'), $e->getMessage());
         }
 
         // No exception should be triggered.
@@ -285,9 +283,8 @@ class core_group_externallib_testcase extends externallib_advanced_testcase {
             $groupings = core_group_external::update_groupings(array($grouping2data));
             $this->fail('Exception expected due to already existing idnumber.');
         } catch (moodle_exception $e) {
-            $this->assertInstanceOf('invalid_parameter_exception', $e);
-            $this->assertEquals('Invalid parameter value detected (A different grouping with the same idnumber already exists)',
-                $e->getMessage());
+            $this->assertInstanceOf('moodle_exception', $e);
+            $this->assertEquals(get_string('idnumbertaken', 'error'), $e->getMessage());
         }
     }
 
diff --git a/group/tests/fixtures/groups_import_multicourse.csv b/group/tests/fixtures/groups_import_multicourse.csv
new file mode 100644 (file)
index 0000000..cbedd60
--- /dev/null
@@ -0,0 +1,6 @@
+coursename,groupname
+C1,group7
+C-non-existing,group-will-not-be-created
+C1,group8
+C2,group9
+,group10
index caa0991..88ecab1 100644 (file)
@@ -72,6 +72,7 @@ $string['cachedef_string'] = 'Language string cache';
 $string['cachedef_tags'] = 'Tags collections and areas';
 $string['cachedef_temp_tables'] = 'Temporary tables cache';
 $string['cachedef_userselections'] = 'Data used to persist user selections throughout Moodle';
+$string['cachedef_user_group_groupings'] = 'User\'s groupings and groups per course';
 $string['cachedef_yuimodules'] = 'YUI Module definitions';
 $string['cachelock_file_default'] = 'Default file locking';
 $string['cachestores'] = 'Cache stores';
index 3b75249..e5c8906 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js and b/lib/amd/build/form-autocomplete.min.js differ
index 76294eb..29ae071 100644 (file)
@@ -448,6 +448,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                 $(ele).prop('selected', true);
             }
         });
+
         // Rerender the selection list.
         updateSelectionList(options, state, originalSelect);
         // Notifiy that the selection changed.
@@ -478,6 +479,8 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
      * @param {Object} ajaxHandler This is a module that does the ajax fetch and translates the results.
      */
     var updateAjax = function(e, options, state, originalSelect, ajaxHandler) {
+        var pendingKey = 'form-autocomplete-updateajax';
+        M.util.js_pending(pendingKey);
         // Get the query to pass to the ajax function.
         var query = $(e.currentTarget).val();
         // Call the transport function to do the ajax (name taken from Select2).
@@ -514,7 +517,11 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
             });
             // Update the list of suggestions now from the new values in the select list.
             updateSuggestions(options, state, '', originalSelect);
-        }, notification.exception);
+            M.util.js_complete(pendingKey);
+        }, function(error) {
+            M.util.js_complete(pendingKey);
+            notification.exception(error);
+        });
     };
 
     /**
@@ -531,11 +538,15 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
         var inputElement = $(document.getElementById(state.inputId));
         // Add keyboard nav with keydown.
         inputElement.on('keydown', function(e) {
+            var pendingKey = 'form-autocomplete-addnav-' + state.inputId + '-' + e.keyCode;
+            M.util.js_pending(pendingKey);
+
             switch (e.keyCode) {
                 case KEYS.DOWN:
                     // If the suggestion list is open, move to the next item.
                     if (!options.showSuggestions) {
                         // Do not consume this event.
+                        M.util.js_complete(pendingKey);
                         return true;
                     } else if (inputElement.attr('aria-expanded') === "true") {
                         activateNextItem(state);
@@ -552,12 +563,14 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                     }
                     // We handled this event, so prevent it.
                     e.preventDefault();
+                    M.util.js_complete(pendingKey);
                     return false;
                 case KEYS.UP:
                     // Choose the previous active item.
                     activatePreviousItem(state);
                     // We handled this event, so prevent it.
                     e.preventDefault();
+                    M.util.js_complete(pendingKey);
                     return false;
                 case KEYS.ENTER:
                     var suggestionsElement = $(document.getElementById(state.suggestionsId));
@@ -571,6 +584,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                     }
                     // We handled this event, so prevent it.
                     e.preventDefault();
+                    M.util.js_complete(pendingKey);
                     return false;
                 case KEYS.ESCAPE:
                     if (inputElement.attr('aria-expanded') === "true") {
@@ -579,12 +593,16 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                     }
                     // We handled this event, so prevent it.
                     e.preventDefault();
+                    M.util.js_complete(pendingKey);
                     return false;
             }
+            M.util.js_complete(pendingKey);
             return true;
         });
         // Support multi lingual COMMA keycode (44).
         inputElement.on('keypress', function(e) {
+            var pendingKey = 'form-autocomplete-keypress-' + e.keyCode;
+            M.util.js_pending(pendingKey);
             if (e.keyCode === KEYS.COMMA) {
                 if (options.tags) {
                     // If we are allowing tags, comma should create a tag (or enter).
@@ -592,13 +610,17 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                 }
                 // We handled this event, so prevent it.
                 e.preventDefault();
+                M.util.js_complete(pendingKey);
                 return false;
             }
+            M.util.js_complete(pendingKey);
             return true;
         });
         // Handler used to force set the value from behat.
         inputElement.on('behat:set-value', function() {
             var suggestionsElement = $(document.getElementById(state.suggestionsId));
+            var pendingKey = 'form-autocomplete-behat';
+            M.util.js_pending(pendingKey);
             if ((inputElement.attr('aria-expanded') === "true") &&
                     (suggestionsElement.children('[aria-selected=true]').length > 0)) {
                 // If the suggestion list has an active item, select it.
@@ -607,8 +629,11 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                 // If tags are enabled, create a tag.
                 createItem(options, state, originalSelect);
             }
+            M.util.js_complete(pendingKey);
         });
         inputElement.on('blur', function() {
+            var pendingKey = 'form-autocomplete-blur';
+            M.util.js_pending(pendingKey);
             window.setTimeout(function() {
                 // Get the current element with focus.
                 var focusElement = $(document.activeElement);
@@ -619,11 +644,14 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                     }
                     closeSuggestions(state);
                 }
+                M.util.js_complete(pendingKey);
             }, 500);
         });
         if (options.showSuggestions) {
             var arrowElement = $(document.getElementById(state.downArrowId));
             arrowElement.on('click', function(e) {
+                var pendingKey = 'form-autocomplete-show-suggestions';
+                M.util.js_pending(pendingKey);
                 // Prevent the close timer, or we will open, then close the suggestions.
                 inputElement.focus();
                 // Handle ajax population of suggestions.
@@ -635,11 +663,14 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                     // Else - open the suggestions list.
                     updateSuggestions(options, state, inputElement.val(), originalSelect);
                 }
+                M.util.js_complete(pendingKey);
             });
         }
 
         var suggestionsElement = $(document.getElementById(state.suggestionsId));
         suggestionsElement.parent().on('click', '[role=option]', function(e) {
+            var pendingKey = 'form-autocomplete-parent';
+            M.util.js_pending(pendingKey);
             // Handle clicks on suggestions.
             var element = $(e.currentTarget).closest('[role=option]');
             var suggestionsElement = $(document.getElementById(state.suggestionsId));
@@ -649,29 +680,37 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
             activateItem(current, state);
             // And select it.
             selectCurrentItem(options, state, originalSelect);
+            M.util.js_complete(pendingKey);
         });
         var selectionElement = $(document.getElementById(state.selectionId));
         // Handle clicks on the selected items (will unselect an item).
         selectionElement.on('click', '[role=listitem]', function(e) {
+            var pendingKey = 'form-autocomplete-clicks';
+            M.util.js_pending(pendingKey);
             // Get the item that was clicked.
             var item = $(e.currentTarget);
             // Remove it from the selection.
             deselectItem(options, state, item, originalSelect);
+            M.util.js_complete(pendingKey);
         });
         // Keyboard navigation for the selection list.
         selectionElement.on('keydown', function(e) {
+            var pendingKey = 'form-autocomplete-keydown-' + e.keyCode;
+            M.util.js_pending(pendingKey);
             switch (e.keyCode) {
                 case KEYS.DOWN:
                     // Choose the next selection item.
                     activateNextSelection(state);
                     // We handled this event, so prevent it.
                     e.preventDefault();
+                    M.util.js_complete(pendingKey);
                     return false;
                 case KEYS.UP:
                     // Choose the previous selection item.
                     activatePreviousSelection(state);
                     // We handled this event, so prevent it.
                     e.preventDefault();
+                    M.util.js_complete(pendingKey);
                     return false;
                 case KEYS.SPACE:
                 case KEYS.ENTER:
@@ -683,8 +722,10 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                         // We handled this event, so prevent it.
                         e.preventDefault();
                     }
+                    M.util.js_complete(pendingKey);
                     return false;
             }
+            M.util.js_complete(pendingKey);
             return true;
         });
         // Whenever the input field changes, update the suggestion list.
@@ -693,8 +734,10 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
             if (options.ajax) {
                 require([options.ajax], function(ajaxHandler) {
                     var throttleTimeout = null;
+                    var pendingKey = 'autocomplete-throttledhandler';
                     var handler = function(e) {
                         updateAjax(e, options, state, originalSelect, ajaxHandler);
+                        M.util.js_complete(pendingKey);
                     };
 
                     // For input events, we do not want to trigger many, many updates.
@@ -702,6 +745,9 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                         if (throttleTimeout !== null) {
                             window.clearTimeout(throttleTimeout);
                             throttleTimeout = null;
+                        } else {
+                            // No existing timeout handler, so this is the start of a throttling check.
+                            M.util.js_pending(pendingKey);
                         }
                         throttleTimeout = window.setTimeout(handler.bind(this, e), 300);
                     };
@@ -756,6 +802,8 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                 showSuggestions: true,
                 noSelectionString: noSelectionString
             };
+            var pendingKey = 'autocomplete-setup-' + selector;
+            M.util.js_pending(pendingKey);
             if (typeof tags !== "undefined") {
                 options.tags = tags;
             }
@@ -778,6 +826,7 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
             var originalSelect = $(selector);
             if (!originalSelect) {
                 log.debug('Selector not found: ' + selector);
+                M.util.js_complete(pendingKey);
                 return false;
             }
 
@@ -839,7 +888,11 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
 
                 // Show the current values in the selection list.
                 updateSelectionList(options, state, originalSelect);
+                M.util.js_complete(pendingKey);
                 return true;
+            }).fail(function(error) {
+                M.util.js_complete(pendingKey);
+                notification.exception(error);
             });
         }
     };
index 62b8bf5..81d563f 100644 (file)
@@ -707,7 +707,7 @@ class block_manager {
         if ($includeinvisible) {
             $visiblecheck = '';
         } else {
-            $visiblecheck = 'AND (bp.visible = 1 OR bp.visible IS NULL)';
+            $visiblecheck = 'AND (bp.visible = 1 OR bp.visible IS NULL) AND (bs.visible = 1 OR bs.visible IS NULL)';
         }
 
         $context = $this->page->context;
@@ -728,24 +728,26 @@ class block_manager {
         $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = bi.id AND ctx.contextlevel = :contextlevel)";
 
         $systemcontext = context_system::instance();
-        list($bpcontext, $bpcontextidparams) = $DB->get_in_or_equal(array($context->id, $systemcontext->id),
-                SQL_PARAMS_NAMED, 'bpcontextid');
         $params = array(
             'contextlevel' => CONTEXT_BLOCK,
             'subpage1' => $this->page->subpage,
             'subpage2' => $this->page->subpage,
+            'subpage3' => $this->page->subpage,
             'contextid1' => $context->id,
             'contextid2' => $context->id,
             'contextid3' => $systemcontext->id,
+            'contextid4' => $systemcontext->id,
             'pagetype' => $this->page->pagetype,
+            'pagetype2' => $this->page->pagetype,
         );
         if ($this->page->subpage === '') {
             $params['subpage1'] = '';
             $params['subpage2'] = '';
+            $params['subpage3'] = '';
         }
         $sql = "SELECT
                     bi.id,
-                    bp.id AS blockpositionid,
+                    COALESCE(bp.id, bs.id) AS blockpositionid,
                     bi.blockname,
                     bi.parentcontextid,
                     bi.showinsubcontexts,
@@ -754,18 +756,22 @@ class block_manager {
                     bi.subpagepattern,
                     bi.defaultregion,
                     bi.defaultweight,
-                    COALESCE(bp.visible, 1) AS visible,
-                    COALESCE(bp.region, bi.defaultregion) AS region,
-                    COALESCE(bp.weight, bi.defaultweight) AS weight,
+                    COALESCE(bp.visible, bs.visible, 1) AS visible,
+                    COALESCE(bp.region, bs.region, bi.defaultregion) AS region,
+                    COALESCE(bp.weight, bs.weight, bi.defaultweight) AS weight,
                     bi.configdata
                     $ccselect
 
                 FROM {block_instances} bi
                 JOIN {block} b ON bi.blockname = b.name
                 LEFT JOIN {block_positions} bp ON bp.blockinstanceid = bi.id
-                                                  AND bp.contextid $bpcontext
+                                                  AND bp.contextid = :contextid1
                                                   AND bp.pagetype = :pagetype
                                                   AND bp.subpage = :subpage1
+                LEFT JOIN {block_positions} bs ON bs.blockinstanceid = bi.id
+                                                  AND bs.contextid = :contextid4
+                                                  AND bs.pagetype = :pagetype2
+                                                  AND bs.subpage = :subpage3
                 $ccjoin
 
                 WHERE
@@ -777,12 +783,11 @@ class block_manager {
                 $requiredbythemecheck
 
                 ORDER BY
-                    COALESCE(bp.region, bi.defaultregion),
-                    COALESCE(bp.weight, bi.defaultweight),
+                    COALESCE(bp.region, bs.region, bi.defaultregion),
+                    COALESCE(bp.weight, bs.weight, bi.defaultweight),
                     bi.id";
 
-        $allparams = $params + $parentcontextparams + $pagetypepatternparams + $requiredbythemeparams;
-        $allparams = $allparams + $requiredbythemenotparams + $bpcontextidparams;
+        $allparams = $params + $parentcontextparams + $pagetypepatternparams + $requiredbythemeparams + $requiredbythemenotparams;
         $blockinstances = $DB->get_recordset_sql($sql, $allparams);
 
         $this->birecordsbyregion = $this->prepare_per_region_arrays();
index 72baad9..7f3cc96 100644 (file)
@@ -70,6 +70,7 @@ class client extends \oauth2_client {
         if (empty($returnurl)) {
             $returnurl = new moodle_url('/');
         }
+        $this->basicauth = $issuer->get('basicauth');
         parent::__construct($issuer->get('clientid'), $issuer->get('clientsecret'), $returnurl, $scopes);
     }
 
@@ -177,11 +178,17 @@ class client extends \oauth2_client {
         $refreshtoken = $systemaccount->get('refreshtoken');
 
         $params = array('refresh_token' => $refreshtoken,
-            'client_id' => $this->issuer->get('clientid'),
-            'client_secret' => $this->issuer->get('clientsecret'),
             'grant_type' => 'refresh_token'
         );
 
+        if ($this->basicauth) {
+            $idsecret = urlencode($this->issuer->get('clientid')) . ':' . urlencode($this->issuer->get('clientsecret'));
+            $this->setHeader('Authorization: Basic ' . base64_encode($idsecret));
+        } else {
+            $params['client_id'] = $this->issuer->get('clientid');
+            $params['client_secret'] = $this->issuer->get('clientsecret');
+        }
+
         // Requests can either use http GET or POST.
         if ($this->use_http_get()) {
             $response = $this->get($this->token_url(), $params);
index c6cb756..8baa221 100644 (file)
@@ -72,6 +72,10 @@ class issuer extends persistent {
                 'type' => PARAM_BOOL,
                 'default' => false
             ),
+            'basicauth' => array(
+                'type' => PARAM_BOOL,
+                'default' => false
+            ),
             'scopessupported' => array(
                 'type' => PARAM_RAW,
                 'null' => NULL_ALLOWED,
index 70d2202..4f57dd7 100644 (file)
@@ -354,4 +354,12 @@ $definitions = array(
         'simpledata' => true,
         'staticacceleration' => false,
     ),
+
+    // Caches grouping and group ids of a user.
+    'user_group_groupings' => array(
+        'mode' => cache_store::MODE_APPLICATION,
+        'simplekeys' => true,
+        'simpledata' => true,
+        'staticacceleration' => true,
+    ),
 );
index 03e5684..c1a4efb 100644 (file)
         <FIELD NAME="scopessupported" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="The list of scopes this service supports."/>
         <FIELD NAME="enabled" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
         <FIELD NAME="showonloginpage" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
+        <FIELD NAME="basicauth" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Use HTTP Basic authentication scheme when sending client ID and password"/>
         <FIELD NAME="sortorder" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The defined sort order."/>
         <FIELD NAME="requireconfirmation" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
       </FIELDS>
index 4024e5a..91eb81f 100644 (file)
@@ -2811,5 +2811,20 @@ function xmldb_main_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2017111300.02) {
+
+        // Define field basicauth to be added to oauth2_issuer.
+        $table = new xmldb_table('oauth2_issuer');
+        $field = new xmldb_field('basicauth', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'showonloginpage');
+
+        // Conditionally launch add field basicauth.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2017111300.02);
+    }
+
     return true;
 }
index df3ecbc..d69094b 100644 (file)
@@ -2431,7 +2431,11 @@ abstract class enrol_plugin {
         $participants->close();
 
         // now clean up all remainders that were not removed correctly
-        $DB->delete_records('groups_members', array('itemid'=>$instance->id, 'component'=>'enrol_'.$name));
+        if ($gms = $DB->get_records('groups_members', array('itemid' => $instance->id, 'component' => 'enrol_' . $name))) {
+            foreach ($gms as $gm) {
+                groups_remove_member($gm->groupid, $gm->userid);
+            }
+        }
         $DB->delete_records('role_assignments', array('itemid'=>$instance->id, 'component'=>'enrol_'.$name));
         $DB->delete_records('user_enrolments', array('enrolid'=>$instance->id));
 
index 0e17a81..6b4f1dc 100644 (file)
@@ -4088,7 +4088,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null, $offlin
         $filename = array_pop($args);
 
         if ($filearea === 'badgeimage') {
-            if ($filename !== 'f1' && $filename !== 'f2') {
+            if ($filename !== 'f1' && $filename !== 'f2' && $filename !== 'f3') {
                 send_file_not_found();
             }
             if (!$file = $fs->get_file($context->id, 'badges', 'badgeimage', $badge->id, '/', $filename.'.png')) {
index 3ca1c63..e0b235a 100644 (file)
@@ -51,7 +51,7 @@ trait templatable_form_element {
         $context = [];
 
         // Not all elements have all of these attributes - but they are common enough to be valid for a few.
-        $standardattributes = ['id', 'name', 'label', 'multiple', 'checked', 'error', 'size', 'value'];
+        $standardattributes = ['id', 'name', 'label', 'multiple', 'checked', 'error', 'size', 'value', 'type'];
         $standardproperties = ['helpbutton', 'hiddenLabel'];
 
         // Standard attributes.
index 8893d13..2f36760 100644 (file)
@@ -472,38 +472,54 @@ function groups_get_user_groups($courseid, $userid=0) {
         $userid = $USER->id;
     }
 
-    $sql = "SELECT g.id, gg.groupingid
-              FROM {groups} g
-                   JOIN {groups_members} gm   ON gm.groupid = g.id
-              LEFT JOIN {groupings_groups} gg ON gg.groupid = g.id
-             WHERE gm.userid = ? AND g.courseid = ?";
-    $params = array($userid, $courseid);
+    $cache = cache::make('core', 'user_group_groupings');
 
-    $rs = $DB->get_recordset_sql($sql, $params);
+    // Try to retrieve group ids from the cache.
+    $usergroups = $cache->get($userid);
 
-    if (!$rs->valid()) {
-        $rs->close(); // Not going to iterate (but exit), close rs
-        return array('0' => array());
-    }
+    if ($usergroups === false) {
+        $sql = "SELECT g.id, g.courseid, gg.groupingid
+                  FROM {groups} g
+                  JOIN {groups_members} gm ON gm.groupid = g.id
+             LEFT JOIN {groupings_groups} gg ON gg.groupid = g.id
+                 WHERE gm.userid = ?";
+
+        $rs = $DB->get_recordset_sql($sql, array($userid));
 
-    $result    = array();
-    $allgroups = array();
+        $usergroups = array();
+        $allgroups  = array();
 
-    foreach ($rs as $group) {
-        $allgroups[$group->id] = $group->id;
-        if (is_null($group->groupingid)) {
-            continue;
+        foreach ($rs as $group) {
+            if (!array_key_exists($group->courseid, $allgroups)) {
+                $allgroups[$group->courseid] = array();
+            }
+            $allgroups[$group->courseid][$group->id] = $group->id;
+            if (!array_key_exists($group->courseid, $usergroups)) {
+                $usergroups[$group->courseid] = array();
+            }
+            if (is_null($group->groupingid)) {
+                continue;
+            }
+            if (!array_key_exists($group->groupingid, $usergroups[$group->courseid])) {
+                $usergroups[$group->courseid][$group->groupingid] = array();
+            }
+            $usergroups[$group->courseid][$group->groupingid][$group->id] = $group->id;
         }
-        if (!array_key_exists($group->groupingid, $result)) {
-            $result[$group->groupingid] = array();
+        $rs->close();
+
+        foreach (array_keys($allgroups) as $cid) {
+            $usergroups[$cid]['0'] = array_keys($allgroups[$cid]); // All user groups in the course.
         }
-        $result[$group->groupingid][$group->id] = $group->id;
-    }
-    $rs->close();
 
-    $result['0'] = array_keys($allgroups); // all groups
+        // Cache the data.
+        $cache->set($userid, $usergroups);
+    }
 
-    return $result;
+    if (array_key_exists($courseid, $usergroups)) {
+        return $usergroups[$courseid];
+    } else {
+        return array('0' => array());
+    }
 }
 
 /**
index d269cc1..c933dec 100644 (file)
@@ -403,6 +403,8 @@ abstract class oauth2_client extends curl {
     private $mocknextresponse = '';
     /** @var array $upgradedcodes list of upgraded codes in this request */
     private static $upgradedcodes = [];
+    /** @var bool basicauth */
+    protected $basicauth = false;
 
     /**
      * Returns the auth url for OAuth 2.0 request
@@ -542,12 +544,18 @@ abstract class oauth2_client extends curl {
     public function upgrade_token($code) {
         $callbackurl = self::callback_url();
         $params = array('code' => $code,
-            'client_id' => $this->clientid,
-            'client_secret' => $this->clientsecret,
             'grant_type' => 'authorization_code',
             'redirect_uri' => $callbackurl->out(false),
         );
 
+        if ($this->basicauth) {
+            $idsecret = urlencode($this->clientid) . ':' . urlencode($this->clientsecret);
+            $this->setHeader('Authorization: Basic ' . base64_encode($idsecret));
+        } else {
+            $params['client_id'] = $this->clientid;
+            $params['client_secret'] = $this->clientsecret;
+        }
+
         // Requests can either use http GET or POST.
         if ($this->use_http_get()) {
             $response = $this->get($this->token_url(), $params);
index 941375b..26a3447 100644 (file)
@@ -4464,7 +4464,9 @@ class action_menu implements renderable, templatable {
 
         if ($actionicon instanceof pix_icon) {
             $primary->icon = $actionicon->export_for_pix();
-            $primary->title = !empty($actionicon->attributes['alt']) ? $this->actionicon->attributes['alt'] : '';
+            if (!empty($actionicon->attributes['alt'])) {
+                $primary->title = $actionicon->attributes['alt'];
+            }
         } else {
             $primary->iconraw = $actionicon ? $output->render($actionicon) : '';
         }
index e3d42a8..4bf2e8b 100644 (file)
@@ -1124,6 +1124,7 @@ EOD;
         $record->timesort = 0;
         $record->eventtype = 'user';
         $record->courseid = 0;
+        $record->categoryid = 0;
 
         foreach ($data as $key => $value) {
             $record->$key = $value;
index 1dcbb3c..92c94d4 100644 (file)
@@ -63,6 +63,23 @@ class core_blocklib_testcase extends advanced_testcase {
         }
     }
 
+    /**
+     * Gets the last block created.
+     *
+     * @return stdClass a record from block_instances
+     */
+    protected function get_last_created_block() {
+        global $DB;
+        // The newest block should be the record with the highest id.
+        $records = $DB->get_records('block_instances', [], 'id DESC', '*', 0, 1);
+        $return = null;
+        foreach ($records as $record) {
+            // There should only be one.
+            $return = $record;
+        }
+        return $return;
+    }
+
     public function test_no_regions_initially() {
         // Exercise SUT & Validate.
         $this->assertEquals(array(), $this->blockmanager->get_regions());
@@ -205,6 +222,7 @@ class core_blocklib_testcase extends advanced_testcase {
         $page->set_context($context);
         $page->set_pagetype($pagetype);
         $page->set_subpage($subpage);
+        $page->set_url(new moodle_url('/'));
 
         $blockmanager = new testable_block_manager($page);
         $blockmanager->add_regions($regions, false);
@@ -688,13 +706,124 @@ class core_blocklib_testcase extends advanced_testcase {
                 $newblockdata->timemodified > $blockdata->timemodified);
         $this->assertEquals($blockdata->timecreated, $newblockdata->timecreated);
     }
+
+    /**
+     * Tests that dashboard pages get their blocks loaded correctly.
+     */
+    public function test_default_dashboard() {
+        global $CFG, $PAGE, $DB;
+        $storedpage = $PAGE;
+        require_once($CFG->dirroot . '/my/lib.php');
+        $this->purge_blocks();
+        $regionname = 'a-region';
+        $blockname = $this->get_a_known_block_type();
+        $user = self::getDataGenerator()->create_user();
+        $syscontext = context_system::instance();
+        $usercontext = context_user::instance($user->id);
+        // Add sitewide 'sticky' blocks. The page is not setup exactly as a site page would be...
+        // but it does seem to mean that the bloacks are added correctly.
+        list($sitepage, $sitebm) = $this->get_a_page_and_block_manager(array($regionname), $syscontext, 'site-index');
+        $sitebm->add_block($blockname, $regionname, 0, true, '*');
+        $sitestickyblock1 = $this->get_last_created_block();
+        $sitebm->add_block($blockname, $regionname, 1, true, '*');
+        $sitestickyblock2 = $this->get_last_created_block();
+        $sitebm->add_block($blockname, $regionname, 8, true, '*');
+        $sitestickyblock3 = $this->get_last_created_block();
+        // Blocks that should not be picked up by any other pages in this unit test.
+        $sitebm->add_block($blockname, $regionname, -8, true, 'site-index-*');
+        $sitebm->add_block($blockname, $regionname, -9, true, 'site-index');
+        $sitebm->load_blocks();
+        // This repositioning should not be picked up.
+        $sitebm->reposition_block($sitestickyblock3->id, $regionname, 9);
+        // Setup the default dashboard page. This adds the blocks with the correct parameters, but seems to not be
+        // an exact page/blockmanager setup for the default dashboard setup page.
+        $defaultmy = my_get_page(null, MY_PAGE_PRIVATE);
+        list($defaultmypage, $defaultmybm) = $this->get_a_page_and_block_manager(array($regionname), null, 'my-index', $defaultmy->id);
+        $PAGE = $defaultmypage;
+        $defaultmybm->add_block($blockname, $regionname, -2, false, $defaultmypage->pagetype, $defaultmypage->subpage);
+        $defaultblock1 = $this->get_last_created_block();
+        $defaultmybm->add_block($blockname, $regionname, 3, false, $defaultmypage->pagetype, $defaultmypage->subpage);
+        $defaultblock2 = $this->get_last_created_block();
+        $defaultmybm->load_blocks();
+        $defaultmybm->reposition_block($sitestickyblock1->id, $regionname, 4);
+        // Setup the user's dashboard.
+        $usermy = my_copy_page($user->id);
+        list($mypage, $mybm) = $this->get_a_page_and_block_manager(array($regionname), $usercontext, 'my-index', $usermy->id);
+        $PAGE = $mypage;
+        $mybm->add_block($blockname, $regionname, 5, false, $mypage->pagetype, $mypage->subpage);
+        $block1 = $this->get_last_created_block();
+        $mybm->load_blocks();
+        $mybm->reposition_block($sitestickyblock2->id, $regionname, -1);
+        // Reload the blocks in the managers.
+        context_helper::reset_caches();
+        $defaultmybm->reset_caches();
+        $this->assertNull($defaultmybm->get_loaded_blocks());
+        $defaultmybm->load_blocks();
+        $this->assertNotNull($defaultmybm->get_loaded_blocks());
+        $defaultbr = $defaultmybm->get_blocks_for_region($regionname);
+        $mybm->reset_caches();
+        $this->assertNull($mybm->get_loaded_blocks());
+        $mybm->load_blocks();
+        $this->assertNotNull($mybm->get_loaded_blocks());
+        $mybr = $mybm->get_blocks_for_region($regionname);
+        // Test that a user dashboard when forced to use the default finds the correct blocks.
+        list($forcedmypage, $forcedmybm) = $this->get_a_page_and_block_manager(array($regionname), $usercontext, 'my-index', $defaultmy->id);
+        $forcedmybm->load_blocks();
+        $forcedmybr = $forcedmybm->get_blocks_for_region($regionname);
+        // Check that the default page is in the expected order.
+        $this->assertCount(5, $defaultbr);
+        $this->assertEquals($defaultblock1->id, $defaultbr[0]->instance->id);
+        $this->assertEquals('-2', $defaultbr[0]->instance->weight);
+        $this->assertEquals($sitestickyblock2->id, $defaultbr[1]->instance->id);
+        $this->assertEquals('1', $defaultbr[1]->instance->weight);
+        $this->assertEquals($defaultblock2->id, $defaultbr[2]->instance->id);
+        $this->assertEquals('3', $defaultbr[2]->instance->weight);
+        $this->assertEquals($sitestickyblock1->id, $defaultbr[3]->instance->id);
+        $this->assertEquals('4', $defaultbr[3]->instance->weight);
+        $this->assertEquals($sitestickyblock3->id, $defaultbr[4]->instance->id);
+        $this->assertEquals('8', $defaultbr[4]->instance->weight);
+        // Check that the correct block are present in the expected order for a.
+        $this->assertCount(5, $forcedmybr);
+        $this->assertEquals($defaultblock1->id, $forcedmybr[0]->instance->id);
+        $this->assertEquals('-2', $forcedmybr[0]->instance->weight);
+        $this->assertEquals($sitestickyblock2->id, $forcedmybr[1]->instance->id);
+        $this->assertEquals('1', $forcedmybr[1]->instance->weight);
+        $this->assertEquals($defaultblock2->id, $forcedmybr[2]->instance->id);
+        $this->assertEquals('3', $forcedmybr[2]->instance->weight);
+        $this->assertEquals($sitestickyblock1->id, $forcedmybr[3]->instance->id);
+        $this->assertEquals('4', $forcedmybr[3]->instance->weight);
+        $this->assertEquals($sitestickyblock3->id, $forcedmybr[4]->instance->id);
+        $this->assertEquals('8', $forcedmybr[4]->instance->weight);
+        // Check that the correct blocks are present in the standard my page.
+        $this->assertCount(6, $mybr);
+        $this->assertEquals('-2', $mybr[0]->instance->weight);
+        $this->assertEquals($sitestickyblock2->id, $mybr[1]->instance->id);
+        $this->assertEquals('-1', $mybr[1]->instance->weight);
+        $this->assertEquals('3', $mybr[2]->instance->weight);
+        // Test the override on the first sticky block was copied and picked up.
+        $this->assertEquals($sitestickyblock1->id, $mybr[3]->instance->id);
+        $this->assertEquals('4', $mybr[3]->instance->weight);
+        $this->assertEquals($block1->id, $mybr[4]->instance->id);
+        $this->assertEquals('5', $mybr[4]->instance->weight);
+        $this->assertEquals($sitestickyblock3->id, $mybr[5]->instance->id);
+        $this->assertEquals('8', $mybr[5]->instance->weight);
+        $PAGE = $storedpage;
+    }
 }
 
 /**
  * Test-specific subclass to make some protected things public.
  */
 class testable_block_manager extends block_manager {
-
+    /**
+     * Resets the caches in the block manager.
+     * This allows blocks to be reloaded correctly.
+     */
+    public function reset_caches() {
+        $this->birecordsbyregion = null;
+        $this->blockinstances = array();
+        $this->visibleblockcontent = array();
+    }
     public function mark_loaded() {
         $this->birecordsbyregion = array();
     }
index 155fe3d..2587997 100644 (file)
@@ -283,7 +283,7 @@ class core_formslib_testcase extends advanced_testcase {
         $this->assertDebuggingCalled("Did you remember to call setType() for 'texttest'? Defaulting to PARAM_RAW cleaning.");
 
         // Check form still there though.
-        $this->expectOutputRegex('/<input[^>]*name="texttest[^>]*type="text/');
+        $this->expectOutputRegex('/<input[^>]*type="text[^>]*name="texttest/');
         $mform->display();
     }
 
@@ -303,7 +303,7 @@ class core_formslib_testcase extends advanced_testcase {
         $this->assertDebuggingCalled("Did you remember to call setType() for 'urltest'? Defaulting to PARAM_RAW cleaning.");
 
         // Check form still there though.
-        $this->expectOutputRegex('/<input[^>]*name="urltest"[^>]*type="text/');
+        $this->expectOutputRegex('/<input[^>]*type="url[^>]*name="urltest"/');
         $mform->display();
     }
 
@@ -312,7 +312,7 @@ class core_formslib_testcase extends advanced_testcase {
         $this->assertDebuggingCalled("Did you remember to call setType() for 'repeattest[0]'? Defaulting to PARAM_RAW cleaning.");
 
         // Check form still there though.
-        $this->expectOutputRegex('/<input[^>]*name="repeattest[^>]*type="text/');
+        $this->expectOutputRegex('/<input[^>]*type="text[^>]*name="repeattest/');
         $mform->display();
     }
 
@@ -320,55 +320,55 @@ class core_formslib_testcase extends advanced_testcase {
         $mform = new formslib_settype_debugging_repeat_ok();
         // No debugging expected here.
 
-        $this->expectOutputRegex('/<input[^>]*name="repeattest[^>]*type="text/');
+        $this->expectOutputRegex('/<input[^>]*type="text[^>]*name="repeattest/');
         $mform->display();
     }
 
     public function test_settype_debugging_group() {
         $mform = new formslib_settype_debugging_group();
         $this->assertDebuggingCalled("Did you remember to call setType() for 'groupel1'? Defaulting to PARAM_RAW cleaning.");
-        $this->expectOutputRegex('/<input[^>]*name="groupel1"[^>]*type="text/');
-        $this->expectOutputRegex('/<input[^>]*name="groupel2"[^>]*type="text/');
+        $this->expectOutputRegex('/<input[^>]*type="text[^>]*name="groupel1"/');
+        $this->expectOutputRegex('/<input[^>]*type="text[^>]*name="groupel2"/');
         $mform->display();
     }
 
     public function test_settype_debugging_namedgroup() {
         $mform = new formslib_settype_debugging_namedgroup();
         $this->assertDebuggingCalled("Did you remember to call setType() for 'namedgroup[groupel1]'? Defaulting to PARAM_RAW cleaning.");
-        $this->expectOutputRegex('/<input[^>]*name="namedgroup\[groupel1\]"[^>]*type="text/');
-        $this->expectOutputRegex('/<input[^>]*name="namedgroup\[groupel2\]"[^>]*type="text/');
+        $this->expectOutputRegex('/<input[^>]*type="text[^>]*name="namedgroup\[groupel1\]"/');
+        $this->expectOutputRegex('/<input[^>]*type="text[^>]*name="namedgroup\[groupel2\]"/');
         $mform->display();
     }
 
     public function test_settype_debugging_funky_name() {
         $mform = new formslib_settype_debugging_funky_name();
         $this->assertDebuggingCalled("Did you remember to call setType() for 'blah[foo][bar][1]'? Defaulting to PARAM_RAW cleaning.");
-        $this->expectOutputRegex('/<input[^>]*name="blah\[foo\]\[bar\]\[0\]"[^>]*type="text/');
-        $this->expectOutputRegex('/<input[^>]*name="blah\[foo\]\[bar\]\[1\]"[^>]*type="text/');
+        $this->expectOutputRegex('/<input[^>]*type="text[^>]*name="blah\[foo\]\[bar\]\[0\]"/');
+        $this->expectOutputRegex('/<input[^>]*type="text[^>]*name="blah\[foo\]\[bar\]\[1\]"/');
         $mform->display();
     }
 
     public function test_settype_debugging_type_inheritance() {
         $mform = new formslib_settype_debugging_type_inheritance();
-        $this->expectOutputRegex('/<input[^>]*name="blah\[foo\]\[bar\]\[0\]"[^>]*type="text/');
-        $this->expectOutputRegex('/<input[^>]*name="blah\[bar\]\[foo\]\[1\]"[^>]*type="text/');
-        $this->expectOutputRegex('/<input[^>]*name="blah\[any\]\[other\]\[2\]"[^>]*type="text/');
+        $this->expectOutputRegex('/<input[^>]*type="text[^>]*name="blah\[foo\]\[bar\]\[0\]"/');
+        $this->expectOutputRegex('/<input[^>]*type="text[^>]*name="blah\[bar\]\[foo\]\[1\]"/');
+        $this->expectOutputRegex('/<input[^>]*type="text[^>]*name="blah\[any\]\[other\]\[2\]"/');
         $mform->display();
     }
 
     public function test_settype_debugging_type_group_in_repeat() {
         $mform = new formslib_settype_debugging_type_group_in_repeat();
         $this->assertDebuggingCalled("Did you remember to call setType() for 'test2[0]'? Defaulting to PARAM_RAW cleaning.");
-        $this->expectOutputRegex('/<input[^>]*name="test1\[0\]"[^>]*type="text/');
-        $this->expectOutputRegex('/<input[^>]*name="test2\[0\]"[^>]*type="text/');
+        $this->expectOutputRegex('/<input[^>]*type="text[^>]*name="test1\[0\]"/');
+        $this->expectOutputRegex('/<input[^>]*type="text[^>]*name="test2\[0\]"/');
         $mform->display();
     }
 
     public function test_settype_debugging_type_namedgroup_in_repeat() {
         $mform = new formslib_settype_debugging_type_namedgroup_in_repeat();
         $this->assertDebuggingCalled("Did you remember to call setType() for 'namedgroup[0][test2]'? Defaulting to PARAM_RAW cleaning.");
-        $this->expectOutputRegex('/<input[^>]*name="namedgroup\[0\]\[test1\]"[^>]*type="text/');
-        $this->expectOutputRegex('/<input[^>]*name="namedgroup\[0\]\[test2\]"[^>]*type="text/');
+        $this->expectOutputRegex('/<input[^>]*type="text[^>]*name="namedgroup\[0\]\[test1\]"/');
+        $this->expectOutputRegex('/<input[^>]*type="text[^>]*name="namedgroup\[0\]\[test2\]"/');
         $mform->display();
     }
 
diff --git a/mod/choice/amd/build/select_all_choices.min.js b/mod/choice/amd/build/select_all_choices.min.js
new file mode 100644 (file)
index 0000000..bdbfb17
Binary files /dev/null and b/mod/choice/amd/build/select_all_choices.min.js differ
diff --git a/mod/choice/amd/src/select_all_choices.js b/mod/choice/amd/src/select_all_choices.js
new file mode 100644 (file)
index 0000000..5a4a4fb
--- /dev/null
@@ -0,0 +1,33 @@
+// 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/>.
+
+/**
+ * Ticks or unticks all checkboxes when clicking the Select all or Deselect all elements when viewing the response overview.
+ *
+ * @module      mod_choice/select_all_choices
+ * @copyright   2017 Marcus Fabriczy <marcus.fabriczy@blackboard.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['jquery'], function($) {
+    return {
+        init: function () {
+            $('.selectallnone a').on('click', function(e) {
+                e.preventDefault();
+                $('#attemptsform').find('input:checkbox').prop('checked', $(this).data('selectInfo'));
+            });
+        }
+    };
+});
index 55aa28c..f33d48a 100644 (file)
@@ -267,13 +267,9 @@ class mod_choice_renderer extends plugin_renderer_base {
             $selecturl = new moodle_url('#');
 
             $actiondata .= html_writer::start_div('selectallnone');
-            $selectallactions = new component_action('click',"checkall");
-            $selectall = new action_link($selecturl, get_string('selectall'), $selectallactions);
-            $actiondata .= $this->output->render($selectall) . ' / ';
+            $actiondata .= html_writer::link($selecturl, get_string('selectall'), ['data-select-info' => true]) . ' / ';
 
-            $deselectallactions = new component_action('click',"checknone");
-            $deselectall = new action_link($selecturl, get_string('deselectall'), $deselectallactions);
-            $actiondata .= $this->output->render($deselectall);
+            $actiondata .= html_writer::link($selecturl, get_string('deselectall'), ['data-select-info' => false]);
 
             $actiondata .= html_writer::end_div();
 
@@ -287,6 +283,9 @@ class mod_choice_renderer extends plugin_renderer_base {
             $select = new single_select($actionurl, 'action', $actionoptions, null,
                     array('' => get_string('chooseaction', 'choice')), 'attemptsform');
             $select->set_label(get_string('withselected', 'choice'));
+
+            $PAGE->requires->js_call_amd('mod_choice/select_all_choices', 'init');
+
             $actiondata .= $this->output->render($select);
         }
         $html .= html_writer::tag('div', $actiondata, array('class'=>'responseaction'));
index e7fc3e4..bad8317 100644 (file)
@@ -3846,7 +3846,7 @@ function glossary_get_entries_by_search($glossary, $context, $query, $fullsearch
     $count = $DB->count_records_sql("SELECT COUNT(DISTINCT(ge.id)) $sqlfrom $sqlwhere", $params);
 
     $query = "$sqlwrapheader $sqlselect $sqlfrom $sqlwhere $sqlwrapfooter $sqlorderby";
-    $entries = $DB->get_recordset_sql($query, $params, $from, $limit);
+    $entries = $DB->get_records_sql($query, $params, $from, $limit);
 
     return array($entries, $count);
 }
index f46b934..9f7bd5f 100644 (file)
@@ -103,5 +103,56 @@ function xmldb_lti_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2017111301) {
+
+        // A bug in the LTI plugin incorrectly inserted a grade item for
+        // LTI instances which were set to not allow grading.
+        // The change finds any LTI which does not have grading enabled,
+        // and updates any grades to delete them.
+
+        $ltis = $DB->get_recordset_sql("
+                SELECT
+                       l.id,
+                       l.course,
+                       l.instructorchoiceacceptgrades,
+                       t.enabledcapability,
+                       t.toolproxyid,
+                       tc.value AS acceptgrades
+                  FROM {lti} l
+            INNER JOIN {grade_items} gt
+                    ON l.id = gt.iteminstance
+             LEFT JOIN {lti_types} t
+                    ON t.id = l.typeid
+             LEFT JOIN {lti_types_config} tc
+                    ON tc.typeid = t.id AND tc.name = 'acceptgrades'
+                 WHERE gt.itemmodule = 'lti'
+                   AND gt.itemtype = 'mod'
+        ");
+
+        foreach ($ltis as $lti) {
+            $acceptgrades = true;
+            if (empty($lti->toolproxyid)) {
+                $typeacceptgrades = isset($lti->acceptgrades) ? $lti->acceptgrades : 2;
+                if (!($typeacceptgrades == 1 ||
+                        ($typeacceptgrades == 2 && $lti->instructorchoiceacceptgrades == 1))) {
+                    $acceptgrades = false;
+                }
+            } else {
+                $enabledcapabilities = explode("\n", $lti->enabledcapability);
+                $acceptgrades = in_array('Result.autocreate', $enabledcapabilities);
+            }
+
+            if (!$acceptgrades) {
+                grade_update('mod/lti', $lti->course, 'mod', 'lti', $lti->id, 0, null, array('deleted' => 1));
+            }
+
+        }
+
+        $ltis->close();
+
+        upgrade_mod_savepoint(true, 2017111301, 'lti');
+    }
+
     return true;
+
 }
index c1e207a..ab9df0e 100644 (file)
@@ -489,6 +489,11 @@ function lti_get_lti_types_from_proxy_id($toolproxyid) {
 function lti_grade_item_update($basiclti, $grades = null) {
     global $CFG;
     require_once($CFG->libdir.'/gradelib.php');
+    require_once($CFG->dirroot.'/mod/lti/servicelib.php');
+
+    if (!lti_accepts_grades($basiclti)) {
+        return 0;
+    }
 
     $params = array('itemname' => $basiclti->name, 'idnumber' => $basiclti->cmidnumber);
 
index 89ee999..32a9566 100644 (file)
@@ -103,7 +103,12 @@ function lti_get_launch_data($instance) {
         if ($tool) {
             $typeid = $tool->id;
         } else {
-            $typeid = null;
+            $tool = lti_get_tool_by_url_match($instance->securetoolurl,  $instance->course);
+            if ($tool) {
+                $typeid = $tool->id;
+            } else {
+                $typeid = null;
+            }
         }
     } else {
         $typeid = $instance->typeid;
@@ -3037,6 +3042,15 @@ function lti_load_type_from_cartridge($url, $type) {
     }
     unset($toolinfo['lti_extension_secureicon']);
 
+    // Ensure Custom icons aren't overridden by cartridge params.
+    if (!empty($type->lti_icon)) {
+        unset($toolinfo['lti_icon']);
+    }
+
+    if (!empty($type->lti_secureicon)) {
+        unset($toolinfo['lti_secureicon']);
+    }
+
     foreach ($toolinfo as $property => $value) {
         $type->$property = $value;
     }
index b5e0d84..a871ec1 100644 (file)
@@ -48,7 +48,7 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version   = 2017111300;    // The current module version (Date: YYYYMMDDXX).
+$plugin->version   = 2017111301;    // The current module version (Date: YYYYMMDDXX).
 $plugin->requires  = 2017110800;    // Requires this Moodle version.
 $plugin->component = 'mod_lti';     // Full name of the plugin (used for diagnostics).
 $plugin->cron      = 0;
index 9b2b227..209c318 100644 (file)
@@ -225,9 +225,16 @@ function quiz_delete_override($quiz, $overrideid) {
     $override = $DB->get_record('quiz_overrides', array('id' => $overrideid), '*', MUST_EXIST);
 
     // Delete the events.
-    $events = $DB->get_records('event', array('modulename' => 'quiz',
-            'instance' => $quiz->id, 'groupid' => (int)$override->groupid,
-            'userid' => (int)$override->userid));
+    if (isset($override->groupid)) {
+        // Create the search array for a group override.
+        $eventsearcharray = array('modulename' => 'quiz',
+            'instance' => $quiz->id, 'groupid' => (int)$override->groupid);
+    } else {
+        // Create the search array for a user override.
+        $eventsearcharray = array('modulename' => 'quiz',
+            'instance' => $quiz->id, 'userid' => (int)$override->userid);
+    }
+    $events = $DB->get_records('event', $eventsearcharray);
     foreach ($events as $event) {
         $eventold = calendar_event::load($event);
         $eventold->delete();
index 7657fb6..3c37a45 100644 (file)
@@ -746,12 +746,17 @@ class mod_workshop_external extends external_api {
             $submission->authorid = 0;
         }
 
-        $isworkshopclosed = $workshop->phase == workshop::PHASE_CLOSED;
-        $canviewsubmissiondetail = $ownsubmission || $canviewallsubmissions;
-        // If the workshop is not closed or the user can't see the submission detail: remove grading or feedback information.
-        if (!$isworkshopclosed || !$canviewsubmissiondetail) {
+        // Remove grade, gradeover, gradeoverby, feedbackauthor and timegraded for non-teachers or invalid phase.
+        // WS mod_workshop_external::get_grades should be used for retrieving grades by students.
+        if ($workshop->phase < workshop::PHASE_EVALUATION || !$canviewallsubmissions) {
             $properties = submission_exporter::properties_definition();
             foreach ($properties as $attribute => $settings) {
+                // Special case, the feedbackauthor (and who did it) should be returned if the workshop is closed and
+                // the user can view it.
+                if (($attribute == 'feedbackauthor' || $attribute == 'gradeoverby') &&
+                        $workshop->phase == workshop::PHASE_CLOSED && $ownsubmission) {
+                    continue;
+                }
                 if (!empty($settings['optional'])) {
                     unset($submission->{$attribute});
                 }
index bb27c19..e8e9f5d 100644 (file)
@@ -123,7 +123,6 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
      * Test test_mod_workshop_get_workshops_by_courses
      */
     public function test_mod_workshop_get_workshops_by_courses() {
-        global $DB;
 
         // Create additional course.
         $course2 = self::getDataGenerator()->create_course();
@@ -278,7 +277,6 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
         $this->assertTrue($result['examplesassessedbeforeassessment']);
 
         // Switch to next (to assessment).
-        $workshop = new workshop($this->workshop, $this->cm, $this->course);
         $workshop->switch_phase(workshop::PHASE_ASSESSMENT);
         $result = mod_workshop_external::get_workshop_access_information($this->workshop->id);
         $result = external_api::clean_returnvalue(mod_workshop_external::get_workshop_access_information_returns(), $result);
@@ -349,7 +347,6 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
      * Test mod_workshop_get_user_plan for teachers.
      */
     public function test_mod_workshop_get_user_plan_teacher() {
-        global $DB;
 
         self::setUser($this->teacher);
         $result = mod_workshop_external::get_user_plan($this->workshop->id);
@@ -862,9 +859,9 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
      * Test test_get_submissions_published_student.
      */
     public function test_get_submissions_published_student() {
-        global $DB;
 
-        $DB->set_field('workshop', 'phase', workshop::PHASE_CLOSED, array('id' => $this->workshop->id));
+        $workshop = new workshop($this->workshop, $this->cm, $this->course);
+        $workshop->switch_phase(workshop::PHASE_CLOSED);
         // Create a couple of submissions with files.
         $workshopgenerator = $this->getDataGenerator()->get_plugin_generator('mod_workshop');
         $submission = array('published' => 1);
@@ -923,7 +920,6 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
      * Test test_get_submissions_from_students_as_teacher.
      */
     public function test_get_submissions_from_students_as_teacher() {
-        global $DB;
 
         // Create a couple of submissions with files.
         $workshopgenerator = $this->getDataGenerator()->get_plugin_generator('mod_workshop');
@@ -962,12 +958,32 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
         // Create a couple of submissions with files.
         $firstsubmissionid = $this->create_test_submission($this->student);  // Create submission with files.
 
+        $workshop = new workshop($this->workshop, $this->cm, $this->course);
+        $workshop->switch_phase(workshop::PHASE_CLOSED);
         $this->setUser($this->student);
         $result = mod_workshop_external::get_submission($firstsubmissionid);
         $result = external_api::clean_returnvalue(mod_workshop_external::get_submission_returns(), $result);
         $this->assertEquals($firstsubmissionid, $result['submission']['id']);
         $this->assertCount(1, $result['submission']['contentfiles']); // Check we retrieve submission text files.
         $this->assertCount(1, $result['submission']['attachmentfiles']); // Check we retrieve attachment files.
+        $this->assertArrayHasKey('feedbackauthor', $result['submission']);
+        $this->assertArrayNotHasKey('grade', $result['submission']);
+        $this->assertArrayNotHasKey('gradeover', $result['submission']);
+        $this->assertArrayHasKey('gradeoverby', $result['submission']);
+        $this->assertArrayNotHasKey('timegraded', $result['submission']);
+
+        // Switch to a different phase (where feedback won't be available).
+        $workshop->switch_phase(workshop::PHASE_EVALUATION);
+        $result = mod_workshop_external::get_submission($firstsubmissionid);
+        $result = external_api::clean_returnvalue(mod_workshop_external::get_submission_returns(), $result);
+        $this->assertEquals($firstsubmissionid, $result['submission']['id']);
+        $this->assertCount(1, $result['submission']['contentfiles']); // Check we retrieve submission text files.
+        $this->assertCount(1, $result['submission']['attachmentfiles']); // Check we retrieve attachment files.
+        $this->assertArrayNotHasKey('feedbackauthor', $result['submission']);
+        $this->assertArrayNotHasKey('grade', $result['submission']);
+        $this->assertArrayNotHasKey('gradeover', $result['submission']);
+        $this->assertArrayNotHasKey('gradeoverby', $result['submission']);
+        $this->assertArrayNotHasKey('timegraded', $result['submission']);
     }
 
     /**
@@ -989,6 +1005,11 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals($firstsubmissionid, $result['submission']['id']);
         $this->assertCount(1, $result['submission']['contentfiles']); // Check we retrieve submission text files.
         $this->assertCount(1, $result['submission']['attachmentfiles']); // Check we retrieve attachment files.
+        $this->assertArrayNotHasKey('feedbackauthor', $result['submission']);
+        $this->assertArrayNotHasKey('grade', $result['submission']);
+        $this->assertArrayNotHasKey('gradeover', $result['submission']);
+        $this->assertArrayNotHasKey('gradeoverby', $result['submission']);
+        $this->assertArrayNotHasKey('timegraded', $result['submission']);
     }
 
     /**
@@ -1008,9 +1029,9 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
      * Test test_get_submission_published_student.
      */
     public function test_get_submission_published_student() {
-        global $DB;
 
-        $DB->set_field('workshop', 'phase', workshop::PHASE_CLOSED, array('id' => $this->workshop->id));
+        $workshop = new workshop($this->workshop, $this->cm, $this->course);
+        $workshop->switch_phase(workshop::PHASE_CLOSED);
         // Create a couple of submissions with files.
         $workshopgenerator = $this->getDataGenerator()->get_plugin_generator('mod_workshop');
         $submission = array('published' => 1);
@@ -1021,16 +1042,13 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
         $result = external_api::clean_returnvalue(mod_workshop_external::get_submission_returns(), $result);
         $this->assertEquals($submissionid, $result['submission']['id']);
         // Check that the student don't see the other student grade/feedback data even if is published.
-        // We shoul not see the grade or feedback information.
+        // We should not see the grade or feedback information.
         $properties = submission_exporter::properties_definition();
-        foreach ($properties as $attribute => $settings) {
-            if (!empty($settings['optional'])) {
-                if (isset($result['submission'][$attribute])) {
-                    echo "error $attribute";
-                }
-                $this->assertFalse(isset($result['submission'][$attribute]));
-            }
-        }
+        $this->assertArrayNotHasKey('feedbackauthor', $result['submission']);
+        $this->assertArrayNotHasKey('grade', $result['submission']);
+        $this->assertArrayNotHasKey('gradeover', $result['submission']);
+        $this->assertArrayNotHasKey('gradeoverby', $result['submission']);
+        $this->assertArrayNotHasKey('timegraded', $result['submission']);
 
         // Check with group restrictions.
         $this->setUser($this->anotherstudentg2);
@@ -1047,6 +1065,8 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
         // Create a couple of submissions with files.
         $workshopgenerator = $this->getDataGenerator()->get_plugin_generator('mod_workshop');
         $submissionid = $workshopgenerator->create_submission($this->workshop->id, $this->student->id);
+        $workshop = new workshop($this->workshop, $this->cm, $this->course);
+        $workshop->switch_phase(workshop::PHASE_CLOSED);
         // Create teacher feedback for submission.
         $record = new stdclass();
         $record->id = $submissionid;
@@ -1055,18 +1075,36 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
         $record->feedbackauthor = 'Hey';
         $record->feedbackauthorformat = FORMAT_MOODLE;
         $record->published = 1;
+        $record->timegraded = time();
         $DB->update_record('workshop_submissions', $record);
 
-        // Remove teacher caps.
-        assign_capability('mod/workshop:viewallsubmissions', CAP_PROHIBIT, $this->teacher->id, $this->context->id);
-        // Empty all the caches that may be affected  by this change.
-        accesslib_clear_all_caches_for_unit_testing();
-        course_modinfo::clear_instance_cache();
-
         $this->setUser($this->teacher);
         $result = mod_workshop_external::get_submission($submissionid);
         $result = external_api::clean_returnvalue(mod_workshop_external::get_submission_returns(), $result);
         $this->assertEquals($submissionid, $result['submission']['id']);
+        $this->assertEquals($record->feedbackauthor, $result['submission']['feedbackauthor']);
+        $this->assertEquals($record->gradeover, $result['submission']['gradeover']);
+        $this->assertEquals($record->gradeoverby, $result['submission']['gradeoverby']);
+        $this->assertEquals($record->timegraded, $result['submission']['timegraded']);
+
+        // Go to phase where feedback and grades are not yet available.
+        $workshop->switch_phase(workshop::PHASE_SUBMISSION);
+        $result = mod_workshop_external::get_submission($submissionid);
+        $result = external_api::clean_returnvalue(mod_workshop_external::get_submission_returns(), $result);
+        $this->assertArrayNotHasKey('feedbackauthor', $result['submission']);
+        $this->assertArrayNotHasKey('grade', $result['submission']);
+        $this->assertArrayNotHasKey('gradeover', $result['submission']);
+        $this->assertArrayNotHasKey('gradeoverby', $result['submission']);
+        $this->assertArrayNotHasKey('timegraded', $result['submission']);
+
+        // Remove teacher caps to view and go to valid phase.
+        $workshop->switch_phase(workshop::PHASE_EVALUATION);
+        unassign_capability('mod/workshop:viewallsubmissions', $this->teacherrole->id);
+        // Empty all the caches that may be affected  by this change.
+        accesslib_clear_all_caches_for_unit_testing();
+
+        $this->expectException('moodle_exception');
+        mod_workshop_external::get_submission($submissionid);
     }
 
     /**
@@ -1094,7 +1132,6 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
      * Test get_submission_assessments_student.
      */
     public function test_get_submission_assessments_student() {
-        global $DB;
 
         // Create the submission that will be deleted.
         $submissionid = $this->create_test_submission($this->student);
@@ -1109,7 +1146,8 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
             'grade' => 90,
         ));
 
-        $DB->set_field('workshop', 'phase', workshop::PHASE_CLOSED, array('id' => $this->workshop->id));
+        $workshop = new workshop($this->workshop, $this->cm, $this->course);
+        $workshop->switch_phase(workshop::PHASE_CLOSED);
         $this->setUser($this->student);
         $result = mod_workshop_external::get_submission_assessments($submissionid);
         $result = external_api::clean_returnvalue(mod_workshop_external::get_submission_assessments_returns(), $result);
@@ -1129,7 +1167,6 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
      * Test get_submission_assessments_invalid_phase.
      */
     public function test_get_submission_assessments_invalid_phase() {
-        global $DB;
 
         // Create the submission that will be deleted.
         $submissionid = $this->create_test_submission($this->student);
@@ -1170,7 +1207,6 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
      * Test get_assessment_author.
      */
     public function test_get_assessment_author() {
-        global $DB;
 
         // Create the submission.
         $submissionid = $this->create_test_submission($this->anotherstudentg1);
@@ -1182,7 +1218,8 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
         ));
 
         // Switch to closed phase.
-        $DB->set_field('workshop', 'phase', workshop::PHASE_CLOSED, array('id' => $this->workshop->id));
+        $workshop = new workshop($this->workshop, $this->cm, $this->course);
+        $workshop->switch_phase(workshop::PHASE_CLOSED);
         $this->setUser($this->anotherstudentg1);
         $result = mod_workshop_external::get_assessment($assessmentid);
         $result = external_api::clean_returnvalue(mod_workshop_external::get_assessment_returns(), $result);
@@ -1196,7 +1233,6 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
      * Test get_assessment_reviewer.
      */
     public function test_get_assessment_reviewer() {
-        global $DB;
 
         // Create the submission.
         $submissionid = $this->create_test_submission($this->anotherstudentg1);
@@ -1208,7 +1244,8 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
         ));
 
         // Switch to closed phase.
-        $DB->set_field('workshop', 'phase', workshop::PHASE_CLOSED, array('id' => $this->workshop->id));
+        $workshop = new workshop($this->workshop, $this->cm, $this->course);
+        $workshop->switch_phase(workshop::PHASE_CLOSED);
         $this->setUser($this->student);
         $result = mod_workshop_external::get_assessment($assessmentid);
         $result = external_api::clean_returnvalue(mod_workshop_external::get_assessment_returns(), $result);
@@ -1222,7 +1259,6 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
      * Test get_assessment_teacher.
      */
     public function test_get_assessment_teacher() {
-        global $DB;
 
         // Create the submission.
         $submissionid = $this->create_test_submission($this->anotherstudentg1);
@@ -1234,7 +1270,8 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
         ));
 
         // Switch to closed phase.
-        $DB->set_field('workshop', 'phase', workshop::PHASE_CLOSED, array('id' => $this->workshop->id));
+        $workshop = new workshop($this->workshop, $this->cm, $this->course);
+        $workshop->switch_phase(workshop::PHASE_CLOSED);
         $this->setUser($this->teacher);
         $result = mod_workshop_external::get_assessment($assessmentid);
         $result = external_api::clean_returnvalue(mod_workshop_external::get_assessment_returns(), $result);
@@ -1246,7 +1283,6 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
      * Test get_assessment_student_invalid_phase.
      */
     public function test_get_assessment_student_invalid_phase() {
-        global $DB;
 
         // Create the submission.
         $submissionid = $this->create_test_submission($this->anotherstudentg1);
@@ -1268,7 +1304,6 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
      * Test get_assessment_student_invalid_user.
      */
     public function test_get_assessment_student_invalid_user() {
-        global $DB;
 
         // Create the submission.
         $submissionid = $this->create_test_submission($this->anotherstudentg1);
@@ -1280,7 +1315,8 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
         ));
 
         // Switch to closed phase.
-        $DB->set_field('workshop', 'phase', workshop::PHASE_CLOSED, array('id' => $this->workshop->id));
+        $workshop = new workshop($this->workshop, $this->cm, $this->course);
+        $workshop->switch_phase(workshop::PHASE_CLOSED);
         $this->setUser($this->anotherstudentg2);
 
         $this->expectException('moodle_exception');
@@ -1291,7 +1327,6 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
      * Test get_assessment_form_definition_reviewer_new_assessment.
      */
     public function test_get_assessment_form_definition_reviewer_new_assessment() {
-        global $DB;
 
         // Create the submission.
         $submissionid = $this->create_test_submission($this->anotherstudentg1);
@@ -1301,7 +1336,7 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
         $assessmentid = $workshop->add_allocation($submission, $this->student->id);
 
         // Switch to assessment phase.
-        $DB->set_field('workshop', 'phase', workshop::PHASE_ASSESSMENT, array('id' => $this->workshop->id));
+        $workshop->switch_phase(workshop::PHASE_ASSESSMENT);
         $this->setUser($this->student);
         $result = mod_workshop_external::get_assessment_form_definition($assessmentid);
         $result = external_api::clean_returnvalue(mod_workshop_external::get_assessment_form_definition_returns(), $result);
@@ -1325,7 +1360,6 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
      * Test get_assessment_form_definition_teacher_new_assessment.
      */
     public function test_get_assessment_form_definition_teacher_new_assessment() {
-        global $DB;
 
         // Create the submission.
         $submissionid = $this->create_test_submission($this->anotherstudentg1);
@@ -1335,7 +1369,7 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
         $assessmentid = $workshop->add_allocation($submission, $this->student->id);
 
         // Switch to assessment phase.
-        $DB->set_field('workshop', 'phase', workshop::PHASE_ASSESSMENT, array('id' => $this->workshop->id));
+        $workshop->switch_phase(workshop::PHASE_ASSESSMENT);
         // Teachers need to be able to view assessments.
         $this->setUser($this->teacher);
         $result = mod_workshop_external::get_assessment_form_definition($assessmentid);
@@ -1347,7 +1381,6 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
      * Test get_assessment_form_definition_invalid_phase.
      */
     public function test_get_assessment_form_definition_invalid_phase() {
-        global $DB;
 
         // Create the submission.
         $submissionid = $this->create_test_submission($this->anotherstudentg1);
@@ -1356,7 +1389,7 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
         $submission = $workshop->get_submission_by_id($submissionid);
         $assessmentid = $workshop->add_allocation($submission, $this->anotherstudentg1->id);
 
-        $DB->set_field('workshop', 'phase', workshop::PHASE_EVALUATION, array('id' => $this->workshop->id));
+        $workshop->switch_phase(workshop::PHASE_EVALUATION);
         $this->setUser($this->student);
         // Since we are not reviewers we can't see the assessment until the workshop is closed.
         $this->expectException('moodle_exception');
@@ -1367,7 +1400,6 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
      * Test get_reviewer_assessments.
      */
     public function test_get_reviewer_assessments() {
-        global $DB;
 
         // Create the submission.
         $submissionid1 = $this->create_test_submission($this->student);
@@ -1384,7 +1416,8 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
         ));
 
         // Switch to assessment phase.
-        $DB->set_field('workshop', 'phase', workshop::PHASE_ASSESSMENT, array('id' => $this->workshop->id));
+        $workshop = new workshop($this->workshop, $this->cm, $this->course);
+        $workshop->switch_phase(workshop::PHASE_ASSESSMENT);
         $this->setUser($this->student);
         // Get my assessments.
         $result = mod_workshop_external::get_reviewer_assessments($this->workshop->id);
@@ -1409,9 +1442,9 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
      * Test get_reviewer_assessments_other_student.
      */
     public function test_get_reviewer_assessments_other_student() {
-        global $DB;
 
-        $DB->set_field('workshop', 'phase', workshop::PHASE_ASSESSMENT, array('id' => $this->workshop->id));
+        $workshop = new workshop($this->workshop, $this->cm, $this->course);
+        $workshop->switch_phase(workshop::PHASE_ASSESSMENT);
         // Try to get other user assessments.
         $this->setUser($this->student);
         $this->expectException('moodle_exception');
@@ -1422,9 +1455,9 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
      * Test get_reviewer_assessments_invalid_phase.
      */
     public function test_get_reviewer_assessments_invalid_phase() {
-        global $DB;
 
-        $DB->set_field('workshop', 'phase', workshop::PHASE_SUBMISSION, array('id' => $this->workshop->id));
+        $workshop = new workshop($this->workshop, $this->cm, $this->course);
+        $workshop->switch_phase(workshop::PHASE_SUBMISSION);
         // Try to get other user assessments.
         $this->setUser($this->student);
         $this->expectException('moodle_exception');
@@ -1435,7 +1468,6 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
      * Test update_assessment.
      */
     public function test_update_assessment() {
-        global $DB;
 
         // Create the submission.
         $submissionid = $this->create_test_submission($this->anotherstudentg1);
@@ -1445,7 +1477,7 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
         $assessmentid = $workshop->add_allocation($submission, $this->student->id);
 
         // Switch to assessment phase.
-        $DB->set_field('workshop', 'phase', workshop::PHASE_ASSESSMENT, array('id' => $this->workshop->id));
+        $workshop->switch_phase(workshop::PHASE_ASSESSMENT);
         $this->setUser($this->student);
         // Get the form definition.
         $result = mod_workshop_external::get_assessment_form_definition($assessmentid);
@@ -1540,7 +1572,6 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
      * Test get_grades.
      */
     public function test_get_grades() {
-        global $DB;
 
         $timenow = time();
         $submissiongrade = array(
@@ -1591,12 +1622,12 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
      * Test get_grades_other_student.
      */
     public function test_get_grades_other_student() {
-        global $DB;
 
         // Create the submission that will be deleted.
         $submissionid = $this->create_test_submission($this->student);
 
-        $DB->set_field('workshop', 'phase', workshop::PHASE_CLOSED, array('id' => $this->workshop->id));
+        $workshop = new workshop($this->workshop, $this->cm, $this->course);
+        $workshop->switch_phase(workshop::PHASE_CLOSED);
         $this->setUser($this->anotherstudentg1);
         $this->expectException('moodle_exception');
         mod_workshop_external::get_grades($this->workshop->id, $this->student->id);
@@ -1700,7 +1731,6 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
      * Test get_grades_report.
      */
     public function test_get_grades_report() {
-        global $DB;
 
         $workshop = new workshop($this->workshop, $this->cm, $this->course);
         $workshopgenerator = $this->getDataGenerator()->get_plugin_generator('mod_workshop');
@@ -1716,7 +1746,7 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
             'grade' => 55,
         ));
 
-        $DB->set_field('workshop', 'phase', workshop::PHASE_CLOSED, array('id' => $this->workshop->id));
+        $workshop->switch_phase(workshop::PHASE_CLOSED);
         $this->setUser($this->teacher);
         $result = mod_workshop_external::get_grades_report($this->workshop->id);
         $result = external_api::clean_returnvalue(mod_workshop_external::get_grades_report_returns(), $result);
@@ -1805,7 +1835,8 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
         $workshopgenerator = $this->getDataGenerator()->get_plugin_generator('mod_workshop');
         $submissionid = $workshopgenerator->create_submission($this->workshop->id, $this->student->id);
 
-        $DB->set_field('workshop', 'phase', workshop::PHASE_EVALUATION, array('id' => $this->workshop->id));
+        $workshop = new workshop($this->workshop, $this->cm, $this->course);
+        $workshop->switch_phase(workshop::PHASE_EVALUATION);
 
         $this->setUser($this->teacher);
         $feedbacktext = 'The feedback';
@@ -1817,7 +1848,6 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
         $result = external_api::clean_returnvalue(mod_workshop_external::evaluate_submission_returns(), $result);
         $this->assertTrue($result['status']);
 
-        $workshop = new workshop($this->workshop, $this->cm, $this->course);
         $submission = $DB->get_record('workshop_submissions', array('id' => $submissionid));
         $this->assertEquals($feedbacktext, $submission->feedbackauthor);
         $this->assertEquals($workshop->raw_grade_value($gradeover, $workshop->grade), $submission->gradeover);  // Expected grade.
@@ -1853,11 +1883,11 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
      * Test evaluate_submission_no_permissions.
      */
     public function test_evaluate_submission_no_permissions() {
-        global $DB;
 
         $workshopgenerator = $this->getDataGenerator()->get_plugin_generator('mod_workshop');
         $submissionid = $workshopgenerator->create_submission($this->workshop->id, $this->student->id);
-        $DB->set_field('workshop', 'phase', workshop::PHASE_EVALUATION, array('id' => $this->workshop->id));
+        $workshop = new workshop($this->workshop, $this->cm, $this->course);
+        $workshop->switch_phase(workshop::PHASE_EVALUATION);
 
         $this->setUser($this->student);
         $feedbacktext = 'The feedback';
@@ -1872,11 +1902,11 @@ class mod_workshop_external_testcase extends externallib_advanced_testcase {
      * Test evaluate_submission_invalid_grade.
      */
     public function test_evaluate_submission_invalid_grade() {
-        global $DB;
 
         $workshopgenerator = $this->getDataGenerator()->get_plugin_generator('mod_workshop');
         $submissionid = $workshopgenerator->create_submission($this->workshop->id, $this->student->id);
-        $DB->set_field('workshop', 'phase', workshop::PHASE_EVALUATION, array('id' => $this->workshop->id));
+        $workshop = new workshop($this->workshop, $this->cm, $this->course);
+        $workshop->switch_phase(workshop::PHASE_EVALUATION);
 
         $this->setUser($this->teacher);
         $feedbacktext = 'The feedback';
index 6deac3a..fff81f9 100644 (file)
@@ -5,6 +5,7 @@
                 class="btn btn-secondary"
                 name="{{element.name}}"
                 id="{{element.id}}"
+                type="button"
                 {{#error}}
                     autofocus aria-describedby="id_error_{{element.name}}"
                 {{/error}}
index 76529cb..10d4851 100644 (file)
@@ -5,6 +5,7 @@
                 class="btn btn-secondary m-l-0"
                 name="{{element.name}}"
                 id="{{element.id}}"
+                type="button"
                 {{#error}}
                     autofocus aria-describedby="id_error_{{element.name}}"
                 {{/error}}
index 517bbb6..d545803 100644 (file)
@@ -88,7 +88,7 @@ class profile_field_base {
         $this->set_userid($userid);
         if ($fielddata) {
             $this->set_field($fielddata);
-            if ($userid && !empty($fielddata->hasuserdata)) {
+            if ($userid > 0 && !empty($fielddata->hasuserdata)) {
                 $this->set_user_data($fielddata->data, $fielddata->dataformat);
             }
         } else {
@@ -395,7 +395,7 @@ class profile_field_base {
             $this->set_field($field);
         }
 
-        if (!empty($this->field) && $this->userid) {
+        if (!empty($this->field) && $this->userid > 0) {
             $params = array('userid' => $this->userid, 'fieldid' => $this->fieldid);
             if ($data = $DB->get_record('user_info_data', $params, 'data, dataformat')) {
                 $this->set_user_data($data->data, $data->dataformat);
@@ -413,7 +413,7 @@ class profile_field_base {
     public function is_visible() {
         global $USER;
 
-        $context = $this->userid ? context_user::instance($this->userid) : context_system::instance();
+        $context = ($this->userid > 0) ? context_user::instance($this->userid) : context_system::instance();
 
         switch ($this->field->visible) {
             case PROFILE_VISIBLE_ALL:
@@ -507,12 +507,12 @@ function profile_get_user_fields_with_data($userid) {
 
     // Join any user info data present with each user info field for the user object.
     $sql = 'SELECT uif.*, uic.name AS categoryname ';
-    if ($userid) {
+    if ($userid > 0) {
         $sql .= ', uind.id AS hasuserdata, uind.data, uind.dataformat ';
     }
     $sql .= 'FROM {user_info_field} uif ';
     $sql .= 'LEFT JOIN {user_info_category} uic ON uif.categoryid = uic.id ';
-    if ($userid) {
+    if ($userid > 0) {
         $sql .= 'LEFT JOIN {user_info_data} uind ON uif.id = uind.fieldid AND uind.userid = :userid ';
     }
     $sql .= 'ORDER BY uic.sortorder ASC, uif.sortorder ASC ';
index 25b57e7..65ec2cb 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2017111300.01;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2017112300.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
-$release  = '3.5dev (Build: 20171116)'; // Human-friendly version name
+$release  = '3.5dev (Build: 20171123)'; // Human-friendly version name
 
 $branch   = '35';                       // This version's branch.
 $maturity = MATURITY_ALPHA;             // This version's maturity level.
index e002689..75268d8 100644 (file)
@@ -68,6 +68,9 @@ class webservice_xmlrpc_client {
      * @throws moodle_exception
      */
     public function call($functionname, $params = array()) {
+        global $CFG;
+        require_once($CFG->libdir . '/filelib.php');
+
         if ($this->token) {
             $this->serverurl->param('wstoken', $this->token);
         }