Merge branch 'wip-MDL-40408-master' of git://github.com/marinaglancy/moodle
authorDan Poltawski <dan@moodle.com>
Mon, 10 Jul 2017 13:36:40 +0000 (14:36 +0100)
committerDan Poltawski <dan@moodle.com>
Mon, 10 Jul 2017 13:36:40 +0000 (14:36 +0100)
310 files changed:
.travis.yml
admin/category.php
admin/cli/build_theme_css.php [new file with mode: 0644]
admin/cli/mysql_collation.php
admin/registration/forms.php
admin/roles/usersroles.php
admin/settings/appearance.php
admin/settings/security.php
admin/settings/subsystems.php
admin/tool/filetypes/lang/en/tool_filetypes.php
admin/tool/langimport/index.php
admin/tool/log/backup/moodle2/restore_tool_log_logstore_subplugin.class.php
admin/tool/log/store/database/classes/log/store.php
admin/tool/log/store/database/lang/en/logstore_database.php
admin/tool/log/store/database/settings.php
admin/tool/log/store/database/test_settings.php
admin/tool/log/store/database/tests/store_test.php
admin/tool/log/store/database/upgrade.txt [new file with mode: 0644]
admin/tool/log/store/database/version.php
admin/tool/lp/templates/competency_grader.mustache
admin/tool/lp/templates/competency_rule_config.mustache
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/mobile/lib.php
admin/tool/mobile/settings.php
admin/tool/oauth2/classes/form/issuer.php
admin/tool/oauth2/lang/en/tool_oauth2.php
admin/tool/task/lang/en/tool_task.php
admin/tool/uploadcourse/classes/course.php
admin/tool/uploadcourse/classes/step2_form.php
admin/tool/uploadcourse/cli/uploadcourse.php
admin/tool/uploadcourse/tests/course_test.php
auth/cas/CAS/CAS/Client.php
auth/cas/CAS/moodle_readme.txt
auth/classes/external.php
auth/oauth2/classes/api.php
auth/oauth2/classes/auth.php
auth/oauth2/tests/api_test.php
availability/classes/info_section.php
availability/condition/date/classes/condition.php
backup/backup.php
backup/import.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.php
backup/moodle2/tests/moodle2_test.php
backup/restore.php
blocks/completionstatus/details.php
blocks/html/classes/search/content.php [new file with mode: 0644]
blocks/html/lang/en/block_html.php
blocks/html/lib.php
blocks/html/tests/search_content_test.php [new file with mode: 0644]
blocks/login/block_login.php
blocks/moodleblock.class.php
blocks/myoverview/amd/build/tab_preferences.min.js [new file with mode: 0644]
blocks/myoverview/amd/src/tab_preferences.js [new file with mode: 0644]
blocks/myoverview/block_myoverview.php
blocks/myoverview/classes/output/courses_view.php
blocks/myoverview/classes/output/main.php
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/lib.php [new file with mode: 0644]
blocks/myoverview/settings.php [new file with mode: 0644]
blocks/myoverview/templates/main.mustache
blocks/myoverview/tests/behat/block_myoverview_progress.feature
blocks/myoverview/version.php
blocks/navigation/amd/build/ajax_response_renderer.min.js
blocks/navigation/amd/src/ajax_response_renderer.js
blocks/navigation/styles.css
blocks/participants/tests/behat/block_participants_course.feature
blocks/settings/styles.css
blocks/upgrade.txt
cache/classes/factory.php
cache/classes/helper.php
cache/tests/cache_test.php
calendar/classes/local/event/container.php
calendar/export.php
calendar/lib.php
calendar/managesubscriptions.php
calendar/renderer.php
calendar/tests/container_test.php
calendar/tests/externallib_test.php
calendar/view.php
completion/completion_completion.php
course/format/renderer.php
course/lib.php
course/publish/backup.php
course/renderer.php
course/switchrole.php
course/tests/courselib_test.php
course/tests/externallib_test.php
dataformat/html/classes/writer.php
dataformat/json/classes/writer.php
dataformat/upgrade.txt
enrol/guest/locallib.php
enrol/ldap/lib.php
enrol/self/locallib.php
filter/urltolink/filter.php
filter/urltolink/tests/filter_test.php
grade/edit/outcome/export.php
grade/edit/outcome/import.php
grade/edit/scale/edit.php
grade/edit/scale/index.php
grade/edit/settings/index.php
grade/edit/tree/action.php
grade/edit/tree/calculation.php
grade/edit/tree/category.php
grade/edit/tree/grade.php
grade/edit/tree/index.php
grade/edit/tree/item.php
grade/edit/tree/item_form.php
grade/edit/tree/outcomeitem.php
grade/export/keymanager.php
grade/export/ods/dump.php
grade/export/ods/export.php
grade/export/ods/index.php
grade/export/txt/dump.php
grade/export/txt/export.php
grade/export/txt/index.php
grade/export/xls/dump.php
grade/export/xls/export.php
grade/export/xls/index.php
grade/export/xml/dump.php
grade/export/xml/export.php
grade/export/xml/index.php
grade/import/csv/index.php
grade/import/direct/index.php
grade/import/keymanager.php
grade/import/xml/fetch.php
grade/import/xml/import.php
grade/import/xml/index.php
grade/report/grader/ajax_callbacks.php
grade/report/grader/index.php
grade/report/grader/preferences.php
grade/report/grader/quickedit_item.php
grade/report/index.php
grade/report/outcomes/index.php
grade/report/overview/index.php
grade/report/singleview/index.php
grade/report/user/index.php
group/classes/output/index_page.php [new file with mode: 0644]
group/classes/output/renderer.php [new file with mode: 0644]
group/externallib.php
group/groupings.php
group/index.php
group/templates/index.mustache [new file with mode: 0644]
group/tests/externallib_test.php
install/lang/no/install.php
install/lang/pt_br/error.php
install/lang/zh_tw/admin.php
lang/en/admin.php
lang/en/blog.php
lang/en/cache.php
lang/en/calendar.php
lang/en/hub.php
lang/en/moodle.php
lang/en/repository.php
lib/adminlib.php
lib/amd/build/ajax.min.js
lib/amd/build/notification.min.js
lib/amd/src/ajax.js
lib/amd/src/notification.js
lib/blocklib.php
lib/classes/dataformat/base.php
lib/classes/dataformat/spout_base.php
lib/classes/oauth2/issuer.php
lib/classes/output/icon_system_fontawesome.php
lib/classes/output/mustache_javascript_helper.php
lib/classes/plugin_manager.php
lib/classes/plugininfo/repository.php
lib/classes/user.php
lib/completionlib.php
lib/csslib.php
lib/dataformatlib.php
lib/db/caches.php
lib/db/install.xml [changed mode: 0644->0755]
lib/db/services.php
lib/db/upgrade.php
lib/filelib.php
lib/filestorage/zip_archive.php
lib/form/filetypes.php
lib/form/tests/behat/filetypes.feature [new file with mode: 0644]
lib/form/tests/fixtures/filetypes.php [new file with mode: 0644]
lib/navigationlib.php
lib/outputcomponents.php
lib/outputlib.php
lib/outputrenderers.php
lib/phpunit/classes/util.php
lib/setuplib.php
lib/tablelib.php
lib/templates/loginform.mustache
lib/testing/generator/block_generator.php
lib/tests/admintree_test.php
lib/tests/behat/alpha_chooser.feature
lib/tests/blocklib_test.php
lib/tests/filelib_test.php
lib/upgrade.txt
lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-debug.js
lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm-min.js
lib/yui/build/moodle-core-languninstallconfirm/moodle-core-languninstallconfirm.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-debug.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-min.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception.js
lib/yui/src/languninstallconfirm/js/languninstallconfirm.js
lib/yui/src/notification/js/exception.js
login/forgot_password_form.php
login/index.php
login/lib.php
login/signup.php
login/signup_form.php
login/tests/lib_test.php [new file with mode: 0644]
media/player/videojs/amd/build/loader.min.js
media/player/videojs/amd/src/loader.js
media/player/videojs/classes/plugin.php
media/player/videojs/settings.php
media/player/videojs/tests/player_test.php
message/amd/build/message_repository.min.js
message/amd/src/message_repository.js
message/classes/api.php
message/output/popup/amd/build/notification_repository.min.js
message/output/popup/amd/src/notification_repository.js
message/output/popup/classes/api.php
mod/assign/backup/moodle2/restore_assign_stepslib.php
mod/assign/db/upgrade.php
mod/assign/feedback/comments/locallib.php
mod/assign/feedback/comments/tests/behat/feedback_comments.feature [new file with mode: 0644]
mod/assign/gradingtable.php
mod/assign/locallib.php
mod/assign/override_form.php
mod/assign/submission/file/lang/en/assignsubmission_file.php
mod/assign/submission/file/locallib.php
mod/assign/submission/file/settings.php
mod/assign/tests/markerallocation_test.php [new file with mode: 0644]
mod/assign/version.php
mod/data/field/latlong/field.class.php
mod/feedback/classes/completion.php
mod/feedback/classes/external.php
mod/feedback/lang/en/feedback.php
mod/feedback/lib.php
mod/forum/user.php
mod/glossary/lang/en/glossary.php
mod/lesson/classes/external.php
mod/lesson/lang/en/lesson.php
mod/lesson/locallib.php
mod/lesson/pagetypes/branchtable.php
mod/lesson/tests/external_test.php
mod/lesson/upgrade.txt
mod/lesson/view.php
mod/lti/db/access.php
mod/lti/db/services.php
mod/lti/lang/en/lti.php
mod/lti/locallib.php
mod/lti/version.php
mod/quiz/report.php
mod/quiz/report/statistics/report.php
mod/resource/locallib.php
mod/scorm/lib.php
mod/scorm/tests/lib_test.php
mod/workshop/lang/en/workshop.php
mod/workshop/submission_form.php
my/lib.php
phpunit.xml.dist
pix/i/mahara_host.gif [deleted file]
pix/i/mahara_host.png [new file with mode: 0644]
pix/i/mahara_host.svg [new file with mode: 0644]
pluginfile.php
question/type/multichoice/classes/admin_setting_answernumbering.php [new file with mode: 0644]
question/type/multichoice/edit_multichoice_form.php
question/type/multichoice/lang/en/qtype_multichoice.php
question/type/multichoice/renderer.php
question/type/multichoice/settings.php [new file with mode: 0644]
report/log/tests/behat/filter_log.feature
report/log/tests/behat/user_log.feature
report/stats/locallib.php
report/stats/user.php
repository/googledocs/lib.php
repository/onedrive/lib.php
repository/repository_callback.php
search/classes/base.php
search/classes/base_block.php [new file with mode: 0644]
search/classes/manager.php
search/tests/base_block_test.php [new file with mode: 0644]
search/tests/fixtures/mock_block_area.php [new file with mode: 0644]
search/tests/manager_test.php
tag/classes/collection.php
theme/boost/config.php
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/user.scss
theme/boost/templates/core/availability_info.mustache
theme/boost/templates/header.mustache
theme/bootstrapbase/less/moodle/blocks.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/tool_usertours.less
theme/bootstrapbase/less/moodle/user.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/block_myoverview/main.mustache
theme/bootstrapbase/templates/core/availability_info.mustache
theme/clean/classes/core_renderer.php
theme/more/config.php
theme/styles.php
user/classes/participants_table.php [new file with mode: 0644]
user/externallib.php
user/files.php
user/index.php
user/lib.php
user/tests/behat/view_full_profile.feature
user/tests/externallib_test.php
user/tests/userlib_test.php
version.php
webservice/externallib.php
webservice/tests/externallib_test.php
webservice/upgrade.txt

index ff2b674..d58effa 100644 (file)
@@ -2,7 +2,7 @@
 # process (which uses our internal CI system) this file is here for the benefit
 # of community developers git clones - see MDL-51458.
 
-sudo: false
+sudo: required
 
 # We currently disable Travis notifications entirely until https://github.com/travis-ci/travis-ci/issues/4976
 # is fixed.
@@ -18,6 +18,10 @@ php:
 
 addons:
   postgresql: "9.3"
+  packages:
+    - mysql-server-5.6
+    - mysql-client-core-5.6
+    - mysql-client-5.6
 
 services:
     - redis-server
@@ -73,6 +77,28 @@ cache:
       - $HOME/.npm
 
 install:
+    - sudo apt-get -y install haveged
+    - sudo service haveged start
+    - >
+        if [ "$DB" = 'mysqli' ];
+        then
+            sudo mkdir /mnt/ramdisk
+            sudo mount -t tmpfs -o size=1024m tmpfs /mnt/ramdisk
+            sudo stop mysql
+            sudo mv /var/lib/mysql /mnt/ramdisk
+            sudo ln -s /mnt/ramdisk/mysql /var/lib/mysql
+            sudo start mysql
+        fi
+    - >
+        if [ "$DB" = 'pgsql' ];
+        then
+            sudo mkdir /mnt/ramdisk
+            sudo mount -t tmpfs -o size=1024m tmpfs /mnt/ramdisk
+            sudo service postgresql stop
+            sudo mv /var/lib/postgresql /mnt/ramdisk
+            sudo ln -s /mnt/ramdisk/postgresql /var/lib/postgresql
+            sudo service postgresql start 9.3
+        fi
     - >
         if [ "$TASK" = 'PHPUNIT' ];
         then
index cb83e0d..0a80394 100644 (file)
@@ -89,7 +89,7 @@ if ($PAGE->user_allowed_editing()) {
 $savebutton = false;
 $outputhtml = '';
 foreach ($settingspage->children as $childpage) {
-    if ($childpage->is_hidden()) {
+    if ($childpage->is_hidden() || !$childpage->check_access()) {
         continue;
     }
     if ($childpage instanceof admin_externalpage) {
diff --git a/admin/cli/build_theme_css.php b/admin/cli/build_theme_css.php
new file mode 100644 (file)
index 0000000..30cd996
--- /dev/null
@@ -0,0 +1,119 @@
+<?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/>.
+
+/**
+ * Build and store theme CSS.
+ *
+ * @package    core
+ * @subpackage cli
+ * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+
+require(__DIR__.'/../../config.php');
+require_once("$CFG->libdir/clilib.php");
+require_once("$CFG->libdir/csslib.php");
+require_once("$CFG->libdir/outputlib.php");
+
+$longparams = [
+    'themes'    => null,
+    'direction' => null,
+    'help'      => false,
+    'verbose'   => false
+];
+
+$shortmappings = [
+    't' => 'themes',
+    'd' => 'direction',
+    'h' => 'help',
+    'v' => 'verbose'
+];
+
+// Get CLI params.
+list($options, $unrecognized) = cli_get_params($longparams, $shortmappings);
+
+if ($unrecognized) {
+    $unrecognized = implode("\n  ", $unrecognized);
+    cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
+}
+
+if ($options['help']) {
+    echo
+"Compile the CSS for one or more installed themes.
+Existing CSS caches will replaced.
+By default all themes will be recompiled unless otherwise specified.
+
+Options:
+-t, --themes    A comma separated list of themes to be compiled
+-d, --direction Only compile a single direction (either ltr or rtl)
+-v, --verbose   Print info comments to stdout
+-h, --help      Print out this help
+
+Example:
+\$ sudo -u www-data /usr/bin/php admin/cli/build_theme_css.php --themes=boost --direction=ltr
+";
+
+    die;
+}
+
+if (empty($options['verbose'])) {
+    $trace = new null_progress_trace();
+} else {
+    $trace = new text_progress_trace();
+}
+
+cli_heading('Build theme css');
+
+// Determine which themes we need to build.
+$themenames = [];
+if (is_null($options['themes'])) {
+    $trace->output('No themes specified. Finding all installed themes.');
+    $themenames = array_keys(core_component::get_plugin_list('theme'));
+} else {
+    if (is_string($options['themes'])) {
+        $themenames = explode(',', $options['themes']);
+    } else {
+        cli_error('--themes must be a comma separated list of theme names');
+    }
+}
+
+$trace->output('Checking that each theme is correctly installed...');
+$themeconfigs = [];
+foreach ($themenames as $themename) {
+    if (is_null(theme_get_config_file_path($themename))) {
+        cli_error("Unable to find theme config for {$themename}");
+    }
+
+    // Load the config for the theme.
+    $themeconfigs[] = theme_config::load($themename);
+}
+
+$directions = ['ltr', 'rtl'];
+
+if (!is_null($options['direction'])) {
+    if (!in_array($options['direction'], $directions)) {
+         cli_error("--direction must be either ltr or rtl");
+    }
+
+    $directions = [$options['direction']];
+}
+
+$trace->output('Building CSS for themes: ' . implode(', ', $themenames));
+theme_build_css_for_themes($themeconfigs, $directions);
+
+exit(0);
index adaadbb..ae3c709 100644 (file)
@@ -53,7 +53,7 @@ Options:
 -h, --help            Print out this help
 
 Example:
-\$ sudo -u www-data /usr/bin/php admin/cli/mysql_collation.php --collation=utf8_general_ci
+\$ sudo -u www-data /usr/bin/php admin/cli/mysql_collation.php --collation=utf8mb4_unicode_ci
 ";
 
 if (!empty($options['collation'])) {
@@ -145,9 +145,22 @@ if (!empty($options['collation'])) {
             $skipped++;
 
         } else {
-            $DB->change_database_structure("ALTER TABLE $table->name DEFAULT CHARACTER SET $charset DEFAULT COLLATE = $collation");
-            echo "CONVERTED\n";
-            $converted++;
+            try {
+                $DB->change_database_structure("ALTER TABLE $table->name CONVERT TO CHARACTER SET $charset COLLATE $collation");
+                echo "CONVERTED\n";
+                $converted++;
+            } catch (ddl_exception $e) {
+                $result = mysql_set_row_format($table->name, $charset, $collation, $engine);
+                if ($result) {
+                    echo "CONVERTED\n";
+                    $converted++;
+                } else {
+                    // We don't know what the problem is. Stop the conversion.
+                    cli_error("Error: Tried to convert $table->name, but there was a problem. Please check the details of this
+                            table and try again.");
+                    die();
+                }
+            }
         }
 
         $sql = "SHOW FULL COLUMNS FROM $table->name WHERE collation IS NOT NULL";
@@ -290,3 +303,26 @@ function mysql_get_column_collations($tablename) {
     $rs->close();
     return $collations;
 }
+
+function mysql_set_row_format($tablename, $charset, $collation, $engine) {
+    global $DB;
+
+    $sql = "SELECT row_format
+              FROM INFORMATION_SCHEMA.TABLES
+             WHERE table_schema = DATABASE() AND table_name = ?";
+    $rs = $DB->get_record_sql($sql, array($tablename));
+    if ($rs) {
+        if ($rs->row_format == 'Compact' || $rs->row_format == 'Redundant') {
+            $rowformat = $DB->get_row_format_sql($engine, $collation);
+            // Try to convert to compressed format and then try updating the collation again.
+            $DB->change_database_structure("ALTER TABLE $tablename $rowformat");
+            $DB->change_database_structure("ALTER TABLE $tablename CONVERT TO CHARACTER SET $charset COLLATE $collation");
+        } else {
+            // Row format may not be the problem. Can not diagnose problem. Send fail reply.
+            return false;
+        }
+    } else {
+        return false;
+    }
+    return true;
+}
index 92b15e8..6e0b8a9 100644 (file)
@@ -232,7 +232,7 @@ class site_registration_form extends moodleform {
         }
         $language = get_config('hub', 'site_language_' . $cleanhuburl);
         if ($language === false) {
-            $language = current_language();
+            $language = explode('_', current_language())[0];
         }
         $geolocation = get_config('hub', 'site_geolocation_' . $cleanhuburl);
         $contactable = get_config('hub', 'site_contactable_' . $cleanhuburl);
index 5e871e6..c455902 100644 (file)
@@ -193,8 +193,8 @@ function print_report_tree($contextid, $contexts, $systemcontext, $fullname, $al
                 $strgoto = get_string('gotoassignroles', 'core_role', $a);
                 $strcheck = get_string('checkuserspermissionshere', 'core_role', $a);
             }
-            echo ' <a title="' . $strgoto . '" href="' . $raurl . '">' . $OUTPUT->pix_icon('t/edit', 'core', $stredit) . '</a> ';
-            echo ' <a title="' . $strcheck . '" href="' . $churl . '">' . $OUTPUT->pix_icon('t/preview', 'core', $strcheckpermissions) . '</a> ';
+            echo ' <a title="' . $strgoto . '" href="' . $raurl . '">' . $OUTPUT->pix_icon('t/edit', $stredit) . '</a> ';
+            echo ' <a title="' . $strcheck . '" href="' . $churl . '">' . $OUTPUT->pix_icon('t/preview', $strcheckpermissions) . '</a> ';
             echo "</p>\n";
         }
     }
index 5a20af8..1a63f4f 100644 (file)
@@ -179,7 +179,8 @@ preferences,moodle|/user/preferences.php|preferences',
         'idnumber' => new lang_string('sort_idnumber', 'admin'),
     );
     $temp->add(new admin_setting_configselect('navsortmycoursessort', new lang_string('navsortmycoursessort', 'admin'), new lang_string('navsortmycoursessort_help', 'admin'), 'sortorder', $sortoptions));
-    $temp->add(new admin_setting_configtext('navcourselimit',new lang_string('navcourselimit','admin'),new lang_string('confignavcourselimit', 'admin'),20,PARAM_INT));
+    $temp->add(new admin_setting_configtext('navcourselimit', new lang_string('navcourselimit', 'admin'),
+        new lang_string('confignavcourselimit', 'admin'), 10, PARAM_INT));
     $temp->add(new admin_setting_configcheckbox('usesitenameforsitepages', new lang_string('usesitenameforsitepages', 'admin'), new lang_string('configusesitenameforsitepages', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('linkadmincategories', new lang_string('linkadmincategories', 'admin'), new lang_string('linkadmincategories_help', 'admin'), 1));
     $temp->add(new admin_setting_configcheckbox('linkcoursesections', new lang_string('linkcoursesections', 'admin'), new lang_string('linkcoursesections_help', 'admin'), 0));
index c36a424..34185cb 100644 (file)
@@ -18,6 +18,11 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $temp->add(new admin_setting_configcheckbox('forceloginforprofiles', new lang_string('forceloginforprofiles', 'admin'), new lang_string('configforceloginforprofiles', 'admin'), 1));
     $temp->add(new admin_setting_configcheckbox('forceloginforprofileimage', new lang_string('forceloginforprofileimage', 'admin'), new lang_string('forceloginforprofileimage_help', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('opentogoogle', new lang_string('opentogoogle', 'admin'), new lang_string('configopentogoogle', 'admin'), 0));
+    $temp->add(new admin_setting_configselect('allowindexing', new lang_string('allowindexing', 'admin'), new lang_string('allowindexing_desc', 'admin'),
+        0,
+        array(0 => new lang_string('allowindexingexceptlogin', 'admin'),
+              1 => new lang_string('allowindexingeverywhere', 'admin'),
+              2 => new lang_string('allowindexingnowhere', 'admin'))));
     $temp->add(new admin_setting_pickroles('profileroles',
         new lang_string('profileroles','admin'),
         new lang_string('configprofileroles', 'admin'),
index 6f5ded5..f2b50d5 100644 (file)
@@ -55,9 +55,6 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $optionalsubsystems->add(new admin_setting_configcheckbox('enableglobalsearch', new lang_string('enableglobalsearch', 'admin'),
         new lang_string('enableglobalsearch_desc', 'admin'), 0, 1, 0));
 
-    $choices = array();
-    $choices[0] = new lang_string('no');
-    $choices[1] = new lang_string('yes');
-    $optionalsubsystems->add(new admin_setting_configselect('allowstealth', new lang_string('allowstealthmodules'),
-        new lang_string('allowstealthmodules_help'), 0, $choices));
+    $optionalsubsystems->add(new admin_setting_configcheckbox('allowstealth', new lang_string('allowstealthmodules'),
+        new lang_string('allowstealthmodules_help'), 0, 1, 0));
 }
index 2640b44..c2a817c 100644 (file)
@@ -31,7 +31,7 @@ $string['delete_confirmation'] = 'Are you absolutely sure you want to remove <st
 $string['deletea'] = 'Delete {$a}';
 $string['deletefiletypes'] = 'Delete a file type';
 $string['description'] = 'Custom description';
-$string['description_help'] = 'Simple file type description, e.g. &lsquo;Kindle ebook&rsquo;. If your site supports multiple languages and uses the multi-language filter, you can enter multi-language tags in this field to supply a description in different languages.';
+$string['description_help'] = 'Simple file type description, e.g. \'Kindle ebook\'. If your site supports multiple languages and uses the multi-language filter, you can enter multi-language tags in this field to supply a description in different languages.';
 $string['descriptiontype'] = 'Description type';
 $string['descriptiontype_help'] = 'There are three possible ways to specify a description.
 
@@ -49,15 +49,15 @@ $string['error_defaulticon'] = 'Another file extension with the same MIME type i
 $string['error_extension'] = 'The file type extension <strong>{$a}</strong> already exists or is invalid. File extensions must be unique and must not contain special characters.';
 $string['error_notfound'] = 'The file type with extension {$a} cannot be found.';
 $string['extension'] = 'Extension';
-$string['extension_help'] = 'File name extension without the dot, e.g. &lsquo;mobi&rsquo;';
+$string['extension_help'] = 'File name extension without the dot, e.g. \'mobi\'';
 $string['groups'] = 'Type groups';
-$string['groups_help'] = 'Optional list of file type groups that this type belongs to. These are generic categories such as &lsquo;document&rsquo; and &lsquo;image&rsquo;.';
+$string['groups_help'] = 'Optional list of file type groups that this type belongs to. These are generic categories such as \'document\' and \'image\'.';
 $string['icon'] = 'File icon';
 $string['icon_help'] = 'Icon filename.
 
 The list of icons is taken from the /pix/f directory inside your Moodle installation. You can add custom icons to this folder if required.';
 $string['mimetype'] = 'MIME type';
-$string['mimetype_help'] = 'MIME type associated with this file type, e.g. &lsquo;application/x-mobipocket-ebook&rsquo;';
+$string['mimetype_help'] = 'MIME type associated with this file type, e.g. \'application/x-mobipocket-ebook\'';
 $string['pluginname'] = 'File types';
 $string['revert'] = 'Restore {$a} to Moodle defaults';
 $string['revert_confirmation'] = 'Are you sure you want to restore <strong>.{$a}</strong> to Moodle defaults, discarding your changes?';
index 0e98a5d..92a09da 100644 (file)
@@ -40,7 +40,7 @@ if (empty($CFG->langotherroot)) {
 $mode               = optional_param('mode', 0, PARAM_INT);              // action
 $pack               = optional_param_array('pack', array(), PARAM_SAFEDIR);    // pack to install
 $uninstalllang      = optional_param_array('uninstalllang', array(), PARAM_LANG);// installed pack to uninstall
-$confirmtounistall  = optional_param('confirmtouninstall', '', PARAM_ALPHAEXT);  // uninstallation confirmation
+$confirmtounistall  = optional_param('confirmtouninstall', '', PARAM_SAFEPATH);  // uninstallation confirmation
 $purgecaches        = optional_param('purgecaches', false, PARAM_BOOL);  // explicit caches reset
 
 if ($purgecaches) {
@@ -74,7 +74,7 @@ if ($mode == DELETION_OF_SELECTED_LANG and (!empty($uninstalllang) or !empty($co
     // Actually deleting languages, languages to delete are passed as GET parameter as string
     // ...need to populate them to array.
     if (empty($uninstalllang)) {
-        $uninstalllang = explode('-', $confirmtounistall);
+        $uninstalllang = explode('/', $confirmtounistall);
     }
 
     if (in_array('en', $uninstalllang)) {
@@ -84,8 +84,10 @@ if ($mode == DELETION_OF_SELECTED_LANG and (!empty($uninstalllang) or !empty($co
     } else if (empty($confirmtounistall) and confirm_sesskey()) { // User chose langs to be deleted, show confirmation.
         echo $OUTPUT->header();
         echo $OUTPUT->confirm(get_string('uninstallconfirm', 'tool_langimport', implode(', ', $uninstalllang)),
-                     'index.php?mode='.DELETION_OF_SELECTED_LANG.'&confirmtouninstall='.implode('-', $uninstalllang),
-                     'index.php');
+            new moodle_url($PAGE->url, array(
+                'mode' => DELETION_OF_SELECTED_LANG,
+                'confirmtouninstall' => implode('/', $uninstalllang),
+            )), $PAGE->url);
         echo $OUTPUT->footer();
         die;
 
index 0c00f00..c48c2c8 100644 (file)
@@ -84,8 +84,7 @@ abstract class restore_tool_log_logstore_subplugin extends restore_subplugin {
             }
         }
 
-        // Roll dates.
-        $data->timecreated = $this->apply_date_offset($data->timecreated);
+        // There is no need to roll dates. Logs are supposed to be immutable. See MDL-44961.
 
         // Revert other to its original php way.
         $data->other = unserialize(base64_decode($data->other));
index cddf3f9..10398df 100644 (file)
@@ -88,6 +88,7 @@ class store implements \tool_log\log\writer, \core\log\sql_reader {
         $dboptions['dbport'] = $this->get_config('dbport', '');
         $dboptions['dbschema'] = $this->get_config('dbschema', '');
         $dboptions['dbcollation'] = $this->get_config('dbcollation', '');
+        $dboptions['dbhandlesoptions'] = $this->get_config('dbhandlesoptions', false);
         try {
             $db->connect($this->get_config('dbhost'), $this->get_config('dbuser'), $this->get_config('dbpass'),
                 $this->get_config('dbname'), false, $dboptions);
index 4e81a4a..0873229 100644 (file)
@@ -31,6 +31,8 @@ $string['databasesettings_help'] = 'Connection details for the external log data
 $string['databasepersist'] = 'Persistent database connections';
 $string['databaseschema'] = 'Database schema';
 $string['databasecollation'] = 'Database collation';
+$string['databasehandlesoptions'] = 'Database handles options';
+$string['databasehandlesoptions_help'] = 'Does the remote database handle its own options.';
 $string['databasetable'] = 'Database table';
 $string['databasetable_help'] = 'Name of the table where logs will be stored. This table should have a structure identical to the one used by logstore_standard (mdl_logstore_standard_log).';
 $string['includeactions'] = 'Include actions of these types';
index 5b930f4..4e45038 100644 (file)
@@ -54,6 +54,8 @@ if ($hassiteconfig) {
         'logstore_database'), '', ''));
     $settings->add(new admin_setting_configtext('logstore_database/dbcollation', get_string('databasecollation',
         'logstore_database'), '', ''));
+    $settings->add(new admin_setting_configcheckbox('logstore_database/dbhandlesoptions', get_string('databasehandlesoptions',
+        'logstore_database'), get_string('databasehandlesoptions_help', 'logstore_database'), '0'));
     $settings->add(new admin_setting_configtext('logstore_database/buffersize', get_string('buffersize',
         'logstore_database'), get_string('buffersize_help', 'logstore_database'), 50));
 
index ce35bc8..21a470e 100644 (file)
@@ -64,6 +64,7 @@ $dboptions['dbsocket'] = get_config('logstore_database', 'dbsocket');
 $dboptions['dbport'] = get_config('logstore_database', 'dbport');
 $dboptions['dbschema'] = get_config('logstore_database', 'dbschema');
 $dboptions['dbcollation'] = get_config('logstore_database', 'dbcollation');
+$dboptions['dbhandlesoptions'] = get_config('logstore_database', 'dbhandlesoptions');
 
 try {
     $db->connect(get_config('logstore_database', 'dbhost'), get_config('logstore_database', 'dbuser'),
index e40a8a4..a05c33f 100644 (file)
@@ -83,6 +83,11 @@ class logstore_database_store_testcase extends advanced_testcase {
         } else {
             set_config('dbcollation', '', 'logstore_database');
         }
+        if (!empty($CFG->dboptions['dbhandlesoptions'])) {
+            set_config('dbhandlesoptions', $CFG->dboptions['dbhandlesoptions'], 'logstore_database');
+        } else {
+            set_config('dbhandlesoptions', false, 'logstore_database');
+        }
 
         // Enable logging plugin.
         set_config('enabled_stores', 'logstore_database', 'tool_log');
diff --git a/admin/tool/log/store/database/upgrade.txt b/admin/tool/log/store/database/upgrade.txt
new file mode 100644 (file)
index 0000000..a8fa834
--- /dev/null
@@ -0,0 +1,11 @@
+This files describes API changes in the logstore_database code.
+
+=== 3.4 ===
+* PostgreSQL connections now use advanced options to reduce connection overhead.  These options are not compatible
+  with some connection poolers.  The dbhandlesoptions parameter has been added to allow the database to configure the
+  required defaults. The parameters that are required in the database are;
+    ALTER DATABASE moodle SET client_encoding = UTF8;
+    ALTER DATABASE moodle SET standard_conforming_strings = on;
+    ALTER DATABASE moodle SET search_path = 'moodle,public';  -- Optional, if you wish to use a custom schema.
+  You can set these options against the database or the moodle user who connects.
+
index 40d4db1..3105716 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version = 2017051500; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version = 2017062600; // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires = 2017050500; // Requires this Moodle version.
 $plugin->component = 'logstore_database'; // Full name of the plugin (used for diagnostics).
index 20060b7..19d1ea0 100644 (file)
@@ -51,7 +51,7 @@
             </div>
         </div>
         <div data-region="footer" class="pull-xs-right">
-            <input type="button" data-action="rate" value="{{#str}}rate, tool_lp{{/str}}" class="btn btn-primary">
+            <button data-action="rate" class="btn btn-primary">{{#str}}rate, tool_lp{{/str}}</button>
             <button data-action="cancel" class="btn btn-secondary">{{#str}}cancel{{/str}}</button>
         </div>
         <div class="clearfix"></div>
index b0e9db8..59f4d57 100644 (file)
@@ -60,7 +60,7 @@
         <div data-region="rule-base" class="form">
             <div data-region="rule-outcome" class="form-group">
                 <label>{{#str}}outcome, tool_lp{{/str}}</label>
-                <select name="outcome" class="custom-select" ng-label="{{#str}}outcome, tool_lp{{/str}}">
+                <select name="outcome" class="custom-select">
                     {{#outcomes}}
                     <option value="{{code}}" {{#selected}}selected{{/selected}}>{{name}}</option>
                     {{/outcomes}}
@@ -68,7 +68,7 @@
             </div>
             <div data-region="rule-type" class="form-group">
                 <label>{{#str}}when, tool_lp{{/str}}</label>
-                <select name="rule" class="custom-select" ng-label="{{#str}}when, tool_lp{{/str}}">
+                <select name="rule" class="custom-select">
                     <option value="-1">{{#str}}choosedots{{/str}}</option>
                     {{#rules}}
                     <option value="{{type}}" {{#selected}}selected{{/selected}}>{{name}}</option>
         {{/config}}
     </div>
 
-    <div data-region="footer" class="pull-xs-right">
+    <div data-region="footer" class="pull-xs-right m-t-1">
         {{#config}}
         <input type="button" class="btn btn-primary" data-action="save" value="{{#str}}savechanges{{/str}}"/>
         {{/config}}
         <input type="button" class="btn btn-secondary" data-action="cancel" value="{{#str}}cancel{{/str}}"/>
     </div>
+    <div class="clearfix"></div>
 </div>
index 56e929a..9955cb6 100644 (file)
@@ -62,14 +62,19 @@ $string['logininthebrowser'] = 'Via a browser window (for SSO plugins)';
 $string['loginintheembeddedbrowser'] = 'Via an embedded browser (for SSO plugins)';
 $string['mainmenu'] = 'Main menu';
 $string['mobileapp'] = 'Mobile app';
+$string['mobileappconnected'] = 'Mobile app connected';
+$string['mobileappenabled'] = 'This site has mobile app access enabled.<br /><a href="{$a}">Download the mobile app</a>.';
 $string['mobileappearance'] = 'Mobile appearance';
 $string['mobileauthentication'] = 'Mobile authentication';
 $string['mobilecssurl'] = 'CSS';
 $string['mobilefeatures'] = 'Mobile features';
 $string['mobilesettings'] = 'Mobile settings';
 $string['pluginname'] = 'Moodle Mobile tools';
+$string['setuplink'] = 'App download page';
+$string['setuplink_desc'] = 'URL of page with links to download the mobile app from the App Store and Google Play.';
 $string['smartappbanners'] = 'App Banners';
 $string['pluginnotenabledorconfigured'] = 'Plugin not enabled or configured.';
 $string['remoteaddons'] = 'Remote add-ons';
 $string['typeoflogin'] = 'Type of login';
 $string['typeoflogin_desc'] = 'If the site uses a SSO authentication method, then select via a browser window or via an embedded browser. An embedded browser provides a better user experience, though it doesn\'t work with all SSO plugins. If using SSO, autologinguests should be disabled.';
+$string['getmoodleonyourmobile'] = 'Get Moodle on your mobile';
index de43b81..0ecfeff 100644 (file)
@@ -50,3 +50,94 @@ function tool_mobile_before_standard_html_head() {
     }
     return $output;
 }
+
+/**
+ * Generate the app download url to promote moodle mobile.
+ *
+ * @return moodle_url|void App download moodle_url object or return if setuplink is not set.
+ */
+function tool_mobile_create_app_download_url() {
+    global $CFG;
+
+    $mobilesettings = get_config('tool_mobile');
+
+    if (empty($mobilesettings->setuplink)) {
+        return;
+    }
+
+    $downloadurl = new moodle_url($mobilesettings->setuplink);
+    $downloadurl->param('version', $CFG->version);
+    $downloadurl->param('lang', current_language());
+
+    if (!empty($mobilesettings->iosappid)) {
+        $downloadurl->param('iosappid', $mobilesettings->iosappid);
+    }
+
+    if (!empty($mobilesettings->androidappid)) {
+        $downloadurl->param('androidappid', $mobilesettings->androidappid);
+    }
+
+    return $downloadurl;
+}
+
+/**
+ * User profile page callback.
+ *
+ * Used add a section about the moodle mobile app.
+ *
+ * @param \core_user\output\myprofile\tree $tree My profile tree where the setting will be added.
+ * @param stdClass $user The user object.
+ * @param bool $iscurrentuser Is this the current user viewing
+ * @return void Return if the mobile web services setting is disabled or if not the current user.
+ */
+function tool_mobile_myprofile_navigation(\core_user\output\myprofile\tree $tree, $user, $iscurrentuser) {
+    global $CFG, $DB;
+
+    if (empty($CFG->enablemobilewebservice)) {
+        return;
+    }
+
+    if (!$iscurrentuser) {
+        return;
+    }
+
+    if (!$url = tool_mobile_create_app_download_url()) {
+        return;
+    }
+
+    $sql = "SELECT 1
+              FROM {external_tokens} t, {external_services} s
+             WHERE t.externalserviceid = s.id
+               AND s.enabled = 1
+               AND s.shortname IN ('moodle_mobile_app', 'local_mobile')
+               AND t.userid = ?";
+    $userhastoken = $DB->record_exists_sql($sql, [$user->id]);
+
+    $mobilecategory = new core_user\output\myprofile\category('mobile', get_string('mobileapp', 'tool_mobile'),
+            'loginactivity');
+    $tree->add_category($mobilecategory);
+
+    if ($userhastoken) {
+        $mobilestr = get_string('mobileappconnected', 'tool_mobile');
+    } else {
+        $mobilestr = get_string('mobileappenabled', 'tool_mobile', $url->out());
+    }
+
+    $node = new  core_user\output\myprofile\node('mobile', 'mobileappnode', $mobilestr, null);
+    $tree->add_node($node);
+}
+
+/**
+ * Callback to add footer elements.
+ *
+ * @return str valid html footer content
+ * @since  Moodle 3.4
+ */
+function tool_mobile_standard_footer_html() {
+    global $CFG;
+    $output = '';
+    if (!empty($CFG->enablemobilewebservice) && $url = tool_mobile_create_app_download_url()) {
+        $output .= html_writer::link($url, get_string('getmoodleonyourmobile', 'tool_mobile'));
+    }
+    return $output;
+}
index 4f8630c..8af64c6 100644 (file)
@@ -83,6 +83,9 @@ if ($hassiteconfig) {
         $temp->add(new admin_setting_configtext('tool_mobile/androidappid', new lang_string('androidappid', 'tool_mobile'),
                     new lang_string('androidappid_desc', 'tool_mobile'), 'com.moodle.moodlemobile', PARAM_NOTAGS));
 
+        $temp->add(new admin_setting_configtext('tool_mobile/setuplink', new lang_string('setuplink', 'tool_mobile'),
+            new lang_string('setuplink_desc', 'tool_mobile'), 'https://download.moodle.org/mobile', PARAM_URL));
+
         $ADMIN->add('mobileapp', $temp);
 
         // Features related settings.
index 3a0b64d..a57b4c2 100644 (file)
@@ -119,6 +119,10 @@ class issuer extends persistent {
         $mform->addElement('checkbox', 'showonloginpage', get_string('issuershowonloginpage', 'tool_oauth2'));
         $mform->addHelpButton('showonloginpage', 'issuershowonloginpage', 'tool_oauth2');
 
+        // Require confirmation email for new accounts.
+        $mform->addElement('advcheckbox', 'requireconfirmation', get_string('issuerrequireconfirmation', 'tool_oauth2'));
+        $mform->addHelpButton('requireconfirmation', 'issuerrequireconfirmation', 'tool_oauth2');
+
         $mform->addElement('hidden', 'sortorder');
         $mform->setType('sortorder', PARAM_INT);
 
index 3fdb55f..8c20c6a 100644 (file)
@@ -72,15 +72,17 @@ $string['issuerimage'] = 'Logo URL';
 $string['issuerloginparams'] = 'Additional parameters included in a login request.';
 $string['issuerloginparams_help'] = 'Some systems require additional parameters for a login request in order to read the user\'s basic profile.';
 $string['issuerloginparamsoffline'] = 'Additional parameters included in a login request for offline access.';
-$string['issuerloginparamsoffline_help'] = 'Each OAuth system defines a different way to request offline access. E.g. Google requires the additional params: "access_type=offline&prompt=consent" these parameters should be in url query parameter format.';
-$string['issuerloginscopes_help'] = 'Some systems require additional scopes for a login request in order to read the users basic profile. The standard scopes for an OpenID Connect compliant system are "openid profile email".';
-$string['issuerloginscopesoffline_help'] = 'Each OAuth system defines a different way to request offline access. E.g. Microsoft requires an additional scope "offline_access"';
+$string['issuerloginparamsoffline_help'] = 'Each OAuth system defines a different way to request offline access. E.g. Google requires the additional params: "access_type=offline&prompt=consent". These parameters should be in URL query parameter format.';
+$string['issuerloginscopes_help'] = 'Some systems require additional scopes for a login request in order to read the user\'s basic profile. The standard scopes for an OpenID Connect compliant system are "openid profile email".';
+$string['issuerloginscopesoffline_help'] = 'Each OAuth system defines a different way to request offline access. E.g. Microsoft requires an additional scope "offline_access".';
 $string['issuerloginscopesoffline'] = 'Scopes included in a login request for offline access.';
 $string['issuerloginscopes'] = 'Scopes included in a login request.';
 $string['issuername_help'] = 'Name of the identity issuer. May be displayed on login page.';
 $string['issuername'] = 'Name';
-$string['issuershowonloginpage_help'] = 'If the OpenID Connect Authentication plugin is enabled, this login issuer will be listed on the login page to allow users to log in with accounts from this issuer.';
-$string['issuershowonloginpage'] = 'Show on login page.';
+$string['issuershowonloginpage_help'] = 'If the OAuth 2 authentication plugin is enabled, this login issuer will be listed on the login page to allow users to log in with accounts from this issuer.';
+$string['issuershowonloginpage'] = 'Show on login page';
+$string['issuerrequireconfirmation_help'] = 'Require that all users verify their email address before they can log in with OAuth. This applies to newly created accounts as part of the login process, or when an existing Moodle account is connected to an OAuth login via matching email addresses.';
+$string['issuerrequireconfirmation'] = 'Require email verification';
 $string['issuers'] = 'Issuers';
 $string['loginissuer'] = 'Allow login';
 $string['notconfigured'] = 'Not configured';
index 593ea98..8663112 100644 (file)
@@ -31,7 +31,7 @@ $string['default'] = 'Default';
 $string['disabled'] = 'Disabled';
 $string['disabled_help'] = 'Disabled scheduled tasks are not executed from cron, however they can still be executed manually via the CLI tool.';
 $string['edittaskschedule'] = 'Edit task schedule: {$a}';
-$string['enablerunnow'] = 'Allow &lsquo;Run now&rsquo; for scheduled tasks';
+$string['enablerunnow'] = 'Allow \'Run now\' for scheduled tasks';
 $string['enablerunnow_desc'] = 'Allows administrators to run a single scheduled task immediately, rather than waiting for it to run as scheduled. The task runs on the web server, so some sites may wish to disable this feature to avoid potential performance issues.';
 $string['faildelay'] = 'Fail delay';
 $string['lastruntime'] = 'Last run';
@@ -41,7 +41,7 @@ $string['pluginname'] = 'Scheduled task configuration';
 $string['resettasktodefaults'] = 'Reset task schedule to defaults';
 $string['resettasktodefaults_help'] = 'This will discard any local changes and revert the schedule for this task back to its original settings.';
 $string['runnow'] = 'Run now';
-$string['runnow_confirm'] = 'Are you sure you want to run this task &lsquo;{$a}&rsquo; now? The task will run on the web server and may take some time to complete.';
+$string['runnow_confirm'] = 'Are you sure you want to run this task \'{$a}\' now? The task will run on the web server and may take some time to complete.';
 $string['scheduledtasks'] = 'Scheduled tasks';
 $string['scheduledtaskchangesdisabled'] = 'Modifications to the list of scheduled tasks have been prevented in Moodle configuration';
 $string['taskdisabled'] = 'Task disabled';
index 75500b2..da1592e 100644 (file)
@@ -95,7 +95,7 @@ class tool_uploadcourse_course {
     /** @var array fields allowed as course data. */
     static protected $validfields = array('fullname', 'shortname', 'idnumber', 'category', 'visible', 'startdate', 'enddate',
         'summary', 'format', 'theme', 'lang', 'newsitems', 'showgrades', 'showreports', 'legacyfiles', 'maxbytes',
-        'groupmode', 'groupmodeforce', 'groupmodeforce', 'enablecompletion');
+        'groupmode', 'groupmodeforce', 'enablecompletion');
 
     /** @var array fields required on course creation. */
     static protected $mandatoryfields = array('fullname', 'category');
@@ -676,6 +676,17 @@ class tool_uploadcourse_course {
             return false;
         }
 
+        // TODO MDL-59259 allow to set course format options for the current course format.
+
+        // Special case, 'numsections' is not a course format option any more but still should apply from defaults.
+        if (!$exists || !array_key_exists('numsections', $coursedata)) {
+            if (isset($this->rawdata['numsections']) && is_numeric($this->rawdata['numsections'])) {
+                $coursedata['numsections'] = (int)$this->rawdata['numsections'];
+            } else {
+                $coursedata['numsections'] = get_config('moodlecourse', 'numsections');
+            }
+        }
+
         // Saving data.
         $this->data = $coursedata;
         $this->enrolmentdata = tool_uploadcourse_helper::get_enrolment_data($this->rawdata);
index 4601500..d3c5024 100644 (file)
@@ -166,6 +166,13 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
         $mform->addHelpButton('defaults[groupmodeforce]', 'groupmodeforce', 'group');
         $mform->setDefault('defaults[groupmodeforce]', $courseconfig->groupmodeforce);
 
+        // Completion tracking.
+        if (!empty($CFG->enablecompletion)) {
+            $mform->addElement('selectyesno', 'defaults[enablecompletion]', get_string('enablecompletion', 'completion'));
+            $mform->setDefault('defaults[enablecompletion]', $courseconfig->enablecompletion);
+            $mform->addHelpButton('defaults[enablecompletion]', 'enablecompletion', 'completion');
+        }
+
         // Hidden fields.
         $mform->addElement('hidden', 'importid');
         $mform->setType('importid', PARAM_INT);
index 7d9970f..75c39d8 100644 (file)
@@ -166,6 +166,7 @@ $defaults['groupmode'] = $courseconfig->groupmode;
 $defaults['groupmodeforce'] = $courseconfig->groupmodeforce;
 $defaults['visible'] = $courseconfig->visible;
 $defaults['lang'] =  $courseconfig->lang;
+$defaults['enablecompletion'] = $courseconfig->enablecompletion;
 
 // Course template.
 if (isset($options['templatecourse'])) {
index 3ac9fe0..3356163 100644 (file)
@@ -120,6 +120,36 @@ class tool_uploadcourse_course_testcase extends advanced_testcase {
         $this->assertTrue($DB->record_exists('course', array('shortname' => 'c2')));
     }
 
+    public function test_create_with_sections() {
+        global $DB;
+        $this->resetAfterTest(true);
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+        $defaultnumsections = get_config('moodlecourse', 'numsections');
+
+        // Add new course, make sure default number of sections is created.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $data = array('shortname' => 'newcourse1', 'fullname' => 'New course1', 'format' => 'topics', 'category' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $courseid = $DB->get_field('course', 'id', array('shortname' => 'newcourse1'));
+        $this->assertNotEmpty($courseid);
+        $this->assertEquals($defaultnumsections + 1,
+            $DB->count_records('course_sections', ['course' => $courseid]));
+
+        // Add new course specifying number of sections.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $data = array('shortname' => 'newcourse2', 'fullname' => 'New course2', 'format' => 'topics', 'category' => 1,
+            'numsections' => 15);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $courseid = $DB->get_field('course', 'id', array('shortname' => 'newcourse2'));
+        $this->assertNotEmpty($courseid);
+        $this->assertEquals(15 + 1,
+            $DB->count_records('course_sections', ['course' => $courseid]));
+    }
+
     public function test_delete() {
         global $DB;
         $this->resetAfterTest(true);
index 7282e28..522d6c6 100644 (file)
@@ -3187,6 +3187,18 @@ class CAS_Client
                 false/*$no_response*/, true/*$bad_response*/, $text_response
             );
             $result = false;
+       } else if ( $tree_response->getElementsByTagName("authenticationFailure")->length != 0) {
+            // authentication failed, extract the error code and message and throw exception
+            $auth_fail_list = $tree_response
+                ->getElementsByTagName("authenticationFailure");
+            throw new CAS_AuthenticationException(
+                $this, 'Ticket not validated', $validate_url,
+                false/*$no_response*/, false/*$bad_response*/,
+                $text_response,
+                $auth_fail_list->item(0)->getAttribute('code')/*$err_code*/,
+                trim($auth_fail_list->item(0)->nodeValue)/*$err_msg*/
+            );
+            $result = false;
         } else if ($tree_response->getElementsByTagName("authenticationSuccess")->length != 0) {
             // authentication succeded, extract the user name
             $success_elements = $tree_response
@@ -3227,18 +3239,6 @@ class CAS_Client
                     $result = true;
                 }
             }
-        } else if ( $tree_response->getElementsByTagName("authenticationFailure")->length != 0) {
-            // authentication succeded, extract the error code and message
-            $auth_fail_list = $tree_response
-                ->getElementsByTagName("authenticationFailure");
-            throw new CAS_AuthenticationException(
-                $this, 'Ticket not validated', $validate_url,
-                false/*$no_response*/, false/*$bad_response*/,
-                $text_response,
-                $auth_fail_list->item(0)->getAttribute('code')/*$err_code*/,
-                trim($auth_fail_list->item(0)->nodeValue)/*$err_msg*/
-            );
-            $result = false;
         } else {
             throw new CAS_AuthenticationException(
                 $this, 'Ticket not validated', $validate_url,
index e0d726d..2ef9d66 100644 (file)
@@ -2,3 +2,4 @@ Description of phpCAS 1.3.4 library import
 
 * downloaded from http://downloads.jasig.org/cas-clients/php/current/
 
+* MDL-59456 phpCAS library has been patched because of an authentication bypass security vulnerability.
\ No newline at end of file
index 3bf9979..baa86d9 100644 (file)
@@ -121,4 +121,98 @@ class core_auth_external extends external_api {
             )
         );
     }
+
+    /**
+     * Describes the parameters for request_password_reset.
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.4
+     */
+    public static function request_password_reset_parameters() {
+        return new external_function_parameters(
+            array(
+                'username' => new external_value(core_user::get_property_type('username'), 'User name', VALUE_DEFAULT, ''),
+                'email' => new external_value(core_user::get_property_type('email'), 'User email', VALUE_DEFAULT, ''),
+            )
+        );
+    }
+
+    /**
+     * Requests a password reset.
+     *
+     * @param  string $username user name
+     * @param  string $email    user email
+     * @return array warnings and success status (including notices and errors while processing)
+     * @since Moodle 3.4
+     * @throws moodle_exception
+     */
+    public static function request_password_reset($username = '', $email = '') {
+        global $CFG, $PAGE;
+        require_once($CFG->dirroot . '/login/lib.php');
+
+        $warnings = array();
+        $params = self::validate_parameters(
+            self::request_password_reset_parameters(),
+            array(
+                'username' => $username,
+                'email' => $email,
+            )
+        );
+
+        $context = context_system::instance();
+        $PAGE->set_context($context);   // Needed by format_string calls.
+
+        // Check if an alternate forgotten password method is set.
+        if (!empty($CFG->forgottenpasswordurl)) {
+            throw new moodle_exception('cannotmailconfirm');
+        }
+
+        $errors = core_login_validate_forgot_password_data($params);
+        if (!empty($errors)) {
+            $status = 'dataerror';
+            $notice = '';
+
+            foreach ($errors as $itemname => $message) {
+                $warnings[] = array(
+                    'item' => $itemname,
+                    'itemid' => 0,
+                    'warningcode' => 'fielderror',
+                    'message' => s($message)
+                );
+            }
+        } else {
+            list($status, $notice, $url) = core_login_process_password_reset($params['username'], $params['email']);
+        }
+
+        return array(
+            'status' => $status,
+            'notice' => $notice,
+            'warnings' => $warnings,
+        );
+    }
+
+    /**
+     * Describes the request_password_reset return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.4
+     */
+    public static function request_password_reset_returns() {
+
+        return new external_single_structure(
+            array(
+                'status' => new external_value(PARAM_ALPHANUMEXT, 'The returned status of the process:
+                    dataerror: Error in the sent data (username or email). More information in warnings field.
+                    emailpasswordconfirmmaybesent: Email sent or not (depends on user found in database).
+                    emailpasswordconfirmnotsent: Failure, user not found.
+                    emailpasswordconfirmnoemail: Failure, email not found.
+                    emailalreadysent: Email already sent.
+                    emailpasswordconfirmsent: User pending confirmation.
+                    emailresetconfirmsent: Email sent.
+                '),
+                'notice' => new external_value(PARAM_RAW, 'Important information for the user about the process.'),
+                'warnings'  => new external_warnings(),
+            )
+        );
+    }
 }
index 689ad1c..1b152b5 100644 (file)
@@ -192,10 +192,10 @@ class api {
         ];
         $confirmationurl = new moodle_url('/auth/oauth2/confirm-linkedlogin.php', $params);
 
-        // Remove data parameter just in case it was included in the confirmation so we can add it manually later.
-        $data->link = $confirmationurl->out();
+        $data->link = $confirmationurl->out(false);
+        $message = get_string('confirmlinkedloginemail', 'auth_oauth2', $data);
 
-        $message     = get_string('confirmlinkedloginemail', 'auth_oauth2', $data);
+        $data->link = $confirmationurl->out();
         $messagehtml = text_to_html(get_string('confirmlinkedloginemail', 'auth_oauth2', $data), false, false, true);
 
         $user->mailformat = 1;  // Always send HTML version as well.
@@ -239,6 +239,50 @@ class api {
         return true;
     }
 
+    /**
+     * Create an account with a linked login that is already confirmed.
+     *
+     * @param array $userinfo as returned from an oauth client.
+     * @param \core\oauth2\issuer $issuer
+     * @return bool
+     */
+    public static function create_new_confirmed_account($userinfo, $issuer) {
+        global $CFG, $DB;
+        require_once($CFG->dirroot.'/user/profile/lib.php');
+        require_once($CFG->dirroot.'/user/lib.php');
+
+        $user = new stdClass();
+        $user->username = $userinfo['username'];
+        $user->email = $userinfo['email'];
+        $user->auth = 'oauth2';
+        $user->mnethostid = $CFG->mnet_localhost_id;
+        $user->lastname = isset($userinfo['lastname']) ? $userinfo['lastname'] : '';
+        $user->firstname = isset($userinfo['firstname']) ? $userinfo['firstname'] : '';
+        $user->url = isset($userinfo['url']) ? $userinfo['url'] : '';
+        $user->alternatename = isset($userinfo['alternatename']) ? $userinfo['alternatename'] : '';
+        $user->secret = random_string(15);
+
+        $user->password = '';
+        // This user is confirmed.
+        $user->confirmed = 1;
+
+        $user->id = user_create_user($user, false, true);
+
+        // The linked account is pre-confirmed.
+        $record = new stdClass();
+        $record->issuerid = $issuer->get('id');
+        $record->username = $userinfo['username'];
+        $record->userid = $user->id;
+        $record->email = $userinfo['email'];
+        $record->confirmtoken = '';
+        $record->confirmtokenexpires = 0;
+
+        $linkedlogin = new linked_login(0, $record);
+        $linkedlogin->create();
+
+        return $user;
+    }
+
     /**
      * Send an email with a link to confirm creating this account.
      *
@@ -303,9 +347,10 @@ class api {
         ];
         $confirmationurl = new moodle_url('/auth/oauth2/confirm-account.php', $params);
 
-        $data->link = $confirmationurl->out();
+        $data->link = $confirmationurl->out(false);
+        $message = get_string('confirmaccountemail', 'auth_oauth2', $data);
 
-        $message     = get_string('confirmaccountemail', 'auth_oauth2', $data);
+        $data->link = $confirmationurl->out();
         $messagehtml = text_to_html(get_string('confirmaccountemail', 'auth_oauth2', $data), false, false, true);
 
         $user->mailformat = 1;  // Always send HTML version as well.
index 812cf98..be99012 100644 (file)
@@ -346,7 +346,7 @@ class auth extends \auth_plugin_base {
      * Complete the login process after oauth handshake is complete.
      * @param \core\oauth2\client $client
      * @param string $redirecturl
-     * @return none Either redirects or throws an exception
+     * @return void Either redirects or throws an exception
      */
     public function complete_login(client $client, $redirecturl) {
         global $CFG, $SESSION, $PAGE;
@@ -356,7 +356,7 @@ class auth extends \auth_plugin_base {
         if (!$userinfo) {
             // Trigger login failed event.
             $failurereason = AUTH_LOGIN_NOUSER;
-            $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
+            $event = \core\event\user_login_failed::create(['other' => ['username' => 'unknown',
                                                                         'reason' => $failurereason]]);
             $event->trigger();
 
@@ -368,7 +368,7 @@ class auth extends \auth_plugin_base {
         if (empty($userinfo['username']) || empty($userinfo['email'])) {
             // Trigger login failed event.
             $failurereason = AUTH_LOGIN_NOUSER;
-            $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'],
+            $event = \core\event\user_login_failed::create(['other' => ['username' => 'unknown',
                                                                         'reason' => $failurereason]]);
             $event->trigger();
 
@@ -450,15 +450,21 @@ class auth extends \auth_plugin_base {
 
             $moodleuser = \core_user::get_user_by_email($userinfo['email']);
             if (!empty($moodleuser)) {
-                $PAGE->set_url('/auth/oauth2/confirm-link-login.php');
-                $PAGE->set_context(context_system::instance());
-
-                \auth_oauth2\api::send_confirm_link_login_email($userinfo, $issuer, $moodleuser->id);
-                // Request to link to existing account.
-                $emailconfirm = get_string('emailconfirmlink', 'auth_oauth2');
-                $message = get_string('emailconfirmlinksent', 'auth_oauth2', $moodleuser->email);
-                $this->print_confirm_required($emailconfirm, $message);
-                exit();
+                if ($issuer->get('requireconfirmation')) {
+                    $PAGE->set_url('/auth/oauth2/confirm-link-login.php');
+                    $PAGE->set_context(context_system::instance());
+
+                    \auth_oauth2\api::send_confirm_link_login_email($userinfo, $issuer, $moodleuser->id);
+                    // Request to link to existing account.
+                    $emailconfirm = get_string('emailconfirmlink', 'auth_oauth2');
+                    $message = get_string('emailconfirmlinksent', 'auth_oauth2', $moodleuser->email);
+                    $this->print_confirm_required($emailconfirm, $message);
+                    exit();
+                } else {
+                    \auth_oauth2\api::link_login($userinfo, $issuer, $moodleuser->id, true);
+                    $userinfo = get_complete_user_data('id', $moodleuser->id);
+                    // No redirect, we will complete this login.
+                }
 
             } else {
                 // This is a new account.
@@ -506,17 +512,25 @@ class auth extends \auth_plugin_base {
                     redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php'));
                 }
 
-                $PAGE->set_url('/auth/oauth2/confirm-account.php');
-                $PAGE->set_context(context_system::instance());
+                if ($issuer->get('requireconfirmation')) {
+                    $PAGE->set_url('/auth/oauth2/confirm-account.php');
+                    $PAGE->set_context(context_system::instance());
+
+                    // Create a new (unconfirmed account) and send an email to confirm it.
+                    $user = \auth_oauth2\api::send_confirm_account_email($userinfo, $issuer);
 
-                // Create a new (unconfirmed account) and send an email to confirm it.
-                $user = \auth_oauth2\api::send_confirm_account_email($userinfo, $issuer);
+                    $this->update_picture($user);
+                    $emailconfirm = get_string('emailconfirm');
+                    $message = get_string('emailconfirmsent', '', $userinfo['email']);
+                    $this->print_confirm_required($emailconfirm, $message);
+                    exit();
+                } else {
+                    // Create a new confirmed account.
+                    $newuser = \auth_oauth2\api::create_new_confirmed_account($userinfo, $issuer);
+                    $userinfo = get_complete_user_data('id', $newuser->id);
 
-                $this->update_picture($user);
-                $emailconfirm = get_string('emailconfirm');
-                $message = get_string('emailconfirmsent', '', $userinfo['email']);
-                $this->print_confirm_required($emailconfirm, $message);
-                exit();
+                    // No redirect, we will complete this login.
+                }
             }
         }
 
index 817430a..83bf1a6 100644 (file)
@@ -98,4 +98,46 @@ class auth_oauth2_external_testcase extends advanced_testcase {
         $this->assertCount(1, $linkedlogins);
     }
 
+    /**
+     * Test auto-confirming linked logins.
+     */
+    public function test_linked_logins() {
+        $this->resetAfterTest();
+
+        $this->setAdminUser();
+        $issuer = \core\oauth2\api::create_standard_issuer('google');
+
+        $user = $this->getDataGenerator()->create_user();
+
+        $info = [];
+        $info['username'] = 'banana';
+        $info['email'] = 'banana@example.com';
+
+        \auth_oauth2\api::link_login($info, $issuer, $user->id, false);
+
+        // Try and match a user with a linked login.
+        $match = \auth_oauth2\api::match_username_to_user('banana', $issuer);
+
+        $this->assertEquals($user->id, $match->get('userid'));
+        $linkedlogins = \auth_oauth2\api::get_linked_logins($user->id, $issuer);
+        \auth_oauth2\api::delete_linked_login($linkedlogins[0]->get('id'));
+
+        $match = \auth_oauth2\api::match_username_to_user('banana', $issuer);
+        $this->assertFalse($match);
+
+        $info = [];
+        $info['username'] = 'apple';
+        $info['email'] = 'apple@example.com';
+        $info['firstname'] = 'Apple';
+        $info['lastname'] = 'Fruit';
+        $info['url'] = 'http://apple.com/';
+        $info['alternamename'] = 'Beatles';
+
+        $newuser = \auth_oauth2\api::create_new_confirmed_account($info, $issuer);
+
+        $match = \auth_oauth2\api::match_username_to_user('apple', $issuer);
+
+        $this->assertEquals($newuser->id, $match->get('userid'));
+    }
+
 }
index d65729a..6ef4fc9 100644 (file)
@@ -62,8 +62,12 @@ class info_section extends info {
 
     protected function set_in_database($availability) {
         global $DB;
-        $DB->set_field('course_sections', 'availability', $availability,
-                array('id' => $this->section->id));
+
+        $section = new \stdClass();
+        $section->id = $this->section->id;
+        $section->availability = $availability;
+        $section->timemodified = time();
+        $DB->update_record('course_sections', $section);
     }
 
     /**
index 722b024..63b61b4 100644 (file)
@@ -287,8 +287,12 @@ class condition extends \core_availability\condition {
 
             // Save the updated course module.
             if ($changed) {
-                $DB->set_field('course_sections', 'availability', json_encode($tree->save()),
-                        array('id' => $section->id));
+                $updatesection = new \stdClass();
+                $updatesection->id = $section->id;
+                $updatesection->availability = json_encode($tree->save());
+                $updatesection->timemodified = time();
+                $DB->update_record('course_sections', $updatesection);
+
                 $anychanged = true;
             }
         }
index 4e8ba89..3df327a 100644 (file)
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
+/**
+ * This script is used to configure and execute the backup proccess.
+ *
+ * @package    core
+ * @subpackage backup
+ * @copyright  Moodle
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('NO_OUTPUT_BUFFERING', true);
+
 require_once('../config.php');
 require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_plan_builder.class.php');
index 04681c9..57be494 100644 (file)
@@ -1,5 +1,31 @@
 <?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/>.
+
+/**
+ * This script is used to configure and execute the import proccess.
+ *
+ * @package    core
+ * @subpackage backup
+ * @copyright  Moodle
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('NO_OUTPUT_BUFFERING', true);
+
 // Require both the backup and restore libs
 require_once('../config.php');
 require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
index acdf34b..b0c6e89 100644 (file)
@@ -325,7 +325,7 @@ class backup_section_structure_step extends backup_structure_step {
 
         $section = new backup_nested_element('section', array('id'), array(
                 'number', 'name', 'summary', 'summaryformat', 'sequence', 'visible',
-                'availabilityjson'));
+                'availabilityjson', 'timemodified'));
 
         // attach format plugin structure to $section element, only one allowed
         $this->add_plugin_structure('format', $section, false);
@@ -1410,8 +1410,9 @@ class backup_block_instance_structure_step extends backup_structure_step {
         // Define each element separated
 
         $block = new backup_nested_element('block', array('id', 'contextid', 'version'), array(
-            'blockname', 'parentcontextid', 'showinsubcontexts', 'pagetypepattern',
-            'subpagepattern', 'defaultregion', 'defaultweight', 'configdata'));
+                'blockname', 'parentcontextid', 'showinsubcontexts', 'pagetypepattern',
+                'subpagepattern', 'defaultregion', 'defaultweight', 'configdata',
+                'timecreated', 'timemodified'));
 
         $positions = new backup_nested_element('block_positions');
 
index 5d607ef..00132f6 100644 (file)
@@ -795,7 +795,8 @@ class restore_rebuild_course_cache extends restore_execution_step {
             if (!$DB->record_exists('course_sections', array('course' => $this->get_courseid(), 'section' => $i))) {
                 $sectionrec = array(
                     'course' => $this->get_courseid(),
-                    'section' => $i);
+                    'section' => $i,
+                    'timemodified' => time());
                 $DB->insert_record('course_sections', $sectionrec); // missing section created
             }
         }
@@ -1575,8 +1576,9 @@ class restore_section_structure_step extends restore_structure_step {
         $section = new stdclass();
         $section->course  = $this->get_courseid();
         $section->section = $data->number;
+        $section->timemodified = isset($data->timemodified) ? $this->apply_date_offset($data->timemodified) : 0;
         // Section doesn't exist, create it with all the info from backup
-        if (!$secrec = $DB->get_record('course_sections', (array)$section)) {
+        if (!$secrec = $DB->get_record('course_sections', ['course' => $this->get_courseid(), 'section' => $data->number])) {
             $section->name = $data->name;
             $section->summary = $data->summary;
             $section->summaryformat = $data->summaryformat;
@@ -1721,8 +1723,12 @@ class restore_section_structure_step extends restore_structure_step {
                     array('id' => $availfield->coursesectionid), MUST_EXIST);
             $newvalue = \core_availability\info::add_legacy_availability_field_condition(
                     $currentvalue, $availfield, $show);
-            $DB->set_field('course_sections', 'availability', $newvalue,
-                    array('id' => $availfield->coursesectionid));
+
+            $section = new stdClass();
+            $section->id = $availfield->coursesectionid;
+            $section->availability = $newvalue;
+            $section->timemodified = time();
+            $DB->update_record('course_sections', $section);
         }
     }
 
@@ -3083,7 +3089,8 @@ class restore_course_logs_structure_step extends restore_structure_step {
 
         $data = (object)($data);
 
-        $data->time = $this->apply_date_offset($data->time);
+        // There is no need to roll dates. Logs are supposed to be immutable. See MDL-44961.
+
         $data->userid = $this->get_mappingid('user', $data->userid);
         $data->course = $this->get_courseid();
         $data->cmid = 0;
@@ -3130,7 +3137,8 @@ class restore_activity_logs_structure_step extends restore_course_logs_structure
 
         $data = (object)($data);
 
-        $data->time = $this->apply_date_offset($data->time);
+        // There is no need to roll dates. Logs are supposed to be immutable. See MDL-44961.
+
         $data->userid = $this->get_mappingid('user', $data->userid);
         $data->course = $this->get_courseid();
         $data->cmid = $this->task->get_moduleid();
@@ -3929,6 +3937,14 @@ class restore_block_instance_structure_step extends restore_structure_step {
             $data->configdata = base64_encode(serialize((object)$configdata));
         }
 
+        // Set timecreated, timemodified if not included (older backup).
+        if (empty($data->timecreated)) {
+            $data->timecreated = time();
+        }
+        if (empty($data->timemodified)) {
+            $data->timemodified = $data->timecreated;
+        }
+
         // Create the block instance
         $newitemid = $DB->insert_record('block_instances', $data);
         // Save the mapping (with restorefiles support)
@@ -4032,11 +4048,13 @@ class restore_module_structure_step extends restore_structure_step {
         if (!$data->section) { // no sections in course, create section 0 and 1 and assign module to 1
             $sectionrec = array(
                 'course' => $this->get_courseid(),
-                'section' => 0);
+                'section' => 0,
+                'timemodified' => time());
             $DB->insert_record('course_sections', $sectionrec); // section 0
             $sectionrec = array(
                 'course' => $this->get_courseid(),
-                'section' => 1);
+                'section' => 1,
+                'timemodified' => time());
             $data->section = $DB->insert_record('course_sections', $sectionrec); // section 1
         }
         $data->groupingid= $this->get_mappingid('grouping', $data->groupingid);      // grouping
@@ -4090,7 +4108,12 @@ class restore_module_structure_step extends restore_structure_step {
         } else {
             $sequence = $newitemid;
         }
-        $DB->set_field('course_sections', 'sequence', $sequence, array('id' => $data->section));
+
+        $updatesection = new \stdClass();
+        $updatesection->id = $data->section;
+        $updatesection->sequence = $sequence;
+        $updatesection->timemodified = time();
+        $DB->update_record('course_sections', $updatesection);
 
         // If there is the legacy showavailability data, store this for later use.
         // (This data is not present when restoring 'new' backups.)
index a5f531c..21773b9 100644 (file)
@@ -464,9 +464,10 @@ class core_backup_moodle2_testcase extends advanced_testcase {
      *
      * @param stdClass $course Course object to backup
      * @param int $newdate If non-zero, specifies custom date for new course
+     * @param callable|null $inbetween If specified, function that is called before restore
      * @return int ID of newly restored course
      */
-    protected function backup_and_restore($course, $newdate = 0) {
+    protected function backup_and_restore($course, $newdate = 0, $inbetween = null) {
         global $USER, $CFG;
 
         // Turn off file logging, otherwise it can't delete the file (Windows).
@@ -481,6 +482,10 @@ class core_backup_moodle2_testcase extends advanced_testcase {
         $bc->execute_plan();
         $bc->destroy();
 
+        if ($inbetween) {
+            $inbetween($backupid);
+        }
+
         // Do restore to new course with default settings.
         $newcourseid = restore_dbops::create_new_course(
                 $course->fullname, $course->shortname . '_2', $course->category);
@@ -802,4 +807,64 @@ class core_backup_moodle2_testcase extends advanced_testcase {
         $enrolment = reset($enrolments);
         $this->assertEquals('self', $enrolment->enrol);
     }
+
+    /**
+     * Test the block instance time fields (timecreated, timemodified) through a backup and restore.
+     */
+    public function test_block_instance_times_backup() {
+        global $DB;
+        $this->resetAfterTest();
+
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+
+        // Create course and add HTML block.
+        $course = $generator->create_course();
+        $context = context_course::instance($course->id);
+        $page = new moodle_page();
+        $page->set_context($context);
+        $page->set_course($course);
+        $page->set_pagelayout('standard');
+        $page->set_pagetype('course-view');
+        $page->blocks->load_blocks();
+        $page->blocks->add_block_at_end_of_default_region('html');
+
+        // Update (hack in database) timemodified and timecreated to specific values for testing.
+        $blockdata = $DB->get_record('block_instances',
+                ['blockname' => 'html', 'parentcontextid' => $context->id]);
+        $originalblockid = $blockdata->id;
+        $blockdata->timecreated = 12345;
+        $blockdata->timemodified = 67890;
+        $DB->update_record('block_instances', $blockdata);
+
+        // Do backup and restore.
+        $newcourseid = $this->backup_and_restore($course);
+
+        // Confirm that values were transferred correctly into HTML block on new course.
+        $newcontext = context_course::instance($newcourseid);
+        $blockdata = $DB->get_record('block_instances',
+                ['blockname' => 'html', 'parentcontextid' => $newcontext->id]);
+        $this->assertEquals(12345, $blockdata->timecreated);
+        $this->assertEquals(67890, $blockdata->timemodified);
+
+        // Simulate what happens with an older backup that doesn't have those fields, by removing
+        // them from the backup before doing a restore.
+        $before = time();
+        $newcourseid = $this->backup_and_restore($course, 0, function($backupid) use($originalblockid) {
+            global $CFG;
+            $path = $CFG->dataroot . '/temp/backup/' . $backupid . '/course/blocks/html_' .
+                    $originalblockid . '/block.xml';
+            $xml = file_get_contents($path);
+            $xml = preg_replace('~<timecreated>.*?</timemodified>~s', '', $xml);
+            file_put_contents($path, $xml);
+        });
+        $after = time();
+
+        // The fields not specified should default to current time.
+        $newcontext = context_course::instance($newcourseid);
+        $blockdata = $DB->get_record('block_instances',
+                ['blockname' => 'html', 'parentcontextid' => $newcontext->id]);
+        $this->assertTrue($before <= $blockdata->timecreated && $after >= $blockdata->timecreated);
+        $this->assertTrue($before <= $blockdata->timemodified && $after >= $blockdata->timemodified);
+    }
 }
index ab97369..06f58e6 100644 (file)
@@ -1,5 +1,29 @@
 <?php
-    //This script is used to configure and execute the restore proccess.
+// 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/>.
+
+/**
+ * This script is used to configure and execute the restore proccess.
+ *
+ * @package    core
+ * @subpackage backup
+ * @copyright  Moodle
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('NO_OUTPUT_BUFFERING', true);
 
 require_once('../config.php');
 require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
index abb479b..33bbe77 100644 (file)
@@ -90,7 +90,7 @@ echo html_writer::start_tag('tbody');
 if ($USER->id != $user->id) {
     echo html_writer::start_tag('tr');
     echo html_writer::start_tag('td', array('colspan' => '2'));
-    echo html_writer::tag('b', get_string('showinguser', 'completion'));
+    echo html_writer::tag('b', get_string('showinguser', 'completion') . ' ');
     $url = new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $course->id));
     echo html_writer::link($url, fullname($user));
     echo html_writer::end_tag('td');
@@ -99,7 +99,7 @@ if ($USER->id != $user->id) {
 
 echo html_writer::start_tag('tr');
 echo html_writer::start_tag('td', array('colspan' => '2'));
-echo html_writer::tag('b', get_string('status'));
+echo html_writer::tag('b', get_string('status') . ' ');
 
 // Is course complete?
 $coursecomplete = $info->is_course_complete($user->id);
@@ -141,7 +141,7 @@ if (empty($completions)) {
 } else {
     echo html_writer::start_tag('tr');
     echo html_writer::start_tag('td', array('colspan' => '2'));
-    echo html_writer::tag('b', get_string('required'));
+    echo html_writer::tag('b', get_string('required') . ' ');
 
     // Get overall aggregation method.
     $overall = $info->get_aggregation_method();
@@ -214,7 +214,7 @@ if (empty($completions)) {
                     echo core_text::strtolower(get_string('any', 'completion'));
                 }
 
-                echo html_writer::end_tag('i') .core_text::strtolower(get_string('required')).')';
+                echo ' ' . html_writer::end_tag('i') .core_text::strtolower(get_string('required')).')';
                 $agg_type = false;
             }
         }
diff --git a/blocks/html/classes/search/content.php b/blocks/html/classes/search/content.php
new file mode 100644 (file)
index 0000000..32b20b9
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Search area for block_html blocks
+ *
+ * @package block_html
+ * @copyright 2017 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_html\search;
+
+use core_search\moodle_recordset;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Search area for block_html blocks
+ *
+ * @package block_html
+ * @copyright 2017 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class content extends \core_search\base_block {
+
+    public function get_document($record, $options = array()) {
+        // Create empty document.
+        $doc = \core_search\document_factory::instance($record->id,
+                $this->componentname, $this->areaname);
+
+        // Get stdclass object with data from DB.
+        $data = unserialize(base64_decode($record->configdata));
+
+        // Get content.
+        $content = content_to_text($data->text, $data->format);
+        $doc->set('content', $content);
+
+        if (isset($data->title)) {
+            // If there is a title, use it as title.
+            $doc->set('title', content_to_text($data->title, false));
+        } else {
+            // If there is no title, use the content text again.
+            $doc->set('title', shorten_text($content));
+        }
+
+        // Set standard fields.
+        $doc->set('contextid', $record->contextid);
+        $doc->set('type', \core_search\manager::TYPE_TEXT);
+        $doc->set('courseid', $record->courseid);
+        $doc->set('modified', $record->timemodified);
+        $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID);
+
+        // Mark document new if appropriate.
+        if (isset($options['lastindexedtime']) &&
+                ($options['lastindexedtime'] < $record->timecreated)) {
+            // If the document was created after the last index time, it must be new.
+            $doc->set_is_new(true);
+        }
+
+        return $doc;
+    }
+
+    public function uses_file_indexing() {
+        return true;
+    }
+
+    public function attach_files($document) {
+        $fs = get_file_storage();
+
+        $context = \context::instance_by_id($document->get('contextid'));
+
+        $files = $fs->get_area_files($context->id, 'block_html', 'content');
+        foreach ($files as $file) {
+            $document->add_stored_file($file);
+        }
+    }
+}
index bf6df6f..7eb2ad4 100644 (file)
@@ -33,3 +33,4 @@ $string['html:myaddinstance'] = 'Add a new HTML block to Dashboard';
 $string['leaveblanktohide'] = 'leave blank to hide the title';
 $string['newhtmlblock'] = '(new HTML block)';
 $string['pluginname'] = 'HTML';
+$string['search:content'] = 'HTML block content';
index 531a32a..a86b827 100644 (file)
@@ -104,7 +104,8 @@ function block_html_global_db_replace($search, $replace) {
         $config = unserialize(base64_decode($instance->configdata));
         if (isset($config->text) and is_string($config->text)) {
             $config->text = str_replace($search, $replace, $config->text);
-            $DB->set_field('block_instances', 'configdata', base64_encode(serialize($config)), array('id' => $instance->id));
+            $DB->update_record('block_instances', ['id' => $instance->id,
+                    'configdata' => base64_encode(serialize($config)), 'timemodified' => time()]);
         }
     }
     $instances->close();
diff --git a/blocks/html/tests/search_content_test.php b/blocks/html/tests/search_content_test.php
new file mode 100644 (file)
index 0000000..5afb8cd
--- /dev/null
@@ -0,0 +1,191 @@
+<?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/>.
+
+/**
+ * Unit test for search indexing.
+ *
+ * @package block_html
+ * @copyright 2017 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_html;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Unit test for search indexing.
+ *
+ * @package block_html
+ * @copyright 2017 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class search_content_testcase extends \advanced_testcase {
+
+    /**
+     * Creates an HTML block on a course.
+     *
+     * @param \stdClass $course Course object
+     * @return \block_html Block instance object
+     */
+    protected function create_block($course) {
+        $page = self::construct_page($course);
+        $page->blocks->add_block_at_end_of_default_region('html');
+
+        // Load the block.
+        $page = self::construct_page($course);
+        $page->blocks->load_blocks();
+        $blocks = $page->blocks->get_blocks_for_region($page->blocks->get_default_region());
+        $block = end($blocks);
+        return $block;
+    }
+
+    /**
+     * Constructs a page object for the test course.
+     *
+     * @param \stdClass $course Moodle course object
+     * @return \moodle_page Page object representing course view
+     */
+    protected static function construct_page($course) {
+        $context = \context_course::instance($course->id);
+        $page = new \moodle_page();
+        $page->set_context($context);
+        $page->set_course($course);
+        $page->set_pagelayout('standard');
+        $page->set_pagetype('course-view');
+        $page->blocks->load_blocks();
+        return $page;
+    }
+
+    /**
+     * Tests all functionality in the search area.
+     */
+    public function test_search_area() {
+        global $CFG, $USER, $DB;
+        require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php');
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // Create course and add HTML block.
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+        $before = time();
+        $block = $this->create_block($course);
+
+        // Change block settings to add some text and a file.
+        $itemid = file_get_unused_draft_itemid();
+        $fs = get_file_storage();
+        $usercontext = \context_user::instance($USER->id);
+        $fs->create_file_from_string(['component' => 'user', 'filearea' => 'draft',
+                'contextid' => $usercontext->id, 'itemid' => $itemid, 'filepath' => '/',
+                'filename' => 'file.txt'], 'File content');
+        $data = (object)['title' => 'Block title', 'text' => ['text' => 'Block text',
+                'itemid' => $itemid, 'format' => FORMAT_HTML]];
+        $block->instance_config_save($data);
+        $after = time();
+
+        // Set up fake search engine so we can create documents.
+        \testable_core_search::instance();
+
+        // Do indexing query.
+        $area = new \block_html\search\content();
+        $this->assertEquals('html', $area->get_block_name());
+        $rs = $area->get_recordset_by_timestamp();
+        $count = 0;
+        foreach ($rs as $record) {
+            $count++;
+
+            $this->assertEquals($course->id, $record->courseid);
+
+            // Check context is correct.
+            $blockcontext = \context::instance_by_id($record->contextid);
+            $this->assertInstanceOf('\context_block', $blockcontext);
+            $coursecontext = $blockcontext->get_parent_context();
+            $this->assertEquals($course->id, $coursecontext->instanceid);
+
+            // Check created and modified times are correct.
+            $this->assertTrue($record->timecreated >= $before && $record->timecreated <= $after);
+            $this->assertTrue($record->timemodified >= $before && $record->timemodified <= $after);
+
+            // Get config data.
+            $data = unserialize(base64_decode($record->configdata));
+            $this->assertEquals('Block title', $data->title);
+            $this->assertEquals('Block text', $data->text);
+            $this->assertEquals(FORMAT_HTML, $data->format);
+
+            // Check the get_document function 'new' flag.
+            $doc = $area->get_document($record, ['lastindexedtime' => 1]);
+            $this->assertTrue($doc->get_is_new());
+            $doc = $area->get_document($record, ['lastindexedtime' => time() + 1]);
+            $this->assertFalse($doc->get_is_new());
+
+            // Check the attach_files function results in correct list of associated files.
+            $this->assertCount(0, $doc->get_files());
+            $area->attach_files($doc);
+            $files = $doc->get_files();
+            $this->assertCount(2, $files);
+            foreach ($files as $file) {
+                if ($file->is_directory()) {
+                    continue;
+                }
+                $this->assertEquals('file.txt', $file->get_filename());
+                $this->assertEquals('File content', $file->get_content());
+            }
+
+            // Check the document fields are all as expected.
+            $this->assertEquals('Block title', $doc->get('title'));
+            $this->assertEquals('Block text', $doc->get('content'));
+            $this->assertEquals($blockcontext->id, $doc->get('contextid'));
+            $this->assertEquals(\core_search\manager::TYPE_TEXT, $doc->get('type'));
+            $this->assertEquals($course->id, $doc->get('courseid'));
+            $this->assertEquals($record->timemodified, $doc->get('modified'));
+            $this->assertEquals(\core_search\manager::NO_OWNER_ID, $doc->get('owneruserid'));
+
+            // Also check getting the doc url and context url.
+            $url = new \moodle_url('/course/view.php', ['id' => $course->id], 'inst' . $record->id);
+            $this->assertTrue($url->compare($area->get_doc_url($doc)));
+            $this->assertTrue($url->compare($area->get_context_url($doc)));
+        }
+        $rs->close();
+
+        // Should only be one HTML block systemwide.
+        $this->assertEquals(1, $count);
+
+        // If we run the query starting from 1 second after now, there should be no results.
+        $rs = $area->get_recordset_by_timestamp($after + 1);
+        $count = 0;
+        foreach ($rs as $record) {
+            $count++;
+        }
+        $rs->close();
+        $this->assertEquals(0, $count);
+
+        // Create another block, but this time leave it empty (no data set). Hack the time though.
+        $block = $this->create_block($course);
+        $DB->set_field('block_instances', 'timemodified',
+                $after + 10, ['id' => $block->instance->id]);
+        $rs = $area->get_recordset_by_timestamp($after + 10);
+        $count = 0;
+        foreach ($rs as $record) {
+            // Because there is no configdata we don't index it.
+            $count++;
+        }
+        $rs->close();
+        $this->assertEquals(0, $count);
+    }
+}
+
index 636b1ba..e8f2491 100644 (file)
@@ -115,7 +115,7 @@ class block_login extends block_base {
                 $this->content->text .= '<div class="potentialidplist">';
                 foreach ($potentialidps as $idp) {
                     $this->content->text .= '<div class="potentialidp">';
-                    $this->content->text .= '<a class="btn btn-secondary btn-block" ';
+                    $this->content->text .= '<a class="btn btn-default btn-block" ';
                     $this->content->text .= 'href="' . $idp['url']->out() . '" title="' . s($idp['name']) . '">';
                     if (!empty($idp['iconurl'])) {
                         $this->content->text .= '<img src="' . s($idp['iconurl']) . '" width="24" height="24" class="m-r-1"/>';
index 57ae2b3..a81f7ac 100644 (file)
@@ -474,8 +474,8 @@ class block_base {
      */
     function instance_config_save($data, $nolongerused = false) {
         global $DB;
-        $DB->set_field('block_instances', 'configdata', base64_encode(serialize($data)),
-                array('id' => $this->instance->id));
+        $DB->update_record('block_instances', ['id' => $this->instance->id,
+                'configdata' => base64_encode(serialize($data)), 'timemodified' => time()]);
     }
 
     /**
diff --git a/blocks/myoverview/amd/build/tab_preferences.min.js b/blocks/myoverview/amd/build/tab_preferences.min.js
new file mode 100644 (file)
index 0000000..da5bd97
Binary files /dev/null and b/blocks/myoverview/amd/build/tab_preferences.min.js differ
diff --git a/blocks/myoverview/amd/src/tab_preferences.js b/blocks/myoverview/amd/src/tab_preferences.js
new file mode 100644 (file)
index 0000000..25ac2ee
--- /dev/null
@@ -0,0 +1,61 @@
+// 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/>.
+
+/**
+ * Javascript used to save the user's tab preference.
+ *
+ * @package    block_myoverview
+ * @copyright  2017 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['jquery', 'core/ajax', 'core/custom_interaction_events',
+    'core/notification'], function($, Ajax, CustomEvents, Notification) {
+
+    /**
+     * Registers an event that saves the user's tab preference when switching between them.
+     *
+     * @param {object} root The container element
+     */
+    var registerEventListeners = function(root) {
+        CustomEvents.define(root, [CustomEvents.events.activate]);
+        root.on(CustomEvents.events.activate, "[data-toggle='tab']", function(e) {
+            var tabname = $(e.currentTarget).data('tabname');
+            // Bootstrap does not change the URL when using BS tabs, so need to do this here.
+            // Also check to make sure the browser supports the history API.
+            if (typeof window.history.pushState === "function") {
+                window.history.pushState(null, null, '?myoverviewtab=' + tabname);
+            }
+            var request = {
+                methodname: 'core_user_update_user_preferences',
+                args: {
+                    preferences: [
+                        {
+                            type: 'block_myoverview_last_tab',
+                            value: tabname
+                        }
+                    ]
+                }
+            };
+
+            Ajax.call([request])[0]
+                .fail(Notification.exception);
+        });
+    };
+
+    return {
+        registerEventListeners: registerEventListeners
+    };
+});
index f22ce15..8afd4a1 100644 (file)
@@ -50,7 +50,16 @@ class block_myoverview extends block_base {
             return $this->content;
         }
 
-        $renderable = new \block_myoverview\output\main();
+        // Check if the tab to select wasn't passed in the URL, if so see if the user has any preference.
+        if (!$tab = optional_param('myoverviewtab', null, PARAM_ALPHA)) {
+            // Check if the user has no preference, if so get the site setting.
+            if (!$tab = get_user_preferences('block_myoverview_last_tab')) {
+                $config = get_config('block_myoverview');
+                $tab = $config->defaulttab;
+            }
+        }
+
+        $renderable = new \block_myoverview\output\main($tab);
         $renderer = $this->page->get_renderer('block_myoverview');
 
         $this->content = new stdClass();
@@ -68,4 +77,13 @@ class block_myoverview extends block_base {
     public function applicable_formats() {
         return array('my' => true);
     }
+
+    /**
+     * This block does contain a configuration settings.
+     *
+     * @return boolean
+     */
+    public function has_config() {
+        return true;
+    }
 }
index 31dc8c6..2949c09 100644 (file)
@@ -63,7 +63,8 @@ class courses_view implements renderable, templatable {
      * @return array
      */
     public function export_for_template(renderer_base $output) {
-        $today = time();
+        global $CFG;
+        require_once($CFG->dirroot.'/course/lib.php');
 
         // Build courses view data structure.
         $coursesview = [
@@ -73,8 +74,6 @@ class courses_view implements renderable, templatable {
         // How many courses we have per status?
         $coursesbystatus = ['past' => 0, 'inprogress' => 0, 'future' => 0];
         foreach ($this->courses as $course) {
-            $startdate = $course->startdate;
-            $enddate = $course->enddate;
             $courseid = $course->id;
             $context = \context_course::instance($courseid);
             $exporter = new course_summary_exporter($course, [
@@ -84,14 +83,17 @@ class courses_view implements renderable, templatable {
             // Convert summary to plain text.
             $exportedcourse->summary = content_to_text($exportedcourse->summary, $exportedcourse->summaryformat);
 
+            $courseprogress = null;
+
+            $classified = course_classify_for_timeline($course);
+
             if (isset($this->coursesprogress[$courseid])) {
-                $coursecompleted = $this->coursesprogress[$courseid]['completed'];
                 $courseprogress = $this->coursesprogress[$courseid]['progress'];
                 $exportedcourse->hasprogress = !is_null($courseprogress);
                 $exportedcourse->progress = $courseprogress;
             }
 
-            if ((isset($coursecompleted) && $coursecompleted) || (!empty($enddate) && $enddate < $today)) {
+            if ($classified == COURSE_TIMELINE_PAST) {
                 // Courses that have already ended.
                 $pastpages = floor($coursesbystatus['past'] / $this::COURSES_PER_PAGE);
 
@@ -100,7 +102,7 @@ class courses_view implements renderable, templatable {
                 $coursesview['past']['pages'][$pastpages]['page'] = $pastpages + 1;
                 $coursesview['past']['haspages'] = true;
                 $coursesbystatus['past']++;
-            } else if ($startdate > $today) {
+            } else if ($classified == COURSE_TIMELINE_FUTURE) {
                 // Courses that have not started yet.
                 $futurepages = floor($coursesbystatus['future'] / $this::COURSES_PER_PAGE);
 
index 6215a5a..2435f54 100644 (file)
@@ -29,6 +29,7 @@ use renderer_base;
 use templatable;
 use core_completion\progress;
 
+require_once($CFG->dirroot . '/blocks/myoverview/lib.php');
 require_once($CFG->libdir . '/completionlib.php');
 
 /**
@@ -39,6 +40,20 @@ require_once($CFG->libdir . '/completionlib.php');
  */
 class main implements renderable, templatable {
 
+    /**
+     * @var string The tab to display.
+     */
+    public $tab;
+
+    /**
+     * Constructor.
+     *
+     * @param string $tab The tab to display.
+     */
+    public function __construct($tab) {
+        $this->tab = $tab;
+    }
+
     /**
      * Export this data so it can be used as the context for a mustache template.
      *
@@ -73,13 +88,24 @@ class main implements renderable, templatable {
         $nocoursesurl = $output->image_url('courses', 'block_myoverview')->out();
         $noeventsurl = $output->image_url('activities', 'block_myoverview')->out();
 
+        // Now, set the tab we are going to be viewing.
+        $viewingtimeline = false;
+        $viewingcourses = false;
+        if ($this->tab == BLOCK_MYOVERVIEW_TIMELINE_VIEW) {
+            $viewingtimeline = true;
+        } else {
+            $viewingcourses = true;
+        }
+
         return [
             'midnight' => usergetmidnight(time()),
             'coursesview' => $coursesview->export_for_template($output),
             'urls' => [
                 'nocourses' => $nocoursesurl,
                 'noevents' => $noeventsurl
-            ]
+            ],
+            'viewingtimeline' => $viewingtimeline,
+            'viewingcourses' => $viewingcourses
         ];
     }
 }
index 4c464f5..2176ec5 100644 (file)
@@ -22,6 +22,8 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['defaulttab'] = 'Default tab';
+$string['defaulttab_desc'] = 'The tab that will be displayed when a user first views their course overview. When returning to their course overview, the user\'s active tab is remembered.';
 $string['future'] = 'Future';
 $string['inprogress'] = 'In progress';
 $string['morecourses'] = 'More courses';
diff --git a/blocks/myoverview/lib.php b/blocks/myoverview/lib.php
new file mode 100644 (file)
index 0000000..a73db25
--- /dev/null
@@ -0,0 +1,52 @@
+<?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/>.
+
+/**
+ * Contains functions called by core.
+ *
+ * @package    block_myoverview
+ * @copyright  2017 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The timeline view.
+ */
+define('BLOCK_MYOVERVIEW_TIMELINE_VIEW', 'timeline');
+
+/**
+ * The courses view.
+ */
+define('BLOCK_MYOVERVIEW_COURSES_VIEW', 'courses');
+
+/**
+ * Returns the name of the user preferences as well as the details this plugin uses.
+ *
+ * @return array
+ */
+function block_myoverview_user_preferences() {
+    $preferences = array();
+    $preferences['block_myoverview_last_tab'] = array(
+        'type' => PARAM_ALPHA,
+        'null' => NULL_NOT_ALLOWED,
+        'default' => BLOCK_MYOVERVIEW_TIMELINE_VIEW,
+        'choices' => array(BLOCK_MYOVERVIEW_TIMELINE_VIEW, BLOCK_MYOVERVIEW_COURSES_VIEW)
+    );
+
+    return $preferences;
+}
diff --git a/blocks/myoverview/settings.php b/blocks/myoverview/settings.php
new file mode 100644 (file)
index 0000000..10f084d
--- /dev/null
@@ -0,0 +1,39 @@
+<?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/>.
+
+/**
+ * Settings for the overview block.
+ *
+ * @package    block_myoverview
+ * @copyright  2017 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once($CFG->dirroot . '/blocks/myoverview/lib.php');
+
+if ($ADMIN->fulltree) {
+
+    $options = [
+        BLOCK_MYOVERVIEW_TIMELINE_VIEW => get_string('timeline', 'block_myoverview'),
+        BLOCK_MYOVERVIEW_COURSES_VIEW => get_string('courses')
+    ];
+
+    $settings->add(new admin_setting_configselect('block_myoverview/defaulttab',
+        get_string('defaulttab', 'block_myoverview'),
+        get_string('defaulttab_desc', 'block_myoverview'), 'timeline', $options));
+}
index 3a1a942..e9b21bd 100644 (file)
 }}
 
 <div id="block-myoverview-{{uniqid}}" class="block-myoverview" data-region="myoverview">
-    <ul class="nav nav-tabs" role="tablist">
+    <ul id="block-myoverview-view-choices-{{uniqid}}" class="nav nav-tabs" role="tablist">
         <li class="nav-item">
-            <a class="nav-link active" href="#myoverview_timeline_view" role="tab" data-toggle="tab">
+            <a class="nav-link {{#viewingtimeline}}active{{/viewingtimeline}}" href="#myoverview_timeline_view" role="tab" data-toggle="tab" data-tabname="timeline">
                 {{#str}} timeline, block_myoverview {{/str}}
             </a>
         </li>
         <li class="nav-item">
-            <a class="nav-link" href="#myoverview_courses_view" role="tab" data-toggle="tab">
+            <a class="nav-link {{#viewingcourses}}active{{/viewingcourses}}" href="#myoverview_courses_view" role="tab" data-toggle="tab" data-tabname="courses">
                 {{#str}} courses {{/str}}
             </a>
         </li>
     </ul>
     <div class="tab-content content-centred">
-        <div role="tabpanel" class="tab-pane fade in active" id="myoverview_timeline_view">
+        <div role="tabpanel" class="tab-pane fade {{#viewingtimeline}}in active{{/viewingtimeline}}" id="myoverview_timeline_view">
             {{> block_myoverview/timeline-view }}
         </div>
-        <div role="tabpanel" class="tab-pane fade" id="myoverview_courses_view">
+        <div role="tabpanel" class="tab-pane fade {{#viewingcourses}}in active{{/viewingcourses}}" id="myoverview_courses_view">
             {{#coursesview}}
                 {{> block_myoverview/courses-view }}
             {{/coursesview}}
         </div>
     </div>
 </div>
+{{#js}}
+require(['jquery', 'block_myoverview/tab_preferences'], function($, TabPreferences) {
+    var root = $('#block-myoverview-view-choices-{{uniqid}}');
+    TabPreferences.registerEventListeners(root);
+});
+{{/js}}
index 8bd3afe..e8d8692 100644 (file)
@@ -53,6 +53,7 @@ Feature: Course overview block show users their progress on courses
     And I am on "Course 1" course homepage
     And I follow "Test choice 1"
     And I follow "Dashboard" in the user menu
+    And I click on "Timeline" "link" in the "Course overview" "block"
     And I click on "Sort by courses" "link" in the "Course overview" "block"
     And I should see "100%" in the "Course overview" "block"
     And I click on "Courses" "link" in the "Course overview" "block"
index fd45f77..a637f48 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2017051500;         // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2017051502;         // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2017050500;         // Requires this Moodle version.
 $plugin->component = 'block_myoverview'; // Full name of the plugin (used for diagnostics).
index 4c69492..02578ec 100644 (file)
Binary files a/blocks/navigation/amd/build/ajax_response_renderer.min.js and b/blocks/navigation/amd/build/ajax_response_renderer.min.js differ
index d935742..99f2e28 100644 (file)
@@ -22,7 +22,7 @@
  * @copyright  2015 John Okely <john@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery'], function($) {
+define(['jquery', 'core/templates', 'core/notification', 'core/url'], function($, Templates, Notification, Url) {
 
     // Mappings for the different types of nodes coming from the navigation.
     // Copied from lib/navigationlib.php navigation_node constants.
@@ -75,28 +75,12 @@ define(['jquery'], function($) {
                 p.addClass('branch');
             }
 
-            if (node.icon && (!isBranch || node.type === NODETYPE.ACTIVITY || node.type === NODETYPE.RESOURCE)) {
-                li.addClass('item_with_icon');
-                p.addClass('hasicon');
-
-                icon = $('<img/>');
-                icon.attr('alt', node.icon.alt);
-                icon.attr('title', node.icon.title);
-                icon.attr('src', M.util.image_url(node.icon.pix, node.icon.component));
-                $.each(node.icon.classes, function(index, className) {
-                    icon.addClass(className);
-                });
-            }
-
+            var eleToAddIcon = null;
             if (node.link) {
                 var link = $('<a title="' + node.title + '" href="' + node.link + '"></a>');
 
-                if (icon) {
-                    link.append(icon);
-                    link.append('<span class="item-content-wrap">' + node.name + '</span>');
-                } else {
-                    link.append(node.name);
-                }
+                eleToAddIcon = link;
+                link.append('<span class="item-content-wrap">' + node.name + '</span>');
 
                 if (node.hidden) {
                     link.addClass('dimmed');
@@ -106,12 +90,8 @@ define(['jquery'], function($) {
             } else {
                 var span = $('<span></span>');
 
-                if (icon) {
-                    span.append(icon);
-                    span.append('<span class="item-content-wrap">' + node.name + '</span>');
-                } else {
-                    span.append(node.name);
-                }
+                eleToAddIcon = span;
+                span.append('<span class="item-content-wrap">' + node.name + '</span>');
 
                 if (node.hidden) {
                     span.addClass('dimmed');
@@ -120,6 +100,31 @@ define(['jquery'], function($) {
                 p.append(span);
             }
 
+            if (node.icon && (!isBranch || node.type === NODETYPE.ACTIVITY || node.type === NODETYPE.RESOURCE)) {
+                li.addClass('item_with_icon');
+                p.addClass('hasicon');
+
+                if (node.type === NODETYPE.ACTIVITY || node.type === NODETYPE.RESOURCE) {
+                    icon = $('<img/>');
+                    icon.attr('alt', node.icon.alt);
+                    icon.attr('title', node.icon.title);
+                    icon.attr('src', Url.imageUrl(node.icon.pix, node.icon.component));
+                    $.each(node.icon.classes, function(index, className) {
+                        icon.addClass(className);
+                    });
+                    eleToAddIcon.prepend(icon);
+                } else {
+                    if (node.icon.component == 'moodle') {
+                        node.icon.component = 'core';
+                    }
+                    Templates.renderPix(node.icon.pix, node.icon.component, node.icon.title).then(function(html) {
+                        // Prepend.
+                        eleToAddIcon.prepend(html);
+                        return;
+                    }).catch(Notification.exception);
+                }
+            }
+
             li.append(p);
             ul.append(li);
 
index bb541f5..2369c1a 100644 (file)
@@ -71,6 +71,6 @@
     display: block;
 }
 
-.block_navigation .block_tree [aria-hidden="true"] {
+.block_navigation .block_tree [aria-hidden="true"]:not(.icon) {
     display: none;
 }
index aeb75be..a850602 100644 (file)
@@ -30,7 +30,6 @@ Feature: People Block used in a course
     And I am on "Course 1" course homepage
     And I click on "Participants" "link" in the "People" "block"
     Then I should see "All participants" in the "#page-content" "css_element"
-    And the "My courses" select box should contain "C101"
 
   Scenario: Student without permission can not view participants link
     Given the following "permission overrides" exist:
index e0c94b2..3d7e1d1 100644 (file)
@@ -58,7 +58,7 @@
     display: block;
 }
 
-.block_settings .block_tree  [aria-hidden="true"] {
+.block_settings .block_tree  [aria-hidden="true"]:not(.icon) {
     display: none;
 }
 
index 0fa83c7..6b8fc56 100644 (file)
@@ -1,6 +1,15 @@
 This files describes API changes in /blocks/* - activity modules,
 information provided here is intended especially for developers.
 
+=== 3.4 ===
+
+* The block_instances table now contains fields timecreated and timemodified. If third-party code
+  creates or updates these rows (without using the standard API), it should be modified to set
+  these fields as appropriate.
+* Blocks can now be included in Moodle global search, with some limitations (at present, the search
+  works only for blocks located directly on course pages or site home page). See the HTML block for
+  an example.
+
 === 3.3 ===
 
 * block_manager::get_required_by_theme_block_types() is no longer static.
index 6ae461d..9abfe0e 100644 (file)
@@ -335,6 +335,15 @@ class cache_factory {
         return $this->cachesfromdefinitions;
     }
 
+    /**
+     * Gets all adhoc caches that have been used within this request.
+     *
+     * @return cache_store[] Caches currently in use
+     */
+    public function get_adhoc_caches_in_use() {
+        return $this->cachesfromparams;
+    }
+
     /**
      * Creates a cache config instance with the ability to write if required.
      *
index f15e2ee..c2aa399 100644 (file)
@@ -484,6 +484,9 @@ class cache_helper {
         foreach ($config->get_all_stores() as $store) {
             self::purge_store($store['name'], $config);
         }
+        foreach ($factory->get_adhoc_caches_in_use() as $cache) {
+            $cache->purge();
+        }
     }
 
     /**
index 0a47e4f..8d04cd1 100644 (file)
@@ -1730,6 +1730,16 @@ class core_cache_testcase extends advanced_testcase {
         }
     }
 
+    /**
+     * Tests that ad-hoc caches are correctly purged with a purge_all call.
+     */
+    public function test_purge_all_with_adhoc_caches() {
+        $cache = \cache::make_from_params(\cache_store::MODE_REQUEST, 'core_cache', 'test');
+        $cache->set('test', 123);
+        cache_helper::purge_all();
+        $this->assertFalse($cache->get('test'));
+    }
+
     /**
      * Test that the default stores all support searching.
      */
index 20b39ec..70c2cb7 100644 (file)
@@ -71,11 +71,6 @@ class container {
      */
     protected static $eventretrievalstrategy;
 
-    /**
-     * @var array A list of callbacks to use.
-     */
-    protected static $callbacks = array();
-
     /**
      * @var \stdClass[] An array of cached courses to use with the event factory.
      */
@@ -91,16 +86,6 @@ class container {
      */
     private static function init() {
         if (empty(self::$eventfactory)) {
-            // When testing the container's components, we need to make sure
-            // the callback implementations in modules are not executed, since
-            // we cannot control their output from PHPUnit. To do this we have
-            // a set of 'testing' callbacks that the factory can use. This way
-            // we know exactly how the factory behaves when being tested.
-            $getcallback = function($which) {
-                return self::$callbacks[PHPUNIT_TEST ? 'testing' : 'production'][$which];
-            };
-
-            self::initcallbacks();
             self::$actionfactory = new action_factory();
             self::$eventmapper = new event_mapper(
                 // The event mapper we return from here needs to know how to
@@ -129,8 +114,8 @@ class container {
             );
 
             self::$eventfactory = new event_factory(
-                $getcallback('action'),
-                $getcallback('visibility'),
+                [self::class, 'apply_component_provide_event_action'],
+                [self::class, 'apply_component_is_event_visible'],
                 function ($dbrow) {
                     // At present we only have a bail-out check for events in course modules.
                     if (empty($dbrow->modulename)) {
@@ -152,10 +137,15 @@ class container {
                     // have that capability set on the "Authenticated User" role rather than
                     // on "Student" role, which means uservisible returns true even when the user
                     // is no longer enrolled in the course.
-                    $modulecontext = \context_module::instance($cm->id);
-                    // A user with the 'moodle/course:view' capability is able to see courses
-                    // that they are not a participant in.
-                    $canseecourse = (has_capability('moodle/course:view', $modulecontext) || is_enrolled($modulecontext));
+                    // So, with the following we are checking -
+                    // 1) Only process modules if $cm->uservisible is true.
+                    // 2) Only process modules for courses a user has the capability to view OR they are enrolled in.
+                    // 3) Only process modules for courses that are visible OR if the course is not visible, the user
+                    //    has the capability to view hidden courses.
+                    $coursecontext = \context_course::instance($dbrow->courseid);
+                    $canseecourse = has_capability('moodle/course:view', $coursecontext) || is_enrolled($coursecontext);
+                    $canseecourse = $canseecourse &&
+                        ($cm->get_course()->visible || has_capability('moodle/course:viewhiddencourses', $coursecontext));
                     if (!$cm->uservisible || !$canseecourse) {
                         return true;
                     }
@@ -183,6 +173,19 @@ class container {
         }
     }
 
+    /**
+     * Reset all static caches, called between tests.
+     */
+    public static function reset_caches() {
+        self::$eventfactory = null;
+        self::$eventmapper = null;
+        self::$eventvault = null;
+        self::$actionfactory = null;
+        self::$eventretrievalstrategy = null;
+        self::$coursecache = [];
+        self::$modulecache = [];
+    }
+
     /**
      * Gets the event factory.
      *
@@ -214,88 +217,74 @@ class container {
     }
 
     /**
-     * Initialises the callbacks.
+     * Calls callback 'core_calendar_provide_event_action' from the component responsible for the event
      *
-     * There are two sets here, one is used during PHPUnit runs.
-     * See the comment at the start of the init method for more
-     * detail.
+     * If no callback is present or callback returns null, there is no action on the event
+     * and it will not be displayed on the dashboard.
+     *
+     * @param event_interface $event
+     * @return action_event|event_interface
      */
-    private static function initcallbacks() {
-        self::$callbacks = array(
-            'testing' => array(
-                // Always return an action event.
-                'action' => function (event_interface $event) {
-                    return new action_event(
-                        $event,
-                        new \core_calendar\local\event\value_objects\action(
-                            'test',
-                            new \moodle_url('http://example.com'),
-                            420,
-                            true
-                        ));
-                },
-                // Always be visible.
-                'visibility' => function (event_interface $event) {
-                    return true;
-                }
-            ),
-            'production' => array(
-                // This function has type event_interface -> event_interface.
-                // This is enforced by the event_factory.
-                'action' => function (event_interface $event) {
-                    // Callbacks will get supplied a "legacy" version
-                    // of the event class.
-                    $mapper = self::$eventmapper;
-                    $action = null;
-                    if ($event->get_course_module()) {
-                        // TODO MDL-58866 Only activity modules currently support this callback.
-                        // Any other event will not be displayed on the dashboard.
-                        $action = component_callback(
-                            'mod_' . $event->get_course_module()->get('modname'),
-                            'core_calendar_provide_event_action',
-                            [
-                                $mapper->from_event_to_legacy_event($event),
-                                self::$actionfactory
-                            ]
-                        );
-                    }
+    public static function apply_component_provide_event_action(event_interface $event) {
+        // Callbacks will get supplied a "legacy" version
+        // of the event class.
+        $mapper = self::$eventmapper;
+        $action = null;
+        if ($event->get_course_module()) {
+            // TODO MDL-58866 Only activity modules currently support this callback.
+            // Any other event will not be displayed on the dashboard.
+            $action = component_callback(
+                'mod_' . $event->get_course_module()->get('modname'),
+                'core_calendar_provide_event_action',
+                [
+                    $mapper->from_event_to_legacy_event($event),
+                    self::$actionfactory
+                ]
+            );
+        }
 
-                    // If we get an action back, return an action event, otherwise
-                    // continue piping through the original event.
-                    //
-                    // If a module does not implement the callback, component_callback
-                    // returns null.
-                    return $action ? new action_event($event, $action) : $event;
-                },
-                // This function has type event_interface -> bool.
-                // This is enforced by the event_factory.
-                'visibility' => function (event_interface $event) {
-                    $mapper = self::$eventmapper;
-                    $eventvisible = null;
-                    if ($event->get_course_module()) {
-                        // TODO MDL-58866 Only activity modules currently support this callback.
-                        $eventvisible = component_callback(
-                            'mod_' . $event->get_course_module()->get('modname'),
-                            'core_calendar_is_event_visible',
-                            [
-                                $mapper->from_event_to_legacy_event($event)
-                            ]
-                        );
-                    }
+        // If we get an action back, return an action event, otherwise
+        // continue piping through the original event.
+        //
+        // If a module does not implement the callback, component_callback
+        // returns null.
+        return $action ? new action_event($event, $action) : $event;
+    }
 
-                    // Do not display the event if there is nothing to action.
-                    if ($event instanceof action_event_interface && $event->get_action()->get_item_count() === 0) {
-                        return false;
-                    }
+    /**
+     * Calls callback 'core_calendar_is_event_visible' from the component responsible for the event
+     *
+     * The visibility callback is optional, if not present it is assumed as visible.
+     * If it is an actionable event but the get_item_count() returns 0 the visibility
+     * is set to false.
+     *
+     * @param event_interface $event
+     * @return bool
+     */
+    public static function apply_component_is_event_visible(event_interface $event) {
+        $mapper = self::$eventmapper;
+        $eventvisible = null;
+        if ($event->get_course_module()) {
+            // TODO MDL-58866 Only activity modules currently support this callback.
+            $eventvisible = component_callback(
+                'mod_' . $event->get_course_module()->get('modname'),
+                'core_calendar_is_event_visible',
+                [
+                    $mapper->from_event_to_legacy_event($event)
+                ]
+            );
+        }
 
-                    // Module does not implement the callback, event should be visible.
-                    if (is_null($eventvisible)) {
-                        return true;
-                    }
+        // Do not display the event if there is nothing to action.
+        if ($event instanceof action_event_interface && $event->get_action()->get_item_count() === 0) {
+            return false;
+        }
+
+        // Module does not implement the callback, event should be visible.
+        if (is_null($eventvisible)) {
+            return true;
+        }
 
-                    return $eventvisible ? true : false;
-                }
-            ),
-        );
+        return $eventvisible ? true : false;
     }
 }
index 8c5d175..04e6cf3 100644 (file)
@@ -77,7 +77,8 @@ if (!empty($day) && !empty($mon) && !empty($year)) {
 }
 
 if ($courseid != SITEID && !empty($courseid)) {
-    $course = $DB->get_record('course', array('id' => $courseid));
+    // Course ID must be valid and existing.
+    $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
     $courses = array($course->id => $course);
     $issite = false;
 } else {
@@ -85,7 +86,7 @@ if ($courseid != SITEID && !empty($courseid)) {
     $courses = calendar_get_default_courses();
     $issite = true;
 }
-require_course_login($course);
+require_login($course, false);
 
 $url = new moodle_url('/calendar/export.php', array('time' => $time));
 
index 55ad67f..f6f1e4a 100644 (file)
@@ -1457,6 +1457,15 @@ function calendar_get_mini($courses, $groups, $users, $calmonth = false, $calyea
                         $name = format_string($event->name, true);
                     }
                 }
+                // Include course's shortname into the event name, if applicable.
+                if (!empty($event->courseid) && $event->courseid !== SITEID) {
+                    $course = get_course($event->courseid);
+                    $eventnameparams = (object)[
+                        'name' => $name,
+                        'course' => format_string($course->shortname, true, array('context' => $event->context))
+                    ];
+                    $name = get_string('eventnameandcourse', 'calendar', $eventnameparams);
+                }
                 $popupcontent .= \html_writer::link($dayhref, $name);
                 $popupcontent .= \html_writer::end_tag('div');
             }
index 3e4e0e9..f1c74d6 100644 (file)
@@ -45,13 +45,15 @@ $PAGE->set_pagelayout('admin');
 $PAGE->navbar->add(get_string('managesubscriptions', 'calendar'));
 
 if ($courseid != SITEID && !empty($courseid)) {
-    $course = $DB->get_record('course', array('id' => $courseid));
+    // Course ID must be valid and existing.
+    $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
     $courses = array($course->id => $course);
 } else {
     $course = get_site();
     $courses = calendar_get_default_courses();
 }
-require_course_login($course);
+require_login($course, false);
+
 if (!calendar_user_can_add_event($course)) {
     print_error('errorcannotimport', 'calendar');
 }
index 8abfb03..6711693 100644 (file)
@@ -502,7 +502,22 @@ class core_calendar_renderer extends plugin_renderer_base {
                         $attributes['class'] = $events[$eventindex]->class;
                     }
                     $dayhref->set_anchor('event_'.$events[$eventindex]->id);
-                    $link = html_writer::link($dayhref, format_string($events[$eventindex]->name, true));
+
+                    $eventcontext = $events[$eventindex]->context;
+                    $eventformatopts = array('context' => $eventcontext);
+                    // Get event name.
+                    $eventname = format_string($events[$eventindex]->name, true, $eventformatopts);
+                    // Include course's shortname into the event name, if applicable.
+                    $courseid = $events[$eventindex]->courseid;
+                    if (!empty($courseid) && $courseid !== SITEID) {
+                        $course = get_course($courseid);
+                        $eventnameparams = (object)[
+                            'name' => $eventname,
+                            'course' => format_string($course->shortname, true, $eventformatopts)
+                        ];
+                        $eventname = get_string('eventnameandcourse', 'calendar', $eventnameparams);
+                    }
+                    $link = html_writer::link($dayhref, $eventname);
                     $cell->text .= html_writer::tag('li', $link, $attributes);
                 }
                 $cell->text .= html_writer::end_tag('ul');
index 23a4108..ba1eb6c 100644 (file)
@@ -183,6 +183,63 @@ class core_calendar_container_testcase extends advanced_testcase {
         $this->assertNull($event);
     }
 
+    /**
+     * Test that the event factory deals with invisible courses as an admin.
+     *
+     * @dataProvider get_event_factory_testcases()
+     * @param \stdClass $dbrow Row from the "database".
+     */
+    public function test_event_factory_when_course_visibility_is_toggled_as_admin($dbrow) {
+        $legacyevent = $this->create_event($dbrow);
+        $factory = \core_calendar\local\event\container::get_event_factory();
+
+        // Create a hidden course with an assignment.
+        $course = $this->getDataGenerator()->create_course(['visible' => 0]);
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+        $moduleinstance = $generator->create_instance(['course' => $course->id]);
+
+        $dbrow->id = $legacyevent->id;
+        $dbrow->courseid = $course->id;
+        $dbrow->instance = $moduleinstance->id;
+        $dbrow->modulename = 'assign';
+        $event = $factory->create_instance($dbrow);
+
+        // Module is still visible to admins even if the course is invisible.
+        $this->assertInstanceOf(event_interface::class, $event);
+    }
+
+    /**
+     * Test that the event factory deals with invisible courses as a student.
+     *
+     * @dataProvider get_event_factory_testcases()
+     * @param \stdClass $dbrow Row from the "database".
+     */
+    public function test_event_factory_when_course_visibility_is_toggled_as_student($dbrow) {
+        $legacyevent = $this->create_event($dbrow);
+        $factory = \core_calendar\local\event\container::get_event_factory();
+
+        // Create a hidden course with an assignment.
+        $course = $this->getDataGenerator()->create_course(['visible' => 0]);
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+        $moduleinstance = $generator->create_instance(['course' => $course->id]);
+
+        // Enrol a student into this course.
+        $student = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($student->id, $course->id);
+
+        // Set the user to the student.
+        $this->setUser($student);
+
+        $dbrow->id = $legacyevent->id;
+        $dbrow->courseid = $course->id;
+        $dbrow->instance = $moduleinstance->id;
+        $dbrow->modulename = 'assign';
+        $event = $factory->create_instance($dbrow);
+
+        // Module is invisible to students if the course is invisible.
+        $this->assertNull($event);
+    }
+
     /**
      * Test that the event factory deals with completion related events properly.
      */
@@ -285,6 +342,31 @@ class core_calendar_container_testcase extends advanced_testcase {
         $this->assertInstanceOf(event_interface::class, $factory->create_instance($event));
     }
 
+    /**
+     * Test that when course module is deleted all events are also deleted.
+     */
+    public function test_delete_module_delete_events() {
+        global $DB;
+        $user = $this->getDataGenerator()->create_user();
+        // Create the course we will be using.
+        $course = $this->getDataGenerator()->create_course();
+        $group = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
+
+        foreach (core_component::get_plugin_list('mod') as $modname => $unused) {
+            $module = $this->getDataGenerator()->create_module($modname, ['course' => $course->id]);
+
+            // Create bunch of events of different type (user override, group override, module event).
+            $this->create_event(['userid' => $user->id, 'modulename' => $modname, 'instance' => $module->id]);
+            $this->create_event(['groupid' => $group->id, 'modulename' => $modname, 'instance' => $module->id]);
+            $this->create_event(['modulename' => $modname, 'instance' => $module->id]);
+            $this->create_event(['modulename' => $modname, 'instance' => $module->id, 'courseid' => $course->id]);
+
+            // Delete module and make sure all events are deleted.
+            course_delete_module($module->cmid);
+            $this->assertEmpty($DB->get_record('event', ['modulename' => $modname, 'instance' => $module->id]));
+        }
+    }
+
     /**
      * Test getting the event mapper.
      */
index 9912bcc..fc0ecc2 100644 (file)
@@ -674,13 +674,15 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
      */
     public function test_get_calendar_events_override() {
         $user = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
         $teacher = $this->getDataGenerator()->create_user();
         $anotheruser = $this->getDataGenerator()->create_user();
         $course = $this->getDataGenerator()->create_course();
         $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
         $moduleinstance = $generator->create_instance(['course' => $course->id]);
 
-        $this->getDataGenerator()->enrol_user($user->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student');
         $this->getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher');
         $this->resetAfterTest(true);
         $this->setAdminUser();
@@ -692,11 +694,12 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
         ];
 
         $now = time();
+        // Create two events - one for everybody in the course and one only for the first student.
         $event1 = $this->create_calendar_event('Base event', 0, 'due', 0, $now + DAYSECS, $params + ['courseid' => $course->id]);
         $event2 = $this->create_calendar_event('User event', $user->id, 'due', 0, $now + 2*DAYSECS, $params + ['courseid' => 0]);
 
-        // Retrieve course events for teacher - only one "Base event" is returned.
-        $this->setUser($teacher);
+        // Retrieve course events for the second student - only one "Base event" is returned.
+        $this->setUser($user2);
         $paramevents = array('courseids' => array($course->id));
         $options = array ('siteevents' => true, 'userevents' => true);
         $events = core_calendar_external::get_calendar_events($paramevents, $options);
@@ -705,7 +708,7 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals(0, count($events['warnings']));
         $this->assertEquals('Base event', $events['events'][0]['name']);
 
-        // Retrieve events for user - both events are returned.
+        // Retrieve events for the first student - both events are returned.
         $this->setUser($user);
         $events = core_calendar_external::get_calendar_events($paramevents, $options);
         $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
index a55aee6..18245a5 100644 (file)
@@ -85,7 +85,8 @@ $url->param('time', $time);
 $PAGE->set_url($url);
 
 if ($courseid != SITEID && !empty($courseid)) {
-    $course = $DB->get_record('course', array('id' => $courseid));
+    // Course ID must be valid and existing.
+    $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
     $courses = array($course->id => $course);
     $issite = false;
     navigation_node::override_active_url(new moodle_url('/course/view.php', array('id' => $course->id)));
@@ -95,7 +96,7 @@ if ($courseid != SITEID && !empty($courseid)) {
     $issite = true;
 }
 
-require_course_login($course);
+require_login($course, false);
 
 $calendar = new calendar_information(0, 0, 0, $time);
 $calendar->prepare_for_view($course, $courses);
index 4e0a33c..04b05c4 100644 (file)
@@ -74,7 +74,16 @@ class completion_completion extends data_object {
      * @return data_object instance of data_object or false if none found.
      */
     public static function fetch($params) {
-        return self::fetch_helper('course_completions', __CLASS__, $params);
+        $cache = cache::make('core', 'coursecompletion');
+
+        $key = $params['userid'] . '_' . $params['course'];
+        if ($hit = $cache->get($key)) {
+            return $hit['value'];
+        }
+
+        $tocache = self::fetch_helper('course_completions', __CLASS__, $params);
+        $cache->set($key, ['value' => $tocache]);
+        return $tocache;
     }
 
     /**
@@ -179,9 +188,10 @@ class completion_completion extends data_object {
             $this->timeenrolled = 0;
         }
 
+        $result = false;
         // Save record
         if ($this->id) {
-            return $this->update();
+            $result = $this->update();
         } else {
             // Make sure reaggregate field is not null
             if (!$this->reaggregate) {
@@ -193,7 +203,17 @@ class completion_completion extends data_object {
                 $this->timestarted = 0;
             }
 
-            return $this->insert();
+            $result = $this->insert();
+        }
+
+        if ($result) {
+            // Update the cached record.
+            $cache = cache::make('core', 'coursecompletion');
+            $data = $this->get_record_data();
+            $key = $data->userid . '_' . $data->course;
+            $cache->set($key, ['value' => $data]);
         }
+
+        return $result;
     }
 }
index f16bdd6..b630ad8 100644 (file)
@@ -559,7 +559,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
                 // so there is definitely something to print.
                 $formattedinfo = \core_availability\info::format_info(
                         $section->availableinfo, $section->course);
-                $o .= $this->courserenderer->availability_info($formattedinfo);
+                $o .= $this->courserenderer->availability_info($formattedinfo, 'isrestricted');
             }
         } else if ($canviewhidden && !empty($CFG->enableavailability)) {
             // Check if there is an availability restriction.
@@ -568,7 +568,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
             if ($fullinfo) {
                 $formattedinfo = \core_availability\info::format_info(
                         $fullinfo, $section->course);
-                $o .= $this->courserenderer->availability_info($formattedinfo);
+                $o .= $this->courserenderer->availability_info($formattedinfo, 'isrestricted isfullinfo');
             }
         }
         return $o;
index e60bd2c..dbd8b18 100644 (file)
@@ -55,6 +55,10 @@ define('FIRSTUSEDEXCELROW', 3);
 define('MOD_CLASS_ACTIVITY', 0);
 define('MOD_CLASS_RESOURCE', 1);
 
+define('COURSE_TIMELINE_PAST', 'past');
+define('COURSE_TIMELINE_INPROGRESS', 'inprogress');
+define('COURSE_TIMELINE_FUTURE', 'future');
+
 function make_log_url($module, $url) {
     switch ($module) {
         case 'course':
@@ -863,6 +867,7 @@ function course_create_section($courseorid, $position = 0, $skipcheck = false) {
     $cw->name = null;
     $cw->visible = 1;
     $cw->availability = null;
+    $cw->timemodified = time();
     $cw->id = $DB->insert_record("course_sections", $cw);
 
     // Now move it to the specified position.
@@ -1200,8 +1205,10 @@ function course_delete_module($cmid, $async = false) {
 
     // Delete events from calendar.
     if ($events = $DB->get_records('event', array('instance' => $cm->instance, 'modulename' => $modulename))) {
+        $coursecontext = context_course::instance($cm->course);
         foreach($events as $event) {
-            $calendarevent = calendar_event::load($event->id);
+            $event->context = $coursecontext;
+            $calendarevent = calendar_event::load($event);
             $calendarevent->delete();
         }
     }
@@ -1611,6 +1618,7 @@ function course_update_section($course, $section, $data) {
 
     // Update record in the DB and course format options.
     $data['id'] = $section->id;
+    $data['timemodified'] = time();
     $DB->update_record('course_sections', $data);
     rebuild_course_cache($courseid, true);
     course_get_format($courseid)->update_section_format_options($data);
@@ -2934,6 +2942,7 @@ class course_request {
         $data->visibleold         = $data->visible;
         $data->lang               = $courseconfig->lang;
         $data->enablecompletion   = $courseconfig->enablecompletion;
+        $data->numsections        = $courseconfig->numsections;
 
         $course = create_course($data);
         $context = context_course::instance($course->id, MUST_EXIST);
@@ -4001,6 +4010,46 @@ function course_check_updates($course, $tocheck, $filter = array()) {
     return array($instances, $warnings);
 }
 
+/**
+ * This function classifies a course as past, in progress or future.
+ *
+ * This function may incur a DB hit to calculate course completion.
+ * @param stdClass $course Course record
+ * @param stdClass $user User record (optional - defaults to $USER).
+ * @param completion_info $completioninfo Completion record for the user (optional - will be fetched if required).
+ * @return string (one of COURSE_TIMELINE_FUTURE, COURSE_TIMELINE_INPROGRESS or COURSE_TIMELINE_PAST)
+ */
+function course_classify_for_timeline($course, $user = null, $completioninfo = null) {
+    global $USER;
+
+    if ($user == null) {
+        $user = $USER;
+    }
+
+    $today = time();
+    // End date past.
+    if (!empty($course->enddate) && $course->enddate < $today) {
+        return COURSE_TIMELINE_PAST;
+    }
+
+    if ($completioninfo == null) {
+        $completioninfo = new completion_info($course);
+    }
+
+    // Course was completed.
+    if ($completioninfo->is_enabled() && $completioninfo->is_course_complete($user->id)) {
+        return COURSE_TIMELINE_PAST;
+    }
+
+    // Start date not reached.
+    if (!empty($course->startdate) && $course->startdate > $today) {
+        return COURSE_TIMELINE_FUTURE;
+    }
+
+    // Everything else is in progress.
+    return COURSE_TIMELINE_INPROGRESS;
+}
+
 /**
  * Check module updates since a given time.
  * This function checks for updates in the module config, file areas, completion, grades, comments and ratings.
index c081b73..ebd5c49 100644 (file)
 //                                                                       //
 ///////////////////////////////////////////////////////////////////////////
 
-/*
+/**
+ * This page display the publication backup form
+ *
  * @package    course
  * @subpackage publish
  * @author     Jerome Mouneyrac <jerome@mouneyrac.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL
  * @copyright  (C) 1999 onwards Martin Dougiamas  http://dougiamas.com
- *
- * This page display the publication backup form
  */
 
+define('NO_OUTPUT_BUFFERING', true);
+
 require_once('../../config.php');
 require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_plan_builder.class.php');
index bee3fe7..609ae6a 100644 (file)
@@ -450,7 +450,7 @@ class core_course_renderer extends plugin_renderer_base {
     public function course_section_cm_completion($course, &$completioninfo, cm_info $mod, $displayoptions = array()) {
         global $CFG;
         $output = '';
-        if (!$mod->is_visible_on_course_page()) {
+        if (!empty($displayoptions['hidecompletion']) || !isloggedin() || isguestuser() || !$mod->uservisible) {
             return $output;
         }
         if ($completioninfo === null) {
@@ -728,7 +728,24 @@ class core_course_renderer extends plugin_renderer_base {
      * @return string
      */
     public function availability_info($text, $additionalclasses = '') {
+
         $data = ['text' => $text, 'classes' => $additionalclasses];
+        $additionalclasses = array_filter(explode(' ', $additionalclasses));
+
+        if (in_array('ishidden', $additionalclasses)) {
+            $data['ishidden'] = 1;
+
+        } else if (in_array('isstealth', $additionalclasses)) {
+            $data['isstealth'] = 1;
+
+        } else if (in_array('isrestricted', $additionalclasses)) {
+            $data['isrestricted'] = 1;
+
+            if (in_array('isfullinfo', $additionalclasses)) {
+                $data['isfullinfo'] = 1;
+            }
+        }
+
         return $this->render_from_template('core/availability_info', $data);
     }
 
@@ -752,7 +769,7 @@ class core_course_renderer extends plugin_renderer_base {
             if (!empty($mod->availableinfo)) {
                 $formattedinfo = \core_availability\info::format_info(
                         $mod->availableinfo, $mod->get_course());
-                $output = $this->availability_info($formattedinfo);
+                $output = $this->availability_info($formattedinfo, 'isrestricted');
             }
             return $output;
         }
@@ -775,9 +792,9 @@ class core_course_renderer extends plugin_renderer_base {
             // Display information about conditional availability.
             // Don't add availability information if user is not editing and activity is hidden.
             if ($mod->visible || $this->page->user_is_editing()) {
-                $hidinfoclass = '';
+                $hidinfoclass = 'isrestricted isfullinfo';
                 if (!$mod->visible) {
-                    $hidinfoclass = 'hide';
+                    $hidinfoclass .= ' hide';
                 }
                 $ci = new \core_availability\info_module($mod);
                 $fullinfo = $ci->get_full_information();
index 4b50090..b4840f9 100644 (file)
@@ -101,7 +101,8 @@ if ($switchrole > 0 && has_capability('moodle/role:switchroles', $context)) {
 
     foreach ($roles as $key => $role) {
         $url = new moodle_url('/course/switchrole.php', array('id' => $id, 'switchrole' => $key, 'returnurl' => $returnurl));
-        echo $OUTPUT->container($OUTPUT->single_button($url, $role), 'm-x-3 m-b-1');
+        // Button encodes special characters, apply htmlspecialchars_decode() to avoid double escaping.
+        echo $OUTPUT->container($OUTPUT->single_button($url, htmlspecialchars_decode($role)), 'm-x-3 m-b-1');
     }
 
     $url = new moodle_url($returnurl);
index 61169d5..5ce1ffa 100644 (file)
@@ -678,6 +678,29 @@ class core_course_courselib_testcase extends advanced_testcase {
         }
     }
 
+    public function test_update_course_section_time_modified() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Create the course with sections.
+        $course = $this->getDataGenerator()->create_course(array('numsections' => 10), array('createsections' => true));
+        $sections = $DB->get_records('course_sections', array('course' => $course->id));
+
+        // Get the last section's time modified value.
+        $section = array_pop($sections);
+        $oldtimemodified = $section->timemodified;
+
+        // Update the section.
+        $this->waitForSecond(); // Ensuring that the section update occurs at a different timestamp.
+        course_update_section($course, $section, array());
+
+        // Check that the time has changed.
+        $section = $DB->get_record('course_sections', array('id' => $section->id));
+        $newtimemodified = $section->timemodified;
+        $this->assertGreaterThan($oldtimemodified, $newtimemodified);
+    }
+
     public function test_course_add_cm_to_section() {
         global $DB;
         $this->resetAfterTest(true);
@@ -3684,4 +3707,52 @@ class core_course_courselib_testcase extends advanced_testcase {
         }
         $this->assertEquals(2, $count);
     }
+
+    public function test_classify_course_for_timeline() {
+        global $DB, $CFG;
+
+        require_once($CFG->dirroot.'/completion/criteria/completion_criteria_self.php');
+
+        set_config('enablecompletion', COMPLETION_ENABLED);
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        // Create courses for testing.
+        $generator = $this->getDataGenerator();
+        $future = time() + 3600;
+        $past = time() - 3600;
+        $futurecourse = $generator->create_course(['startdate' => $future]);
+        $pastcourse = $generator->create_course(['startdate' => $past - 60, 'enddate' => $past]);
+        $completedcourse = $generator->create_course(['enablecompletion' => COMPLETION_ENABLED]);
+        $inprogresscourse = $generator->create_course();
+
+        // Set completion rules.
+        $criteriadata = new stdClass();
+        $criteriadata->id = $completedcourse->id;
+
+        // Self completion.
+        $criteriadata->criteria_self = COMPLETION_CRITERIA_TYPE_SELF;
+        $class = 'completion_criteria_self';
+        $criterion = new $class();
+        $criterion->update_config($criteriadata);
+
+        $user = $this->getDataGenerator()->create_user();
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($user->id, $futurecourse->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($user->id, $pastcourse->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($user->id, $completedcourse->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($user->id, $inprogresscourse->id, $studentrole->id);
+
+        $this->setUser($user);
+        core_completion_external::mark_course_self_completed($completedcourse->id);
+        $ccompletion = new completion_completion(array('course' => $completedcourse->id, 'userid' => $user->id));
+        $ccompletion->mark_complete();
+
+        // Aggregate the completions.
+        $this->assertEquals(COURSE_TIMELINE_PAST, course_classify_for_timeline($pastcourse));
+        $this->assertEquals(COURSE_TIMELINE_FUTURE, course_classify_for_timeline($futurecourse));
+        $this->assertEquals(COURSE_TIMELINE_PAST, course_classify_for_timeline($completedcourse));
+        $this->assertEquals(COURSE_TIMELINE_INPROGRESS, course_classify_for_timeline($inprogresscourse));
+    }
 }
index 8012a7f..d932dd8 100644 (file)
@@ -361,6 +361,40 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         core_course_external::update_categories($categories);
     }
 
+    /**
+     * Test create_courses numsections
+     */
+    public function test_create_course_numsections() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        // Set the required capabilities by the external function.
+        $contextid = context_system::instance()->id;
+        $roleid = $this->assignUserCapability('moodle/course:create', $contextid);
+        $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
+
+        $numsections = 10;
+        $category  = self::getDataGenerator()->create_category();
+
+        // Create base categories.
+        $course1['fullname'] = 'Test course 1';
+        $course1['shortname'] = 'Testcourse1';
+        $course1['categoryid'] = $category->id;
+        $course1['courseformatoptions'][] = array('name' => 'numsections', 'value' => $numsections);
+
+        $courses = array($course1);
+
+        $createdcourses = core_course_external::create_courses($courses);
+        foreach ($createdcourses as $createdcourse) {
+            $existingsections = $DB->get_records('course_sections', array('course' => $createdcourse['id']));
+            $modinfo = get_fast_modinfo($createdcourse['id']);
+            $sections = $modinfo->get_section_info_all();
+            $this->assertEquals(count($sections), $numsections + 1); // Includes generic section.
+            $this->assertEquals(count($existingsections), $numsections + 1); // Includes generic section.
+        }
+    }
+
     /**
      * Test create_courses
      */
index 90ca368..0eaa555 100644 (file)
@@ -42,11 +42,9 @@ class writer extends \core\dataformat\base {
     public $extension = ".html";
 
     /**
-     * Write the start of the format
-     *
-     * @param array $columns
+     * Write the start of the output
      */
-    public function write_header($columns) {
+    public function start_output() {
         echo "<!DOCTYPE html><html>";
         echo \html_writer::tag('title', $this->filename);
         echo "<style>
@@ -75,9 +73,16 @@ table {
     margin: auto;
 }
 </style>
-<body>
-<table border=1 cellspacing=0 cellpadding=3>
-";
+<body>";
+    }
+
+    /**
+     * Write the start of the sheet we will be adding data to.
+     *
+     * @param array $columns
+     */
+    public function start_sheet($columns) {
+        echo "<table border=1 cellspacing=0 cellpadding=3>";
         echo \html_writer::start_tag('tr');
         foreach ($columns as $k => $v) {
             echo \html_writer::tag('th', $v);
@@ -100,12 +105,18 @@ table {
     }
 
     /**
-     * Write the end of the format
+     * Write the end of the sheet containing the data.
      *
      * @param array $columns
      */
-    public function write_footer($columns) {
-        echo "</table></body></html>";
+    public function close_sheet($columns) {
+        echo "</table>";
     }
 
+    /**
+     * Write the end of the sheet containing the data.
+     */
+    public function close_output() {
+        echo "</body></html>";
+    }
 }
index 84f3cc6..8e3cfba 100644 (file)
@@ -41,12 +41,31 @@ class writer extends \core\dataformat\base {
     /** @var $extension */
     public $extension = ".json";
 
+    /** @var $sheetstarted */
+    public $sheetstarted = false;
+
+    /** @var $sheetdatadded */
+    public $sheetdatadded = false;
+
+    /**
+     * Write the start of the file.
+     */
+    public function start_output() {
+        echo "[";
+    }
+
     /**
-     * Write the start of the format
+     * Write the start of the sheet we will be adding data to.
      *
      * @param array $columns
      */
-    public function write_header($columns) {
+    public function start_sheet($columns) {
+        if ($this->sheetstarted) {
+            echo ",";
+        } else {
+            $this->sheetstarted = true;
+        }
+        $this->sheetdatadded = false;
         echo "[";
     }
 
@@ -57,19 +76,28 @@ class writer extends \core\dataformat\base {
      * @param int $rownum
      */
     public function write_record($record, $rownum) {
-        if ($rownum) {
+        if ($this->sheetdatadded) {
             echo ",";
         }
+
         echo json_encode($record);
+
+        $this->sheetdatadded = true;
     }
 
     /**
-     * Write the end of the format
+     * Write the end of the sheet containing the data.
      *
      * @param array $columns
      */
-    public function write_footer($columns) {
+    public function close_sheet($columns) {
         echo "]";
     }
 
+    /**
+     * Write the end of the file.
+     */
+    public function close_output() {
+        echo "]";
+    }
 }
index bfd29d1..5868991 100644 (file)
@@ -1,7 +1,14 @@
 This files describes API changes in /dataformat/ download system,
 information provided here is intended especially for developers.
 
-=== 3.1 ===
-* Added new plugin system with low memory support for csv, ods, xls and json
+=== 3.4 ===
 
+* In order to allow multiple sheets in an exported file the functions write_header() and write_footer() have
+  been removed from core dataformat plugins and have been replaced.
+  - write_header() has been replaced with the two functions start_output() and start_sheet().
+  - write_footer() has been replaced with the two functions close_output() and close_sheet().
+  For backwards compatibility write_header() and write_footer() will continue to work but if used will
+  trigger the function error_log().
 
+=== 3.1 ===
+* Added new plugin system with low memory support for csv, ods, xls and json
index b1757a7..934d47a 100644 (file)
@@ -38,7 +38,7 @@ class enrol_guest_enrol_form extends moodleform {
         $heading = $plugin->get_instance_name($instance);
         $mform->addElement('header', 'guestheader', $heading);
 
-        $mform->addElement('passwordunmask', 'guestpassword', get_string('password', 'enrol_guest'));
+        $mform->addElement('password', 'guestpassword', get_string('password', 'enrol_guest'));
 
         $this->add_action_buttons(false, get_string('submit'));
 
index aafc0e7..e6f4140 100644 (file)
@@ -328,7 +328,7 @@ class enrol_ldap_plugin extends enrol_plugin {
             return;
         }
 
-        $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'));
+        $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'), $this->ldapconnection);
 
         // we may need a lot of memory here
         core_php_time_limit::raise();
@@ -761,7 +761,7 @@ class enrol_ldap_plugin extends enrol_plugin {
 
         // Get all contexts and look for first matching user
         $ldap_contexts = explode(';', $ldap_contexts);
-        $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'));
+        $ldap_pagedresults = ldap_paged_results_supported($this->get_config('ldap_version'), $this->ldapconnection);
         foreach ($ldap_contexts as $context) {
             $context = trim($context);
             if (empty($context)) {
index a60224e..dc17af1 100644 (file)
@@ -78,7 +78,7 @@ class enrol_self_enrol_form extends moodleform {
 
         if ($instance->password) {
             // Change the id of self enrolment key input as there can be multiple self enrolment methods.
-            $mform->addElement('passwordunmask', 'enrolpassword', get_string('password', 'enrol_self'),
+            $mform->addElement('password', 'enrolpassword', get_string('password', 'enrol_self'),
                     array('id' => 'enrolpassword_'.$instance->id));
             $context = context_course::instance($this->instance->courseid);
             $keyholders = get_users_by_capability($context, 'enrol/self:holdkey', user_picture::fields('u'));
index b232eda..211e149 100644 (file)
@@ -73,8 +73,8 @@ class filter_urltolink extends moodle_text_filter {
         //<a href="blah">
         //&lt;a href="blah"&gt;
         //&lt;a href="blah">
-        $filterignoretagsopen  = array('<a\s[^>]+?>');
-        $filterignoretagsclose = array('</a>');
+        $filterignoretagsopen  = array('<a\s[^>]+?>', '<span[^>]+?class="nolink"[^>]*?>');
+        $filterignoretagsclose = array('</a>', '</span>');
         filter_save_ignore_tags($text,$filterignoretagsopen,$filterignoretagsclose,$ignoretags);
 
         // Check if we support unicode modifiers in regular expressions. Cache it.
@@ -174,4 +174,3 @@ function filter_urltolink_img_callback($link) {
     }
     return '<img class="filter_urltolink_image" alt="" src="'.$link[1].'" />';
 }
-
index 0ae7f89..1dca5b7 100644 (file)
@@ -174,6 +174,9 @@ class filter_urltolink_filter_testcase extends basic_testcase {
             '<link rel="search" type="application/opensearchdescription+xml" href="/osd.jsp" title="Peer review - Moodle Tracker"/>' => '<link rel="search" type="application/opensearchdescription+xml" href="/osd.jsp" title="Peer review - Moodle Tracker"/>',
             '<a href="https://docs.moodle.org/dev/Main_Page"></a><span>www.google.com</span><span class="placeholder"></span>' => '<a href="https://docs.moodle.org/dev/Main_Page"></a><span><a href="http://www.google.com" class="_blanktarget">www.google.com</a></span><span class="placeholder"></span>',
             'http://nolandforzombies.com <a href="zombiesFTW.com">Zombies FTW</a> http://aliens.org' => '<a href="http://nolandforzombies.com" class="_blanktarget">http://nolandforzombies.com</a> <a href="zombiesFTW.com">Zombies FTW</a> <a href="http://aliens.org" class="_blanktarget">http://aliens.org</a>',
+            // Test 'nolink' class.
+            'URL: <span class="nolink">http://moodle.org</span>' => 'URL: <span class="nolink">http://moodle.org</span>',
+            '<span class="nolink">URL: http://moodle.org</span>' => '<span class="nolink">URL: http://moodle.org</span>',
             //URLs in Javascript. Commented out as part of MDL-21183
             //'var url="http://moodle.org";'=>'var url="http://moodle.org";',
             //'var url = "http://moodle.org";'=>'var url = "http://moodle.org";',
index 395b072..65179a7 100644 (file)
@@ -32,7 +32,7 @@ $action   = optional_param('action', '', PARAM_ALPHA);
 /// Make sure they can even access this course
 if ($courseid) {
     if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-        print_error('nocourseid');
+        print_error('invalidcourseid');
     }
     require_login($course);
     $context = context_course::instance($course->id);
index 3377552..ec5117e 100644 (file)
@@ -38,7 +38,7 @@ $PAGE->set_pagelayout('admin');
 /// Make sure they can even access this course
 if ($courseid) {
     if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-        print_error('nocourseid');
+        print_error('invalidcourseid');
     }
     require_login($course);
     $context = context_course::instance($course->id);
index e016560..830c675 100644 (file)
@@ -71,7 +71,7 @@ if ($id) {
     $heading = get_string('addscale', 'grades');
     /// adding new scale from course
     if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-        print_error('nocourseid');
+        print_error('invalidcourseid');
     }
     $scale_rec = new stdClass();
     $scale_rec->standard = 0;
index 0b8e207..30b2ef8 100644 (file)
@@ -34,7 +34,7 @@ $PAGE->set_url('/grade/edit/scale/index.php', array('id' => $courseid));
 /// Make sure they can even access this course
 if ($courseid) {
     if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-        print_error('nocourseid');
+        print_error('invalidcourseid');
     }
     require_login($course);
     $context = context_course::instance($course->id);
index 5b885d4..5b73474 100644 (file)
@@ -33,7 +33,7 @@ $PAGE->set_url('/grade/edit/settings/index.php', array('id'=>$courseid));
 $PAGE->set_pagelayout('admin');
 
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 require_login($course);
 $context = context_course::instance($course->id);
index cea8791..1117f9d 100644 (file)
@@ -33,7 +33,7 @@ $PAGE->set_url('/grade/edit/tree/action.php', array('id'=>$courseid, 'action'=>$
 
 /// Make sure they can even access this course
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 require_login($course);
 $context = context_course::instance($course->id);
index e0dabcf..76fea61 100644 (file)
@@ -39,7 +39,7 @@ if ($section !== 'calculation') {
 $PAGE->set_url($url);
 
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 9886fe2..deb397b 100644 (file)
@@ -40,7 +40,7 @@ navigation_node::override_active_url(new moodle_url('/grade/edit/tree/index.php'
     array('id'=>$courseid)));
 
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index dd9cce3..698cde7 100644 (file)
@@ -45,7 +45,7 @@ if ($userid !== 0) {
 $PAGE->set_url($url);
 
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 $PAGE->set_pagelayout('incourse');
index bcdbbb8..3a1ce5e 100644 (file)
@@ -38,7 +38,7 @@ $PAGE->set_pagelayout('admin');
 
 /// Make sure they can even access this course
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 7ee5cbe..2ece98f 100644 (file)
@@ -41,7 +41,7 @@ navigation_node::override_active_url(new moodle_url('/grade/edit/tree/index.php'
     array('id'=>$courseid)));
 
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 2313c63..5dfb8e9 100644 (file)
@@ -264,142 +264,143 @@ class edit_item_form extends moodleform {
         $mform =& $this->_form;
 
         if ($id = $mform->getElementValue('id')) {
-            $grade_item = grade_item::fetch(array('id'=>$id));
+            $gradeitem = grade_item::fetch(array('id' => $id));
+            $parentcategory = $gradeitem->get_parent_category();
+        } else {
+            // If we do not have an id, we are creating a new grade item.
+            $gradeitem = new grade_item(array('courseid' => $COURSE->id, 'itemtype' => 'manual'), false);
+
+            // Assign the course category to this grade item.
+            $parentcategory = grade_category::fetch_course_category($COURSE->id);
+            $gradeitem->parent_category = $parentcategory;
+        }
+
+        if (!$gradeitem->is_raw_used()) {
+            $mform->removeElement('plusfactor');
+            $mform->removeElement('multfactor');
+        }
 
-            if (!$grade_item->is_raw_used()) {
-                $mform->removeElement('plusfactor');
-                $mform->removeElement('multfactor');
+        if ($gradeitem->is_outcome_item()) {
+            // We have to prevent incompatible modifications of outcomes if outcomes disabled.
+            $mform->removeElement('grademax');
+            if ($mform->elementExists('grademin')) {
+                $mform->removeElement('grademin');
             }
+            $mform->removeElement('gradetype');
+            $mform->removeElement('display');
+            $mform->removeElement('decimals');
+            $mform->hardFreeze('scaleid');
 
-            if ($grade_item->is_outcome_item()) {
-                // we have to prevent incompatible modifications of outcomes if outcomes disabled
-                $mform->removeElement('grademax');
+        } else {
+            if ($gradeitem->is_external_item()) {
+                // Following items are set up from modules and should not be overrided by user.
                 if ($mform->elementExists('grademin')) {
-                    $mform->removeElement('grademin');
+                    // The site setting grade_report_showmin may have prevented grademin being added to the form.
+                    $mform->hardFreeze('grademin');
+                }
+                $mform->hardFreeze('itemname,gradetype,grademax,scaleid');
+                if ($gradeitem->itemnumber == 0) {
+                    // The idnumber of grade itemnumber 0 is synced with course_modules.
+                    $mform->hardFreeze('idnumber');
                 }
-                $mform->removeElement('gradetype');
-                $mform->removeElement('display');
-                $mform->removeElement('decimals');
-                $mform->hardFreeze('scaleid');
 
-            } else {
-                if ($grade_item->is_external_item()) {
-                    // following items are set up from modules and should not be overrided by user
+                // For external items we can not change the grade type, even if no grades exist, so if it is set to
+                // scale, then remove the grademax and grademin fields from the form - no point displaying them.
+                if ($gradeitem->gradetype == GRADE_TYPE_SCALE) {
+                    $mform->removeElement('grademax');
                     if ($mform->elementExists('grademin')) {
-                        // The site setting grade_report_showmin may have prevented grademin being added to the form.
-                        $mform->hardFreeze('grademin');
-                    }
-                    $mform->hardFreeze('itemname,gradetype,grademax,scaleid');
-                    if ($grade_item->itemnumber == 0) {
-                        // the idnumber of grade itemnumber 0 is synced with course_modules
-                        $mform->hardFreeze('idnumber');
+                        $mform->removeElement('grademin');
                     }
+                } else { // Not using scale, so remove it.
+                    $mform->removeElement('scaleid');
+                }
 
-                    // For external items we can not change the grade type, even if no grades exist, so if it is set to
-                    // scale, then remove the grademax and grademin fields from the form - no point displaying them.
-                    if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
-                        $mform->removeElement('grademax');
-                        if ($mform->elementExists('grademin')) {
-                            $mform->removeElement('grademin');
-                        }
-                    } else { // Not using scale, so remove it.
-                        $mform->removeElement('scaleid');
-                    }
+                // Always remove the rescale grades element if it's an external item.
+                $mform->removeElement('rescalegrades');
+            } else if ($gradeitem->has_grades()) {
+                // Can't change the grade type or the scale if there are grades.
+                $mform->hardFreeze('gradetype, scaleid');
 
-                    // Always remove the rescale grades element if it's an external item.
+                // If we are using scales then remove the unnecessary rescale and grade fields.
+                if ($gradeitem->gradetype == GRADE_TYPE_SCALE) {
                     $mform->removeElement('rescalegrades');
-                } else if ($grade_item->has_grades()) {
-                    // Can't change the grade type or the scale if there are grades.
-                    $mform->hardFreeze('gradetype, scaleid');
-
-                    // If we are using scales then remove the unnecessary rescale and grade fields.
-                    if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
-                        $mform->removeElement('rescalegrades');
-                        $mform->removeElement('grademax');
-                        if ($mform->elementExists('grademin')) {
-                            $mform->removeElement('grademin');
-                        }
-                    } else { // Remove the scale field.
-                        $mform->removeElement('scaleid');
-                        // Set the maximum grade to disabled unless a grade is chosen.
-                        $mform->disabledIf('grademax', 'rescalegrades', 'eq', '');
+                    $mform->removeElement('grademax');
+                    if ($mform->elementExists('grademin')) {
+                        $mform->removeElement('grademin');
                     }
-                } else {
-                    // Remove the rescale element if there are no grades.
-                    $mform->removeElement('rescalegrades');
+                } else { // Remove the scale field.
+                    $mform->removeElement('scaleid');
+                    // Set the maximum grade to disabled unless a grade is chosen.
+                    $mform->disabledIf('grademax', 'rescalegrades', 'eq', '');
                 }
+            } else {
+                // Remove the rescale element if there are no grades.
+                $mform->removeElement('rescalegrades');
             }
+        }
+
+        // If we wanted to change parent of existing item - we would have to verify there are no circular references in parents!!!
+        if ($id && $mform->elementExists('parentcategory')) {
+            $mform->hardFreeze('parentcategory');
+        }
+
+        $parentcategory->apply_forced_settings();
 
-            // if we wanted to change parent of existing item - we would have to verify there are no circular references in parents!!!
-            if ($mform->elementExists('parentcategory')) {
-                $mform->hardFreeze('parentcategory');
+        if (!$parentcategory->is_aggregationcoef_used()) {
+            if ($mform->elementExists('aggregationcoef')) {
+                $mform->removeElement('aggregationcoef');
             }
 
-            $parent_category = $grade_item->get_parent_category();
-            $parent_category->apply_forced_settings();
+        } else {
+            $coefstring = $gradeitem->get_coefstring();
 
-            if (!$parent_category->is_aggregationcoef_used()) {
-                if ($mform->elementExists('aggregationcoef')) {
-                    $mform->removeElement('aggregationcoef');
+            if ($coefstring !== '') {
+                if ($coefstring == 'aggregationcoefextrasum' || $coefstring == 'aggregationcoefextraweightsum') {
+                    // The advcheckbox is not compatible with disabledIf!
+                    $coefstring = 'aggregationcoefextrasum';
+                    $element =& $mform->createElement('checkbox', 'aggregationcoef', get_string($coefstring, 'grades'));
+                } else {
+                    $element =& $mform->createElement('text', 'aggregationcoef', get_string($coefstring, 'grades'));
                 }
-
-            } else {
-                $coefstring = $grade_item->get_coefstring();
-
-                if ($coefstring !== '') {
-                    if ($coefstring == 'aggregationcoefextrasum' || $coefstring == 'aggregationcoefextraweightsum') {
-                        // advcheckbox is not compatible with disabledIf!
-                        $coefstring = 'aggregationcoefextrasum';
-                        $element =& $mform->createElement('checkbox', 'aggregationcoef', get_string($coefstring, 'grades'));
-                    } else {
-                        $element =& $mform->createElement('text', 'aggregationcoef', get_string($coefstring, 'grades'));
-                    }
-                    if ($mform->elementExists('parentcategory')) {
-                        $mform->insertElementBefore($element, 'parentcategory');
-                    } else {
-                        $mform->insertElementBefore($element, 'id');
-                    }
-                    $mform->addHelpButton('aggregationcoef', $coefstring, 'grades');
+                if ($mform->elementExists('parentcategory')) {
+                    $mform->insertElementBefore($element, 'parentcategory');
+                } else {
+                    $mform->insertElementBefore($element, 'id');
                 }
-                $mform->disabledIf('aggregationcoef', 'gradetype', 'eq', GRADE_TYPE_NONE);
-                $mform->disabledIf('aggregationcoef', 'gradetype', 'eq', GRADE_TYPE_TEXT);
-                $mform->disabledIf('aggregationcoef', 'parentcategory', 'eq', $parent_category->id);
+                $mform->addHelpButton('aggregationcoef', $coefstring, 'grades');
             }
+            $mform->disabledIf('aggregationcoef', 'gradetype', 'eq', GRADE_TYPE_NONE);
+            $mform->disabledIf('aggregationcoef', 'gradetype', 'eq', GRADE_TYPE_TEXT);
+            $mform->disabledIf('aggregationcoef', 'parentcategory', 'eq', $parentcategory->id);
+        }
 
-            // Remove fields used by natural weighting if the parent category is not using natural weighting.
-            // Or if the item is a scale and scales are not used in aggregation.
-            if ($parent_category->aggregation != GRADE_AGGREGATE_SUM
-                    || (empty($CFG->grade_includescalesinaggregation) && $grade_item->gradetype == GRADE_TYPE_SCALE)) {
-                if ($mform->elementExists('weightoverride')) {
-                    $mform->removeElement('weightoverride');
-                }
-                if ($mform->elementExists('aggregationcoef2')) {
-                    $mform->removeElement('aggregationcoef2');
-                }
+        // Remove fields used by natural weighting if the parent category is not using natural weighting.
+        // Or if the item is a scale and scales are not used in aggregation.
+        if ($parentcategory->aggregation != GRADE_AGGREGATE_SUM
+                || (empty($CFG->grade_includescalesinaggregation) && $gradeitem->gradetype == GRADE_TYPE_SCALE)) {
+            if ($mform->elementExists('weightoverride')) {
+                $mform->removeElement('weightoverride');
             }
+            if ($mform->elementExists('aggregationcoef2')) {
+                $mform->removeElement('aggregationcoef2');
+            }
+        }
 
-            if ($category = $grade_item->get_item_category()) {
-                if ($category->aggregation == GRADE_AGGREGATE_SUM) {
-                    if ($mform->elementExists('gradetype')) {
-                        $mform->hardFreeze('gradetype');
-                    }
-                    if ($mform->elementExists('grademin')) {
-                        $mform->hardFreeze('grademin');
-                    }
-                    if ($mform->elementExists('grademax')) {
-                        $mform->hardFreeze('grademax');
-                    }
-                    if ($mform->elementExists('scaleid')) {
-                        $mform->removeElement('scaleid');
-                    }
+        if ($category = $gradeitem->get_item_category()) {
+            if ($category->aggregation == GRADE_AGGREGATE_SUM) {
+                if ($mform->elementExists('gradetype')) {
+                    $mform->hardFreeze('gradetype');
+                }
+                if ($mform->elementExists('grademin')) {
+                    $mform->hardFreeze('grademin');
+                }
+                if ($mform->elementExists('grademax')) {
+                    $mform->hardFreeze('grademax');
+                }
+                if ($mform->elementExists('scaleid')) {
+                    $mform->removeElement('scaleid');
                 }
             }
-
-        } else {
-            // all new items are manual, children of course category
-            $mform->removeElement('plusfactor');
-            $mform->removeElement('multfactor');
-            $mform->removeElement('rescalegrades');
         }
 
         // no parent header for course category
index 02c76a8..5d0a628 100644 (file)
@@ -40,7 +40,7 @@ navigation_node::override_active_url(new moodle_url('/grade/edit/tree/index.php'
     array('id'=>$courseid)));
 
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index aca4e87..303a7a6 100644 (file)
@@ -31,7 +31,7 @@ $id = required_param('id', PARAM_INT); // course id
 $PAGE->set_url('/grade/export/keymanager.php', array('id' => $id));
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index c2e7a34..80b2c5a 100644 (file)
@@ -28,7 +28,7 @@ $decimalpoints      = optional_param('decimalpoints', $CFG->grade_export_decimal
 $onlyactive         = optional_param('export_onlyactive', 0, PARAM_BOOL);
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_user_key_login('grade/export', $id); // we want different keys for each course
index 7ab7660..cfb3bfe 100644 (file)
@@ -23,7 +23,7 @@ $id                = required_param('id', PARAM_INT); // course id
 $PAGE->set_url('/grade/export/ods/export.php', array('id'=>$id));
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 29ee8a1..9fe78e7 100644 (file)
@@ -24,7 +24,7 @@ $id = required_param('id', PARAM_INT); // course id
 $PAGE->set_url('/grade/export/ods/index.php', array('id'=>$id));
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 5933954..79f1d43 100644 (file)
@@ -29,7 +29,7 @@ $decimalpoints      = optional_param('decimalpoints', $CFG->grade_export_decimal
 $onlyactive         = optional_param('export_onlyactive', 0, PARAM_BOOL);
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_user_key_login('grade/export', $id); // we want different keys for each course
index 903cfbf..bba045a 100644 (file)
@@ -23,7 +23,7 @@ $id                = required_param('id', PARAM_INT); // course id
 $PAGE->set_url('/grade/export/txt/export.php', array('id'=>$id));
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index c5420ce..98c0f73 100644 (file)
@@ -24,7 +24,7 @@ $id = required_param('id', PARAM_INT); // course id
 $PAGE->set_url('/grade/export/txt/index.php', array('id'=>$id));
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 287c513..5459c04 100644 (file)
@@ -28,7 +28,7 @@ $decimalpoints      = optional_param('decimalpoints', $CFG->grade_export_decimal
 $onlyactive         = optional_param('export_onlyactive', 0, PARAM_BOOL);
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_user_key_login('grade/export', $id); // we want different keys for each course
index 0855517..507ce12 100644 (file)
@@ -23,7 +23,7 @@ $id                = required_param('id', PARAM_INT); // course id
 $PAGE->set_url('/grade/export/xls/export.php', array('id'=>$id));
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 4d86ba8..519f9ea 100644 (file)
@@ -24,7 +24,7 @@ $id = required_param('id', PARAM_INT); // course id
 $PAGE->set_url('/grade/export/xls/index.php', array('id'=>$id));
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index a8ed187..592b1f4 100644 (file)
@@ -29,7 +29,7 @@ $decimalpoints      = optional_param('decimalpoints', $CFG->grade_export_decimal
 $onlyactive         = optional_param('export_onlyactive', 0, PARAM_BOOL);
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_user_key_login('grade/export', $id); // we want different keys for each course
index d7ae8e7..e3a55b0 100644 (file)
@@ -23,7 +23,7 @@ $id                = required_param('id', PARAM_INT); // course id
 $PAGE->set_url('/grade/export/xml/export.php', array('id'=>$id));
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 3b70fd7..40fa2ec 100644 (file)
@@ -24,7 +24,7 @@ $id = required_param('id', PARAM_INT); // course id
 $PAGE->set_url('/grade/export/xml/index.php', array('id'=>$id));
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index ddc1fe9..ecdcb3a 100644 (file)
@@ -39,7 +39,7 @@ if ($verbosescales !== 1) {
 $PAGE->set_url($url);
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 44aabad..0d795d8 100644 (file)
@@ -35,7 +35,7 @@ if ($verbosescales !== 1) {
 $PAGE->set_url($url);
 
 if (!$course = $DB->get_record('course', array('id' => $id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 3ff82cb..6cdc379 100644 (file)
@@ -31,7 +31,7 @@ $id = required_param('id', PARAM_INT); // course id
 $PAGE->set_url('/grade/import/keymanager.php', array('id' => $id));
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 17e1dea..b127968 100644 (file)
@@ -20,7 +20,7 @@ require_once '../../../config.php';
 
 $id = required_param('id', PARAM_INT); // course id
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_user_key_login('grade/import', $id); // we want different keys for each course
index cdb537f..f12e799 100644 (file)
@@ -30,7 +30,7 @@ if ($feedback !== 0) {
 $PAGE->set_url($url);
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index b1ebdff..d587d1f 100644 (file)
@@ -25,7 +25,7 @@ $PAGE->set_url(new moodle_url('/grade/import/xml/index.php', array('id'=>$id)));
 $PAGE->set_pagelayout('admin');
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 0d971a7..8260320 100644 (file)
@@ -37,7 +37,7 @@ $newvalue = optional_param('newvalue', false, PARAM_TEXT);
 
 /// basic access checks
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 $context = context_course::instance($course->id);
 require_login($course);
index 43917fa..10e0c3e 100644 (file)
@@ -56,7 +56,7 @@ $PAGE->requires->yui_module('moodle-gradereport_grader-gradereporttable', 'Y.M.g
 
 // basic access checks
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 require_login($course);
 $context = context_course::instance($course->id);
index 029afa4..4b0fccc 100644 (file)
@@ -35,7 +35,7 @@ $PAGE->set_pagelayout('admin');
 /// Make sure they can even access this course
 
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index cd18dc7..2a9741d 100644 (file)
@@ -44,7 +44,7 @@ $PAGE->set_url($url);
 
 /// basic access checks
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 if (!$item = $DB->get_record('grade_items', array('id' => $itemid))) {
index 5e7522a..261c277 100644 (file)
@@ -30,7 +30,7 @@ $PAGE->set_url('/grade/report/index.php', array('id'=>$courseid));
 
 /// basic access checks
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 require_login($course);
 $context = context_course::instance($course->id);
index 092373a..605a023 100644 (file)
@@ -31,7 +31,7 @@ $courseid = required_param('id', PARAM_INT);                   // course id
 $PAGE->set_url('/grade/report/outcomes/index.php', array('id'=>$courseid));
 
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);
index 4125157..f724fba 100644 (file)
@@ -33,7 +33,7 @@ $userid   = optional_param('userid', $USER->id, PARAM_INT);
 $PAGE->set_url(new moodle_url('/grade/report/overview/index.php', array('id' => $courseid, 'userid' => $userid)));
 
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 require_login(null, false);
 $PAGE->set_course($course);
index 71f7db4..9422fb1 100644 (file)
@@ -22,6 +22,8 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+define('NO_OUTPUT_BUFFERING', true);
+
 require_once('../../../config.php');
 require_once($CFG->dirroot.'/lib/gradelib.php');
 require_once($CFG->dirroot.'/grade/lib.php');
@@ -59,7 +61,7 @@ $PAGE->set_url(new moodle_url('/grade/report/singleview/index.php', $pageparams)
 $PAGE->set_pagelayout('incourse');
 
 if (!$course = $DB->get_record('course', $courseparams)) {
-    print_error('nocourseid');
+    print_error('invalidcourseid');
 }
 
 require_login($course);