Merge branch 'MDL-58497-master' of git://github.com/ankitagarwal/moodle
authorDavid Monllao <davidm@moodle.com>
Thu, 4 May 2017 02:58:36 +0000 (10:58 +0800)
committerDavid Monllao <davidm@moodle.com>
Thu, 4 May 2017 02:58:36 +0000 (10:58 +0800)
107 files changed:
admin/environment.xml
admin/tool/lp/templates/comment_area.mustache
auth/cas/db/upgrade.php
auth/db/db/upgrade.php
auth/email/db/upgrade.php
auth/fc/db/upgrade.php
auth/imap/db/upgrade.php
auth/ldap/db/upgrade.php
auth/manual/db/upgrade.php
auth/mnet/db/upgrade.php
auth/nntp/db/upgrade.php
auth/none/db/upgrade.php
auth/pam/db/upgrade.php
auth/pop3/db/upgrade.php
auth/shibboleth/db/upgrade.php
auth/upgrade.txt
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/templates/course-event-list.mustache
blocks/myoverview/templates/courses-view.mustache
blocks/myoverview/templates/event-list.mustache
blocks/myoverview/templates/main.mustache
blocks/myoverview/templates/progress-chart.mustache
blocks/myoverview/templates/timeline-view-courses.mustache
blog/external_blog_edit.php
blog/lib.php
cache/stores/memcached/lib.php
cache/stores/memcached/tests/memcached_test.php
calendar/classes/local/event/data_access/event_vault.php
calendar/tests/event_vault_test.php
competency/classes/competency.php
course/externallib.php
course/format/lib.php
course/tests/externallib_test.php
course/upgrade.txt
index.php
install/lang/es_mx/install.php
lang/en/auth.php
lang/en/blog.php
lib/amd/build/templates.min.js
lib/amd/src/templates.js
lib/classes/message/manager.php
lib/classes/oauth2/client.php
lib/classes/output/icon_system_fontawesome.php
lib/db/upgrade.php
lib/ltiprovider/readme_moodle.txt
lib/ltiprovider/src/HTTPMessage.php
lib/messagelib.php
lib/oauthlib.php
lib/portfolio/plugin.php
lib/tests/message_test.php
lib/tests/messagelib_test.php
lib/tests/upgradelib_test.php
lib/upgrade.txt
lib/upgradelib.php
message/output/lib.php
message/output/popup/message_output_popup.php
mod/assign/feedback/editpdf/lib.php
mod/assign/feedback/file/lib.php
mod/assign/locallib.php
mod/assign/submission/file/lib.php
mod/assign/submission/file/locallib.php
mod/assign/submission/file/tests/behat/file_type_restriction.feature
mod/assign/submission/file/tests/locallib_test.php
mod/assign/submission/onlinetext/lib.php
mod/assign/tests/locallib_test.php
mod/assign/upgrade.txt
mod/book/lib.php
mod/choice/lib.php
mod/choice/report.php
mod/data/lib.php
mod/data/tests/lib_test.php
mod/feedback/analysis.php
mod/feedback/analysis_course.php
mod/feedback/analysis_to_excel.php [new file with mode: 0644]
mod/feedback/classes/completion.php
mod/feedback/classes/structure.php
mod/feedback/lib.php
mod/feedback/tests/behat/anonymous.feature
mod/feedback/tests/lib_test.php
mod/folder/lib.php
mod/forum/lib.php
mod/glossary/lib.php
mod/imscp/lib.php
mod/label/lib.php
mod/lesson/pagetypes/numerical.php
mod/lti/lib.php
mod/page/lib.php
mod/resource/lib.php
mod/scorm/lib.php
mod/scorm/tests/lib_test.php
mod/survey/lib.php
mod/url/lib.php
mod/wiki/lib.php
repository/onedrive/classes/remove_temp_access_task.php
repository/onedrive/classes/rest.php
repository/onedrive/lang/en/repository_onedrive.php
repository/onedrive/lib.php
theme/boost/pix/fp/alias.svg
theme/boost/pix/fp/alias_sm.svg [new file with mode: 0644]
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/filemanager.scss
theme/boost/scss/moodle/icons.scss
theme/bootstrapbase/less/moodle/blocks.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/block_myoverview/course-event-list.mustache
theme/bootstrapbase/templates/block_myoverview/courses-view.mustache
version.php

index f875211..916c8a7 100644 (file)
       <PHP_EXTENSION name="json" level="required">
       </PHP_EXTENSION>
       <PHP_EXTENSION name="hash" level="required"/>
+      <PHP_EXTENSION name="fileinfo" level="required"/>
     </PHP_EXTENSIONS>
     <PHP_SETTINGS>
       <PHP_SETTING name="memory_limit" value="96M" level="required">
index bd49f87..a73f3ed 100644 (file)
@@ -30,7 +30,7 @@
     * autostart
     * canpost
     * cid
-    * collapsediconurl
+    * collapsediconkey
     * commentarea
     * component
     * contextid
index 9de6515..dc0a265 100644 (file)
@@ -61,7 +61,7 @@ function xmldb_auth_cas_upgrade($oldversion) {
 
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/cas to auth_cas.
-        $DB->set_field('config_plugins', 'plugin', 'auth_cas', array('plugin' => 'auth/cas'));
+        upgrade_fix_config_auth_plugin_names('cas');
         upgrade_plugin_savepoint(true, 2017020700, 'auth', 'cas');
     }
 
index 08db727..f40ea86 100644 (file)
@@ -37,7 +37,7 @@ function xmldb_auth_db_upgrade($oldversion) {
 
     if ($oldversion < 2017032800) {
         // Convert info in config plugins from auth/db to auth_db
-        $DB->set_field('config_plugins', 'plugin', 'auth_db', array('plugin' => 'auth/db'));
+        upgrade_fix_config_auth_plugin_names('db');
         upgrade_plugin_savepoint(true, 2017032800, 'auth', 'db');
     }
 
index 36d6d1f..f1b9ca7 100644 (file)
@@ -37,10 +37,9 @@ function xmldb_auth_email_upgrade($oldversion) {
 
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/email to auth_email.
-        $DB->set_field('config_plugins', 'plugin', 'auth_email', array('plugin' => 'auth/email'));
+        upgrade_fix_config_auth_plugin_names('email');
         upgrade_plugin_savepoint(true, 2017020700, 'auth', 'email');
     }
 
     return true;
 }
-
index 6909e6c..4aefd46 100644 (file)
@@ -37,7 +37,7 @@ function xmldb_auth_fc_upgrade($oldversion) {
 
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/fc to auth_fc.
-        $DB->set_field('config_plugins', 'plugin', 'auth_fc', array('plugin' => 'auth/fc'));
+        upgrade_fix_config_auth_plugin_names('fc');
         upgrade_plugin_savepoint(true, 2017020700, 'auth', 'fc');
     }
 
index 96dc3df..e89b077 100644 (file)
@@ -37,10 +37,9 @@ function xmldb_auth_imap_upgrade($oldversion) {
 
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/imap to auth_imap.
-        $DB->set_field('config_plugins', 'plugin', 'auth_imap', array('plugin' => 'auth/imap'));
+        upgrade_fix_config_auth_plugin_names('imap');
         upgrade_plugin_savepoint(true, 2017020700, 'auth', 'imap');
     }
 
     return true;
 }
-
index e6c02cd..4b8bab0 100644 (file)
@@ -61,7 +61,7 @@ function xmldb_auth_ldap_upgrade($oldversion) {
 
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/ldap to auth_ldap.
-        $DB->set_field('config_plugins', 'plugin', 'auth_ldap', array('plugin' => 'auth/ldap'));
+        upgrade_fix_config_auth_plugin_names('ldap');
         upgrade_plugin_savepoint(true, 2017020700, 'auth', 'ldap');
     }
 
index d9c6e3c..e0dc4a6 100644 (file)
@@ -49,7 +49,7 @@ function xmldb_auth_manual_upgrade($oldversion) {
 
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/manual to auth_manual.
-        $DB->set_field('config_plugins', 'plugin', 'auth_manual', array('plugin' => 'auth/manual'));
+        upgrade_fix_config_auth_plugin_names('manual');
         upgrade_plugin_savepoint(true, 2017020700, 'auth', 'manual');
     }
 
index 5cccdeb..1aceca6 100644 (file)
@@ -48,7 +48,7 @@ function xmldb_auth_mnet_upgrade($oldversion) {
     // Put any upgrade step following this.
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/mnet to auth_mnet.
-        $DB->set_field('config_plugins', 'plugin', 'auth_mnet', array('plugin' => 'auth/mnet'));
+        upgrade_fix_config_auth_plugin_names('mnet');
         upgrade_plugin_savepoint(true, 2017020700, 'auth', 'mnet');
     }
 
index 129d8e9..4fd893d 100644 (file)
@@ -37,7 +37,7 @@ function xmldb_auth_nntp_upgrade($oldversion) {
 
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/nntp to auth_nntp.
-        $DB->set_field('config_plugins', 'plugin', 'auth_nntp', array('plugin' => 'auth/nntp'));
+        upgrade_fix_config_auth_plugin_names('nntp');
         upgrade_plugin_savepoint(true, 2017020700, 'auth', 'nntp');
     }
 
index d54035e..2f858cb 100644 (file)
@@ -37,10 +37,9 @@ function xmldb_auth_none_upgrade($oldversion) {
 
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/none to auth_none.
-        $DB->set_field('config_plugins', 'plugin', 'auth_none', array('plugin' => 'auth/none'));
+        upgrade_fix_config_auth_plugin_names('none');
         upgrade_plugin_savepoint(true, 2017020700, 'auth', 'none');
     }
 
     return true;
 }
-
index 3bff038..480fe39 100644 (file)
@@ -37,7 +37,7 @@ function xmldb_auth_pam_upgrade($oldversion) {
 
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/pam to auth_pam.
-        $DB->set_field('config_plugins', 'plugin', 'auth_pam', array('plugin' => 'auth/pam'));
+        upgrade_fix_config_auth_plugin_names('pam');
         upgrade_plugin_savepoint(true, 2017020700, 'auth', 'pam');
     }
 
index 702fe51..ea91a44 100644 (file)
@@ -37,7 +37,7 @@ function xmldb_auth_pop3_upgrade($oldversion) {
 
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/pop3 to auth_pop3.
-        $DB->set_field('config_plugins', 'plugin', 'auth_pop3', array('plugin' => 'auth/pop3'));
+        upgrade_fix_config_auth_plugin_names('pop3');
         upgrade_plugin_savepoint(true, 2017020700, 'auth', 'pop3');
     }
 
index 8b95151..3bd2d13 100644 (file)
@@ -37,7 +37,7 @@ function xmldb_auth_shibboleth_upgrade($oldversion) {
 
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/shibboleth to auth_shibboleth.
-        $DB->set_field('config_plugins', 'plugin', 'auth_shibboleth', array('plugin' => 'auth/shibboleth'));
+        upgrade_fix_config_auth_plugin_names('shibboleth');
         upgrade_plugin_savepoint(true, 2017020700, 'auth', 'shibboleth');
     }
 
index def7fa2..1a3a50a 100644 (file)
@@ -5,6 +5,8 @@ information provided here is intended especially for developers.
 
 * Authentication plugins have been migrated to use the admin settings API.
   Plugins should use a settings.php file to manage configurations rather than using the config.html files.
+  See how the helper function upgrade_fix_config_auth_plugin_names() can be used to convert the legacy settings to the
+  new ones.
 * The function 'print_auth_lock_options' has been replaced by 'display_auth_lock_options' which uses the admin settings API.
   See auth_manual as an exmple of how it can be used.  More information can be found in MDL-12689.
 * The list of supported identity providers (SSO IdP) returned by the 'loginpage_idp_list' method (used to render the
index a67caec..4c464f5 100644 (file)
@@ -41,3 +41,4 @@ $string['sortbycourses'] = 'Sort by courses';
 $string['sortbydates'] = 'Sort by dates';
 $string['timeline'] = 'Timeline';
 $string['viewcourse'] = 'View course';
+$string['viewcoursename'] = 'View course {$a}';
index ac16e44..74922d6 100644 (file)
             </button>
         </div>
     </div>
-    <div class="hidden text-xs-center text-center" data-region="empty-message">
+    <div class="hidden text-xs-center text-center m-y-3" data-region="empty-message">
         <img class="empty-placeholder-image-sm"
              src="{{urls.noevents}}"
-             alt="{{#str}} noevents, block_myoverview {{/str}}">
+             alt="{{#str}} noevents, block_myoverview {{/str}}"
+             role="presentation">
         <p class="text-muted m-t-1">{{#str}} noevents, block_myoverview {{/str}}</p>
-        <a href="{{viewurl}}" class="btn btn-secondary text-primary">
+        <a href="{{viewurl}}" class="btn btn-secondary text-primary"
+           aria-label="{{#str}} viewcoursename, block_myoverview, {{fullnamedisplay}} {{/str}}">
             {{#str}} viewcourse, block_myoverview {{/str}}
         </a>
     </div>
index baeb696..2a66c0c 100644 (file)
@@ -51,7 +51,8 @@
                 <div class="text-xs-center text-center m-t-3">
                     <img class="empty-placeholder-image-lg"
                          src="{{urls.nocourses}}"
-                         alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}">
+                         alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}"
+                         role="presentation">
                     <p class="text-muted m-t-1">{{#str}} nocoursesinprogress, block_myoverview {{/str}}</p>
                 </div>
             {{/inprogress}}
@@ -69,7 +70,8 @@
                 <div class="text-xs-center text-center m-t-3">
                     <img class="empty-placeholder-image-lg"
                          src="{{urls.nocourses}}"
-                         alt="{{#str}} nocoursesfuture, block_myoverview {{/str}}">
+                         alt="{{#str}} nocoursesfuture, block_myoverview {{/str}}"
+                         role="presentation">
                     <p class="text-muted m-t-1">{{#str}} nocoursesfuture, block_myoverview {{/str}}</p>
                 </div>
             {{/future}}
@@ -87,7 +89,8 @@
                 <div class="text-xs-center text-center m-t-3">
                     <img class="empty-placeholder-image-lg"
                          src="{{urls.nocourses}}"
-                         alt="{{#str}} nocoursespast, block_myoverview {{/str}}">
+                         alt="{{#str}} nocoursespast, block_myoverview {{/str}}"
+                         role="presentation">
                     <p class="text-muted m-t-1">{{#str}} nocoursespast, block_myoverview {{/str}}</p>
                 </div>
             {{/past}}
     <div class="text-xs-center text-center m-t-3">
         <img class="empty-placeholder-image-lg"
              src="{{urls.nocourses}}"
-             alt="{{#str}} nocourses, block_myoverview {{/str}}">
+             alt="{{#str}} nocourses, block_myoverview {{/str}}"
+             role="presentation">
         <p class="text-muted m-t-1">{{#str}} nocourses, block_myoverview {{/str}}</p>
     </div>
     {{/hascourses}}
index ae5553b..dbe3d25 100644 (file)
@@ -74,7 +74,8 @@
     <div class="hidden text-xs-center text-center m-t-3" data-region="empty-message">
         <img class="empty-placeholder-image-lg"
              src="{{urls.noevents}}"
-             alt="{{#str}} noevents, block_myoverview {{/str}}">
+             alt="{{#str}} noevents, block_myoverview {{/str}}"
+             role="presentation">
         <p class="text-muted m-t-1">{{#str}} noevents, block_myoverview {{/str}}</p>
     </div>
 </div>
index 54fc37a..3a1a942 100644 (file)
@@ -36,7 +36,7 @@
             </a>
         </li>
     </ul>
-    <div class="tab-content">
+    <div class="tab-content content-centred">
         <div role="tabpanel" class="tab-pane fade in active" id="myoverview_timeline_view">
             {{> block_myoverview/timeline-view }}
         </div>
index 8c592e7..18ff2a4 100644 (file)
@@ -32,7 +32,7 @@
         <div class="progress-indicator">
             <svg xmlns="http://www.w3.org/2000/svg">
                 <g>
-                    <title>{{progress}}&#37;</title>
+                    <title aria-hidden="true">{{progress}}&#37;</title>
                     <circle class="circle percent-{{progress}}"
                             r="27.5"
                             cx="35"
index 2f0d72a..a569405 100644 (file)
@@ -44,7 +44,8 @@
                 <div class="text-xs-center text-center m-t-3">
                     <img class="empty-placeholder-image-lg"
                          src="{{urls.noevents}}"
-                         alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}">
+                         alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}"
+                         role="presentation">
                     <p class="text-muted m-t-1">{{#str}} nocoursesinprogress, block_myoverview {{/str}}</p>
                 </div>
             {{/haspages}}
@@ -53,7 +54,8 @@
             <div class="text-xs-center text-center m-t-3">
                 <img class="empty-placeholder-image-lg"
                      src="{{urls.noevents}}"
-                     alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}">
+                     alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}"
+                     role="presentation">
                 <p class="text-muted m-t-1">{{#str}} nocoursesinprogress, block_myoverview {{/str}}</p>
             </div>
         {{/inprogress}}
index 5d69243..a1a56f1 100644 (file)
@@ -52,11 +52,11 @@ $action = (empty($id)) ? 'add' : 'edit';
 
 $external = new stdClass();
 
-// Check that this id exists.
-if (!empty($id) && !$DB->record_exists('blog_external', array('id' => $id))) {
-    print_error('wrongexternalid', 'blog');
-} else if (!empty($id)) {
-    $external = $DB->get_record('blog_external', array('id' => $id));
+// Retrieve the external blog record.
+if (!empty($id)) {
+    if (!$external = $DB->get_record('blog_external', array('id' => $id, 'userid' => $USER->id))) {
+        print_error('wrongexternalid', 'blog');
+    }
     $external->autotags = core_tag_tag::get_item_tags_array('core', 'blog_external', $id);
 }
 
index 06b4af8..2e53d97 100644 (file)
@@ -872,7 +872,7 @@ function blog_get_headers($courseid=null, $groupid=null, $userid=null, $tagid=nu
         }
 
         // Append Search info.
-        if (!empty($search)) {
+        if (!empty($search) && has_capability('moodle/blog:search', $sitecontext)) {
             $headers['filters']['search'] = $search;
             $blogurl->param('search', $search);
             $PAGE->navbar->add(get_string('searchterm', 'blog', $search), $blogurl->out());
index 56df937..ba7a6d5 100644 (file)
@@ -229,8 +229,56 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
         $version = phpversion('memcached');
         $this->candeletemulti = ($version && version_compare($version, self::REQUIRED_VERSION, '>='));
 
-        // Test the connection to the main connection.
-        $this->isready = @$this->connection->set("ping", 'ping', 1);
+        $this->isready = $this->is_connection_ready();
+    }
+
+    /**
+     * Confirm whether the connection is ready and usable.
+     *
+     * @return boolean
+     */
+    public function is_connection_ready() {
+        if (!@$this->connection->set("ping", 'ping', 1)) {
+            // Test the connection to the server.
+            return false;
+        }
+
+        if ($this->isshared) {
+            // There is a bug in libmemcached which means that it is not possible to purge the cache in a shared cache
+            // configuration.
+            // This issue currently affects:
+            // - memcached 1.4.23+ with php-memcached <= 2.2.0
+            // The following combinations are not affected:
+            // - memcached <= 1.4.22 with any version of php-memcached
+            // - any version of memcached with php-memcached >= 3.0.1
+
+
+            // This check is cheapest as it does not involve connecting to the server at all.
+            $safecombination = false;
+            $extension = new ReflectionExtension('memcached');
+            if ((version_compare($extension->getVersion(), '3.0.1') >= 0)) {
+                // This is php-memcached version >= 3.0.1 which is a safe combination.
+                $safecombination = true;
+            }
+
+            if (!$safecombination && (version_compare($this->connection->getVersion(), '1.4.22') <= 0)) {
+                // This is memcached server version <= 1.4.22 which is a safe combination.
+                $safecombination = true;
+            }
+
+            if (!$safecombination) {
+                // This is memcached 1.4.23+ and php-memcached < 3.0.1.
+                // The issue may have been resolved in a subsequent update to any of the three libraries.
+                // The only way to safely determine if the combination is safe is to call getAllKeys.
+                // A safe combination will return an array, whilst an affected combination will return false.
+                // This is the most expensive check.
+                if (!is_array($this->connection->getAllKeys())) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
     }
 
     /**
index bef5cbe..db41269 100644 (file)
@@ -288,6 +288,10 @@ class cachestore_memcached_test extends cachestore_tests {
 
         $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcached', 'phpunit_test');
         $cachestore = $this->create_test_cache_with_config($definition, array('isshared' => true));
+        if (!$cachestore->is_connection_ready()) {
+            $this->markTestSkipped('Could not test cachestore_memcached. Connection is not ready.');
+        }
+
         $connection = new Memcached(crc32(__METHOD__));
         $connection->addServers($this->get_servers(TEST_CACHESTORE_MEMCACHED_TESTSERVERS));
         $connection->setOptions(array(
index 967ea4b..eaed118 100644 (file)
@@ -197,6 +197,16 @@ class event_vault implements event_vault_interface {
         event_interface $afterevent = null,
         $limitnum = 20
     ) {
+        $courseids = array_map(function($course) {
+            return $course->id;
+        }, enrol_get_all_users_courses($user->id));
+
+        $groupids = array_reduce($courseids, function($carry, $courseid) use ($user) {
+            $groupings = groups_get_user_groups($courseid, $user->id);
+            // Grouping 0 is all groups.
+            return array_merge($carry, $groupings[0]);
+        }, []);
+
         return $this->get_events(
             null,
             null,
@@ -207,8 +217,8 @@ class event_vault implements event_vault_interface {
             $limitnum,
             CALENDAR_EVENT_TYPE_ACTION,
             [$user->id],
-            null,
-            null,
+            $groupids ? $groupids : null,
+            $courseids ? $courseids : null,
             true,
             true,
             function ($event) {
@@ -225,6 +235,7 @@ class event_vault implements event_vault_interface {
         event_interface $afterevent = null,
         $limitnum = 20
     ) {
+        $groupings = groups_get_user_groups($course->id, $user->id);
         return array_values(
             $this->get_events(
                 null,
@@ -236,7 +247,7 @@ class event_vault implements event_vault_interface {
                 $limitnum,
                 CALENDAR_EVENT_TYPE_ACTION,
                 [$user->id],
-                null,
+                $groupings[0] ? $groupings[0] : null,
                 [$course->id],
                 true,
                 true,
index 31e74cd..e4a1b21 100644 (file)
@@ -417,6 +417,146 @@ class core_calendar_event_vault_testcase extends advanced_testcase {
         $this->assertEmpty($events);
     }
 
+    /**
+     * There are subtle cases where the priority of an event override may be identical to another.
+     * For example, if you duplicate a group override, but make it apply to a different group. Now
+     * there are two overrides with exactly the same overridden dates. In this case the priority of
+     * both is 1.
+     *
+     * In this situation:
+     * - A user in group A should see only the A override
+     * - A user in group B should see only the B override
+     * - A user in both A and B should see both
+     */
+    public function test_get_action_events_by_timesort_with_identical_group_override_priorities() {
+        $this->resetAfterTest();
+        $this->setAdminuser();
+
+        $course = $this->getDataGenerator()->create_course();
+
+        // Create an assign instance.
+        $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+        $assigninstance = $assigngenerator->create_instance(['course' => $course->id]);
+
+        // Create users.
+        $users = [
+            'Only in group A'  => $this->getDataGenerator()->create_user(),
+            'Only in group B'  => $this->getDataGenerator()->create_user(),
+            'In group A and B' => $this->getDataGenerator()->create_user(),
+            'In no groups'     => $this->getDataGenerator()->create_user()
+        ];
+
+        // Enrol users.
+        foreach ($users as $user) {
+            $this->getDataGenerator()->enrol_user($user->id, $course->id);
+        }
+
+        // Create groups.
+        $groupa = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
+        $groupb = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
+
+        // Add members to groups.
+        // Group A.
+        $this->getDataGenerator()->create_group_member(['groupid' => $groupa->id, 'userid' => $users['Only in group A']->id]);
+        $this->getDataGenerator()->create_group_member(['groupid' => $groupa->id, 'userid' => $users['In group A and B']->id]);
+
+        // Group B.
+        $this->getDataGenerator()->create_group_member(['groupid' => $groupb->id, 'userid' => $users['Only in group B']->id]);
+        $this->getDataGenerator()->create_group_member(['groupid' => $groupb->id, 'userid' => $users['In group A and B']->id]);
+
+        // Events with the same module name, instance and event type.
+        $events = [
+            [
+                'name' => 'Assignment 1 due date - Group A override',
+                'description' => '',
+                'format' => 1,
+                'courseid' => $course->id,
+                'groupid' => $groupa->id,
+                'userid' => 2,
+                'modulename' => 'assign',
+                'instance' => $assigninstance->id,
+                'eventtype' => 'due',
+                'type' => CALENDAR_EVENT_TYPE_ACTION,
+                'timestart' => 1,
+                'timeduration' => 0,
+                'visible' => 1,
+                'priority' => 1
+            ],
+            [
+                'name' => 'Assignment 1 due date - Group B override',
+                'description' => '',
+                'format' => 1,
+                'courseid' => $course->id,
+                'groupid' => $groupb->id,
+                'userid' => 2,
+                'modulename' => 'assign',
+                'instance' => $assigninstance->id,
+                'eventtype' => 'due',
+                'type' => CALENDAR_EVENT_TYPE_ACTION,
+                'timestart' => 1,
+                'timeduration' => 0,
+                'visible' => 1,
+                'priority' => 1
+            ],
+            [
+                'name' => 'Assignment 1 due date',
+                'description' => '',
+                'format' => 1,
+                'courseid' => $course->id,
+                'groupid' => 0,
+                'userid' => 2,
+                'modulename' => 'assign',
+                'instance' => $assigninstance->id,
+                'eventtype' => 'due',
+                'type' => CALENDAR_EVENT_TYPE_ACTION,
+                'timestart' => 1,
+                'timeduration' => 0,
+                'visible' => 1,
+                'priority' => null,
+            ]
+        ];
+
+        foreach ($events as $event) {
+            calendar_event::create($event, false);
+        }
+
+        $factory = new action_event_test_factory();
+        $strategy = new raw_event_retrieval_strategy();
+        $vault = new event_vault($factory, $strategy);
+
+        $usersevents = array_reduce(array_keys($users), function($carry, $description) use ($users, $vault) {
+            // NB: This is currently needed to make get_action_events_by_timesort return the right thing.
+            // It needs to be fixed, see MDL-58736.
+            $this->setUser($users[$description]);
+            return $carry + ['For user ' . lcfirst($description) => $vault->get_action_events_by_timesort($users[$description])];
+        }, []);
+
+        foreach ($usersevents as $description => $userevents) {
+            if ($description == 'For user in group A and B') {
+                // User is in both A and B, so they should see the override for both
+                // given that the priority is the same.
+                $this->assertCount(2, $userevents);
+                continue;
+            }
+
+            // Otherwise there should be only one assign event for each user.
+            $this->assertCount(1, $userevents);
+        }
+
+        // User in only group A should see the group A override.
+        $this->assertEquals('Assignment 1 due date - Group A override', $usersevents['For user only in group A'][0]->get_name());
+
+        // User in only group B should see the group B override.
+        $this->assertEquals('Assignment 1 due date - Group B override', $usersevents['For user only in group B'][0]->get_name());
+
+        // User in group A and B should see see both overrides since the priorities are the same.
+        $this->assertEquals('Assignment 1 due date - Group A override', $usersevents['For user in group A and B'][0]->get_name());
+        $this->assertEquals('Assignment 1 due date - Group B override', $usersevents['For user in group A and B'][1]->get_name());
+
+        // User in no groups should see the plain assignment event.
+        $this->assertEquals('Assignment 1 due date', $usersevents['For user in no groups'][0]->get_name());
+    }
+
     /**
      * Test that get_action_events_by_course returns events after the
      * provided timesort value.
@@ -902,4 +1042,146 @@ class core_calendar_event_vault_testcase extends advanced_testcase {
 
         $this->assertEmpty($events);
     }
+
+    /**
+     * There are subtle cases where the priority of an event override may be identical to another.
+     * For example, if you duplicate a group override, but make it apply to a different group. Now
+     * there are two overrides with exactly the same overridden dates. In this case the priority of
+     * both is 1.
+     *
+     * In this situation:
+     * - A user in group A should see only the A override
+     * - A user in group B should see only the B override
+     * - A user in both A and B should see both
+     */
+    public function test_get_action_events_by_course_with_identical_group_override_priorities() {
+        $this->resetAfterTest();
+        $this->setAdminuser();
+
+        $course = $this->getDataGenerator()->create_course();
+
+        // Create an assign instance.
+        $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+        $assigninstance = $assigngenerator->create_instance(['course' => $course->id]);
+
+        // Create users.
+        $users = [
+            'Only in group A'  => $this->getDataGenerator()->create_user(),
+            'Only in group B'  => $this->getDataGenerator()->create_user(),
+            'In group A and B' => $this->getDataGenerator()->create_user(),
+            'In no groups'     => $this->getDataGenerator()->create_user()
+        ];
+
+        // Enrol users.
+        foreach ($users as $user) {
+            $this->getDataGenerator()->enrol_user($user->id, $course->id);
+        }
+
+        // Create groups.
+        $groupa = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
+        $groupb = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
+
+        // Add members to groups.
+        // Group A.
+        $this->getDataGenerator()->create_group_member(['groupid' => $groupa->id, 'userid' => $users['Only in group A']->id]);
+        $this->getDataGenerator()->create_group_member(['groupid' => $groupa->id, 'userid' => $users['In group A and B']->id]);
+
+        // Group B.
+        $this->getDataGenerator()->create_group_member(['groupid' => $groupb->id, 'userid' => $users['Only in group B']->id]);
+        $this->getDataGenerator()->create_group_member(['groupid' => $groupb->id, 'userid' => $users['In group A and B']->id]);
+
+        // Events with the same module name, instance and event type.
+        $events = [
+            [
+                'name' => 'Assignment 1 due date - Group A override',
+                'description' => '',
+                'format' => 1,
+                'courseid' => $course->id,
+                'groupid' => $groupa->id,
+                'userid' => 2,
+                'modulename' => 'assign',
+                'instance' => $assigninstance->id,
+                'eventtype' => 'due',
+                'type' => CALENDAR_EVENT_TYPE_ACTION,
+                'timestart' => 1,
+                'timeduration' => 0,
+                'visible' => 1,
+                'priority' => 1
+            ],
+            [
+                'name' => 'Assignment 1 due date - Group B override',
+                'description' => '',
+                'format' => 1,
+                'courseid' => $course->id,
+                'groupid' => $groupb->id,
+                'userid' => 2,
+                'modulename' => 'assign',
+                'instance' => $assigninstance->id,
+                'eventtype' => 'due',
+                'type' => CALENDAR_EVENT_TYPE_ACTION,
+                'timestart' => 1,
+                'timeduration' => 0,
+                'visible' => 1,
+                'priority' => 1
+            ],
+            [
+                'name' => 'Assignment 1 due date',
+                'description' => '',
+                'format' => 1,
+                'courseid' => $course->id,
+                'groupid' => 0,
+                'userid' => 2,
+                'modulename' => 'assign',
+                'instance' => $assigninstance->id,
+                'eventtype' => 'due',
+                'type' => CALENDAR_EVENT_TYPE_ACTION,
+                'timestart' => 1,
+                'timeduration' => 0,
+                'visible' => 1,
+                'priority' => null,
+            ]
+        ];
+
+        foreach ($events as $event) {
+            calendar_event::create($event, false);
+        }
+
+        $factory = new action_event_test_factory();
+        $strategy = new raw_event_retrieval_strategy();
+        $vault = new event_vault($factory, $strategy);
+
+        $usersevents = array_reduce(array_keys($users), function($carry, $description) use ($users, $course, $vault) {
+            // NB: This is currently needed to make get_action_events_by_timesort return the right thing.
+            // It needs to be fixed, see MDL-58736.
+            $this->setUser($users[$description]);
+            return $carry + [
+                'For user ' . lcfirst($description) => $vault->get_action_events_by_course($users[$description], $course)
+            ];
+        }, []);
+
+        foreach ($usersevents as $description => $userevents) {
+            if ($description == 'For user in group A and B') {
+                // User is in both A and B, so they should see the override for both
+                // given that the priority is the same.
+                $this->assertCount(2, $userevents);
+                continue;
+            }
+
+            // Otherwise there should be only one assign event for each user.
+            $this->assertCount(1, $userevents);
+        }
+
+        // User in only group A should see the group A override.
+        $this->assertEquals('Assignment 1 due date - Group A override', $usersevents['For user only in group A'][0]->get_name());
+
+        // User in only group B should see the group B override.
+        $this->assertEquals('Assignment 1 due date - Group B override', $usersevents['For user only in group B'][0]->get_name());
+
+        // User in group A and B should see see both overrides since the priorities are the same.
+        $this->assertEquals('Assignment 1 due date - Group A override', $usersevents['For user in group A and B'][0]->get_name());
+        $this->assertEquals('Assignment 1 due date - Group B override', $usersevents['For user in group A and B'][1]->get_name());
+
+        // User in no groups should see the plain assignment event.
+        $this->assertEquals('Assignment 1 due date', $usersevents['For user in no groups'][0]->get_name());
+    }
 }
index a38681c..c537508 100644 (file)
@@ -676,7 +676,8 @@ class competency extends persistent {
     public static function share_same_framework(array $ids) {
         global $DB;
         list($insql, $params) = $DB->get_in_or_equal($ids);
-        return $DB->count_records_select(self::TABLE, "id $insql", $params, "COUNT(DISTINCT(competencyframeworkid))") == 1;
+        $sql = "SELECT COUNT('x') FROM (SELECT DISTINCT(competencyframeworkid) FROM {" . self::TABLE . "} WHERE id {$insql}) f";
+        return $DB->count_records_sql($sql, $params) == 1;
     }
 
     /**
index 469cb1c..70349fd 100644 (file)
@@ -166,7 +166,13 @@ class core_course_external extends external_api {
             $modinfosections = $modinfo->get_sections();
             foreach ($sections as $key => $section) {
 
-                if (!$section->uservisible) {
+                // Show the section if the user is permitted to access it, OR if it's not available
+                // but there is some available info text which explains the reason & should display.
+                $showsection = $section->uservisible ||
+                    ($section->visible && !$section->available &&
+                    !empty($section->availableinfo));
+
+                if (!$showsection) {
                     continue;
                 }
 
@@ -203,15 +209,21 @@ class core_course_external extends external_api {
                                 $context->id, 'course', 'section', $section->id, $options);
                 $sectionvalues['section'] = $section->section;
                 $sectionvalues['hiddenbynumsections'] = $section->section > $coursenumsections ? 1 : 0;
+                $sectionvalues['uservisible'] = $section->uservisible;
+                if (!empty($section->availableinfo)) {
+                    $sectionvalues['availabilityinfo'] = \core_availability\info::format_info($section->availableinfo, $course);
+                }
+
                 $sectioncontents = array();
 
-                //for each module of the section
-                if (empty($filters['excludemodules']) and !empty($modinfosections[$section->section])) {
+                // For each module of the section (if it is visible).
+                if ($section->uservisible and empty($filters['excludemodules']) and !empty($modinfosections[$section->section])) {
                     foreach ($modinfosections[$section->section] as $cmid) {
                         $cm = $modinfo->cms[$cmid];
 
-                        // stop here if the module is not visible to the user
-                        if (!$cm->uservisible) {
+                        // Stop here if the module is not visible to the user on the course main page:
+                        // The user can't access the module and the user can't view the module on the course page.
+                        if (!$cm->uservisible && !$cm->is_visible_on_course_page()) {
                             continue;
                         }
 
@@ -271,24 +283,29 @@ class core_course_external extends external_api {
                         //user that can view hidden module should know about the visibility
                         $module['visible'] = $cm->visible;
                         $module['visibleoncoursepage'] = $cm->visibleoncoursepage;
+                        $module['uservisible'] = $cm->uservisible;
+                        if (!empty($cm->availableinfo)) {
+                            $module['availabilityinfo'] = \core_availability\info::format_info($cm->availableinfo, $course);
+                        }
 
                         // Availability date (also send to user who can see hidden module).
                         if ($CFG->enableavailability && ($canviewhidden || $canupdatecourse)) {
                             $module['availability'] = $cm->availability;
                         }
 
-                        $baseurl = 'webservice/pluginfile.php';
-
-                        //call $modulename_export_contents
-                        //(each module callback take care about checking the capabilities)
+                        // Return contents only if the user can access to the module.
+                        if ($cm->uservisible) {
+                            $baseurl = 'webservice/pluginfile.php';
 
-                        require_once($CFG->dirroot . '/mod/' . $cm->modname . '/lib.php');
-                        $getcontentfunction = $cm->modname.'_export_contents';
-                        if (function_exists($getcontentfunction)) {
-                            if (empty($filters['excludecontents']) and $contents = $getcontentfunction($cm, $baseurl)) {
-                                $module['contents'] = $contents;
-                            } else {
-                                $module['contents'] = array();
+                            // Call $modulename_export_contents (each module callback take care about checking the capabilities).
+                            require_once($CFG->dirroot . '/mod/' . $cm->modname . '/lib.php');
+                            $getcontentfunction = $cm->modname.'_export_contents';
+                            if (function_exists($getcontentfunction)) {
+                                if (empty($filters['excludecontents']) and $contents = $getcontentfunction($cm, $baseurl)) {
+                                    $module['contents'] = $contents;
+                                } else {
+                                    $module['contents'] = array();
+                                }
                             }
                         }
 
@@ -334,6 +351,8 @@ class core_course_external extends external_api {
                     'section' => new external_value(PARAM_INT, 'Section number inside the course', VALUE_OPTIONAL),
                     'hiddenbynumsections' => new external_value(PARAM_INT, 'Whether is a section hidden in the course format',
                                                                 VALUE_OPTIONAL),
+                    'uservisible' => new external_value(PARAM_BOOL, 'Is the section visible for the user?', VALUE_OPTIONAL),
+                    'availabilityinfo' => new external_value(PARAM_RAW, 'Availability information.', VALUE_OPTIONAL),
                     'modules' => new external_multiple_structure(
                             new external_single_structure(
                                 array(
@@ -343,6 +362,10 @@ class core_course_external extends external_api {
                                     'instance' => new external_value(PARAM_INT, 'instance id', VALUE_OPTIONAL),
                                     'description' => new external_value(PARAM_RAW, 'activity description', VALUE_OPTIONAL),
                                     'visible' => new external_value(PARAM_INT, 'is the module visible', VALUE_OPTIONAL),
+                                    'uservisible' => new external_value(PARAM_BOOL, 'Is the module visible for the user?',
+                                        VALUE_OPTIONAL),
+                                    'availabilityinfo' => new external_value(PARAM_RAW, 'Availability information.',
+                                        VALUE_OPTIONAL),
                                     'visibleoncoursepage' => new external_value(PARAM_INT, 'is the module visible on course page',
                                         VALUE_OPTIONAL),
                                     'modicon' => new external_value(PARAM_URL, 'activity icon url'),
index 17f2851..4c55764 100644 (file)
@@ -700,7 +700,7 @@ abstract class format_base {
             if (isset($option['type'])) {
                 $mform->setType($optionname, $option['type']);
             }
-            if (is_null($mform->getElementValue($optionname)) && isset($option['default'])) {
+            if (is_null($mform->getElementValue('id')) && isset($option['default'])) {
                 $mform->setDefault($optionname, $option['default']);
             }
         }
index 4604abe..a397d24 100644 (file)
@@ -770,7 +770,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
      */
     private function prepare_get_course_contents_test() {
         global $DB;
-        $course  = self::getDataGenerator()->create_course(['numsections' => 2]);
+        $course  = self::getDataGenerator()->create_course(['numsections' => 3]);
         $forumdescription = 'This is the forum description';
         $forum = $this->getDataGenerator()->create_module('forum',
             array('course' => $course->id, 'intro' => $forumdescription),
@@ -785,9 +785,20 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $label = $this->getDataGenerator()->create_module('label', array('course' => $course->id,
             'intro' => $labeldescription));
         $labelcm = get_coursemodule_from_instance('label', $label->id);
-        $url = $this->getDataGenerator()->create_module('url', array('course' => $course->id,
-            'name' => 'URL: % & $ ../', 'section' => 2));
+        $tomorrow = time() + DAYSECS;
+        // Module with availability restrictions not met.
+        $url = $this->getDataGenerator()->create_module('url',
+            array('course' => $course->id, 'name' => 'URL: % & $ ../', 'section' => 2),
+            array('availability' => '{"op":"&","c":[{"type":"date","d":">=","t":' . $tomorrow . '}],"showc":[true]}'));
         $urlcm = get_coursemodule_from_instance('url', $url->id);
+        // Module for the last section.
+        $this->getDataGenerator()->create_module('url',
+            array('course' => $course->id, 'name' => 'URL for last section', 'section' => 3));
+        // Module for section 1 with availability restrictions met.
+        $yesterday = time() - DAYSECS;
+        $this->getDataGenerator()->create_module('url',
+            array('course' => $course->id, 'name' => 'URL restrictions met', 'section' => 1),
+            array('availability' => '{"op":"&","c":[{"type":"date","d":">=","t":'. $yesterday .'}],"showc":[true]}'));
 
         // Set the required capabilities by the external function.
         $context = context_course::instance($course->id);
@@ -797,6 +808,11 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
 
         $conditions = array('course' => $course->id, 'section' => 2);
         $DB->set_field('course_sections', 'summary', 'Text with iframe <iframe src="https://moodle.org"></iframe>', $conditions);
+
+        // Add date availability condition not met for last section.
+        $availability = '{"op":"&","c":[{"type":"date","d":">=","t":' . $tomorrow . '}],"showc":[true]}';
+        $DB->set_field('course_sections', 'availability', $availability,
+                array('course' => $course->id, 'section' => 3));
         rebuild_course_cache($course->id, true);
 
         return array($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm);
@@ -814,13 +830,9 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         // We need to execute the return values cleaning process to simulate the web service server.
         $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
 
-        // Check that forum and label descriptions are correctly returned.
-        $firstsection = array_shift($sections);
-        $lastsection = array_pop($sections);
-
         $modinfo = get_fast_modinfo($course);
         $testexecuted = 0;
-        foreach ($firstsection['modules'] as $module) {
+        foreach ($sections[0]['modules'] as $module) {
             if ($module['id'] == $forumcm->id and $module['modname'] == 'forum') {
                 $cm = $modinfo->cms[$forumcm->id];
                 $formattedtext = format_text($cm->content, FORMAT_HTML,
@@ -838,15 +850,24 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
             }
         }
         $this->assertEquals(2, $testexecuted);
-        $this->assertEquals(0, $firstsection['section']);
+        $this->assertEquals(0, $sections[0]['section']);
 
         // Check that the only return section has the 5 created modules.
-        $this->assertCount(4, $firstsection['modules']);
-        $this->assertCount(1, $lastsection['modules']);
-        $this->assertEquals(2, $lastsection['section']);
-        $this->assertContains('<iframe', $lastsection['summary']);
-        $this->assertContains('</iframe>', $lastsection['summary']);
-
+        $this->assertCount(4, $sections[0]['modules']);
+        $this->assertCount(1, $sections[1]['modules']);
+        $this->assertCount(1, $sections[2]['modules']);
+        $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
+        $this->assertNotEmpty($sections[3]['availabilityinfo']);
+        $this->assertEquals(1, $sections[1]['section']);
+        $this->assertEquals(2, $sections[2]['section']);
+        $this->assertEquals(3, $sections[3]['section']);
+        $this->assertContains('<iframe', $sections[2]['summary']);
+        $this->assertContains('</iframe>', $sections[2]['summary']);
+        // The module with the availability restriction met is returning contents.
+        $this->assertNotEmpty($sections[1]['modules'][0]['contents']);
+        // The module with the availability restriction not met is not returning contents.
+        $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]);
+        $this->assertNotEmpty($sections[2]['modules'][0]['availabilityinfo']);
         try {
             $sections = core_course_external::get_course_contents($course->id,
                                                                     array(array("name" => "invalid", "value" => 1)));
@@ -871,11 +892,8 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         // We need to execute the return values cleaning process to simulate the web service server.
         $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
 
-        $firstsection = array_shift($sections);
-        $lastsection = array_pop($sections);
-
-        $this->assertEmpty($firstsection['modules']);
-        $this->assertEmpty($lastsection['modules']);
+        $this->assertEmpty($sections[0]['modules']);
+        $this->assertEmpty($sections[1]['modules']);
     }
 
     /**
@@ -934,7 +952,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         // We need to execute the return values cleaning process to simulate the web service server.
         $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
 
-        $this->assertCount(3, $sections);
+        $this->assertCount(4, $sections);
         $this->assertCount(1, $sections[0]['modules']);
         $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
     }
@@ -976,7 +994,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         // We need to execute the return values cleaning process to simulate the web service server.
         $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
 
-        $this->assertCount(3, $sections);
+        $this->assertCount(4, $sections);
         $this->assertCount(1, $sections[0]['modules']);
         $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
     }
@@ -998,7 +1016,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         // We need to execute the return values cleaning process to simulate the web service server.
         $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
 
-        $this->assertCount(3, $sections);
+        $this->assertCount(4, $sections);
         $this->assertCount(1, $sections[0]['modules']);
         $this->assertEquals("page", $sections[0]['modules'][0]["modname"]);
         $this->assertEquals($pagecm->instance, $sections[0]['modules'][0]["instance"]);
index 3e05828..7f0cc37 100644 (file)
@@ -10,6 +10,9 @@ information provided here is intended especially for developers.
    - isexternalfile (if is a file reference to a external repository)
    - repositorytype (the repository name in case is a external file)
    Those fields are VALUE_OPTIONAL for backwards compatibility.
+ * External function core_course_external::get_course_contents now return the following fields for section and modules:
+   - uservisible (whether the section or module is visible by the user)
+   - availabilityinfo (availability information if the course or module has any access restriction set
 
 === 3.2 ===
 
index a07a735..c7bed10 100644 (file)
--- a/index.php
+++ b/index.php
@@ -39,6 +39,7 @@ if (!empty($CFG->defaulthomepage) && ($CFG->defaulthomepage == HOMEPAGE_MY) && o
 }
 $PAGE->set_url('/', $urlparams);
 $PAGE->set_course($SITE);
+$PAGE->set_pagelayout('frontpage');
 $PAGE->set_other_editing_capability('moodle/course:update');
 $PAGE->set_other_editing_capability('moodle/course:manageactivities');
 $PAGE->set_other_editing_capability('moodle/course:activityvisibility');
@@ -106,7 +107,6 @@ if (file_exists($CFG->dirroot.'/local/hub/lib.php') and get_config('local_hub',
 
 $PAGE->set_pagetype('site-index');
 $PAGE->set_docs_path('');
-$PAGE->set_pagelayout('frontpage');
 $editing = $PAGE->user_is_editing();
 $PAGE->set_title($SITE->fullname);
 $PAGE->set_heading($SITE->fullname);
index a77c26c..504bd2f 100644 (file)
@@ -85,9 +85,9 @@ $string['pathsunsecuredataroot'] = 'La ubicación de dataroot no es segura';
 $string['pathswrongadmindir'] = 'El directorio admin no existe';
 $string['phpextension'] = 'Extensión PHP {$a}';
 $string['phpversion'] = 'Versión PHP';
-$string['phpversionhelp'] = '<p>Moodle requiere una versión de PHP de al menos 5.6.5 (7.x tiene algunas limitaciones del motor).</p>
+$string['phpversionhelp'] = '<p>Moodle requiere una versión de PHP de al menos 5.6.5 o 7.1 (7.0.x tiene algunas limitaciones del motor).</p>
 <p>En este momento está ejecutando la versión {$a}</p>
-<p>Usted debe actualizar PHP o trasladarse a otro servidor con una versión más reciente de PHP!<br />';
+<p>¡Usted debe actualizar PHP o trasladarse a otro servidor con una versión más reciente de PHP!<br />';
 $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
 $string['welcomep20'] = 'Si está viendo esta página es porque ha podido instalar y ejecutar exitosamente el paquete <strong>{$a->packname} {$a->packversion}</strong> en su computadora. !Enhorabuena!';
 $string['welcomep30'] = 'Esta versión de <strong>{$a->installername}</strong> incluye las aplicaciones necesarias para que <strong>Moodle</strong> funcione en su computadora,  principalmente:';
index c289b3b..2dbef7e 100644 (file)
@@ -144,6 +144,7 @@ $string['recaptcha_link'] = 'auth/email';
 $string['security_question'] = 'Security question';
 $string['selfregistration'] = 'Self registration';
 $string['selfregistration_help'] = 'If an authentication plugin, such as email-based self-registration, is selected, then it enables potential users to register themselves and create accounts. This results in the possibility of spammers creating accounts in order to use forum posts, blog entries etc. for spam. To avoid this risk, self-registration should be disabled or limited by <em>Allowed email domains</em> setting.';
+$string['settingmigrationmismatch'] = 'Values mismatch detected while correcting the plugin setting names! The authentication plugin \'{$a->plugin}\' had the setting \'{$a->setting}\' configured to \'{$a->legacy}\' under the legacy name and to \'{$a->current}\' under the current name. The latter value has been set as the valid one but you should check and confirm that it is expected.';
 $string['sha1'] = 'SHA-1 hash';
 $string['showguestlogin'] = 'You can hide or show the guest login button on the login page.';
 $string['stdchangepassword'] = 'Use standard page for changing password';
index 2f5c5c6..9eb4b7a 100644 (file)
@@ -185,6 +185,7 @@ $string['viewmyentriesaboutcourse'] = 'View my entries about this course';
 $string['viewsiteentries'] = 'View all entries';
 $string['viewuserentries'] = 'View all entries by {$a}';
 $string['worldblogs'] = 'The world can read entries set to be world-accessible';
+$string['wrongexternalid'] = 'Wrong external blog id';
 $string['wrongpostid'] = 'Wrong blog post id';
 $string['page-blog-edit'] = 'Blog editing pages';
 $string['page-blog-index'] = 'Blog listing pages';
index c465c9a..d0d47f8 100644 (file)
Binary files a/lib/amd/build/templates.min.js and b/lib/amd/build/templates.min.js differ
index 0395f93..e2c2f05 100644 (file)
@@ -212,6 +212,10 @@ define(['core/mustache',
         var searchKey = this.currentThemeName + '/' + templateName;
         var template = templateCache[searchKey];
 
+        // The key might have been escaped by the JS Mustache engine which
+        // converts forward slashes to HTML entities. Let us undo that here.
+        key = key.replace(/&#x2F;/gi, '/');
+
         return iconSystem.renderIcon(key, component, text, template);
     };
 
index 21e046a..d08489d 100644 (file)
@@ -133,13 +133,14 @@ class manager {
             return $savemessage->id;
         }
 
-        $processors = get_message_processors(true);
-
         $failed = false;
         foreach ($processorlist as $procname) {
             // Let new messaging class add custom content based on the processor.
             $proceventdata = ($eventdata instanceof message) ? $eventdata->get_eventobject_for_processor($procname) : $eventdata;
-            if (!$processors[$procname]->object->send_message($proceventdata)) {
+            $stdproc = new \stdClass();
+            $stdproc->name = $procname;
+            $processor = \core_message\api::get_processed_processor_object($stdproc);
+            if (!$processor->object->send_message($proceventdata)) {
                 debugging('Error calling message processor ' . $procname);
                 $failed = true;
                 // Previously the $messageid = false here was overridden
index d5bc03d..528e511 100644 (file)
@@ -29,6 +29,7 @@ require_once($CFG->libdir . '/oauthlib.php');
 require_once($CFG->libdir . '/filelib.php');
 
 use moodle_url;
+use moodle_exception;
 use curl;
 use stdClass;
 
@@ -202,12 +203,6 @@ class client extends \oauth2_client {
             return false;
         }
 
-        if (isset($r->refresh_token)) {
-            $systemaccount->set('refreshtoken', $r->refresh_token);
-            $systemaccount->update();
-            $this->refreshtoken = $r->refresh_token;
-        }
-
         // Store the token an expiry time.
         $accesstoken = new stdClass;
         $accesstoken->token = $r->access_token;
@@ -215,14 +210,22 @@ class client extends \oauth2_client {
             // Expires 10 seconds before actual expiry.
             $accesstoken->expires = (time() + ($r->expires_in - 10));
         }
-        if (isset($r->scope)) {
-            $accesstoken->scope = $r->scope;
-        } else {
-            $accesstoken->scope = $this->scope;
-        }
+        $accesstoken->scope = $this->scope;
         // Also add the scopes.
         $this->store_token($accesstoken);
 
+        if (isset($r->refresh_token)) {
+            $userinfo = $this->get_userinfo();
+
+            if ($userinfo['email'] == $systemaccount->get('email')) {
+                $systemaccount->set('refreshtoken', $r->refresh_token);
+                $systemaccount->update();
+                $this->refreshtoken = $r->refresh_token;
+            } else {
+                throw new moodle_exception('Attempt to store refresh token for non-system user.');
+            }
+        }
+
         return true;
     }
 
index ef217a6..8601211 100644 (file)
@@ -171,7 +171,8 @@ class icon_system_fontawesome extends icon_system_font {
             'core:e/visual_aid' => 'fa-universal-access',
             'core:e/visual_blocks' => 'fa-audio-description',
             'theme:fp/add_file' => 'fa-file-o',
-            'theme:fp/alias' => 'fa-link',
+            'theme:fp/alias' => 'fa-share',
+            'theme:fp/alias_sm' => 'fa-share',
             'theme:fp/check' => 'fa-check',
             'theme:fp/create_folder' => 'fa-folder-o',
             'theme:fp/cross' => 'fa-remove',
index 3b9f6d8..d70037f 100644 (file)
@@ -2857,5 +2857,20 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2017042600.01);
     }
 
+    if ($oldversion < 2017050300.01) {
+        // MDL-58684:
+        // Remove all portfolio_tempdata records as these may contain serialized \file_system type objects, which are now unable to
+        // be unserialized because of changes to the file storage API made in MDL-46375. Portfolio now stores an id reference to
+        // files instead of the object.
+        // These records are normally removed after a successful export, however, can be left behind if the user abandons the
+        // export attempt (a stale record). Additionally, each stale record cannot be reused and is normally cleaned up by the cron
+        // task core\task\portfolio_cron_task. Since the cron task tries to unserialize them, and generates a warning, we'll remove
+        // all records here.
+        $DB->delete_records_select('portfolio_tempdata', 'id > ?', [0]);
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2017050300.01);
+    }
+
     return true;
 }
index 76579e1..d36fe17 100644 (file)
@@ -30,7 +30,13 @@ unzip 3.0.3.zip
 3) Move the source code directory into place
 mv LTI-Tool-Provider-Library-PHP-3.0.3/* lib/ltiprovider/
 
-4) Run unit tests on enrol_lti_testsuite
+4) Updates
+Check that the following pull request is included in the release.
+Then remove this step from this file.
+https://github.com/IMSGlobal/LTI-Tool-Provider-Library-PHP/pull/13
+If not, apply manually.
+
+5) Run unit tests on enrol_lti_testsuite
 
 Upgrading Notes
 ---------------
index 3c043bb..bd20302 100644 (file)
@@ -132,7 +132,6 @@ class HTTPMessage
             curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
             curl_setopt($ch, CURLINFO_HEADER_OUT, true);
             curl_setopt($ch, CURLOPT_HEADER, true);
-            curl_setopt($ch, CURLOPT_SSLVERSION,3);
             $chResp = curl_exec($ch);
             $this->ok = $chResp !== false;
             if ($this->ok) {
index 5347f17..d575cbc 100644 (file)
@@ -181,8 +181,19 @@ function message_send($eventdata) {
         }
     }
 
-    // Fetch enabled processors
-    $processors = get_message_processors(true);
+    // Fetch enabled processors.
+    // If we are dealing with a message some processors may want to handle it regardless of user and site settings.
+    if (empty($savemessage->notification)) {
+        $processors = array_filter(get_message_processors(false), function($processor) {
+            if ($processor->object->force_process_messages()) {
+                return true;
+            }
+
+            return ($processor->enabled && $processor->configured);
+        });
+    } else {
+        $processors = get_message_processors(true);
+    }
 
     // Preset variables
     $processorlist = array();
@@ -215,7 +226,9 @@ function message_send($eventdata) {
         }
 
         // Populate the list of processors we will be using
-        if ($permitted == 'forced' && $userisconfigured) {
+        if (empty($savemessage->notification) && $processor->object->force_process_messages()) {
+            $processorlist[] = $processor->name;
+        } else if ($permitted == 'forced' && $userisconfigured) {
             // An admin is forcing users to use this message processor. Use this processor unconditionally.
             $processorlist[] = $processor->name;
         } else if ($permitted == 'permitted' && $userisconfigured && !$eventdata->userto->emailstop) {
index 1fe80f5..8f3e795 100644 (file)
@@ -394,7 +394,7 @@ abstract class oauth2_client extends curl {
     /** @var stdClass $accesstoken access token object */
     private $accesstoken = null;
     /** @var string $refreshtoken refresh token string */
-    private $refreshtoken = '';
+    protected $refreshtoken = '';
     /** @var string $mocknextresponse string */
     private $mocknextresponse = '';
     /** @var array $upgradedcodes list of upgraded codes in this request */
index 34c40c8..18572f1 100644 (file)
@@ -637,6 +637,11 @@ abstract class portfolio_plugin_base {
      * @return array|string|int|boolean value of the field
      */
     public final function get($field) {
+        // This is a legacy change to the way files are get/set.
+        // We now only set $this->file to the id of the \stored_file. So, we need to convert that id back to a \stored_file here.
+        if ($field === 'file') {
+            return $this->get_file();
+        }
         if (property_exists($this, $field)) {
             return $this->{$field};
         }
@@ -654,6 +659,12 @@ abstract class portfolio_plugin_base {
      * @return bool
      */
     public final function set($field, $value) {
+        // This is a legacy change to the way files are get/set.
+        // Make sure we never save the \stored_file object. Instead, use the id from $file->get_id() - set_file() does this for us.
+        if ($field === 'file') {
+            $this->set_file($value);
+            return true;
+        }
         if (property_exists($this, $field)) {
             $this->{$field} =& $value;
             $this->dirty = true;
@@ -776,7 +787,7 @@ abstract class portfolio_plugin_push_base extends portfolio_plugin_base {
  */
 abstract class portfolio_plugin_pull_base extends portfolio_plugin_base {
 
-    /** @var stdclass single file */
+    /** @var int $file the id of a single file */
     protected $file;
 
     /**
@@ -827,4 +838,36 @@ abstract class portfolio_plugin_pull_base extends portfolio_plugin_base {
         $this->get('exporter')->log_transfer();
     }
 
+    /**
+     * Sets the $file instance var to the id of the supplied \stored_file.
+
+     * This helper allows the $this->get('file') call to return a \stored_file, but means that we only ever record an id reference
+     * in the $file instance var.
+     *
+     * @param \stored_file $file The stored_file instance.
+     * @return void
+     */
+    protected function set_file(\stored_file $file) {
+        $fileid = $file->get_id();
+        if (empty($fileid)) {
+            debugging('stored_file->id should not be empty');
+            $this->file = null;
+        } else {
+            $this->file = $fileid;
+        }
+    }
+
+    /**
+     * Gets the \stored_file object from the file id in the $file instance var.
+     *
+     * @return stored_file|null the \stored_file object if it exists, null otherwise.
+     */
+    protected function get_file() {
+        if (!$this->file) {
+            return null;
+        }
+        // The get_file_by_id call can return false, so normalise to null.
+        $file = get_file_storage()->get_file_by_id($this->file);
+        return ($file) ? $file : null;
+    }
 }
index 2e98623..d3395a7 100644 (file)
@@ -165,7 +165,7 @@ class core_message_testcase extends advanced_testcase {
         $emails = $sink->get_messages();
         $this->assertCount(1, $emails);
         $email = reset($emails);
-        $recordexists = $DB->record_exists('message_read', array('id' => $messageid));
+        $recordexists = $DB->record_exists('message', array('id' => $messageid));
         $this->assertSame(true, $recordexists);
         $this->assertSame($user1->email, $email->from);
         $this->assertSame($user2->email, $email->to);
@@ -205,7 +205,7 @@ class core_message_testcase extends advanced_testcase {
         $emails = $sink->get_messages();
         $this->assertCount(1, $emails);
         $email = reset($emails);
-        $recordexists = $DB->record_exists('message_read', array('id' => $messageid));
+        $recordexists = $DB->record_exists('message', array('id' => $messageid));
         $this->assertSame(true, $recordexists);
         $this->assertSame($user1->email, $email->from);
         $this->assertSame($user2->email, $email->to);
index 1e51f3e..051f61c 100644 (file)
@@ -400,6 +400,7 @@ class core_messagelib_testcase extends advanced_testcase {
 
         $eventsink = $this->redirectEvents();
 
+        // Will always use the pop-up processor.
         set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'none', $user2);
 
         $message = new \core\message\message();
@@ -484,6 +485,7 @@ class core_messagelib_testcase extends advanced_testcase {
         $this->assertInstanceOf('\core\event\message_viewed', $events[1]);
         $eventsink->clear();
 
+        // Will always use the pop-up processor.
         set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user2);
 
         $message = new \core\message\message();
@@ -515,6 +517,7 @@ class core_messagelib_testcase extends advanced_testcase {
         $eventsink->clear();
         $user2->emailstop = '0';
 
+        // Will always use the pop-up processor.
         set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user2);
 
         $message = new \core\message\message();
@@ -534,19 +537,18 @@ class core_messagelib_testcase extends advanced_testcase {
         $emails = $sink->get_messages();
         $this->assertCount(1, $emails);
         $email = reset($emails);
-        $savedmessage = $DB->get_record('message_read', array('id' => $messageid), '*', MUST_EXIST);
+        $savedmessage = $DB->get_record('message', array('id' => $messageid), '*', MUST_EXIST);
         $this->assertSame($user1->email, $email->from);
         $this->assertSame($user2->email, $email->to);
         $this->assertSame($message->subject, $email->subject);
         $this->assertNotEmpty($email->header);
         $this->assertNotEmpty($email->body);
         $sink->clear();
-        $this->assertFalse($DB->record_exists('message', array()));
+        $this->assertFalse($DB->record_exists('message_read', array()));
         $DB->delete_records('message_read', array());
         $events = $eventsink->get_events();
-        $this->assertCount(2, $events);
+        $this->assertCount(1, $events);
         $this->assertInstanceOf('\core\event\message_sent', $events[0]);
-        $this->assertInstanceOf('\core\event\message_viewed', $events[1]);
         $eventsink->clear();
 
         set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email,popup', $user2);
@@ -618,6 +620,7 @@ class core_messagelib_testcase extends advanced_testcase {
         }
         $transaction->allow_commit();
 
+        // Will always use the pop-up processor.
         set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'none', $user2);
 
         $message = new \core\message\message();
@@ -643,13 +646,14 @@ class core_messagelib_testcase extends advanced_testcase {
         $this->assertFalse($DB->record_exists('message_read', array()));
         $DB->delete_records('message', array());
         $events = $eventsink->get_events();
-        $this->assertCount(1, $events);
-        $this->assertInstanceOf('\core\event\message_sent', $events[0]);
+        $this->assertCount(0, $events);
         $eventsink->clear();
         $transaction->allow_commit();
         $events = $eventsink->get_events();
-        $this->assertCount(0, $events);
+        $this->assertCount(1, $events);
+        $this->assertInstanceOf('\core\event\message_sent', $events[0]);
 
+        // Will always use the pop-up processor.
         set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user2);
 
         $message = new \core\message\message();
@@ -674,28 +678,26 @@ class core_messagelib_testcase extends advanced_testcase {
         $sink->clear();
         $this->assertFalse($DB->record_exists('message_read', array()));
         $events = $eventsink->get_events();
-        $this->assertCount(0, $events);
+        $this->assertCount(1, $events);
+        $this->assertInstanceOf('\core\event\message_sent', $events[0]);
         $transaction->allow_commit();
         $events = $eventsink->get_events();
         $this->assertCount(2, $events);
-        $this->assertInstanceOf('\core\event\message_sent', $events[0]);
-        $this->assertInstanceOf('\core\event\message_viewed', $events[1]);
+        $this->assertInstanceOf('\core\event\message_sent', $events[1]);
         $eventsink->clear();
 
         $transaction = $DB->start_delegated_transaction();
         message_send($message);
         message_send($message);
-        $this->assertCount(2, $DB->get_records('message'));
-        $this->assertCount(1, $DB->get_records('message_read'));
+        $this->assertCount(3, $DB->get_records('message'));
+        $this->assertFalse($DB->record_exists('message_read', array()));
         $events = $eventsink->get_events();
         $this->assertCount(0, $events);
         $transaction->allow_commit();
         $events = $eventsink->get_events();
-        $this->assertCount(4, $events);
+        $this->assertCount(2, $events);
         $this->assertInstanceOf('\core\event\message_sent', $events[0]);
-        $this->assertInstanceOf('\core\event\message_viewed', $events[1]);
-        $this->assertInstanceOf('\core\event\message_sent', $events[2]);
-        $this->assertInstanceOf('\core\event\message_viewed', $events[3]);
+        $this->assertInstanceOf('\core\event\message_sent', $events[1]);
         $eventsink->clear();
         $DB->delete_records('message', array());
         $DB->delete_records('message_read', array());
@@ -717,10 +719,11 @@ class core_messagelib_testcase extends advanced_testcase {
         $this->assertCount(0, $DB->get_records('message'));
         $this->assertCount(0, $DB->get_records('message_read'));
         message_send($message);
-        $this->assertCount(0, $DB->get_records('message'));
-        $this->assertCount(1, $DB->get_records('message_read'));
+        $this->assertCount(1, $DB->get_records('message'));
+        $this->assertCount(0, $DB->get_records('message_read'));
         $events = $eventsink->get_events();
-        $this->assertCount(2, $events);
+        $this->assertCount(1, $events);
+        $this->assertInstanceOf('\core\event\message_sent', $events[0]);
         $sink->clear();
         $DB->delete_records('message_read', array());
     }
index 275323b..dd684ad 100644 (file)
@@ -925,4 +925,55 @@ class core_upgradelib_testcase extends advanced_testcase {
         $this->assertEquals(count($blockinstances), $DB->count_records('block_positions', ['subpage' => $page1->id, 'pagetype' => 'my-index', 'contextid' => $context1->id]));
         $this->assertEquals(0, $DB->count_records('block_positions', ['subpage' => $page2->id, 'pagetype' => 'my-index']));
     }
+
+    /**
+     * Test the conversion of auth plugin settings names.
+     */
+    public function test_upgrade_fix_config_auth_plugin_names() {
+        $this->resetAfterTest();
+
+        // Let the plugin auth_foo use legacy format only.
+        set_config('name1', 'val1', 'auth/foo');
+        set_config('name2', 'val2', 'auth/foo');
+
+        // Let the plugin auth_bar use new format only.
+        set_config('name1', 'val1', 'auth_bar');
+        set_config('name2', 'val2', 'auth_bar');
+
+        // Let the plugin auth_baz use a mix of legacy and new format, with no conflicts.
+        set_config('name1', 'val1', 'auth_baz');
+        set_config('name1', 'val1', 'auth/baz');
+        set_config('name2', 'val2', 'auth/baz');
+        set_config('name3', 'val3', 'auth_baz');
+
+        // Let the plugin auth_qux use a mix of legacy and new format, with conflicts.
+        set_config('name1', 'val1', 'auth_qux');
+        set_config('name1', 'val2', 'auth/qux');
+
+        // Execute the migration.
+        upgrade_fix_config_auth_plugin_names('foo');
+        upgrade_fix_config_auth_plugin_names('bar');
+        upgrade_fix_config_auth_plugin_names('baz');
+        upgrade_fix_config_auth_plugin_names('qux');
+
+        // Assert that legacy settings are gone and no new were introduced.
+        $this->assertEmpty((array) get_config('auth/foo'));
+        $this->assertEmpty((array) get_config('auth/bar'));
+        $this->assertEmpty((array) get_config('auth/baz'));
+        $this->assertEmpty((array) get_config('auth/qux'));
+
+        // Assert values were simply kept where there was no conflict.
+        $this->assertSame('val1', get_config('auth_foo', 'name1'));
+        $this->assertSame('val2', get_config('auth_foo', 'name2'));
+
+        $this->assertSame('val1', get_config('auth_bar', 'name1'));
+        $this->assertSame('val2', get_config('auth_bar', 'name2'));
+
+        $this->assertSame('val1', get_config('auth_baz', 'name1'));
+        $this->assertSame('val2', get_config('auth_baz', 'name2'));
+        $this->assertSame('val3', get_config('auth_baz', 'name3'));
+
+        // Assert the new format took precedence in case of conflict.
+        $this->assertSame('val1', get_config('auth_qux', 'name1'));
+    }
 }
index f23f9e7..79ee13b 100644 (file)
@@ -3,6 +3,8 @@ information provided here is intended especially for developers.
 
 === 3.3 ===
 
+* Behat compatibility changes are now being documented at
+  https://docs.moodle.org/dev/Acceptance_testing/Compatibility_changes
 * PHPUnit's bootstrap has been changed to use HTTPS wwwroot (https://www.example.com/moodle) from previous HTTP version. Any
   existing test expecting the old HTTP URLs will need to be switched to the new HTTPS value (reference: MDL-54901).
 * The information returned by the idp list has changed. This is usually only rendered by the login page and login block.
index 2cdd6eb..12c7a6e 100644 (file)
@@ -2516,3 +2516,56 @@ function check_libcurl_version(environment_results $result) {
 
     return null;
 }
+
+/**
+ * Fix how auth plugins are called in the 'config_plugins' table.
+ *
+ * For legacy reasons, the auth plugins did not always use their frankenstyle
+ * component name in the 'plugin' column of the 'config_plugins' table. This is
+ * a helper function to correctly migrate the legacy settings into the expected
+ * and consistent way.
+ *
+ * @param string $plugin the auth plugin name such as 'cas', 'manual' or 'mnet'
+ */
+function upgrade_fix_config_auth_plugin_names($plugin) {
+    global $CFG, $DB, $OUTPUT;
+
+    $legacy = (array) get_config('auth/'.$plugin);
+    $current = (array) get_config('auth_'.$plugin);
+
+    // I don't want to rely on array_merge() and friends here just in case
+    // there was some crazy setting with a numerical name.
+
+    if ($legacy) {
+        $new = $legacy;
+    } else {
+        $new = [];
+    }
+
+    if ($current) {
+        foreach ($current as $name => $value) {
+            if (isset($legacy[$name]) && ($legacy[$name] !== $value)) {
+                // No need to pollute the output during unit tests.
+                if (!empty($CFG->upgraderunning)) {
+                    $message = get_string('settingmigrationmismatch', 'core_auth', [
+                        'plugin' => 'auth_'.$plugin,
+                        'setting' => s($name),
+                        'legacy' => s($legacy[$name]),
+                        'current' => s($value),
+                    ]);
+                    echo $OUTPUT->notification($message, \core\output\notification::NOTIFY_ERROR);
+
+                    upgrade_log(UPGRADE_LOG_NOTICE, 'auth_'.$plugin, 'Setting values mismatch detected',
+                        'SETTING: '.$name. ' LEGACY: '.$legacy[$name].' CURRENT: '.$value);
+                }
+            }
+
+            $new[$name] = $value;
+        }
+    }
+
+    foreach ($new as $name => $value) {
+        set_config($name, $value, 'auth_'.$plugin);
+        unset_config($name, 'auth/'.$plugin);
+    }
+}
index cb81b1f..ce7fd76 100644 (file)
@@ -112,6 +112,15 @@ abstract class message_output {
     public function has_message_preferences() {
         return true;
     }
+
+    /**
+     * Determines if this processor should process a message regardless of user preferences or site settings.
+     *
+     * @return bool
+     */
+    public function force_process_messages() {
+        return false;
+    }
 }
 
 
index 0091cec..67409cc 100644 (file)
@@ -128,4 +128,15 @@ class message_output_popup extends message_output {
             $DB->update_record('message_popup', $record);
         }
     }
+
+    /**
+     * Determines if this processor should process a message regardless of user preferences or site settings.
+     *
+     * @return bool
+     */
+    public function force_process_messages() {
+        global $CFG;
+
+        return !empty($CFG->messaging);
+    }
 }
index 80f98e5..85eeba7 100644 (file)
@@ -33,6 +33,7 @@ defined('MOODLE_INTERNAL') || die();
  * @param string $filearea
  * @param array $args
  * @param bool $forcedownload
+ * @param array $options - List of options affecting file serving.
  * @return bool false if file not found, does not return if found - just send the file
  */
 function assignfeedback_editpdf_pluginfile($course,
@@ -40,7 +41,8 @@ function assignfeedback_editpdf_pluginfile($course,
                                            context $context,
                                            $filearea,
                                            $args,
-                                           $forcedownload) {
+                                           $forcedownload,
+                                           array $options=array()) {
     global $USER, $DB, $CFG;
 
     if ($context->contextlevel == CONTEXT_MODULE) {
@@ -72,7 +74,7 @@ function assignfeedback_editpdf_pluginfile($course,
             return false;
         }
         // Download MUST be forced - security!
-        send_stored_file($file, 0, 0, true);// Check if we want to retrieve the stamps.
+        send_stored_file($file, 0, 0, true, $options);// Check if we want to retrieve the stamps.
     }
 
 }
index 5b74c7d..29427c4 100644 (file)
@@ -32,6 +32,7 @@ defined('MOODLE_INTERNAL') || die();
  * @param string $filearea
  * @param array $args
  * @param bool $forcedownload
+ * @param array $options - List of options affecting file serving.
  * @return bool false if file not found, does not return if found - just send the file
  */
 function assignfeedback_file_pluginfile($course,
@@ -39,7 +40,8 @@ function assignfeedback_file_pluginfile($course,
                                         context $context,
                                         $filearea,
                                         $args,
-                                        $forcedownload) {
+                                        $forcedownload,
+                                        array $options=array()) {
     global $USER, $DB;
 
     if ($context->contextlevel != CONTEXT_MODULE) {
@@ -73,5 +75,5 @@ function assignfeedback_file_pluginfile($course,
         return false;
     }
     // Download MUST be forced - security!
-    send_stored_file($file, 0, 0, true);
+    send_stored_file($file, 0, 0, true, $options);
 }
index 27c4ae8..f32def9 100644 (file)
@@ -885,74 +885,47 @@ class assign {
      * Returns user override
      *
      * Algorithm:  For each assign setting, if there is a matching user-specific override,
-     *   then use that otherwise, if there are group-specific overrides, return the most
-     *   lenient combination of them.  If neither applies, leave the assign setting unchanged.
+     *   then use that otherwise, if there are group-specific overrides, use the one with the
+     *   lowest sort order. If neither applies, leave the assign setting unchanged.
      *
      * @param int $userid The userid.
-     * @return override  if exist
+     * @return stdClass The override
      */
     public function override_exists($userid) {
         global $DB;
 
-        // Check for user override.
-        $override = $DB->get_record('assign_overrides', array('assignid' => $this->get_instance()->id, 'userid' => $userid));
+        // Gets an assoc array containing the keys for defined user overrides only.
+        $getuseroverride = function($userid) use ($DB) {
+            $useroverride = $DB->get_record('assign_overrides', ['assignid' => $this->get_instance()->id, 'userid' => $userid]);
+            return $useroverride ? get_object_vars($useroverride) : [];
+        };
 
-        if (!$override) {
-            $override = new stdClass();
-            $override->duedate = null;
-            $override->cutoffdate = null;
-            $override->allowsubmissionsfromdate = null;
-        }
+        // Gets an assoc array containing the keys for defined group overrides only.
+        $getgroupoverride = function($userid) use ($DB) {
+            $groupings = groups_get_user_groups($this->get_instance()->course, $userid);
 
-        // Check for group overrides.
-        $groupings = groups_get_user_groups($this->get_instance()->course, $userid);
+            if (empty($groupings[0])) {
+                return [];
+            }
 
-        if (!empty($groupings[0])) {
             // Select all overrides that apply to the User's groups.
             list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
             $sql = "SELECT * FROM {assign_overrides}
-                    WHERE groupid $extra AND assignid = ?";
+                    WHERE groupid $extra AND assignid = ? ORDER BY sortorder ASC";
             $params[] = $this->get_instance()->id;
-            $records = $DB->get_records_sql($sql, $params);
-
-            // Combine the overrides.
-            $duedates = array();
-            $cutoffdates = array();
-            $allowsubmissionsfromdates = array();
-
-            foreach ($records as $gpoverride) {
-                if (isset($gpoverride->duedate)) {
-                    $duedates[] = $gpoverride->duedate;
-                }
-                if (isset($gpoverride->cutoffdate)) {
-                    $cutoffdates[] = $gpoverride->cutoffdate;
-                }
-                if (isset($gpoverride->allowsubmissionsfromdate)) {
-                    $allowsubmissionsfromdates[] = $gpoverride->allowsubmissionsfromdate;
-                }
-            }
-            // If there is a user override for a setting, ignore the group override.
-            if (is_null($override->allowsubmissionsfromdate) && count($allowsubmissionsfromdates)) {
-                $override->allowsubmissionsfromdate = min($allowsubmissionsfromdates);
-            }
-            if (is_null($override->cutoffdate) && count($cutoffdates)) {
-                if (in_array(0, $cutoffdates)) {
-                    $override->cutoffdate = 0;
-                } else {
-                    $override->cutoffdate = max($cutoffdates);
-                }
-            }
-            if (is_null($override->duedate) && count($duedates)) {
-                if (in_array(0, $duedates)) {
-                    $override->duedate = 0;
-                } else {
-                    $override->duedate = max($duedates);
-                }
-            }
-
-        }
-
-        return $override;
+            $groupoverride = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE);
+
+            return $groupoverride ? get_object_vars($groupoverride) : [];
+        };
+
+        // Later arguments clobber earlier ones with array_merge. The two helper functions
+        // return arrays containing keys for only the defined overrides. So we get the
+        // desired behaviour as per the algorithm.
+        return (object)array_merge(
+            ['duedate' => null, 'cutoffdate' => null, 'allowsubmissionsfromdate' => null],
+            $getgroupoverride($userid),
+            $getuseroverride($userid)
+        );
     }
 
     /**
index 0c8b1e0..2098381 100644 (file)
@@ -32,6 +32,7 @@ defined('MOODLE_INTERNAL') || die();
  * @param string $filearea
  * @param array $args
  * @param bool $forcedownload
+ * @param array $options - List of options affecting file serving.
  * @return bool false if file not found, does not return if found - just send the file
  */
 function assignsubmission_file_pluginfile($course,
@@ -39,7 +40,8 @@ function assignsubmission_file_pluginfile($course,
                                           context $context,
                                           $filearea,
                                           $args,
-                                          $forcedownload) {
+                                          $forcedownload,
+                                          array $options=array()) {
     global $DB, $CFG;
 
     if ($context->contextlevel != CONTEXT_MODULE) {
@@ -83,5 +85,5 @@ function assignsubmission_file_pluginfile($course,
     }
 
     // Download MUST be forced - security!
-    send_stored_file($file, 0, 0, true);
+    send_stored_file($file, 0, 0, true, $options);
 }
index b0816c3..e2fe32f 100644 (file)
@@ -678,15 +678,10 @@ class assign_submission_file extends assign_submission_plugin {
     private function get_nonexistent_file_types($types) {
         $nonexistent = [];
         foreach ($this->get_typesets($types) as $type) {
-            $coretypes = core_filetypes::get_types();
-            // We can allow any extension, but validate groups & mimetypes.
-            if (strpos($type, '.') === false) {
-                // If there's no dot, check if it's a group.
-                $extensions = file_get_typegroup('extension', [$type]);
-                if (empty($extensions)) {
-                    // If there's no extensions under that group, it doesn't exist.
-                    $nonexistent[$type] = true;
-                }
+            // If there's no extensions under that group, it doesn't exist.
+            $extensions = file_get_typegroup('extension', [$type]);
+            if (empty($extensions)) {
+                $nonexistent[$type] = true;
             }
         }
         return array_keys($nonexistent);
index 4990dae..34f1c07 100644 (file)
@@ -30,7 +30,7 @@ Feature: In an assignment, limit submittable file types
     And I navigate to "Edit settings" in current page administration
     When I set the field "Accepted file types" to "image/png;doesntexist;.anything;unreal/mimetype;nodot"
     And I press "Save and display"
-    And I should see "The following file types were not recognised: doesntexist unreal/mimetype nodot"
+    And I should see "The following file types were not recognised: doesntexist .anything unreal/mimetype nodot"
     And I set the field "Accepted file types" to "image/png;spreadsheet"
     And I press "Save and display"
     And I navigate to "Edit settings" in current page administration
index c8b1082..618a787 100644 (file)
@@ -144,13 +144,13 @@ class assignsubmission_file_locallib_testcase extends advanced_testcase {
      */
     public function get_nonexistent_file_types_provider() {
         return [
-            'Nonexistent extensions are allowed' => [
+            'Nonexistent extensions are not allowed' => [
                 'filetypes' => '.rat',
-                'expected' => []
+                'expected' => ['.rat']
             ],
-            'Multiple nonexistent extensions are allowed' => [
+            'Multiple nonexistent extensions are not allowed' => [
                 'filetypes' => '.ricefield .rat',
-                'expected' => []
+                'expected' => ['.ricefield', '.rat']
             ],
             'Existent extension is allowed' => [
                 'filetypes' => '.xml',
index 4b9325a..ef82532 100644 (file)
@@ -32,9 +32,16 @@ defined('MOODLE_INTERNAL') || die();
  * @param string $filearea
  * @param array $args
  * @param bool $forcedownload
+ * @param array $options - List of options affecting file serving.
  * @return bool false if file not found, does not return if found - just send the file
  */
-function assignsubmission_onlinetext_pluginfile($course, $cm, context $context, $filearea, $args, $forcedownload) {
+function assignsubmission_onlinetext_pluginfile($course,
+                                                $cm,
+                                                context $context,
+                                                $filearea,
+                                                $args,
+                                                $forcedownload,
+                                                array $options=array()) {
     global $DB, $CFG;
 
     if ($context->contextlevel != CONTEXT_MODULE) {
@@ -78,5 +85,5 @@ function assignsubmission_onlinetext_pluginfile($course, $cm, context $context,
     }
 
     // Download MUST be forced - security!
-    send_stored_file($file, 0, 0, true);
+    send_stored_file($file, 0, 0, true, $options);
 }
index fac2273..193f7fa 100644 (file)
@@ -2680,6 +2680,119 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
         $this->assertEquals(2, $usingfilearea);
     }
 
+    /**
+     * Test override exists
+     *
+     * This function needs to obey the group override logic as per the assign grading table and
+     * the overview block.
+     */
+    public function test_override_exists() {
+        global $DB;
+
+        $this->setAdminUser();
+
+        $course = $this->getDataGenerator()->create_course();
+
+        // Create an assign instance.
+        $assign = $this->create_instance(['course' => $course]);
+        $assigninstance = $assign->get_instance();
+
+        // Create users.
+        $users = [
+            'Only in group A'                     => $this->getDataGenerator()->create_user(),
+            'Only in group B'                     => $this->getDataGenerator()->create_user(),
+            'In group A and B (no user override)' => $this->getDataGenerator()->create_user(),
+            'In group A and B (user override)'    => $this->getDataGenerator()->create_user(),
+            'In no groups'                        => $this->getDataGenerator()->create_user()
+        ];
+
+        // Enrol users.
+        foreach ($users as $user) {
+            $this->getDataGenerator()->enrol_user($user->id, $course->id);
+        }
+
+        // Create groups.
+        $groupa = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
+        $groupb = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
+
+        // Add members to groups.
+        // Group A.
+        $this->getDataGenerator()->create_group_member(
+            ['groupid' => $groupa->id, 'userid' => $users['Only in group A']->id]);
+        $this->getDataGenerator()->create_group_member(
+            ['groupid' => $groupa->id, 'userid' => $users['In group A and B (no user override)']->id]);
+        $this->getDataGenerator()->create_group_member(
+            ['groupid' => $groupa->id, 'userid' => $users['In group A and B (user override)']->id]);
+
+        // Group B.
+        $this->getDataGenerator()->create_group_member(
+            ['groupid' => $groupb->id, 'userid' => $users['Only in group B']->id]);
+        $this->getDataGenerator()->create_group_member(
+            ['groupid' => $groupb->id, 'userid' => $users['In group A and B (no user override)']->id]);
+        $this->getDataGenerator()->create_group_member(
+            ['groupid' => $groupb->id, 'userid' => $users['In group A and B (user override)']->id]);
+
+        // Overrides for each of the groups, and a user override.
+        $overrides = [
+            // Override for group A, highest priority (numerically lowest sortorder).
+            [
+                'assignid' => $assigninstance->id,
+                'groupid' => $groupa->id,
+                'userid' => null,
+                'sortorder' => 1,
+                'allowsubmissionsfromdate' => 1,
+                'duedate' => 2,
+                'cutoffdate' => 3
+            ],
+            // Override for group B, lower priority (numerically higher sortorder).
+            [
+                'assignid' => $assigninstance->id,
+                'groupid' => $groupb->id,
+                'userid' => null,
+                'sortorder' => 2,
+                'allowsubmissionsfromdate' => 5,
+                'duedate' => 6,
+                'cutoffdate' => 6
+            ],
+            // User override.
+            [
+                'assignid' => $assigninstance->id,
+                'groupid' => null,
+                'userid' => $users['In group A and B (user override)']->id,
+                'sortorder' => null,
+                'allowsubmissionsfromdate' => 7,
+                'duedate' => 8,
+                'cutoffdate' => 9
+            ],
+        ];
+
+        // Kinda hacky, need to add the ID to the overrides in the above array
+        // for later.
+        foreach ($overrides as &$override) {
+            $override['id'] = $DB->insert_record('assign_overrides', $override);
+        }
+
+        $returnedoverrides = array_reduce(array_keys($users), function($carry, $description) use ($users, $assign) {
+            return $carry + ['For user ' . lcfirst($description) => $assign->override_exists($users[$description]->id)];
+        }, []);
+
+        // Test we get back the correct override from override_exists (== checks all object members match).
+        // User only in group A should see the group A override.
+        $this->assertTrue($returnedoverrides['For user only in group A'] == (object)$overrides[0]);
+        // User only in group B should see the group B override.
+        $this->assertTrue($returnedoverrides['For user only in group B'] == (object)$overrides[1]);
+        // User in group A and B, with no user override should see the group A override
+        // as it has higher priority (numerically lower sortorder).
+        $this->assertTrue($returnedoverrides['For user in group A and B (no user override)'] == (object)$overrides[0]);
+        // User in group A and B, with a user override should see the user override
+        // as it has higher priority (numerically lower sortorder).
+        $this->assertTrue($returnedoverrides['For user in group A and B (user override)'] == (object)$overrides[2]);
+        // User with no overrides should get nothing.
+        $this->assertNull($returnedoverrides['For user in no groups']->duedate);
+        $this->assertNull($returnedoverrides['For user in no groups']->cutoffdate);
+        $this->assertNull($returnedoverrides['For user in no groups']->allowsubmissionsfromdate);
+    }
+
     /**
      * Test the quicksave grades processor
      */
index c1d3e6c..aed4fb5 100644 (file)
@@ -1,6 +1,8 @@
 This files describes API changes in the assign code.
 
 === 3.3 ===
+* All pluginfile file serving functions now pass through the options to send_stored_file() (all assignment plugins should do
+  the same).
 * Fixed calendar event types for overridden due dates from 'close' to 'due'.
 * Removed calendar event type of 'open', since mod_assign only has the 'due' event type. No point in creating an override event
 for an event type that does not exist.
index 2672a7f..c3bd4d0 100644 (file)
@@ -729,9 +729,7 @@ function mod_book_core_calendar_provide_event_action(calendar_event $event,
         return null;
     }
 
-    $course = new stdClass();
-    $course->id = $event->courseid;
-    $completion = new \completion_info($course);
+    $completion = new \completion_info($cm->get_course());
 
     $completiondata = $completion->get_data($cm, false);
 
index 01affa1..f09321a 100644 (file)
@@ -1194,22 +1194,20 @@ function choice_check_updates_since(cm_info $cm, $from, $filter = array()) {
  */
 function mod_choice_core_calendar_provide_event_action(calendar_event $event,
                                                        \core_calendar\action_factory $factory) {
-    global $DB;
 
     $cm = get_fast_modinfo($event->courseid)->instances['choice'][$event->instance];
-    $choice = $DB->get_record('choice', array('id' => $event->instance), 'id, timeopen, timeclose');
     $now = time();
 
-    if ($choice->timeclose && $choice->timeclose < $now) {
+    if (!empty($cm->customdata['timeclose']) && $cm->customdata['timeclose'] < $now) {
         // The choice has closed so the user can no longer submit anything.
         return null;
     }
 
     // The choice is actionable if we don't have a start time or the start time is
     // in the past.
-    $actionable = (!$choice->timeopen || $choice->timeopen <= $now);
+    $actionable = (empty($cm->customdata['timeopen']) || $cm->customdata['timeopen'] <= $now);
 
-    if ($actionable && choice_get_my_response($choice)) {
+    if ($actionable && choice_get_my_response((object)['id' => $event->instance])) {
         // There is no action if the user has already submitted their choice.
         return null;
     }
@@ -1247,7 +1245,7 @@ function choice_get_coursemodule_info($coursemodule) {
     global $DB;
 
     $dbparams = ['id' => $coursemodule->instance];
-    $fields = 'id, name, intro, introformat, completionsubmit';
+    $fields = 'id, name, intro, introformat, completionsubmit, timeopen, timeclose';
     if (!$choice = $DB->get_record('choice', $dbparams, $fields)) {
         return false;
     }
@@ -1264,6 +1262,13 @@ function choice_get_coursemodule_info($coursemodule) {
     if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) {
         $result->customdata['customcompletionrules']['completionsubmit'] = $choice->completionsubmit;
     }
+    // Populate some other values that can be used in calendar or on dashboard.
+    if ($choice->timeopen) {
+        $result->customdata['timeopen'] = $choice->timeopen;
+    }
+    if ($choice->timeclose) {
+        $result->customdata['timeclose'] = $choice->timeclose;
+    }
 
     return $result;
 }
index ef14497..8c0293b 100644 (file)
@@ -95,6 +95,8 @@
 
     $users = choice_get_response_data($choice, $cm, $groupmode, $onlyactive);
 
+    $extrafields = get_extra_user_fields($context);
+
     if ($download == "ods" && has_capability('mod/choice:downloadresponses', $context)) {
         require_once("$CFG->libdir/odslib.class.php");
 
         $myxls = $workbook->add_worksheet($strresponses);
 
     /// Print names of all the fields
-        $myxls->write_string(0,0,get_string("lastname"));
-        $myxls->write_string(0,1,get_string("firstname"));
-        $myxls->write_string(0,2,get_string("idnumber"));
-        $myxls->write_string(0,3,get_string("group"));
-        $myxls->write_string(0,4,get_string("choice","choice"));
+        $i = 0;
+        $myxls->write_string(0, $i++, get_string("lastname"));
+        $myxls->write_string(0, $i++, get_string("firstname"));
 
-    /// generate the data for the body of the spreadsheet
-        $i=0;
-        $row=1;
+        // Add headers for extra user fields.
+        foreach ($extrafields as $field) {
+            $myxls->write_string(0, $i++, get_user_field_name($field));
+        }
+
+        $myxls->write_string(0, $i++, get_string("group"));
+        $myxls->write_string(0, $i++, get_string("choice", "choice"));
+
+        // Generate the data for the body of the spreadsheet.
+        $row = 1;
         if ($users) {
             foreach ($users as $option => $userid) {
                 $option_text = choice_get_option_text($choice, $option);
-                foreach($userid as $user) {
-                    $myxls->write_string($row,0,$user->lastname);
-                    $myxls->write_string($row,1,$user->firstname);
-                    $studentid=(!empty($user->idnumber) ? $user->idnumber : " ");
-                    $myxls->write_string($row,2,$studentid);
+                foreach ($userid as $user) {
+                    $i = 0;
+                    $myxls->write_string($row, $i++, $user->lastname);
+                    $myxls->write_string($row, $i++, $user->firstname);
+                    foreach ($extrafields as $field) {
+                        $myxls->write_string($row, $i++, $user->$field);
+                    }
                     $ug2 = '';
                     if ($usergrps = groups_get_all_groups($course->id, $user->id)) {
                         foreach ($usergrps as $ug) {
-                            $ug2 = $ug2. $ug->name;
+                            $ug2 = $ug2 . $ug->name;
                         }
                     }
-                    $myxls->write_string($row,3,$ug2);
+                    $myxls->write_string($row, $i++, $ug2);
 
                     if (isset($option_text)) {
-                        $myxls->write_string($row,4,format_string($option_text,true));
+                        $myxls->write_string($row, $i++, format_string($option_text, true));
                     }
                     $row++;
-                    $pos=4;
                 }
             }
         }
         $myxls = $workbook->add_worksheet($strresponses);
 
     /// Print names of all the fields
-        $myxls->write_string(0,0,get_string("lastname"));
-        $myxls->write_string(0,1,get_string("firstname"));
-        $myxls->write_string(0,2,get_string("idnumber"));
-        $myxls->write_string(0,3,get_string("group"));
-        $myxls->write_string(0,4,get_string("choice","choice"));
+        $i = 0;
+        $myxls->write_string(0, $i++, get_string("lastname"));
+        $myxls->write_string(0, $i++, get_string("firstname"));
 
+        // Add headers for extra user fields.
+        foreach ($extrafields as $field) {
+            $myxls->write_string(0, $i++, get_user_field_name($field));
+        }
 
-    /// generate the data for the body of the spreadsheet
-        $i=0;
-        $row=1;
+        $myxls->write_string(0, $i++, get_string("group"));
+        $myxls->write_string(0, $i++, get_string("choice", "choice"));
+
+        // Generate the data for the body of the spreadsheet.
+        $row = 1;
         if ($users) {
             foreach ($users as $option => $userid) {
+                $i = 0;
                 $option_text = choice_get_option_text($choice, $option);
                 foreach($userid as $user) {
-                    $myxls->write_string($row,0,$user->lastname);
-                    $myxls->write_string($row,1,$user->firstname);
-                    $studentid=(!empty($user->idnumber) ? $user->idnumber : " ");
-                    $myxls->write_string($row,2,$studentid);
+                    $i = 0;
+                    $myxls->write_string($row, $i++, $user->lastname);
+                    $myxls->write_string($row, $i++, $user->firstname);
+                    foreach ($extrafields as $field) {
+                        $myxls->write_string($row, $i++, $user->$field);
+                    }
                     $ug2 = '';
                     if ($usergrps = groups_get_all_groups($course->id, $user->id)) {
                         foreach ($usergrps as $ug) {
-                            $ug2 = $ug2. $ug->name;
+                            $ug2 = $ug2 . $ug->name;
                         }
                     }
-                    $myxls->write_string($row,3,$ug2);
+                    $myxls->write_string($row, $i++, $ug2);
                     if (isset($option_text)) {
-                        $myxls->write_string($row,4,format_string($option_text,true));
+                        $myxls->write_string($row, $i++, format_string($option_text, true));
                     }
                     $row++;
                 }
             }
-            $pos=4;
         }
         /// Close the workbook
         $workbook->close();
 
         /// Print names of all the fields
 
-        echo get_string("lastname")."\t".get_string("firstname") . "\t". get_string("idnumber") . "\t";
+        echo get_string("lastname") . "\t" . get_string("firstname") . "\t";
+
+        // Add headers for extra user fields.
+        foreach ($extrafields as $field) {
+            echo get_user_field_name($field) . "\t";
+        }
+
         echo get_string("group"). "\t";
         echo get_string("choice","choice"). "\n";
 
             foreach ($users as $option => $userid) {
                 $option_text = choice_get_option_text($choice, $option);
                 foreach($userid as $user) {
-                    echo $user->lastname;
-                    echo "\t".$user->firstname;
-                    $studentid = " ";
-                    if (!empty($user->idnumber)) {
-                        $studentid = $user->idnumber;
+                    echo $user->lastname . "\t";
+                    echo $user->firstname . "\t";
+                    foreach ($extrafields as $field) {
+                        echo $user->$field . "\t";
                     }
-                    echo "\t". $studentid."\t";
                     $ug2 = '';
                     if ($usergrps = groups_get_all_groups($course->id, $user->id)) {
                         foreach ($usergrps as $ug) {
index 4bedd87..fba3839 100644 (file)
@@ -4298,21 +4298,19 @@ function data_check_updates_since(cm_info $cm, $from, $filter = array()) {
  */
 function mod_data_core_calendar_provide_event_action(calendar_event $event,
                                                        \core_calendar\action_factory $factory) {
-    global $DB;
 
     $cm = get_fast_modinfo($event->courseid)->instances['data'][$event->instance];
-    $data = $DB->get_record('data', array('id' => $event->instance), 'id, timeavailablefrom, timeavailableto');
-
-    if ($data->timeavailablefrom && $data->timeavailableto) {
-        $actionable = (time() >= $data->timeavailablefrom) && (time() <= $data->timeavailableto);
-    } else if ($data->timeavailableto) {
-        $actionable = time() < $data->timeavailableto;
-    } else if ($data->timeavailablefrom) {
-        $actionable = time() >= $data->timeavailablefrom;
-    } else {
-        $actionable = true;
+    $now = time();
+
+    if (!empty($cm->customdata['timeavailableto']) && $cm->customdata['timeavailableto'] < $now) {
+        // The module has closed so the user can no longer submit anything.
+        return null;
     }
 
+    // The module is actionable if we don't have a start time or the start time is
+    // in the past.
+    $actionable = (empty($cm->customdata['timeavailablefrom']) || $cm->customdata['timeavailablefrom'] <= $now);
+
     return $factory->create_instance(
         get_string('add', 'data'),
         new \moodle_url('/mod/data/view.php', array('id' => $cm->id)),
@@ -4336,7 +4334,7 @@ function data_get_coursemodule_info($coursemodule) {
     global $DB;
 
     $dbparams = ['id' => $coursemodule->instance];
-    $fields = 'id, name, intro, introformat, completionentries';
+    $fields = 'id, name, intro, introformat, completionentries, timeavailablefrom, timeavailableto';
     if (!$data = $DB->get_record('data', $dbparams, $fields)) {
         return false;
     }
@@ -4353,6 +4351,13 @@ function data_get_coursemodule_info($coursemodule) {
     if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) {
         $result->customdata['customcompletionrules']['completionentries'] = $data->completionentries;
     }
+    // Other properties that may be used in calendar or on dashboard.
+    if ($data->timeavailablefrom) {
+        $result->customdata['timeavailablefrom'] = $data->timeavailablefrom;
+    }
+    if ($data->timeavailableto) {
+        $result->customdata['timeavailableto'] = $data->timeavailableto;
+    }
 
     return $result;
 }
index d2a525d..ca6e0be 100644 (file)
@@ -1099,12 +1099,8 @@ class mod_data_lib_testcase extends advanced_testcase {
         // Decorate action event.
         $actionevent = mod_data_core_calendar_provide_event_action($event, $factory);
 
-        // Confirm the event was decorated.
-        $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
-        $this->assertEquals(get_string('add', 'data'), $actionevent->get_name());
-        $this->assertInstanceOf('moodle_url', $actionevent->get_url());
-        $this->assertEquals(1, $actionevent->get_item_count());
-        $this->assertFalse($actionevent->is_actionable());
+        // No event on the dashboard if module is closed.
+        $this->assertNull($actionevent);
     }
 
     public function test_data_core_calendar_provide_event_action_open_in_future() {
index 7628d5b..406e022 100644 (file)
@@ -59,6 +59,14 @@ require('tabs.php');
 $mygroupid = groups_get_activity_group($cm, true);
 groups_print_activity_menu($cm, $url);
 
+// Button "Export to excel".
+if (has_capability('mod/feedback:viewreports', $context) && $feedbackstructure->get_items()) {
+    echo $OUTPUT->container_start('form-buttons');
+    $aurl = new moodle_url('/mod/feedback/analysis_to_excel.php', ['sesskey' => sesskey(), 'id' => $id]);
+    echo $OUTPUT->single_button($aurl, get_string('export_to_excel', 'feedback'));
+    echo $OUTPUT->container_end();
+}
+
 // Show the summary.
 $summary = new mod_feedback\output\summary($feedbackstructure, $mygroupid);
 echo $OUTPUT->render_from_template('mod_feedback/summary', $summary->export_for_template($OUTPUT));
index 2765baa..04dfdd7 100644 (file)
@@ -82,6 +82,15 @@ $mygroupid = false;
 
 $courseselectform->display();
 
+// Button "Export to excel".
+if (has_capability('mod/feedback:viewreports', $context) && $feedbackstructure->get_items()) {
+    echo $OUTPUT->container_start('form-buttons');
+    $aurl = new moodle_url('/mod/feedback/analysis_to_excel.php',
+        ['sesskey' => sesskey(), 'id' => $id, 'courseid' => (int)$courseid]);
+    echo $OUTPUT->single_button($aurl, get_string('export_to_excel', 'feedback'));
+    echo $OUTPUT->container_end();
+}
+
 // Show the summary.
 $summary = new mod_feedback\output\summary($feedbackstructure);
 echo $OUTPUT->render_from_template('mod_feedback/summary', $summary->export_for_template($OUTPUT));
diff --git a/mod/feedback/analysis_to_excel.php b/mod/feedback/analysis_to_excel.php
new file mode 100644 (file)
index 0000000..b5762f6
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * prints an analysed excel-spreadsheet of the feedback
+ *
+ * @copyright Andreas Grabs
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ * @package mod_feedback
+ */
+
+require_once("../../config.php");
+require_once("lib.php");
+require_once("$CFG->libdir/excellib.class.php");
+
+$id = required_param('id', PARAM_INT); // Course module id.
+$courseid = optional_param('courseid', '0', PARAM_INT);
+
+$url = new moodle_url('/mod/feedback/analysis_to_excel.php', array('id' => $id));
+if ($courseid) {
+    $url->param('courseid', $courseid);
+}
+$PAGE->set_url($url);
+
+list($course, $cm) = get_course_and_cm_from_cmid($id, 'feedback');
+require_login($course, false, $cm);
+$context = context_module::instance($cm->id);
+require_capability('mod/feedback:viewreports', $context);
+
+$feedback = $PAGE->activityrecord;
+
+// Buffering any output. This prevents some output before the excel-header will be send.
+ob_start();
+ob_end_clean();
+
+// Get the questions (item-names).
+$feedbackstructure = new mod_feedback_structure($feedback, $cm, $course->id);
+if (!$items = $feedbackstructure->get_items(true)) {
+    print_error('no_items_available_yet', 'feedback', $cm->url);
+}
+
+$mygroupid = groups_get_activity_group($cm);
+
+// Creating a workbook.
+$filename = "feedback_" . clean_filename($cm->get_formatted_name()) . ".xls";
+$workbook = new MoodleExcelWorkbook($filename);
+
+// Creating the worksheet.
+error_reporting(0);
+$worksheet1 = $workbook->add_worksheet();
+error_reporting($CFG->debug);
+$worksheet1->hide_gridlines();
+$worksheet1->set_column(0, 0, 10);
+$worksheet1->set_column(1, 1, 30);
+$worksheet1->set_column(2, 20, 15);
+
+// Creating the needed formats.
+$xlsformats = new stdClass();
+$xlsformats->head1 = $workbook->add_format(['bold' => 1, 'size' => 12]);
+$xlsformats->head2 = $workbook->add_format(['align' => 'left', 'bold' => 1, 'bottum' => 2]);
+$xlsformats->default = $workbook->add_format(['align' => 'left', 'v_align' => 'top']);
+$xlsformats->value_bold = $workbook->add_format(['align' => 'left', 'bold' => 1, 'v_align' => 'top']);
+$xlsformats->procent = $workbook->add_format(['align' => 'left', 'bold' => 1, 'v_align' => 'top', 'num_format' => '#,##0.00%']);
+
+// Writing the table header.
+$rowoffset1 = 0;
+$worksheet1->write_string($rowoffset1, 0, userdate(time()), $xlsformats->head1);
+
+// Get the completeds.
+$completedscount = feedback_get_completeds_group_count($feedback, $mygroupid, $courseid);
+if ($completedscount > 0) {
+    // Write the count of completeds.
+    $rowoffset1++;
+    $worksheet1->write_string($rowoffset1,
+        0,
+        $cm->get_module_type_name(true).': '.strval($completedscount),
+        $xlsformats->head1);
+}
+
+$rowoffset1++;
+$worksheet1->write_string($rowoffset1,
+    0,
+    get_string('questions', 'feedback').': '. strval(count($items)),
+    $xlsformats->head1);
+
+$rowoffset1 += 2;
+$worksheet1->write_string($rowoffset1, 0, get_string('item_label', 'feedback'), $xlsformats->head1);
+$worksheet1->write_string($rowoffset1, 1, get_string('question', 'feedback'), $xlsformats->head1);
+$worksheet1->write_string($rowoffset1, 2, get_string('responses', 'feedback'), $xlsformats->head1);
+$rowoffset1++;
+
+foreach ($items as $item) {
+    // Get the class of item-typ.
+    $itemobj = feedback_get_item_class($item->typ);
+    $rowoffset1 = $itemobj->excelprint_item($worksheet1,
+        $rowoffset1,
+        $xlsformats,
+        $item,
+        $mygroupid,
+        $courseid);
+}
+
+$workbook->close();
index fd413c0..4cf34b1 100644 (file)
@@ -53,9 +53,9 @@ class mod_feedback_completion extends mod_feedback_structure {
     /**
      * Constructor
      *
-     * @param stdClass $feedback feedback object, in case of the template
-     *     this is the current feedback the template is accessed from
+     * @param stdClass $feedback feedback object
      * @param cm_info $cm course module object corresponding to the $feedback
+     *     (at least one of $feedback or $cm is required)
      * @param int $courseid current course (for site feedbacks only)
      * @param bool $iscompleted has feedback been already completed? If yes either completedid or userid must be specified.
      * @param int $completedid id in the table feedback_completed, may be omitted if userid is specified
@@ -66,17 +66,15 @@ class mod_feedback_completion extends mod_feedback_structure {
      */
     public function __construct($feedback, $cm, $courseid, $iscompleted = false, $completedid = null, $userid = null) {
         global $DB;
-        // Make sure courseid is always set for site feedback and never for course feedback.
-        if ($feedback->course == SITEID) {
-            $courseid = $courseid ?: SITEID;
-        } else {
-            $courseid = 0;
-        }
         parent::__construct($feedback, $cm, $courseid, 0);
+        // Make sure courseid is always set for site feedback.
+        if ($this->feedback->course == SITEID && !$this->courseid) {
+            $this->courseid = SITEID;
+        }
         if ($iscompleted) {
             // Retrieve information about the completion.
             $this->iscompleted = true;
-            $params = array('feedback' => $feedback->id);
+            $params = array('feedback' => $this->feedback->id);
             if (!$userid && !$completedid) {
                 throw new coding_exception('Either $completedid or $userid must be specified for completed feedbacks');
             }
@@ -596,7 +594,7 @@ class mod_feedback_completion extends mod_feedback_structure {
             // Not possible to retrieve completed anonymous feedback.
             return false;
         }
-        $params = array('feedback' => $this->feedback->id, 'userid' => $USER->id);
+        $params = array('feedback' => $this->feedback->id, 'userid' => $USER->id, 'anonymous_response' => FEEDBACK_ANONYMOUS_NO);
         if ($this->get_courseid()) {
             $params['courseid'] = $this->get_courseid();
         }
@@ -713,7 +711,7 @@ class mod_feedback_completion extends mod_feedback_structure {
                     $this->jumpto = $nextpage;
                 } else {
                     $this->save_response();
-                    if (!$this->feedback->page_after_submit) {
+                    if (!$this->get_feedback()->page_after_submit) {
                         \core\notification::success(get_string('entries_saved', 'feedback'));
                     }
                     $this->justcompleted = true;
index 9c3597d..2dedb67 100644 (file)
@@ -32,7 +32,10 @@ defined('MOODLE_INTERNAL') || die();
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class mod_feedback_structure {
-    /** @var stdClass */
+    /** @var stdClass record from 'feedback' table.
+     * Reliably has fields: id, course, timeopen, timeclose, anonymous, completionsubmit.
+     * For full object or to access any other field use $this->get_feedback()
+     */
     protected $feedback;
     /** @var cm_info */
     protected $cm;
@@ -50,15 +53,31 @@ class mod_feedback_structure {
      *
      * @param stdClass $feedback feedback object, in case of the template
      *     this is the current feedback the template is accessed from
-     * @param cm_info $cm course module object corresponding to the $feedback
+     * @param stdClass|cm_info $cm course module object corresponding to the $feedback
+     *     (at least one of $feedback or $cm is required)
      * @param int $courseid current course (for site feedbacks only)
      * @param int $templateid template id if this class represents the template structure
      */
     public function __construct($feedback, $cm, $courseid = 0, $templateid = null) {
-        $this->feedback = $feedback;
-        $this->cm = $cm;
-        $this->courseid = ($feedback->course == SITEID) ? $courseid : 0;
+        if ((empty($feedback->id) || empty($feedback->course)) && (empty($cm->instance) || empty($cm->course))) {
+            throw new coding_exception('Either $feedback or $cm must be passed to constructor');
+        }
+        $this->feedback = $feedback ?: (object)['id' => $cm->instance, 'course' => $cm->course];
+        $this->cm = ($cm && $cm instanceof cm_info) ? $cm :
+            get_fast_modinfo($this->feedback->course)->instances['feedback'][$this->feedback->id];
         $this->templateid = $templateid;
+        $this->courseid = ($this->feedback->course == SITEID) ? $courseid : 0;
+
+        if (!$feedback) {
+            // If feedback object was not specified, populate object with fields required for the most of methods.
+            // These fields were added to course module cache in feedback_get_coursemodule_info().
+            // Full instance record can be retrieved by calling mod_feedback_structure::get_feedback().
+            $customdata = ($this->cm->customdata ?: []) + ['timeopen' => 0, 'timeclose' => 0, 'anonymous' => 0];
+            $this->feedback->timeopen = $customdata['timeopen'];
+            $this->feedback->timeclose = $customdata['timeclose'];
+            $this->feedback->anonymous = $customdata['anonymous'];
+            $this->feedback->completionsubmit = empty($this->cm->customdata['customcompletionrules']['completionsubmit']) ? 0 : 1;
+        }
     }
 
     /**
@@ -66,6 +85,11 @@ class mod_feedback_structure {
      * @return stdClass
      */
     public function get_feedback() {
+        global $DB;
+        if (!isset($this->feedback->publish_stats) || !isset($this->feedback->name)) {
+            // Make sure the full object is retrieved.
+            $this->feedback = $DB->get_record('feedback', ['id' => $this->feedback->id], '*', MUST_EXIST);
+        }
         return $this->feedback;
     }
 
@@ -182,7 +206,7 @@ class mod_feedback_structure {
             return true;
         }
 
-        if (intval($this->feedback->publish_stats) != 1 ||
+        if (intval($this->get_feedback()->publish_stats) != 1 ||
                 !has_capability('mod/feedback:viewanalysepage', $context)) {
             return false;
         }
index ede74ff..5fdca5a 100644 (file)
@@ -3428,8 +3428,7 @@ function mod_feedback_core_calendar_is_event_visible(calendar_event $event) {
     global $DB;
 
     $cm = get_fast_modinfo($event->courseid)->instances['feedback'][$event->instance];
-    $feedback = $DB->get_record('feedback', ['id' => $event->instance]);
-    $feedbackcompletion = new mod_feedback_completion($feedback, $cm, 0);
+    $feedbackcompletion = new mod_feedback_completion(null, $cm, 0);
 
     // The event is only visible if the user can submit it.
     return $feedbackcompletion->can_complete();
@@ -3447,26 +3446,21 @@ function mod_feedback_core_calendar_is_event_visible(calendar_event $event) {
  */
 function mod_feedback_core_calendar_provide_event_action(calendar_event $event,
                                                          \core_calendar\action_factory $factory) {
-    global $DB;
 
     $cm = get_fast_modinfo($event->courseid)->instances['feedback'][$event->instance];
-    $feedback = $DB->get_record('feedback', ['id' => $event->instance]);
-    $feedbackcompletion = new mod_feedback_completion($feedback, $cm, 0);
+    $feedbackcompletion = new mod_feedback_completion(null, $cm, 0);
 
-    if ($feedbackcompletion->is_already_submitted()) {
-        // There is no action if the user has already submitted the feedback.
+    if (!empty($cm->customdata['timeclose']) && $cm->customdata['timeclose'] < time()) {
+        // Feedback is already closed, do not display it even if it was never submitted.
         return null;
     }
 
-    $now = time();
-    if ($feedback->timeopen && $feedback->timeclose) {
-        $actionable = ($now >= $feedback->timeopen) && ($now <= $feedback->timeclose);
-    } else if ($feedback->timeclose) {
-        $actionable = $now < $feedback->timeclose;
-    } else if ($feedback->timeopen) {
-        $actionable = $now >= $feedback->timeopen;
-    } else {
-        $actionable = true;
+    // The feedback is actionable if it does not have timeopen or timeopen is in the past.
+    $actionable = $feedbackcompletion->is_open();
+
+    if ($actionable && $feedbackcompletion->is_already_submitted()) {
+        // There is no need to display anything if the user has already submitted the feedback.
+        return null;
     }
 
     return $factory->create_instance(
@@ -3492,7 +3486,7 @@ function feedback_get_coursemodule_info($coursemodule) {
     global $DB;
 
     $dbparams = ['id' => $coursemodule->instance];
-    $fields = 'id, name, intro, introformat, completionsubmit';
+    $fields = 'id, name, intro, introformat, completionsubmit, timeopen, timeclose, anonymous';
     if (!$feedback = $DB->get_record('feedback', $dbparams, $fields)) {
         return false;
     }
@@ -3509,6 +3503,16 @@ function feedback_get_coursemodule_info($coursemodule) {
     if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) {
         $result->customdata['customcompletionrules']['completionsubmit'] = $feedback->completionsubmit;
     }
+    // Populate some other values that can be used in calendar or on dashboard.
+    if ($feedback->timeopen) {
+        $result->customdata['timeopen'] = $feedback->timeopen;
+    }
+    if ($feedback->timeclose) {
+        $result->customdata['timeclose'] = $feedback->timeclose;
+    }
+    if ($feedback->anonymous) {
+        $result->customdata['anonymous'] = $feedback->anonymous;
+    }
 
     return $result;
 }
index 7ab33ab..b6d6959 100644 (file)
@@ -244,3 +244,58 @@ Feature: Anonymous feedback
     And I should not see "Response number: 1"
     And I should see "Response number: 2"
     And I log out
+
+  Scenario: Collecting new non-anonymous feedback from a previously anonymous feedback activity
+    When I log in as "teacher"
+    And I follow "Course 1"
+    And I follow "Course feedback"
+    And I click on "Edit settings" "link" in the "Administration" "block"
+    And I set the following fields to these values:
+      | Allow multiple submissions | Yes |
+    And I press "Save and display"
+    And I follow "Edit questions"
+    And I add a "Short text answer" question to the feedback with:
+      | Question               | this is a short text answer |
+      | Label                  | shorttext                   |
+      | Maximum characters accepted | 200                    |
+    And I log out
+    When I log in as "user1"
+    And I follow "Course 1"
+    And I follow "Course feedback"
+    And I follow "Answer the questions..."
+    And I set the following fields to these values:
+      | this is a short text answer  | anontext |
+    And I press "Submit your answers"
+    And I log out
+    # Switch to non-anon responses.
+    And I log in as "teacher"
+    And I follow "Course 1"
+    And I follow "Course feedback"
+    And I click on "Edit settings" "link" in the "Administration" "block"
+    And I set the following fields to these values:
+        | Record user names | User's name will be logged and shown with answers |
+    And I press "Save and display"
+    And I log out
+    # Now leave a non-anon feedback as user1
+    When I log in as "user1"
+    And I follow "Course 1"
+    And I follow "Course feedback"
+    And I follow "Answer the questions..."
+    And I set the following fields to these values:
+      | this is a short text answer  | usertext |
+    And I press "Submit your answers"
+    And I log out
+    # Now check the responses are correct.
+    When I log in as "teacher"
+    And I follow "Course 1"
+    And I follow "Course feedback"
+    And I follow "Show responses"
+    And I should see "Anonymous entries (1)"
+    And I should see "Non anonymous entries (1)"
+    And I click on "," "link" in the "Username 1" "table_row"
+    And I should see "(Username 1)"
+    And I should see "usertext"
+    And I follow "Back"
+    And I follow "Response number: 1"
+    And I should see "Response number: 1 (Anonymous)"
+    Then I should see "anontext"
index 90e83e3..99e024f 100644 (file)
@@ -32,6 +32,39 @@ require_once($CFG->dirroot . '/mod/feedback/lib.php');
  */
 class mod_feedback_lib_testcase extends advanced_testcase {
 
+    public function test_feedback_initialise() {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $course = $this->getDataGenerator()->create_course();
+        $params['course'] = $course->id;
+        $params['timeopen'] = time() - 5 * MINSECS;
+        $params['timeclose'] = time() + DAYSECS;
+        $params['anonymous'] = 1;
+        $params['intro'] = 'Some introduction text';
+        $feedback = $this->getDataGenerator()->create_module('feedback', $params);
+
+        // Test different ways to construct the structure object.
+        $pseudocm = get_coursemodule_from_instance('feedback', $feedback->id); // Object similar to cm_info.
+        $cm = get_fast_modinfo($course)->instances['feedback'][$feedback->id]; // Instance of cm_info.
+
+        $constructorparams = [
+            [$feedback, null],
+            [null, $pseudocm],
+            [null, $cm],
+            [$feedback, $pseudocm],
+            [$feedback, $cm],
+        ];
+
+        foreach ($constructorparams as $params) {
+            $structure = new mod_feedback_completion($params[0], $params[1], 0);
+            $this->assertTrue($structure->is_open());
+            $this->assertTrue($structure->get_cm() instanceof cm_info);
+            $this->assertEquals($feedback->cmid, $structure->get_cm()->id);
+            $this->assertEquals($feedback->intro, $structure->get_feedback()->intro);
+        }
+    }
+
     /**
      * Tests for mod_feedback_refresh_events.
      */
@@ -175,11 +208,8 @@ class mod_feedback_lib_testcase extends advanced_testcase {
         $factory = new \core_calendar\action_factory();
         $actionevent = mod_feedback_core_calendar_provide_event_action($event, $factory);
 
-        $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
-        $this->assertEquals(get_string('answerquestions', 'feedback'), $actionevent->get_name());
-        $this->assertInstanceOf('moodle_url', $actionevent->get_url());
-        $this->assertEquals(1, $actionevent->get_item_count());
-        $this->assertFalse($actionevent->is_actionable());
+        // No event on the dashboard if feedback is closed.
+        $this->assertNull($actionevent);
     }
 
     /**
index 4736638..f07e63f 100644 (file)
@@ -790,9 +790,7 @@ function mod_folder_core_calendar_provide_event_action(calendar_event $event,
                                                      \core_calendar\action_factory $factory) {
     $cm = get_fast_modinfo($event->courseid)->instances['folder'][$event->instance];
 
-    $course = new stdClass();
-    $course->id = $event->courseid;
-    $completion = new \completion_info($course);
+    $completion = new \completion_info($cm->get_course());
 
     $completiondata = $completion->get_data($cm, false);
 
index 1ed8ef2..9a07d66 100644 (file)
@@ -8214,9 +8214,7 @@ function mod_forum_core_calendar_provide_event_action(calendar_event $event,
         return null;
     }
 
-    $course = new stdClass();
-    $course->id = $event->courseid;
-    $completion = new \completion_info($course);
+    $completion = new \completion_info($cm->get_course());
 
     $completiondata = $completion->get_data($cm, false);
 
index 3e20961..b155586 100644 (file)
@@ -4187,9 +4187,7 @@ function mod_glossary_core_calendar_provide_event_action(calendar_event $event,
                                                       \core_calendar\action_factory $factory) {
     $cm = get_fast_modinfo($event->courseid)->instances['glossary'][$event->instance];
 
-    $course = new stdClass();
-    $course->id = $event->courseid;
-    $completion = new \completion_info($course);
+    $completion = new \completion_info($cm->get_course());
 
     $completiondata = $completion->get_data($cm, false);
 
index bf096aa..7fc8a6e 100644 (file)
@@ -487,9 +487,7 @@ function mod_imscp_core_calendar_provide_event_action(calendar_event $event,
                                                       \core_calendar\action_factory $factory) {
     $cm = get_fast_modinfo($event->courseid)->instances['imscp'][$event->instance];
 
-    $course = new stdClass();
-    $course->id = $event->courseid;
-    $completion = new \completion_info($course);
+    $completion = new \completion_info($cm->get_course());
 
     $completiondata = $completion->get_data($cm, false);
 
index 44ca596..fffe2af 100644 (file)
@@ -364,9 +364,7 @@ function mod_label_core_calendar_provide_event_action(calendar_event $event,
                                                       \core_calendar\action_factory $factory) {
     $cm = get_fast_modinfo($event->courseid)->instances['label'][$event->instance];
 
-    $course = new stdClass();
-    $course->id = $event->courseid;
-    $completion = new \completion_info($course);
+    $completion = new \completion_info($cm->get_course());
 
     $completiondata = $completion->get_data($cm, false);
 
index 338d44b..d1dc8c8 100644 (file)
@@ -215,8 +215,9 @@ class lesson_page_type_numerical extends lesson_page {
                     $answerdata->answers[] = array(get_string("nooneansweredthisquestion", "lesson"), " ");
                 }
                 $i++;
-            } else if ($useranswer != null && ($answer->id == $useranswer->answerid || ($answer == end($answers) && empty($answerdata)))) {
-                 // get in here when what the user entered is not one of the answers
+            } else if ($useranswer != null && ($answer->id == $useranswer->answerid || ($answer == end($answers) &&
+                    empty($answerdata->answers)))) {
+                // Get in here when the user answered or for the last answer.
                 $data = '<input class="form-control" type="text" size="50" ' .
                         'disabled="disabled" readonly="readonly" value="'.s($useranswer->useranswer).'">';
                 if (isset($pagestats[$this->properties->id][$useranswer->useranswer])) {
index 18bc722..c1e207a 100644 (file)
@@ -662,9 +662,7 @@ function mod_lti_core_calendar_provide_event_action(calendar_event $event,
                                                       \core_calendar\action_factory $factory) {
     $cm = get_fast_modinfo($event->courseid)->instances['lti'][$event->instance];
 
-    $course = new stdClass();
-    $course->id = $event->courseid;
-    $completion = new \completion_info($course);
+    $completion = new \completion_info($cm->get_course());
 
     $completiondata = $completion->get_data($cm, false);
 
index 93609fd..12bbdfc 100644 (file)
@@ -551,9 +551,7 @@ function mod_page_core_calendar_provide_event_action(calendar_event $event,
                                                       \core_calendar\action_factory $factory) {
     $cm = get_fast_modinfo($event->courseid)->instances['page'][$event->instance];
 
-    $course = new stdClass();
-    $course->id = $event->courseid;
-    $completion = new \completion_info($course);
+    $completion = new \completion_info($cm->get_course());
 
     $completiondata = $completion->get_data($cm, false);
 
index b26ec98..104b5f0 100644 (file)
@@ -566,9 +566,7 @@ function mod_resource_core_calendar_provide_event_action(calendar_event $event,
                                                       \core_calendar\action_factory $factory) {
     $cm = get_fast_modinfo($event->courseid)->instances['resource'][$event->instance];
 
-    $course = new stdClass();
-    $course->id = $event->courseid;
-    $completion = new \completion_info($course);
+    $completion = new \completion_info($cm->get_course());
 
     $completiondata = $completion->get_data($cm, false);
 
index 8daa28d..c4a7c0f 100644 (file)
@@ -1627,12 +1627,21 @@ function scorm_refresh_events($courseid = 0) {
  */
 function mod_scorm_core_calendar_provide_event_action(calendar_event $event,
                                                       \core_calendar\action_factory $factory) {
-    global $CFG, $DB;
+    global $CFG;
 
     require_once($CFG->dirroot . '/mod/scorm/locallib.php');
 
     $cm = get_fast_modinfo($event->courseid)->instances['scorm'][$event->instance];
-    $scorm = $DB->get_record('scorm', array('id' => $event->instance));
+
+    if (!empty($cm->customdata['timeclose']) && $cm->customdata['timeclose'] < time()) {
+        // The scorm has closed so the user can no longer submit anything.
+        return null;
+    }
+
+    // Restore scorm object from cached values in $cm, we only need id, timeclose and timeopen.
+    $customdata = $cm->customdata ?: [];
+    $customdata['id'] = $cm->instance;
+    $scorm = (object)($customdata + ['timeclose' => 0, 'timeopen' => 0]);
 
     // Check that the SCORM activity is open.
     list($actionable, $warnings) = scorm_get_availability_status($scorm);
@@ -1660,7 +1669,8 @@ function scorm_get_coursemodule_info($coursemodule) {
     global $DB;
 
     $dbparams = ['id' => $coursemodule->instance];
-    $fields = 'id, name, intro, introformat, completionstatusrequired, completionscorerequired, completionstatusallscos';
+    $fields = 'id, name, intro, introformat, completionstatusrequired, completionscorerequired, completionstatusallscos, '.
+        'timeopen, timeclose';
     if (!$scorm = $DB->get_record('scorm', $dbparams, $fields)) {
         return false;
     }
@@ -1679,6 +1689,13 @@ function scorm_get_coursemodule_info($coursemodule) {
         $result->customdata['customcompletionrules']['completionscorerequired'] = $scorm->completionscorerequired;
         $result->customdata['customcompletionrules']['completionstatusallscos'] = $scorm->completionstatusallscos;
     }
+    // Populate some other values that can be used in calendar or on dashboard.
+    if ($scorm->timeopen) {
+        $result->customdata['timeopen'] = $scorm->timeopen;
+    }
+    if ($scorm->timeclose) {
+        $result->customdata['timeclose'] = $scorm->timeclose;
+    }
 
     return $result;
 }
index 6aac252..cd33822 100644 (file)
@@ -242,12 +242,8 @@ class mod_scorm_lib_testcase extends externallib_advanced_testcase {
         // Decorate action event.
         $actionevent = mod_scorm_core_calendar_provide_event_action($event, $factory);
 
-        // Confirm the event was decorated.
-        $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent);
-        $this->assertEquals(get_string('enter', 'scorm'), $actionevent->get_name());
-        $this->assertInstanceOf('moodle_url', $actionevent->get_url());
-        $this->assertEquals(1, $actionevent->get_item_count());
-        $this->assertFalse($actionevent->is_actionable());
+        // No event on the dashboard if module is closed.
+        $this->assertNull($actionevent);
     }
 
     public function test_scorm_core_calendar_provide_event_action_open_in_future() {
index e6f7e8f..f7dcc33 100644 (file)
@@ -1128,9 +1128,7 @@ function mod_survey_core_calendar_provide_event_action(calendar_event $event,
         return null;
     }
 
-    $course = new stdClass();
-    $course->id = $event->courseid;
-    $completion = new \completion_info($course);
+    $completion = new \completion_info($cm->get_course());
 
     $completiondata = $completion->get_data($cm, false);
 
index 779fd4a..f9bf4df 100644 (file)
@@ -389,9 +389,7 @@ function mod_url_core_calendar_provide_event_action(calendar_event $event,
                                                        \core_calendar\action_factory $factory) {
     $cm = get_fast_modinfo($event->courseid)->instances['url'][$event->instance];
 
-    $course = new stdClass();
-    $course->id = $event->courseid;
-    $completion = new \completion_info($course);
+    $completion = new \completion_info($cm->get_course());
 
     $completiondata = $completion->get_data($cm, false);
 
index 7b08b74..728af4c 100644 (file)
@@ -815,9 +815,7 @@ function mod_wiki_core_calendar_provide_event_action(calendar_event $event,
                                                     \core_calendar\action_factory $factory) {
     $cm = get_fast_modinfo($event->courseid)->instances['wiki'][$event->instance];
 
-    $course = new stdClass();
-    $course->id = $event->courseid;
-    $completion = new \completion_info($course);
+    $completion = new \completion_info($cm->get_course());
 
     $completiondata = $completion->get_data($cm, false);
 
index d6189d4..8e41657 100644 (file)
 namespace repository_onedrive;
 
 use \core\task\scheduled_task;
+use DateTime;
+use DateInterval;
+use repository_exception;
+use \core\oauth2\rest_exception;
 
 defined('MOODLE_INTERNAL') || die();
 
+require_once($CFG->dirroot . '/repository/lib.php');
+
 /**
  * Simple task to delete temporary permission records.
  * @package    repository_onedrive
@@ -55,8 +61,8 @@ class remove_temp_access_task extends scheduled_task {
         $expires->sub(new DateInterval("P7D"));
         $timestamp = $expires->getTimestamp();
 
-        $issuerid = get_config('repository_onedrive', 'issuerid');
-        $issuer = \core\oauth2\api::get_issuer_by_id($issuerid);
+        $issuerid = get_config('onedrive', 'issuerid');
+        $issuer = \core\oauth2\api::get_issuer($issuerid);
 
         // Add the current user as an OAuth writer.
         $systemauth = \core\oauth2\api::get_system_oauth_client($issuer);
@@ -65,12 +71,17 @@ class remove_temp_access_task extends scheduled_task {
             $details = 'Cannot connect as system user';
             throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
         }
-        $systemservice = new repository_onedrive\rest($systemauth);
+        $systemservice = new \repository_onedrive\rest($systemauth);
 
         foreach ($accessrecords as $access) {
             if ($access->get('timemodified') < $timestamp) {
                 $params = ['permissionid' => $access->get('permissionid'), 'itemid' => $access->get('itemid')];
-                $systemservice->call('delete_permission', $params);
+                try {
+                    $systemservice->call('delete_permission', $params);
+                } catch (rest_exception $re) {
+                    // We log and give up here or we will always fail for eternity.
+                    mtrace('Failed to remove access from file: ' . $access->get('itemid'));
+                }
                 $access->delete();
             }
         }
index 47fac12..46fed9c 100644 (file)
@@ -77,24 +77,20 @@ class rest extends \core\oauth2\rest {
                 ],
                 'response' => 'json'
             ],
-            'list_permissions' => [
-                'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/items/{fileid}/permissions',
-                'method' => 'get',
+            'create_permission' => [
+                'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/items/{fileid}/invite',
+                'method' => 'post',
                 'args' => [
-                    '$select' => PARAM_RAW,
-                    '$expand' => PARAM_RAW,
-                    'fileid' => PARAM_RAW,
-                    '$skip' => PARAM_INT,
-                    '$skipToken' => PARAM_RAW,
-                    '$count' => PARAM_INT
+                    'fileid' => PARAM_RAW
                 ],
                 'response' => 'json'
             ],
-            'create_permission' => [
-                'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/items/{fileid}/invite',
+            'create_upload' => [
+                'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/items/{parentid}:/{filename}:/createUploadSession',
                 'method' => 'post',
                 'args' => [
-                    'fileid' => PARAM_RAW
+                    'parentid' => PARAM_RAW,
+                    'filename' => PARAM_RAW
                 ],
                 'response' => 'json'
             ],
@@ -123,12 +119,6 @@ class rest extends \core\oauth2\rest {
                 ],
                 'response' => 'json'
             ],
-            'get_drive' => [
-                'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive',
-                'method' => 'get',
-                'args' => [],
-                'response' => 'json'
-            ],
             'delete_file_by_path' => [
                 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/root:/{fullpath}',
                 'method' => 'delete',
@@ -137,14 +127,6 @@ class rest extends \core\oauth2\rest {
                 ],
                 'response' => 'json'
             ],
-            'copy_share' => [
-                'endpoint' => 'https://graph.microsoft.com/v1.0/shares/{sharetoken}/root/copy',
-                'method' => 'post',
-                'args' => [
-                    'sharetoken' => PARAM_RAW,
-                ],
-                'response' => 'json'
-            ],
             'delete_permission' => [
                 'endpoint' => 'https://graph.microsoft.com/v1.0/me/drive/items/{fileid}/permissions/{permissionid}',
                 'method' => 'delete',
index 4227e14..e97eb85 100644 (file)
@@ -34,6 +34,7 @@ $string['importskydrivefiles'] = 'Import files from Microsoft SkyDrive repositor
 $string['internal'] = 'Internal (files stored in Moodle)';
 $string['issuer_help'] = 'Select the OAuth 2 service that is configured to talk to the OneDrive API. If the services does not exist yet, you might need to create it.';
 $string['issuer'] = 'OAuth 2 service';
+$string['mysitenotfound'] = 'You have never logged into OneDrive before. You must login to OneDrive at least once it before it can be used with Moodle.';
 $string['oauth2serviceslink'] = '<a href="{$a}" title="Link to OAuth Services configuration">OAuth 2 Services Configuration</a>';
 $string['owner'] = 'Owned by: {$a}';
 $string['pluginname'] = 'Microsoft OneDrive';
index 3e80212..43c5dcb 100644 (file)
@@ -311,8 +311,8 @@ class repository_onedrive extends repository {
         } catch (Exception $e) {
             if ($e->getCode() == 403 && strpos($e->getMessage(), 'Access Not Configured') !== false) {
                 throw new repository_exception('servicenotenabled', 'repository_onedrive');
-            } else {
-                throw $e;
+            } else if (strpos($e->getMessage(), 'mysite not found') !== false) {
+                throw new repository_exception('mysitenotfound', 'repository_onedrive');
             }
         }
 
@@ -410,11 +410,17 @@ class repository_onedrive extends repository {
         if ($this->disabled) {
             throw new repository_exception('cannotdownload', 'repository');
         }
+        $sourceinfo = json_decode($reference);
+
+        $client = null;
+        if (!empty($sourceinfo->usesystem)) {
+            $client = \core\oauth2\api::get_system_oauth_client($this->issuer);
+        } else {
+            $client = $this->get_user_oauth_client();
+        }
 
-        $client = $this->get_user_oauth_client();
         $base = 'https://graph.microsoft.com/v1.0/';
 
-        $sourceinfo = json_decode($reference);
         $sourceurl = new moodle_url($base . 'me/drive/items/' . $sourceinfo->id . '/content');
         $source = $sourceurl->out(false);
 
@@ -539,7 +545,7 @@ class repository_onedrive extends repository {
                                    $storedfile->get_filepath(),
                                    $storedfile->get_filename());
 
-        if (empty($options['offline']) && !empty($info) && $info->is_writable()) {
+        if (empty($options['offline']) && !empty($info) && $info->is_writable() && !empty($source->usesystem)) {
             // Add the current user as an OAuth writer.
             $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
 
@@ -584,18 +590,6 @@ class repository_onedrive extends repository {
         }
     }
 
-    /**
-     * List the permissions on a file.
-     *
-     * @param \repository_onedrive\rest $client Authenticated client.
-     * @param string $fileid The id of the file.
-     * @return array
-     */
-    protected function list_file_permissions(\repository_onedrive\rest $client, $fileid) {
-        $fields = "id,roles,link,grantedTo";
-        return $client->call('list_permissions', ['fileid' => $fileid, '$select' => $fields]);
-    }
-
     /**
      * See if a folder exists within a folder
      *
@@ -629,24 +623,6 @@ class repository_onedrive extends repository {
         return true;
     }
 
-
-    /**
-     * Get a file summary by full path.
-     *
-     * @param \repository_onedrive\rest $client Authenticated client.
-     * @param string $fullpath
-     * @return stdClass
-     */
-    protected function get_file_summary_by_path(\repository_onedrive\rest $client, $fullpath) {
-        $fields = "folder,id,lastModifiedDateTime,name,size,webUrl,createdByUser";
-        $response = $client->call('get_file_by_path', ['fullpath' => $fullpath, '$select' => $fields]);
-        if (empty($response->id)) {
-            $details = 'Cannot get file summary:' . $fullpath;
-            throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
-        }
-        return $response;
-    }
-
     /**
      * Create a folder within a folder
      *
@@ -681,23 +657,6 @@ class repository_onedrive extends repository {
         return $response;
     }
 
-    /**
-     * Get the id of this users root drive.
-     *
-     * @param \repository_onedrive\rest $client Authenticated client.
-     *
-     * @return string id
-     */
-    protected function get_root_drive_id(\repository_onedrive\rest $client) {
-        $response = $client->call('get_drive', []);
-
-        if (empty($response->id)) {
-            $details = 'Cannot get driveid';
-            throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
-        }
-        return $response->id;
-    }
-
     /**
      * Add a writer to the permissions on the file (temporary).
      *
@@ -735,30 +694,6 @@ class repository_onedrive extends repository {
         return true;
     }
 
-    /**
-     * Add a writer to the permissions on the file.
-     *
-     * @param \repository_onedrive\rest $client Authenticated client.
-     * @param string $fileid The file we are updating.
-     * @param string $useremail The user email of the writer account to add.
-     * @return boolean
-     */
-    protected function add_writer_to_file(\repository_onedrive\rest $client, $fileid, $useremail) {
-        $updateeditor = [
-            'recipients' => [ [ 'email' => $useremail ] ],
-            'roles' => ['write'],
-            'requireSignIn' => true,
-            'sendInvitation' => false
-        ];
-        $params = [ 'fileid' => $fileid ];
-        $response = $client->call('create_permission', $params, json_encode($updateeditor));
-        if (empty($response->value)) {
-            $details = 'Cannot add user ' . $useremail . ' as a writer for document: ' . $fileid;
-            throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
-        }
-        return true;
-    }
-
     /**
      * Allow anyone with the link to read the file.
      *
@@ -777,45 +712,72 @@ class repository_onedrive extends repository {
             $details = 'Cannot update link sharing for the document: ' . $fileid;
             throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
         }
-        return true;
+        return $response->link->webUrl;
     }
 
     /**
-     * Copy a shared file to a new folder.
+     * Given a filename, use the core_filetypes registered types to guess a mimetype.
      *
-     * @param \repository_onedrive\rest $client Authenticated client.
-     * @param string $sharetoken The share we are querying.
-     * @param string $newdrive Id of the drive to copy to.
-     * @param string $parentid Id of the folder to copy to.
-     * @return stdClass
+     * If no mimetype is known, return 'application/unknown';
+     *
+     * @param string $filename
+     * @return string $mimetype
      */
-    protected function copy_share(\repository_onedrive\rest $client, $sharetoken, $newdrive, $parentid) {
-        $folder = [
-            'parentReference' => ['id' => $parentid, 'driveId' => $newdrive]
-        ];
-        $params = ['sharetoken' => $sharetoken];
-        $response = $client->call('copy_share', $params, json_encode($folder));
-        return true;
+    protected function get_mimetype_from_filename($filename) {
+        $mimetype = 'application/unknown';
+        $types = core_filetypes::get_types();
+        $fileextension = '.bin';
+        if (strpos($filename, '.') !== false) {
+            $fileextension = substr($filename, strrpos($filename, '.') + 1);
+        }
+
+        if (isset($types[$fileextension])) {
+            $mimetype = $types[$fileextension]['type'];
+        }
+        return $mimetype;
     }
 
     /**
-     * From MS docs - to get a share token from a url, do this:
-     * Reference: https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/shares_get
-     * To access a sharing URL using the shares API, the URL needs to be transformed into a sharing token.
-     *   To transform a URL into a sharing token:
-     *   Base64 encode the sharing URL.
-     *   Convert the base64 encoded data to unpadded base64url format by:
-     *   Trim trailing = characeters from the string.
-     *   Replace unsafe URL characters with an equivelent character; replace / with _ and + with -.
-     *   Append u! to the beginning of the string.
+     * Upload a file to onedrive.
      *
-     * @param string $shareurl
-     * @return string The sharing token
+     * @param \repository_onedrive\rest $service Authenticated client.
+     * @param \curl $curl Curl client to perform the put operation.
+     * @param string $filepath The local path to the file to upload
+     * @param string $mimetype The new mimetype
+     * @param string $parentid The folder to put it.
+     * @param string $filename The name of the new file
+     * @return string $fileid
      */
-    protected function get_share_token($shareurl) {
-        return 'u!' . str_replace(['/', '+'], ['_', '-'], rtrim(base64_encode($shareurl), '='));
+    protected function upload_file(\repository_onedrive\rest $service, \curl $curl, $filepath, $mimetype, $parentid, $filename) {
+        // Start an upload session.
+        // Docs https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/item_createuploadsession link.
+
+        $params = ['parentid' => $parentid, 'filename' => urlencode($filename)];
+        $behaviour = [ 'item' => [ "@microsoft.graph.conflictBehavior" => "rename" ] ];
+        $created = $service->call('create_upload', $params, json_encode($behaviour));
+        if (empty($created->uploadUrl)) {
+            $details = 'Cannot begin upload session:' . $parentid;
+            throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
+        }
+
+        $options = ['file' => $filepath];
+        $curl->setHeader('Content-type: ' . $mimetype);
+        $size = filesize($filepath);
+        $curl->setHeader('Content-Range: bytes 0-' . ($size - 1) . '/' . $size);
+        $response = $curl->put($created->uploadUrl, $options);
+        if ($curl->errno == 0) {
+            $response = json_decode($response);
+        }
+
+        if (empty($response->id)) {
+            $details = 'File not created';
+            throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
+        }
+
+        return $response->id;
     }
 
+
     /**
      * Called when a file is selected as a "link".
      * Invoked at MOODLE/repository/repository_ajax.php
@@ -846,8 +808,6 @@ class repository_onedrive extends repository {
             $details = 'Cannot connect as system user';
             throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
         }
-        $systemuserinfo = $systemauth->get_userinfo();
-        $systemuseremail = $systemuserinfo['email'];
 
         $source = json_decode($reference);
 
@@ -856,31 +816,23 @@ class repository_onedrive extends repository {
             $details = 'Cannot connect as current user';
             throw new repository_exception('errorwhilecommunicatingwith', 'repository', '', $details);
         }
-        $userinfo = $userauth->get_userinfo();
-        $useremail = $userinfo['email'];
 
-        $userservice = new repository_onedrive\rest($userauth);
         $systemservice = new repository_onedrive\rest($systemauth);
 
-        // Get the list of existing permissions so we can see if the owner is already the system account,
-        // and whether we need to update the link sharing options.
-        $permissions = $this->list_file_permissions($userservice, $source->id);
-
-        $readshareupdaterequired = true;
-        $ownerupdaterequired = true;
-        foreach ($permissions->value as $permission) {
-            if (!empty($permission->link)) {
-                if ($permission->link->scope == 'anonymous' &&
-                        $permission->link->type == 'view') {
-                    $shareurl = $permission->link->webUrl;
-                    $readshareupdaterequired = false;
-                    break;
-                }
-            }
-        }
+        // Download the file.
+        $tmpfilename = clean_param($source->id, PARAM_PATH);
+        $temppath = make_request_directory() . $tmpfilename;
 
-        // Add Moodle as writer.
-        $this->add_writer_to_file($userservice, $source->id, $systemuseremail);
+        $options = ['filepath' => $temppath, 'timeout' => 60, 'followlocation' => true, 'maxredirs' => 5];
+        $base = 'https://graph.microsoft.com/v1.0/';
+        $sourceurl = new moodle_url($base . 'me/drive/items/' . $source->id . '/content');
+        $sourceurl = $sourceurl->out(false);
+
+        $result = $userauth->download_one($sourceurl, null, $options);
+
+        if (!$result) {
+            throw new repository_exception('cannotdownload', 'repository');
+        }
 
         // Now copy it to a sensible folder.
         $contextlist = array_reverse($context->get_parent_contexts(true));
@@ -921,31 +873,26 @@ class repository_onedrive extends repository {
             }
         }
 
-        // Get the users drive id.
-        $newdrive = $this->get_root_drive_id($systemservice);
-
-        if ($readshareupdaterequired) {
-            $response = $this->set_file_sharing_anyone_with_link_can_read($userservice, $source->id);
-            $shareurl = $response->value->webUrl;
-        }
-
-        // Turn the share url into a sharing token.
-        $sharetoken = $this->get_share_token($shareurl);
-
         // Delete any existing file at this path.
-        $path = $fullpath . '/' . $source->name;
+        $path = $fullpath . '/' . urlencode(clean_param($source->name, PARAM_PATH));
         $this->delete_file_by_path($systemservice, $path);
 
-        // Copy the file to the moodle account.
-        // Note this method (copying via a share link) is the only way to copy a file in
-        // office 365 from one user to another.
-        $this->copy_share($systemservice, $sharetoken, $newdrive, $parentid);
+        // Upload the file.
+        $safefilename = clean_param($source->name, PARAM_PATH);
+        $mimetype = $this->get_mimetype_from_filename($safefilename);
+        // We cannot send authorization headers in the upload or personal microsoft accounts will fail (what a joke!).
+        $curl = new \curl();
+        $fileid = $this->upload_file($systemservice, $curl, $temppath, $mimetype, $parentid, $safefilename);
+
+        // Read with link.
+        $link = $this->set_file_sharing_anyone_with_link_can_read($systemservice, $fileid);
 
-        $summary = $this->get_file_summary_by_path($systemservice, $path);
+        $summary = $this->get_file_summary($systemservice, $fileid);
 
         // Update the details in the file reference before it is saved.
         $source->id = $summary->id;
-        $source->link = $summary->webUrl;
+        $source->link = $link;
+        $source->usesystem = true;
 
         $reference = json_encode($source);
 
@@ -963,6 +910,9 @@ class repository_onedrive extends repository {
             return get_string('unknownsource', 'repository');
         }
         $source = json_decode($reference);
+        if (empty($source->usesystem)) {
+            return '';
+        }
         $systemauth = \core\oauth2\api::get_system_oauth_client($this->issuer);
 
         if ($systemauth === false) {
index f02d2ce..0f2acaa 100644 (file)
@@ -1 +1 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet"><path d="M1520 1216q0-40-28-68l-208-208q-28-28-68-28-42 0-72 32 3 3 19 18.5t21.5 21.5 15 19 13 25.5 3.5 27.5q0 40-28 68t-68 28q-15 0-27.5-3.5t-25.5-13-19-15-21.5-21.5-18.5-19q-33 31-33 73 0 40 28 68l206 207q27 27 68 27 40 0 68-26l147-146q28-28 28-67zm-703-705q0-40-28-68l-206-207q-28-28-68-28-39 0-68 27l-147 146q-28 28-28 67 0 40 28 68l208 208q27 27 68 27 42 0 72-31-3-3-19-18.5t-21.5-21.5-15-19-13-25.5-3.5-27.5q0-40 28-68t68-28q15 0 27.5 3.5t25.5 13 19 15 21.5 21.5 18.5 19q33-31 33-73zm895 705q0 120-85 203l-147 146q-83 83-203 83-121 0-204-85l-206-207q-83-83-83-203 0-123 88-209l-88-88q-86 88-208 88-120 0-204-84l-208-208q-84-84-84-204t85-203l147-146q83-83 203-83 121 0 204 85l206 207q83 83 83 203 0 123-88 209l88 88q86-88 208-88 120 0 204 84l208 208q84 84 84 204z"/></svg>
\ No newline at end of file
+<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1792 640q0 26-19 45l-512 512q-19 19-45 19t-45-19-19-45v-256h-224q-98 0-175.5 6t-154 21.5-133 42.5-105.5 69.5-80 101-48.5 138.5-17.5 181q0 55 5 123 0 6 2.5 23.5t2.5 26.5q0 15-8.5 25t-23.5 10q-16 0-28-17-7-9-13-22t-13.5-30-10.5-24q-127-285-127-451 0-199 53-333 162-403 875-403h224v-256q0-26 19-45t45-19 45 19l512 512q19 19 19 45z"/></svg>
\ No newline at end of file
diff --git a/theme/boost/pix/fp/alias_sm.svg b/theme/boost/pix/fp/alias_sm.svg
new file mode 100644 (file)
index 0000000..0f2acaa
--- /dev/null
@@ -0,0 +1 @@
+<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1792 640q0 26-19 45l-512 512q-19 19-45 19t-45-19-19-45v-256h-224q-98 0-175.5 6t-154 21.5-133 42.5-105.5 69.5-80 101-48.5 138.5-17.5 181q0 55 5 123 0 6 2.5 23.5t2.5 26.5q0 15-8.5 25t-23.5 10q-16 0-28-17-7-9-13-22t-13.5-30-10.5-24q-127-285-127-451 0-199 53-333 162-403 875-403h224v-256q0-26 19-45t45-19 45 19l512 512q19 19 19 45z"/></svg>
\ No newline at end of file
index 6a0962c..bfa7815 100644 (file)
@@ -179,14 +179,18 @@ $doughnut-fill-colour: $brand-warning;
         }
     }
 
+    .content-centred {
+        max-width: 900px;
+        margin-left: auto;
+        margin-right: auto;
+    }
+
     .empty-placeholder-image-sm {
-        height: 70px;
-        width: 85px;
+        height: 50px;
     }
 
     .empty-placeholder-image-lg {
         height: 125px;
-        width: 145px;
     }
 
     .courses-view-course-item {
@@ -194,3 +198,11 @@ $doughnut-fill-colour: $brand-warning;
         overflow-y: hidden;
     }
 }
+
+@media (min-width: 1600px) {
+    .block_myoverview {
+        .courses-view-course-item {
+            height: 190px;
+        }
+    }
+}
index 5e0c53f..65d3382 100644 (file)
@@ -642,6 +642,7 @@ a.ygtvspacer:hover {
     */
     /*rtl:ignore*/
     background-position: bottom right;
+    background-size: 16px 16px;
 }
 
 .fp-iconview .fp-file.fp-isreference .fp-reficons2 {
@@ -651,6 +652,7 @@ a.ygtvspacer:hover {
     */
     /*rtl:ignore*/
     background-position: bottom left;
+    background-size: 16px 16px;
 }
 
 .filemanager .fp-iconview .fp-file.fp-originalmissing .fp-thumbnail img {
@@ -700,6 +702,7 @@ a.ygtvspacer:hover {
     position: absolute;
     top: 8px;
     left: 17px;
+    background-size: 16px 16px;
 }
 
 .filemanager .fp-filename-icon.fp-isreference .fp-reficons2 {
@@ -712,6 +715,7 @@ a.ygtvspacer:hover {
     position: absolute;
     top: 9px;
     left: -6px;
+    background-size: 16px 16px;
 }
 // Folder Context Menu (File Manager only)
 .filemanager .fp-contextmenu {
index d1826a6..a0ad54e 100644 (file)
@@ -25,6 +25,7 @@ $icon-big-height: 64px;
     &.iconsize-big {
         width: $icon-big-width;
         height: $icon-big-height;
+        font-size: $icon-big-height;
     }
 }
 
index caf8d62..c42131d 100644 (file)
     }
 
     .empty-placeholder-image-sm {
-        height: 70px;
-        width: 85px;
+        height: 50px;
     }
 
     .empty-placeholder-image-lg {
         height: 125px;
-        width: 145px;
     }
 
     .courses-view-course-item {
index cc7e9c6..d8496eb 100644 (file)
@@ -16406,12 +16406,10 @@ body {
   display: block;
 }
 .block_myoverview .empty-placeholder-image-sm {
-  height: 70px;
-  width: 85px;
+  height: 50px;
 }
 .block_myoverview .empty-placeholder-image-lg {
   height: 125px;
-  width: 145px;
 }
 .block_myoverview .courses-view-course-item {
   height: 220px;
index 555966e..f6bcb03 100644 (file)
@@ -28,6 +28,7 @@
      data-limit="{{$limit}}20{{/limit}}"
      data-course-id="{{$courseid}}{{/courseid}}"
      data-last-id="{{$lastid}}{{/lastid}}"
+     data-midnight="{{midnight}}"
      id="event-list-container-{{$courseid}}{{/courseid}}">
 
     <div data-region="event-list-content">
             </button>
         </div>
     </div>
-    <div class="hidden text-xs-center text-center" data-region="empty-message">
+    <div class="hidden text-xs-center text-center m-y-3" data-region="empty-message">
         <img class="empty-placeholder-image-sm"
              src="{{urls.noevents}}"
-             alt="{{#str}} noevents, block_myoverview {{/str}}">
+             alt="{{#str}} noevents, block_myoverview {{/str}}"
+             role="presentation">
         <p class="text-muted m-t-1">{{#str}} noevents, block_myoverview {{/str}}</p>
-        <a href="{{viewurl}}" class="btn btn-secondary text-primary">
+        <a href="{{viewurl}}" class="btn btn-secondary text-primary"
+           aria-label="{{#str}} viewcoursename, block_myoverview, {{fullnamedisplay}} {{/str}}">
             {{#str}} viewcourse, block_myoverview {{/str}}
         </a>
     </div>
index 45a38d5..950ab5a 100644 (file)
@@ -51,7 +51,8 @@
                 <div class="text-xs-center text-center m-t-3">
                     <img class="empty-placeholder-image-lg"
                          src="{{urls.nocourses}}"
-                         alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}">
+                         alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}"
+                         role="presentation">
                     <p class="text-muted m-t-1">{{#str}} nocoursesinprogress, block_myoverview {{/str}}</p>
                 </div>
             {{/inprogress}}
@@ -69,7 +70,8 @@
                 <div class="text-xs-center text-center m-t-3">
                     <img class="empty-placeholder-image-lg"
                          src="{{urls.nocourses}}"
-                         alt="{{#str}} nocoursesfuture, block_myoverview {{/str}}">
+                         alt="{{#str}} nocoursesfuture, block_myoverview {{/str}}"
+                         role="presentation">
                     <p class="text-muted m-t-1">{{#str}} nocoursesfuture, block_myoverview {{/str}}</p>
                 </div>
             {{/future}}
@@ -87,7 +89,8 @@
                 <div class="text-xs-center text-center m-t-3">
                     <img class="empty-placeholder-image-lg"
                          src="{{urls.nocourses}}"
-                         alt="{{#str}} nocoursespast, block_myoverview {{/str}}">
+                         alt="{{#str}} nocoursespast, block_myoverview {{/str}}"
+                         role="presentation">
                     <p class="text-muted m-t-1">{{#str}} nocoursespast, block_myoverview {{/str}}</p>
                 </div>
             {{/past}}
index 17ca4b7..b631505 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2017042800.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2017050300.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
-$release  = '3.3beta+ (Build: 20170428)'; // Human-friendly version name
+$release  = '3.3beta+ (Build: 20170503)'; // Human-friendly version name
 
 $branch   = '33';                       // This version's branch.
 $maturity = MATURITY_BETA;             // This version's maturity level.