Merge branch 'MDL-62968-master' of git://github.com/mickhawkins/moodle
authorSara Arjona <sara@moodle.com>
Fri, 18 Jan 2019 11:38:14 +0000 (12:38 +0100)
committerSara Arjona <sara@moodle.com>
Fri, 18 Jan 2019 11:38:14 +0000 (12:38 +0100)
253 files changed:
.travis.yml
admin/classes/task_log_table.php [new file with mode: 0644]
admin/environment.xml
admin/settings/server.php
admin/settings/subsystems.php
admin/tasklogs.php [new file with mode: 0644]
admin/templates/tasklogs.mustache [new file with mode: 0644]
admin/tool/customlang/db/upgrade.php
admin/tool/dataprivacy/amd/build/effective_retention_period.min.js
admin/tool/dataprivacy/amd/src/effective_retention_period.js
admin/tool/dataprivacy/styles.css
admin/tool/log/db/upgrade.php
admin/tool/log/store/database/db/upgrade.php
admin/tool/log/store/standard/db/upgrade.php
admin/tool/monitor/db/upgrade.php
admin/tool/task/cli/schedule_task.php
admin/tool/task/lang/en/tool_task.php
admin/tool/task/renderer.php
admin/tool/task/settings.php
admin/tool/usertours/db/upgrade.php
admin/tool/xmldb/lang/en/tool_xmldb.php
auth/cas/db/upgrade.php
auth/db/db/upgrade.php
auth/email/db/upgrade.php
auth/ldap/db/upgrade.php
auth/ldap/lang/en/auth_ldap.php
auth/manual/db/upgrade.php
auth/mnet/db/upgrade.php
auth/mnet/lang/en/auth_mnet.php
auth/none/db/upgrade.php
auth/oauth2/classes/auth.php
auth/oauth2/db/upgrade.php
auth/shibboleth/db/upgrade.php
backup/util/ui/backup_ui_setting.class.php
badges/backpack_form.php
blocks/badges/db/upgrade.php
blocks/badges/lang/en/block_badges.php
blocks/calendar_month/db/upgrade.php
blocks/calendar_upcoming/db/upgrade.php
blocks/community/db/upgrade.php
blocks/completionstatus/db/upgrade.php
blocks/course_list/lang/en/block_course_list.php
blocks/course_summary/db/upgrade.php
blocks/html/db/upgrade.php
blocks/navigation/db/upgrade.php
blocks/quiz_results/db/upgrade.php
blocks/recent_activity/db/upgrade.php
blocks/rss_client/db/upgrade.php
blocks/rss_client/lang/en/block_rss_client.php
blocks/section_links/db/upgrade.php
blocks/selfcompletion/db/upgrade.php
blocks/settings/db/upgrade.php
blocks/timeline/amd/build/event_list.min.js
blocks/timeline/amd/src/event_list.js
cache/stores/memcached/lang/en/cachestore_memcached.php
composer.json
composer.lock
config-dist.php
course/renderer.php
enrol/database/db/upgrade.php
enrol/flatfile/db/upgrade.php
enrol/guest/db/upgrade.php
enrol/imsenterprise/db/upgrade.php
enrol/ldap/lang/en/enrol_ldap.php
enrol/lti/db/upgrade.php
enrol/manual/db/upgrade.php
enrol/mnet/db/upgrade.php
enrol/paypal/db/upgrade.php
enrol/self/db/upgrade.php
files/converter/unoconv/lang/en/fileconverter_unoconv.php
filter/mathjaxloader/db/upgrade.php
filter/mediaplugin/db/upgrade.php
filter/tex/db/upgrade.php
grade/export/txt/tests/behat/export.feature
grade/export/xml/tests/behat/export.feature
grade/grading/form/guide/db/upgrade.php
grade/grading/form/rubric/db/upgrade.php
grade/grading/form/rubric/styles.css
grade/report/grader/lang/en/gradereport_grader.php
grade/report/user/db/upgrade.php
group/autogroup.php
group/autogroup_form.php
group/import.php
group/tests/behat/auto_creation.feature
group/tests/behat/groups_import.feature
group/tests/fixtures/groups_import.csv
install/lang/hr/error.php
lang/en/admin.php
lang/en/error.php
lang/en/grades.php
lang/en/message.php
lang/en/moodle.php
lib/accesslib.php
lib/amd/build/modal.min.js
lib/amd/build/str.min.js
lib/amd/build/templates.min.js
lib/amd/src/modal.js
lib/amd/src/str.js
lib/amd/src/templates.js
lib/antivirus/clamav/db/upgrade.php
lib/behat/lib.php
lib/classes/output/external.php
lib/classes/output/icon_system_fontawesome.php
lib/classes/output/mustache_template_source_loader.php [new file with mode: 0644]
lib/classes/privacy/provider.php
lib/classes/shutdown_manager.php
lib/classes/task/database_logger.php [new file with mode: 0644]
lib/classes/task/logmanager.php [new file with mode: 0644]
lib/classes/task/manager.php
lib/classes/task/messaging_cleanup_task.php
lib/classes/task/task_log_cleanup_task.php [new file with mode: 0644]
lib/classes/task/task_logger.php [new file with mode: 0644]
lib/cronlib.php
lib/db/access.php
lib/db/caches.php
lib/db/install.xml
lib/db/services.php
lib/db/tasks.php
lib/db/upgrade.php
lib/editor/atto/db/upgrade.php
lib/editor/atto/plugins/equation/db/upgrade.php
lib/editor/tinymce/db/upgrade.php
lib/editor/tinymce/plugins/spellchecker/db/upgrade.php
lib/filebrowser/file_info_context_course.php
lib/gradelib.php
lib/moodlelib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/phpminimumversionlib.php
lib/questionlib.php
lib/setuplib.php
lib/statslib.php
lib/tablelib.php
lib/tests/accesslib_test.php
lib/tests/environment_test.php
lib/tests/mustache_template_source_loader_test.php [new file with mode: 0644]
lib/tests/output_external_test.php [deleted file]
lib/tests/questionlib_test.php
lib/tests/statslib_test.php
lib/tests/task_database_logger_test.php [new file with mode: 0644]
lib/tests/task_logging_test.php [new file with mode: 0644]
media/player/videojs/lang/en/media_videojs.php
message/amd/build/message_drawer_view_conversation_patcher.min.js
message/amd/build/message_drawer_view_search.min.js
message/amd/src/message_drawer_view_conversation_patcher.js
message/amd/src/message_drawer_view_search.js
message/classes/api.php
message/externallib.php
message/output/email/db/upgrade.php
message/output/email/message_output_email.php
message/output/jabber/db/upgrade.php
message/output/jabber/lang/en/message_jabber.php
message/output/popup/db/upgrade.php
message/templates/message_drawer_view_overview_section.mustache
message/templates/message_drawer_view_search_body.mustache
message/templates/message_drawer_view_search_results_content.mustache
mod/assign/db/upgrade.php
mod/assign/feedback/comments/db/upgrade.php
mod/assign/feedback/editpdf/db/upgrade.php
mod/assign/feedback/file/db/upgrade.php
mod/assign/gradingtable.php
mod/assign/styles.css
mod/assign/submission/comments/db/upgrade.php
mod/assign/submission/file/db/upgrade.php
mod/assign/submission/onlinetext/db/upgrade.php
mod/assign/tests/locallib_test.php
mod/assignment/db/upgrade.php
mod/book/db/upgrade.php
mod/chat/db/upgrade.php
mod/choice/db/upgrade.php
mod/data/db/upgrade.php
mod/feedback/db/upgrade.php
mod/folder/db/upgrade.php
mod/forum/db/upgrade.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/tests/behat/no_groups_in_course.feature
mod/forum/tests/mail_test.php
mod/glossary/db/upgrade.php
mod/imscp/db/upgrade.php
mod/label/db/upgrade.php
mod/lesson/db/upgrade.php
mod/lti/db/upgrade.php
mod/page/db/upgrade.php
mod/quiz/classes/question/bank/add_action_column.php
mod/quiz/db/upgrade.php
mod/quiz/lang/en/quiz.php
mod/quiz/report/overview/db/upgrade.php
mod/quiz/report/statistics/db/upgrade.php
mod/quiz/tests/behat/editing_add_from_question_bank.feature
mod/quiz/tests/quiz_question_bank_view_test.php [new file with mode: 0644]
mod/resource/db/upgrade.php
mod/resource/locallib.php
mod/scorm/db/upgrade.php
mod/survey/db/upgrade.php
mod/url/db/upgrade.php
mod/wiki/db/upgrade.php
mod/workshop/db/upgrade.php
mod/workshop/form/accumulative/db/upgrade.php
mod/workshop/form/comments/db/upgrade.php
mod/workshop/form/numerrors/db/upgrade.php
mod/workshop/form/rubric/db/upgrade.php
pix/e/file-text.png [new file with mode: 0644]
pix/e/file-text.svg [new file with mode: 0644]
portfolio/boxnet/db/upgrade.php
portfolio/googledocs/db/upgrade.php
portfolio/mahara/lang/en/portfolio_mahara.php
portfolio/picasa/db/upgrade.php
privacy/classes/local/request/moodle_content_writer.php
question/behaviour/manualgraded/db/upgrade.php
question/classes/bank/delete_action_column.php
question/classes/bank/preview_action_column.php
question/engine/bank.php
question/tests/bank_view_test.php
question/type/calculated/db/upgrade.php
question/type/ddimageortext/amd/build/question.min.js
question/type/ddimageortext/amd/src/question.js
question/type/ddmarker/db/upgrade.php
question/type/ddwtos/amd/build/ddwtos.min.js
question/type/ddwtos/amd/src/ddwtos.js
question/type/ddwtos/tests/behat/preview.feature
question/type/ddwtos/tests/helper.php
question/type/essay/db/upgrade.php
question/type/match/db/upgrade.php
question/type/multianswer/db/upgrade.php
question/type/multichoice/db/upgrade.php
question/type/numerical/db/upgrade.php
question/type/random/db/upgrade.php
question/type/randomsamatch/db/upgrade.php
question/type/shortanswer/db/upgrade.php
report/performance/lang/en/report_performance.php
report/security/lang/en/report_security.php
repository/boxnet/db/upgrade.php
repository/dropbox/db/upgrade.php
repository/googledocs/db/upgrade.php
repository/picasa/db/upgrade.php
tag/tests/external_test.php
theme/boost/scss/moodle/admin.scss
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/forms.scss
theme/boost/style/moodle.css
theme/boost/templates/core_form/element-template.mustache
theme/boost/upgrade.txt
theme/bootstrapbase/less/moodle/blocks.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/block_myoverview/course-action-menu.mustache
theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_private.mustache
theme/bootstrapbase/templates/core_message/message_drawer_view_overview_section.mustache
theme/more/db/upgrade.php
user/classes/output/myprofile/renderer.php
user/lib.php
userpix/index.php
version.php

index aa0db16..ec1b68c 100644 (file)
@@ -14,7 +14,7 @@ language: php
 php:
     # We only run the highest and lowest supported versions to reduce the load on travis-ci.org.
     - 7.2
-    - 7.0
+    - 7.1
 
 addons:
   postgresql: "9.6"
@@ -63,7 +63,7 @@ matrix:
         # Exclude it on all versions except for 7.2
 
         - env: DB=mysqli   TASK=PHPUNIT
-          php: 7.0
+          php: 7.1
 
 cache:
     directories:
diff --git a/admin/classes/task_log_table.php b/admin/classes/task_log_table.php
new file mode 100644 (file)
index 0000000..2bb892d
--- /dev/null
@@ -0,0 +1,288 @@
+<?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/>.
+
+/**
+ * Task log table.
+ *
+ * @package    core_admin
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_admin;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/tablelib.php');
+
+/**
+ * Table to display list of task logs.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class task_log_table extends \table_sql {
+
+    /**
+     * Constructor for the task_log table.
+     *
+     * @param   string      $filter
+     * @param   int         $resultfilter
+     */
+    public function __construct(string $filter = '', int $resultfilter = null) {
+        global $DB;
+
+        if (-1 === $resultfilter) {
+            $resultfilter = null;
+        }
+
+        parent::__construct('tasklogs');
+
+        $columnheaders = [
+            'classname'  => get_string('name'),
+            'type'       => get_string('tasktype', 'admin'),
+            'userid'     => get_string('user', 'admin'),
+            'timestart'  => get_string('task_starttime', 'admin'),
+            'duration'   => get_string('task_duration', 'admin'),
+            'db'         => get_string('task_dbstats', 'admin'),
+            'result'     => get_string('task_result', 'admin'),
+            'actions'    => '',
+        ];
+        $this->define_columns(array_keys($columnheaders));
+        $this->define_headers(array_values($columnheaders));
+
+        // The name column is a header.
+        $this->define_header_column('classname');
+
+        // This table is not collapsible.
+        $this->collapsible(false);
+
+        // The actions class should not wrap. Use the BS text utility class.
+        $this->column_class('actions', 'text-nowrap');
+
+        // Allow pagination.
+        $this->pageable(true);
+
+        // Allow sorting. Default to sort by timestarted DESC.
+        $this->sortable(true, 'timestart', SORT_DESC);
+
+        // Add filtering.
+        $where = [];
+        $params = [];
+        if (!empty($filter)) {
+            $orwhere = [];
+            $filter = str_replace('\\', '\\\\', $filter);
+
+            // Check the class name.
+            $orwhere[] = $DB->sql_like('classname', ':classfilter', false, false);
+            $params['classfilter'] = '%' . $DB->sql_like_escape($filter) . '%';
+
+            $orwhere[] = $DB->sql_like('output', ':outputfilter', false, false);
+            $params['outputfilter'] = '%' . $DB->sql_like_escape($filter) . '%';
+
+            $where[] = "(" . implode(' OR ', $orwhere) . ")";
+        }
+
+        if (null !== $resultfilter) {
+            $where[] = 'tl.result = :result';
+            $params['result'] = $resultfilter;
+        }
+
+        $where = implode(' AND ', $where);
+
+        $this->set_sql('', '', $where, $params);
+    }
+
+    /**
+     * Query the db. Store results in the table object for use by build_table.
+     *
+     * @param int $pagesize size of page for paginated displayed table.
+     * @param bool $useinitialsbar do you want to use the initials bar. Bar
+     * will only be used if there is a fullname column defined for the table.
+     */
+    public function query_db($pagesize, $useinitialsbar = true) {
+        global $DB;
+
+        // Fetch the attempts.
+        $sort = $this->get_sql_sort();
+        if ($sort) {
+            $sort = "ORDER BY $sort";
+        }
+
+        $extrafields = get_extra_user_fields(\context_system::instance());
+        $userfields = \user_picture::fields('u', $extrafields, 'userid2', 'user');
+
+        $where = '';
+        if (!empty($this->sql->where)) {
+            $where = "WHERE {$this->sql->where}";
+        }
+
+        $sql = "SELECT
+                    tl.*,
+                    tl.dbreads + tl.dbwrites AS db,
+                    tl.timeend - tl.timestart AS duration,
+                    {$userfields}
+                FROM {task_log} tl
+           LEFT JOIN {user} u ON u.id = tl.userid
+                {$where}
+                {$sort}";
+
+        $this->pagesize($pagesize, $DB->count_records_sql("SELECT COUNT('x') FROM {task_log} tl {$where}", $this->sql->params));
+        if (!$this->is_downloading()) {
+            $this->rawdata = $DB->get_records_sql($sql, $this->sql->params, $this->get_page_start(), $this->get_page_size());
+        } else {
+            $this->rawdata = $DB->get_records_sql($sql, $this->sql->params);
+        }
+    }
+
+    /**
+     * Format the name cell.
+     *
+     * @param   \stdClass $row
+     * @return  string
+     */
+    public function col_classname($row) : string {
+        $output = '';
+        if (class_exists($row->classname)) {
+            $task = new $row->classname;
+            if ($task instanceof \core\task\scheduled_task) {
+                $output = $task->get_name();
+            }
+        }
+
+        $output .= \html_writer::tag('div', "\\{$row->classname}", [
+                'class' => 'task-class',
+            ]);
+        return $output;
+    }
+
+    /**
+     * Format the type cell.
+     *
+     * @param   \stdClass $row
+     * @return  string
+     */
+    public function col_type($row) : string {
+        if (\core\task\database_logger::TYPE_SCHEDULED == $row->type) {
+            return get_string('task_type:scheduled', 'admin');
+        } else {
+            return get_string('task_type:adhoc', 'admin');
+        }
+    }
+
+    /**
+     * Format the timestart cell.
+     *
+     * @param   \stdClass $row
+     * @return  string
+     */
+    public function col_result($row) : string {
+        if ($row->result) {
+            return get_string('task_result:failed', 'admin');
+        } else {
+            return get_string('success');
+        }
+    }
+
+    /**
+     * Format the timestart cell.
+     *
+     * @param   \stdClass $row
+     * @return  string
+     */
+    public function col_timestart($row) : string {
+        return userdate($row->timestart, get_string('strftimedatetimeshort', 'langconfig'));
+    }
+
+    /**
+     * Format the duration cell.
+     *
+     * @param   \stdClass $row
+     * @return  string
+     */
+    public function col_duration($row) : string {
+        $duration = round($row->timeend - $row->timestart, 2);
+
+        if (empty($duration)) {
+            // The format_time function returns 'now' when the difference is exactly 0.
+            // Note: format_time performs concatenation in exactly this fashion so we should do this for consistency.
+            return '0 ' . get_string('secs', 'moodle');
+        }
+
+        return format_time($duration);
+    }
+
+    /**
+     * Format the DB details cell.
+     *
+     * @param   \stdClass $row
+     * @return  string
+     */
+    public function col_db($row) : string {
+        $output = '';
+
+        $output .= \html_writer::div(get_string('task_stats:dbreads', 'admin', $row->dbreads));
+        $output .= \html_writer::div(get_string('task_stats:dbwrites', 'admin', $row->dbwrites));
+
+        return $output;
+    }
+
+    /**
+     * Format the actions cell.
+     *
+     * @param   \stdClass $row
+     * @return  string
+     */
+    public function col_actions($row) : string {
+        global $OUTPUT;
+
+        $actions = [];
+
+        $url = new \moodle_url('/admin/tasklogs.php', ['logid' => $row->id]);
+
+        // Quick view.
+        $actions[] = $OUTPUT->action_icon(
+            $url,
+            new \pix_icon('e/search', get_string('view')),
+            new \popup_action('click', $url)
+        );
+
+        // Download.
+        $actions[] = $OUTPUT->action_icon(
+            new \moodle_url($url, ['download' => true]),
+            new \pix_icon('t/download', get_string('download'))
+        );
+
+        return implode('&nbsp;', $actions);
+    }
+
+    /**
+     * Format the user cell.
+     *
+     * @param   \stdClass $row
+     * @return  string
+     */
+    public function col_userid($row) : string {
+        if (empty($row->userid)) {
+            return '';
+        }
+
+        $user = (object) [];
+        username_load_fields_from_object($user, $row, 'user');
+
+        return fullname($user);
+    }
+}
index 40e66be..ca20d0c 100644 (file)
       </CUSTOM_CHECK>
     </CUSTOM_CHECKS>
   </MOODLE>
+  <MOODLE version="3.7" requires="3.2">
+    <UNICODE level="required">
+      <FEEDBACK>
+        <ON_ERROR message="unicoderequired" />
+      </FEEDBACK>
+    </UNICODE>
+    <DATABASE level="required">
+      <VENDOR name="mariadb" version="5.5.31" />
+      <VENDOR name="mysql" version="5.6" />
+      <VENDOR name="postgres" version="9.4" />
+      <VENDOR name="mssql" version="10.0" />
+      <VENDOR name="oracle" version="11.2" />
+    </DATABASE>
+    <PHP version="7.1.0" level="required">
+    </PHP>
+    <PCREUNICODE level="optional">
+      <FEEDBACK>
+        <ON_CHECK message="pcreunicodewarning" />
+      </FEEDBACK>
+    </PCREUNICODE>
+    <PHP_EXTENSIONS>
+      <PHP_EXTENSION name="iconv" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="iconvrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="mbstring" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="mbstringrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="curl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="curlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="openssl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="opensslrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="tokenizer" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="tokenizerrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlrpc" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="xmlrpcrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="soap" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="soaprecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="ctype" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ctyperequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zip" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ziprequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zlib" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="gd" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="gdrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="simplexml" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="simplexmlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="spl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="splrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="pcre" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="dom" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xml" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlreader" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="intl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="intlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="json" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="hash" level="required"/>
+      <PHP_EXTENSION name="fileinfo" level="required"/>
+    </PHP_EXTENSIONS>
+    <PHP_SETTINGS>
+      <PHP_SETTING name="memory_limit" value="96M" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="settingmemorylimit" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="file_uploads" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="settingfileuploads" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="opcache.enable" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="opcacherecommended" />
+        </FEEDBACK>
+      </PHP_SETTING>
+    </PHP_SETTINGS>
+    <CUSTOM_CHECKS>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbstorageengine" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="quizattemptsupgradedmessage" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="slashargumentswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="unsupporteddbtablerowformat" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="unoconvwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="libcurlwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbfileformat" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbfilepertable" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddblargeprefix" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="ishttpswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="incompleteunicodesupport" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_sixtyfour_bits" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="sixtyfourbitswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+    </CUSTOM_CHECKS>
+  </MOODLE>
 </COMPATIBILITY_MATRIX>
index f4c24c0..96c7871 100644 (file)
@@ -4,7 +4,6 @@
 
 if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
 
-
 // "systempaths" settingpage
 $temp = new admin_settingpage('systempaths', new lang_string('systempaths','admin'));
 $temp->add(new admin_setting_configexecutable('pathtophp', new lang_string('pathtophp', 'admin'),
@@ -212,6 +211,92 @@ $temp->add(new admin_setting_configtext('curltimeoutkbitrate', new lang_string('
 $ADMIN->add('server', $temp);
 
 
+$ADMIN->add('server', new admin_category('taskconfig', new lang_string('taskadmintitle', 'admin')));
+$temp = new admin_settingpage('taskprocessing', new lang_string('taskprocessing','admin'));
+$temp->add(
+    new admin_setting_configtext(
+        'task_scheduled_concurrency_limit',
+        new lang_string('task_scheduled_concurrency_limit', 'admin'),
+        new lang_string('task_scheduled_concurrency_limit_desc', 'admin'),
+        3,
+        PARAM_INT
+    )
+);
+
+$temp->add(
+    new admin_setting_configduration(
+        'task_scheduled_max_runtime',
+        new lang_string('task_scheduled_max_runtime', 'admin'),
+        new lang_string('task_scheduled_max_runtime_desc', 'admin'),
+        30 * MINSECS
+    )
+);
+
+$temp->add(
+    new admin_setting_configtext(
+        'task_adhoc_concurrency_limit',
+        new lang_string('task_adhoc_concurrency_limit', 'admin'),
+        new lang_string('task_adhoc_concurrency_limit_desc', 'admin'),
+        3,
+        PARAM_INT
+    )
+);
+
+$temp->add(
+    new admin_setting_configduration(
+        'task_adhoc_max_runtime',
+        new lang_string('task_adhoc_max_runtime', 'admin'),
+        new lang_string('task_adhoc_max_runtime_desc', 'admin'),
+        30 * MINSECS
+    )
+);
+$ADMIN->add('taskconfig', $temp);
+
+$temp = new admin_settingpage('tasklogging', new lang_string('tasklogging','admin'));
+$temp->add(
+    new admin_setting_configselect(
+        'task_logmode',
+        new lang_string('task_logmode', 'admin'),
+        new lang_string('task_logmode_desc', 'admin'),
+        \core\task\logmanager::MODE_ALL,
+        [
+            \core\task\logmanager::MODE_ALL => new lang_string('task_logmode_all', 'admin'),
+            \core\task\logmanager::MODE_FAILONLY => new lang_string('task_logmode_failonly', 'admin'),
+            \core\task\logmanager::MODE_NONE => new lang_string('task_logmode_none', 'admin'),
+        ]
+    )
+);
+
+if (\core\task\logmanager::uses_standard_settings()) {
+    $temp->add(
+        new admin_setting_configduration(
+            'task_logretention',
+            new \lang_string('task_logretention', 'admin'),
+            new \lang_string('task_logretention_desc', 'admin'),
+            28 * DAYSECS
+        )
+    );
+
+    $temp->add(
+        new admin_setting_configtext(
+            'task_logretainruns',
+            new \lang_string('task_logretainruns', 'admin'),
+            new \lang_string('task_logretainruns_desc', 'admin'),
+            20,
+            PARAM_INT
+        )
+    );
+}
+$ADMIN->add('taskconfig', $temp);
+
+if (\core\task\logmanager::uses_standard_settings()) {
+    $ADMIN->add('taskconfig', new admin_externalpage(
+        'tasklogs',
+        new lang_string('tasklogs','admin'),
+        "{$CFG->wwwroot}/{$CFG->admin}/tasklogs.php"
+    ));
+}
+
 // E-mail settings.
 $ADMIN->add('server', new admin_category('email', new lang_string('categoryemail', 'admin')));
 
index db26385..a108b42 100644 (file)
@@ -21,8 +21,28 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
         0)
     );
 
-    $options = array(DAYSECS=>new lang_string('secondstotime86400'), WEEKSECS=>new lang_string('secondstotime604800'), 2620800=>new lang_string('nummonths', 'moodle', 1), 15724800=>new lang_string('nummonths', 'moodle', 6),0=>new lang_string('never'));
-    $optionalsubsystems->add(new admin_setting_configselect('messagingdeletereadnotificationsdelay', new lang_string('messagingdeletereadnotificationsdelay', 'admin'), new lang_string('configmessagingdeletereadnotificationsdelay', 'admin'), 604800, $options));
+    $options = array(
+        DAYSECS => new lang_string('secondstotime86400'),
+        WEEKSECS => new lang_string('secondstotime604800'),
+        2620800 => new lang_string('nummonths', 'moodle', 1),
+        7862400 => new lang_string('nummonths', 'moodle', 3),
+        15724800 => new lang_string('nummonths', 'moodle', 6),
+        0 => new lang_string('never')
+    );
+    $optionalsubsystems->add(new admin_setting_configselect(
+        'messagingdeletereadnotificationsdelay',
+        new lang_string('messagingdeletereadnotificationsdelay', 'admin'),
+        new lang_string('configmessagingdeletereadnotificationsdelay', 'admin'),
+        604800,
+        $options)
+    );
+    $optionalsubsystems->add(new admin_setting_configselect(
+        'messagingdeleteallnotificationsdelay',
+        new lang_string('messagingdeleteallnotificationsdelay', 'admin'),
+        new lang_string('configmessagingdeleteallnotificationsdelay', 'admin'),
+        2620800,
+        $options)
+    );
 
     $optionalsubsystems->add(new admin_setting_configcheckbox('messagingallowemailoverride', new lang_string('messagingallowemailoverride', 'admin'), new lang_string('configmessagingallowemailoverride','admin'), 0));
 
diff --git a/admin/tasklogs.php b/admin/tasklogs.php
new file mode 100644 (file)
index 0000000..6684a7e
--- /dev/null
@@ -0,0 +1,92 @@
+<?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/>.
+
+/**
+ * Task log.
+ *
+ * @package    admin
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../config.php');
+require_once("{$CFG->libdir}/adminlib.php");
+require_once("{$CFG->libdir}/tablelib.php");
+require_once("{$CFG->libdir}/filelib.php");
+
+$filter = optional_param('filter', '', PARAM_RAW);
+$result = optional_param('result', null, PARAM_INT);
+
+$pageurl = new \moodle_url('/admin/tasklogs.php');
+$pageurl->param('filter', $filter);
+
+$PAGE->set_url($pageurl);
+$PAGE->set_context(context_system::instance());
+$PAGE->set_pagelayout('admin');
+$strheading = get_string('tasklogs', 'tool_task');
+$PAGE->set_title($strheading);
+$PAGE->set_heading($strheading);
+
+require_login();
+
+require_capability('moodle/site:config', context_system::instance());
+admin_externalpage_setup('tasklogs');
+
+$logid = optional_param('logid', null, PARAM_INT);
+$download = optional_param('download', false, PARAM_BOOL);
+
+if (null !== $logid) {
+    $log = $DB->get_record('task_log', ['id' => $logid], '*', MUST_EXIST);
+
+    if ($download) {
+        $filename = str_replace('\\', '_', $log->classname) . "-{$log->id}.log";
+        header("Content-Disposition: attachment; filename=\"{$filename}\"");
+    }
+
+    readstring_accel($log->output, 'text/plain', false);
+    exit;
+}
+
+$renderer = $PAGE->get_renderer('tool_task');
+
+echo $OUTPUT->header();
+echo $OUTPUT->render_from_template('core_admin/tasklogs', (object) [
+    'action' => $pageurl->out(),
+    'filter' => $filter,
+    'resultfilteroptions' => [
+        (object) [
+            'value' => -1,
+            'title' => get_string('all'),
+            'selected' => (-1 === $result),
+        ],
+        (object) [
+            'value' => 0,
+            'title' => get_string('success'),
+            'selected' => (0 === $result),
+        ],
+        (object) [
+            'value' => 1,
+            'title' => get_string('task_result:failed', 'admin'),
+            'selected' => (1 === $result),
+        ],
+    ],
+]);
+
+$table = new \core_admin\task_log_table($filter, $result);
+$table->baseurl = $pageurl;
+$table->out(100, false);
+
+echo $OUTPUT->footer();
diff --git a/admin/templates/tasklogs.mustache b/admin/templates/tasklogs.mustache
new file mode 100644 (file)
index 0000000..c3180a2
--- /dev/null
@@ -0,0 +1,34 @@
+{{!
+    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/>.
+}}
+{{!
+    @template core_admin/tasklogs
+
+    Task Logs template.
+}}
+<form class="form-inline" method="GET" action="{{{action}}}">
+    <label class="sr-only" for="tasklog-filter">{{#str}}filter{{/str}}</label>
+    <input class="form-control" type="text" id="tasklog-filter" name="filter" value="{{{filter}}}">
+
+    <label class="sr-only" for="tasklog-resultfilter">{{#str}}resultfilter, admin{{/str}}</label>
+    <select class="form-control custom-select" name="result" id="tasklog-resultfilter">
+        {{#resultfilteroptions}}
+        <option value="{{{value}}}"{{#selected}} selected="selected"{{/selected}}>{{title}}</option>
+        {{/resultfilteroptions}}
+    </select>
+
+    <input class="btn btn-primary" type="submit" value="{{#str}}filter{{/str}}">
+</form>
index 5849311..53fa80a 100644 (file)
@@ -29,9 +29,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_tool_customlang_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 48f7d3c..1fba508 100644 (file)
Binary files a/admin/tool/dataprivacy/amd/build/effective_retention_period.min.js and b/admin/tool/dataprivacy/amd/build/effective_retention_period.min.js differ
index 8d587ca..7e3a102 100644 (file)
@@ -26,8 +26,7 @@ define(['jquery'],
 
         var SELECTORS = {
             PURPOSE_SELECT: '#id_purposeid',
-            RETENTION_FIELD_BOOST: '#id_error_retention_current',
-            RETENTION_FIELD_CLEAN: '#fitem_id_retention_current [data-fieldtype=static]',
+            RETENTION_FIELD: '#fitem_id_retention_current [data-fieldtype=static]',
         };
 
         /**
@@ -65,18 +64,7 @@ define(['jquery'],
             $(SELECTORS.PURPOSE_SELECT).on('change', function(ev) {
                 var selected = $(ev.currentTarget).val();
                 var selectedPurpose = this.purposeRetentionPeriods[selected];
-
-                var cleanSelector = $(SELECTORS.RETENTION_FIELD_CLEAN);
-                if (cleanSelector.length > 0) {
-                    cleanSelector.text(selectedPurpose);
-                } else {
-                    var boostSelector = $(SELECTORS.RETENTION_FIELD_BOOST);
-                    var retentionField = boostSelector.siblings();
-                    if (retentionField.length > 0) {
-                        retentionField.text(selectedPurpose);
-                    }
-                }
-
+                $(SELECTORS.RETENTION_FIELD).text(selectedPurpose);
             }.bind(this));
         };
 
index 7a7015a..e510ae0 100644 (file)
     overflow-y: scroll;
 }
 
-dd a.contactdpo {
-    /* Reverting dd's left margin */
-    margin-left: -10px;
-}
-
-.card dd a.contactdpo {
-    /* Reverting dd's left margin */
-    margin-left: inherit;
-}
-
 [data-region="data-requests-table"] .moodle-actionmenu {
     min-width: 150px;
 }
index 70a76cc..bbf1961 100644 (file)
@@ -33,9 +33,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_tool_log_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 0804ce8..04137ab 100644 (file)
@@ -27,9 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_logstore_database_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 14946e8..d6cd26d 100644 (file)
@@ -27,9 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_logstore_standard_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index ee22b83..f8579c9 100644 (file)
@@ -33,26 +33,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_tool_monitor_upgrade($oldversion) {
     global $CFG, $DB;
 
-    $dbman = $DB->get_manager();
-
-    if ($oldversion < 2016052305) {
-
-        // Define field inactivedate to be added to tool_monitor_subscriptions.
-        $table = new xmldb_table('tool_monitor_subscriptions');
-        $field = new xmldb_field('inactivedate', XMLDB_TYPE_INTEGER, '10', null, true, null, 0, 'lastnotificationsent');
-
-        // Conditionally launch add field inactivedate.
-        if (!$dbman->field_exists($table, $field)) {
-            $dbman->add_field($table, $field);
-        }
-
-        // Monitor savepoint reached.
-        upgrade_plugin_savepoint(true, 2016052305, 'tool', 'monitor');
-    }
-
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     if ($oldversion < 2017021300) {
 
         // Delete "orphaned" subscriptions.
index 0de826c..cf03700 100644 (file)
@@ -129,6 +129,7 @@ if ($execute = $options['execute']) {
     $predbqueries = $DB->perf_get_queries();
     $pretime = microtime(true);
 
+    \core\task\logmanager::start_logging($task);
     $fullname = $task->get_name() . ' (' . get_class($task) . ')';
     mtrace('Execute scheduled task: ' . $fullname);
     // NOTE: it would be tricky to move this code to \core\task\manager class,
index b1172e0..a28b478 100644 (file)
@@ -34,7 +34,7 @@ $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 \'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['enablerunnow_desc'] = 'Allows administrators to run a single scheduled task immediately, rather than waiting for it to run as scheduled. The feature requires \'Path to PHP CLI\' (pathtophp) to be set in System paths. The task runs on the web server, so you may wish to disable this feature to avoid potential performance issues.';
 $string['faildelay'] = 'Fail delay';
 $string['lastruntime'] = 'Last run';
 $string['nextruntime'] = 'Next run';
@@ -48,6 +48,7 @@ $string['runpattern'] = 'Run pattern';
 $string['scheduledtasks'] = 'Scheduled tasks';
 $string['scheduledtaskchangesdisabled'] = 'Modifications to the list of scheduled tasks have been prevented in Moodle configuration';
 $string['taskdisabled'] = 'Task disabled';
+$string['tasklogs'] = 'Task logs';
 $string['taskscheduleday'] = 'Day';
 $string['taskscheduleday_help'] = 'Day of month field for task schedule. The field uses the same format as unix cron. Some examples are:<br/><ul><li><strong>*</strong> Every day</li><li><strong>*/2</strong> Every 2nd day</li><li><strong>1</strong> The first of every month</li><li><strong>1,15</strong> The first and fifteenth of every month</li></ul>';
 $string['taskscheduledayofweek'] = 'Day of week';
@@ -59,4 +60,4 @@ $string['taskscheduleminute_help'] = 'Minute field for task schedule. The field
 $string['taskschedulemonth'] = 'Month';
 $string['taskschedulemonth_help'] = 'Month field for task schedule. The field uses the same format as unix cron. Some examples are:<br/><ul><li><strong>*</strong> Every month</li><li><strong>*/2</strong> Every second month</li><li><strong>1</strong> Every January</li><li><strong>1,5</strong> Every January and May</li></ul>';
 $string['privacy:metadata'] = 'The Scheduled task configuration plugin does not store any personal data.';
-
+$string['viewlogs'] = 'View logs for {$a}';
index 88dbdd4..3afd20c 100644 (file)
@@ -41,20 +41,33 @@ class tool_task_renderer extends plugin_renderer_base {
     public function scheduled_tasks_table($tasks) {
         global $CFG;
 
+        $showloglink = \core\task\logmanager::has_log_report();
+
         $table = new html_table();
-        $table->head  = array(get_string('name'),
-                              get_string('component', 'tool_task'),
-                              get_string('edit'),
-                              get_string('lastruntime', 'tool_task'),
-                              get_string('nextruntime', 'tool_task'),
-                              get_string('taskscheduleminute', 'tool_task'),
-                              get_string('taskschedulehour', 'tool_task'),
-                              get_string('taskscheduleday', 'tool_task'),
-                              get_string('taskscheduledayofweek', 'tool_task'),
-                              get_string('taskschedulemonth', 'tool_task'),
-                              get_string('faildelay', 'tool_task'),
-                              get_string('default', 'tool_task'));
+        $table->head = [
+            get_string('name'),
+            get_string('component', 'tool_task'),
+            get_string('edit'),
+            get_string('logs'),
+            get_string('lastruntime', 'tool_task'),
+            get_string('nextruntime', 'tool_task'),
+            get_string('taskscheduleminute', 'tool_task'),
+            get_string('taskschedulehour', 'tool_task'),
+            get_string('taskscheduleday', 'tool_task'),
+            get_string('taskscheduledayofweek', 'tool_task'),
+            get_string('taskschedulemonth', 'tool_task'),
+            get_string('faildelay', 'tool_task'),
+            get_string('default', 'tool_task'),
+        ];
+
         $table->attributes['class'] = 'admintable generaltable';
+        $table->colclasses = [];
+
+        if (!$showloglink) {
+            // Hide the log links.
+            $table->colclasses['3'] = 'hidden';
+        }
+
         $data = array();
         $yes = get_string('yes');
         $no = get_string('no');
@@ -72,6 +85,14 @@ class tool_task_renderer extends plugin_renderer_base {
                 $editlink = $this->render(new pix_icon('t/locked', get_string('scheduledtaskchangesdisabled', 'tool_task')));
             }
 
+            $loglink = '';
+            if ($showloglink) {
+                $loglink = $this->action_icon(
+                    \core\task\logmanager::get_url_for_task_class(get_class($task)),
+                    new pix_icon('e/file-text', get_string('viewlogs', 'tool_task', $task->get_name())
+                ));
+            }
+
             $namecell = new html_table_cell($task->get_name() . "\n" . html_writer::tag('span', '\\'.get_class($task),
                 array('class' => 'task-class text-ltr')));
             $namecell->header = true;
@@ -125,6 +146,7 @@ class tool_task_renderer extends plugin_renderer_base {
                         $namecell,
                         $componentcell,
                         new html_table_cell($editlink),
+                        new html_table_cell($loglink),
                         new html_table_cell($lastrun . $runnow),
                         new html_table_cell($nextrun),
                         new html_table_cell($task->get_minute()),
@@ -136,11 +158,11 @@ class tool_task_renderer extends plugin_renderer_base {
                         new html_table_cell($customised)));
 
             // Cron-style values must always be LTR.
-            $row->cells[5]->attributes['class'] = 'text-ltr';
             $row->cells[6]->attributes['class'] = 'text-ltr';
             $row->cells[7]->attributes['class'] = 'text-ltr';
             $row->cells[8]->attributes['class'] = 'text-ltr';
             $row->cells[9]->attributes['class'] = 'text-ltr';
+            $row->cells[10]->attributes['class'] = 'text-ltr';
 
             if ($disabled) {
                 $row->attributes['class'] = 'disabled';
index ac75859..ac9858e 100644 (file)
 defined('MOODLE_INTERNAL') || die;
 
 if ($hassiteconfig) {
-    $ADMIN->add('server', new admin_externalpage('scheduledtasks', new lang_string('scheduledtasks','tool_task'), "$CFG->wwwroot/$CFG->admin/tool/task/scheduledtasks.php"));
+    $ADMIN->add(
+        'taskconfig',
+        new admin_externalpage(
+            'scheduledtasks',
+            new lang_string('scheduledtasks', 'tool_task'),
+            "$CFG->wwwroot/$CFG->admin/tool/task/scheduledtasks.php"
+        )
+    );
 }
index 9bdf9ee..a8582de 100644 (file)
@@ -35,9 +35,6 @@ use tool_usertours\manager;
 function xmldb_tool_usertours_upgrade($oldversion) {
     global $CFG, $DB;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 9dd0838..d531a86 100644 (file)
@@ -215,8 +215,8 @@ $string['yesmissingindexesfound'] = '<p>Some missing indexes have been found in
 <p>After doing that, it\'s highly recommended to execute this utility again to check that no more missing indexes are found.</p>';
 $string['yeswrongdefaultsfound'] = '<p>Some inconsistent defaults have been found in your DB. Here are their details and the needed SQL statements to be executed with your favourite SQL interface to fix them all. Remember to backup your data first!</p>
 <p>After doing that, it\'s highly recommended to execute this utility again to check that no more inconsistent defaults are found.</p>';
-$string['yeswrongintsfound'] = '<p>Some wrong integers have been found in your DB. Here are their details and the needed SQL statements to be executed with your favourite SQL interface to create all them. Remember to backup your data first!</p>
-<p>After doing that, it\'s highly recommended to execute this utility again to check that no more wrong integers are found.</p>';
+$string['yeswrongintsfound'] = '<p>Some wrong integers have been found in your DB. Here are their details and the needed SQL statements to be executed with your favourite SQL interface to fix them. Remember to backup your data first!</p>
+<p>After fixing them, it is highly recommended to execute this utility again to check that no more wrong integers are found.</p>';
 $string['yeswrongoraclesemanticsfound'] = '<p>Some Oracle columns using BYTE semantics have been found in your DB. Here are their details and the needed SQL statements to be executed with your favourite SQL interface to create all them. Remember to backup your data first!</p>
 <p>After doing that, it\'s highly recommended to execute this utility again to check that no more wrong semantics are found.</p>';
 $string['privacy:metadata'] = 'The XMLDB editor plugin does not store any personal data.';
index 469661c..4df297c 100644 (file)
@@ -32,9 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_cas_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/cas to auth_cas.
         upgrade_fix_config_auth_plugin_names('cas');
index 7955ffe..f860220 100644 (file)
@@ -32,9 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_db_upgrade($oldversion) {
     global $CFG, $DB;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     if ($oldversion < 2017032800) {
         // Convert info in config plugins from auth/db to auth_db
         upgrade_fix_config_auth_plugin_names('db');
index f00705b..f636d6f 100644 (file)
@@ -32,9 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_email_upgrade($oldversion) {
     global $CFG, $DB;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/email to auth_email.
         upgrade_fix_config_auth_plugin_names('email');
index 0a4c344..6c57255 100644 (file)
@@ -32,9 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_ldap_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/ldap to auth_ldap.
         upgrade_fix_config_auth_plugin_names('ldap');
index 6d92020..13f87ce 100644 (file)
@@ -48,16 +48,16 @@ $string['auth_ldap_expiration_warning_key'] = 'Expiry warning';
 $string['auth_ldap_expireattr_desc'] = 'Optional: Overrides the LDAP attribute that stores password expiry time.';
 $string['auth_ldap_expireattr_key'] = 'Expiry attribute';
 $string['auth_ldapextrafields'] = 'These fields are optional.  You can choose to pre-fill some Moodle user fields with information from the <b>LDAP fields</b> that you specify here. <p>If you leave these fields blank, then nothing will be transferred from LDAP and Moodle defaults will be used instead.</p><p>In either case, the user will be able to edit all of these fields after they log in.</p>';
-$string['auth_ldap_graceattr_desc'] = 'Optional: Overrides  gracelogin attribute';
+$string['auth_ldap_graceattr_desc'] = 'Optional: Overrides grace login attribute';
 $string['auth_ldap_gracelogin_key'] = 'Grace login attribute';
-$string['auth_ldap_gracelogins_desc'] = 'Enable LDAP gracelogin support. After password has expired user can login until gracelogin count is 0. Enabling this setting displays grace login message if password is expired.';
+$string['auth_ldap_gracelogins_desc'] = 'Enable LDAP grace login support. After password has expired, user can log in until grace login count is 0. Enabling this setting displays grace login message if password has expired.';
 $string['auth_ldap_gracelogins_key'] = 'Grace logins';
 $string['auth_ldap_groupecreators'] = 'List of groups or contexts whose members are allowed to create groups. Separate multiple groups with \';\'. Usually something like \'cn=teachers,ou=staff,o=myorg\'';
 $string['auth_ldap_groupecreators_key'] = 'Group creators';
 $string['auth_ldap_host_url'] = 'Specify LDAP host in URL-form like \'ldap://ldap.myorg.com/\' or \'ldaps://ldap.myorg.com/\'. Separate multiple servers with \';\' to get failover support.';
 $string['auth_ldap_host_url_key'] = 'Host URL';
 $string['auth_ldap_changepasswordurl_key'] = 'Password-change URL';
-$string['auth_ldap_ldap_encoding'] = 'Specify encoding used by LDAP server. Most probably utf-8, MS AD v2 uses default platform encoding such as cp1252, cp1250, etc.';
+$string['auth_ldap_ldap_encoding'] = 'Encoding used by the LDAP server, most likely utf-8. If LDAP v2 is selected, Active Directory uses its configured encoding, such as cp1252 or cp1250.';
 $string['auth_ldap_ldap_encoding_key'] = 'LDAP encoding';
 $string['auth_ldap_login_settings'] = 'Login settings';
 $string['auth_ldap_memberattribute'] = 'Optional: Overrides user member attribute, when users belongs to a group. Usually \'member\'';
@@ -121,7 +121,7 @@ $string['didntfindexpiretime'] = 'password_expire() didn\'t find expiration time
 $string['didntgetusersfromldap'] = "Did not get any users from LDAP -- error? -- exiting\n";
 $string['gotcountrecordsfromldap'] = "Got {\$a} records from LDAP\n";
 $string['ldapnotconfigured'] = 'The LDAP host url is currently not configured';
-$string['morethanoneuser'] = 'Strange! More than one user record found in ldap. Only using the first one.';
+$string['morethanoneuser'] = 'More than one user record found in LDAP. Using only the first one.';
 $string['needbcmath'] = 'You need the BCMath extension to use expired password checking with Active Directory.';
 $string['needmbstring'] = 'You need the mbstring extension to change passwords in Active Directory';
 $string['nodnforusername'] = 'Error in user_update_password(). No DN for: {$a->username}';
@@ -152,7 +152,7 @@ $string['updateremfailamb'] = 'Failed to update LDAP with ambiguous field {$a->k
 $string['updateremfailfield'] = 'Failed to update LDAP with non-existent field (\'{$a->ldapkey}\'). Key ({$a->key}) - old Moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\'';
 $string['updatepasserror'] = 'Error in user_update_password(). Error code: {$a->errno}; Error string: {$a->errstring}';
 $string['updatepasserrorexpire'] = 'Error in user_update_password() when reading password expiry time. Error code: {$a->errno}; Error string: {$a->errstring}';
-$string['updatepasserrorexpiregrace'] = 'Error in user_update_password() when modifying expirationtime and/or gracelogins. Error code: {$a->errno}; Error string: {$a->errstring}';
+$string['updatepasserrorexpiregrace'] = 'Error in user_update_password() when modifying expiry time and/or grace logins. Error code: {$a->errno}; Error string: {$a->errstring}';
 $string['updateusernotfound'] = 'Could not find user while updating externally. Details follow: search base: \'{$a->userdn}\'; search filter: \'(objectClass=*)\'; search attributes: {$a->attribs}';
 $string['user_activatenotsupportusertype'] = 'auth: ldap user_activate() does not support selected usertype: {$a}';
 $string['user_disablenotsupportusertype'] = 'auth: ldap user_disable() does not support selected usertype: {$a}';
index d691552..9209e9d 100644 (file)
@@ -32,9 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_manual_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/manual to auth_manual.
         upgrade_fix_config_auth_plugin_names('manual');
index 7d4d88d..be29bcd 100644 (file)
@@ -32,8 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_mnet_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/mnet to auth_mnet.
         upgrade_fix_config_auth_plugin_names('mnet');
index 06f36e7..a8c7ed3 100644 (file)
@@ -91,11 +91,11 @@ $string['privacy:metadata:mnet_log:remoteid'] = 'Remote ID of the user who carri
 $string['privacy:metadata:mnet_log:time'] = 'Time when the action occurred.';
 $string['privacy:metadata:mnet_log:url'] = 'Remote system URL where the action occurred.';
 $string['privacy:metadata:mnet_log:userid'] = 'Local ID of the user who carried out the action in the remote system.';
-$string['privacy:metadata:mnet_session'] = 'The details of each MNet user session in a remote system is stored temporarily.';
+$string['privacy:metadata:mnet_session'] = 'The details of each MNet user session in a remote system. The data is stored temporarily.';
 $string['privacy:metadata:mnet_session:expires'] = 'Time when the session expires.';
 $string['privacy:metadata:mnet_session:mnethostid'] = 'Remote system MNet ID.';
 $string['privacy:metadata:mnet_session:token'] = 'Unique session identifier';
-$string['privacy:metadata:mnet_session:useragent'] = 'String denoting the user agent being which is accessing the page.';
+$string['privacy:metadata:mnet_session:useragent'] = 'User agent used to access the remote system';
 $string['privacy:metadata:mnet_session:userid'] = 'ID of the user jumping to remote system.';
 $string['privacy:metadata:mnet_session:username'] = 'Username of the user jumping to remote system.';
 $string['unknownhost'] = 'Unknown host';
\ No newline at end of file
index 596740b..2d5fc97 100644 (file)
@@ -32,9 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_none_upgrade($oldversion) {
     global $CFG, $DB;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/none to auth_none.
         upgrade_fix_config_auth_plugin_names('none');
index 19cc83c..1ecfca8 100644 (file)
@@ -305,18 +305,54 @@ class auth extends \auth_plugin_base {
      * Update user data according to data sent by authorization server.
      *
      * @param array $externaldata data from authorization server
-     * @param int $userid ID of the user to update
-     * @return stdClass The updated user record
+     * @param stdClass $userdata Current data of the user to be updated
+     * @return stdClass The updated user record, or the existing one if there's nothing to be updated.
      */
-    private function update_user(array $externaldata, int $userid) {
+    private function update_user(array $externaldata, $userdata) {
         $user = (object) [
-            'id' => $userid,
+            'id' => $userdata->id,
         ];
+
+        // We can only update if the default authentication type of the user is set to OAuth2 as well. Otherwise, we might mess
+        // up the user data of other users that use different authentication mechanisms (e.g. linked logins).
+        if ($userdata->auth !== $this->authtype) {
+            return $userdata;
+        }
+
+        // Go through each field from the external data.
         foreach ($externaldata as $fieldname => $value) {
-            $user->$fieldname = $value;
+            if (!in_array($fieldname, $this->userfields)) {
+                // Skip if this field doesn't belong to the list of fields that can be synced with the OAuth2 issuer.
+                continue;
+            }
+
+            if (!property_exists($userdata, $fieldname)) {
+                // Just in case this field is on the list, but not part of the user data. This shouldn't happen though.
+                continue;
+            }
+
+            // Get the old value.
+            $oldvalue = (string)$userdata->$fieldname;
+
+            // Get the lock configuration of the field.
+            $lockvalue = $this->config->{'field_lock_' . $fieldname};
+
+            // We should update fields that meet the following criteria:
+            // - Lock value set to 'unlocked'; or 'unlockedifempty', given the current value is empty.
+            // - The value has changed.
+            if ($lockvalue === 'unlocked' || ($lockvalue === 'unlockedifempty' && empty($oldvalue))) {
+                $value = (string)$value;
+                if ($oldvalue !== $value) {
+                    $user->$fieldname = $value;
+                }
+            }
         }
+        // Update the user data.
         user_update_user($user, false);
 
+        // Save user profile data.
+        profile_save_data($user);
+
         // Refresh user for $USER variable.
         return get_complete_user_data('id', $user->id);
     }
@@ -439,7 +475,7 @@ class auth extends \auth_plugin_base {
                 redirect(new moodle_url('/login/index.php'));
             } else if ($mappeduser && $mappeduser->confirmed) {
                 // Update user fields.
-                $userinfo = $this->update_user($userinfo, $mappeduser->id);
+                $userinfo = $this->update_user($userinfo, $mappeduser);
                 $userwasmapped = true;
             } else {
                 // Trigger login failed event.
@@ -497,7 +533,7 @@ class auth extends \auth_plugin_base {
                     exit();
                 } else {
                     \auth_oauth2\api::link_login($userinfo, $issuer, $moodleuser->id, true);
-                    $userinfo = $this->update_user($userinfo, $moodleuser->id);
+                    $userinfo = $this->update_user($userinfo, $moodleuser);
                     // No redirect, we will complete this login.
                 }
 
@@ -562,8 +598,7 @@ class auth extends \auth_plugin_base {
                 } else {
                     // Create a new confirmed account.
                     $newuser = \auth_oauth2\api::create_new_confirmed_account($userinfo, $issuer);
-                    // Update new user's fields.
-                    $userinfo = $this->update_user($userinfo, $newuser->id);
+                    $userinfo = get_complete_user_data('id', $newuser->id);
                     // No redirect, we will complete this login.
                 }
             }
index 8d0a6a6..7582cc9 100644 (file)
@@ -35,9 +35,6 @@ function xmldb_auth_oauth2_upgrade($oldversion) {
 
     $dbman = $DB->get_manager();
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 416ac20..640a023 100644 (file)
@@ -32,9 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_shibboleth_upgrade($oldversion) {
     global $CFG, $DB;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     if ($oldversion < 2017020700) {
         // Convert info in config plugins from auth/shibboleth to auth_shibboleth.
         upgrade_fix_config_auth_plugin_names('shibboleth');
index 7109162..973b155 100644 (file)
@@ -751,8 +751,11 @@ class backup_setting_ui_defaultcustom extends backup_setting_ui_text {
         if ($value === false) {
             $value = $this->attributes['defaultvalue'];
         }
-        if (!empty($value) && $this->attributes['type'] === 'date_selector') {
-            return userdate($value);
+        if (!empty($value)) {
+            if ($this->attributes['type'] === 'date_selector' ||
+                    $this->attributes['type'] === 'date_time_selector') {
+                return userdate($value);
+            }
         }
         return $value;
     }
index e1b5195..391b556 100644 (file)
@@ -50,9 +50,6 @@ class edit_backpack_form extends moodleform {
         $mform->addElement('hidden', 'userid', $USER->id);
         $mform->setType('userid', PARAM_INT);
 
-        $mform->addElement('hidden', 'backpackurl', BADGE_BACKPACKURL);
-        $mform->setType('backpackurl', PARAM_URL);
-
         if (isset($this->_customdata['email'])) {
             // Email will be passed in when we're in the process of verifying the user's email address,
             // so set the connection status, lock the email field, and provide options to resend the verification
@@ -94,7 +91,7 @@ class edit_backpack_form extends moodleform {
         // We don't need to verify the email address if we're clearing a pending email verification attempt.
         if (!isset($data['revertbutton'])) {
             $check = new stdClass();
-            $check->backpackurl = $data['backpackurl'];
+            $check->backpackurl = BADGE_BACKPACKURL;
             $check->email = $data['email'];
 
             $bp = new OpenBadgesBackpackHandler($check);
index 433e0c7..6948646 100644 (file)
@@ -45,9 +45,6 @@
 function xmldb_block_badges_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index d1cb144..90912a5 100644 (file)
@@ -26,6 +26,6 @@
 $string['pluginname'] = 'Latest badges';
 $string['numbadgestodisplay'] = 'Number of latest badges to display';
 $string['nothingtodisplay'] = 'You have no badges to display';
-$string['badges:addinstance'] = 'Add a new My latest badges block';
-$string['badges:myaddinstance'] = 'Add a new My latest badges block to Dashboard';
+$string['badges:addinstance'] = 'Add a new Latest badges block';
+$string['badges:myaddinstance'] = 'Add a new Latest badges block to Dashboard';
 $string['privacy:metadata'] = 'The Latest badges block only shows data stored in other locations.';
index 913ccca..3205127 100644 (file)
@@ -45,9 +45,6 @@
 function xmldb_block_calendar_month_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index b1e95d6..14100c8 100644 (file)
@@ -45,9 +45,6 @@
 function xmldb_block_calendar_upcoming_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index ecfdca9..0bdc51e 100644 (file)
@@ -46,9 +46,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_community_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 770a27b..8a9ec89 100644 (file)
@@ -48,9 +48,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_completionstatus_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 1cd9244..54538b9 100644 (file)
@@ -24,7 +24,7 @@
 
 $string['adminview'] = 'Admin view';
 $string['allcourses'] = 'Admin user sees all courses';
-$string['configadminview'] = 'What should the admin see in the course list block?';
+$string['configadminview'] = 'Whether to display all courses in the Courses block, or only courses that the admin is enrolled in.';
 $string['confighideallcourseslink'] = 'Remove the \'All courses\' link under the list of courses. (This setting does not affect the admin view.)';
 $string['course_list:addinstance'] = 'Add a new courses block';
 $string['course_list:myaddinstance'] = 'Add a new courses block to Dashboard';
index f3d591b..fb0cbae 100644 (file)
@@ -48,9 +48,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_course_summary_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 221dd38..4949b3f 100644 (file)
@@ -33,9 +33,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_html_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index fcd820a..97b1063 100644 (file)
@@ -55,9 +55,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_navigation_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index a9ef40e..60b035e 100644 (file)
@@ -45,9 +45,6 @@
 function xmldb_block_quiz_results_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 148cd37..77e6f61 100644 (file)
@@ -47,9 +47,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_recent_activity_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index ee46406..878f198 100644 (file)
@@ -33,9 +33,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_rss_client_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 19a9ca5..afc0bab 100644 (file)
@@ -78,7 +78,7 @@ $string['rss_client:createprivatefeeds'] = 'Create private RSS feeds';
 $string['rss_client:createsharedfeeds'] = 'Create shared RSS feeds';
 $string['rss_client:manageanyfeeds'] = 'Manage any RSS feeds';
 $string['rss_client:manageownfeeds'] = 'Manage own RSS feeds';
-$string['rss_client:myaddinstance'] = 'Add a new RSS feeds block to Dashboard';
+$string['rss_client:myaddinstance'] = 'Add a new Remote RSS feeds block to Dashboard';
 $string['seeallfeeds'] = 'See all feeds';
 $string['sharedfeed'] = 'Shared feed';
 $string['shownumentrieslabel'] = 'Max number entries to show per block.';
index a8e7bfd..0a8e2b0 100644 (file)
@@ -49,9 +49,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_section_links_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 34a9814..9b8d87b 100644 (file)
@@ -48,9 +48,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_selfcompletion_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 3e93f8b..7b8618d 100644 (file)
@@ -55,9 +55,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_settings_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 025c596..893a2ac 100644 (file)
Binary files a/blocks/timeline/amd/build/event_list.min.js and b/blocks/timeline/amd/build/event_list.min.js differ
index b6d2a43..9699ed1 100644 (file)
@@ -325,7 +325,7 @@ function(
         return Str.get_string(
                 'ariaeventlistpagelimit',
                 'block_timeline',
-                $.isArray(pageLimit) ? pageLimit[0] : pageLimit
+                $.isArray(pageLimit) ? pageLimit[0].value : pageLimit
             )
             .then(function(string) {
                 config.ariaLabels.itemsperpage = string;
index e697808..bbcf079 100644 (file)
@@ -77,7 +77,7 @@ servername:port:weight
 If *Enable clustered servers* is enabled below, there must be only one server listed here. This would usually be a name that always resolves to the local machine, like 127.0.0.1 or localhost.';
 $string['serversclusterinvalid'] = 'Exactly one server is required when clustering is enabled.';
 $string['setservers'] = 'Set Servers';
-$string['setservers_help'] = 'This is the list of servers that will updated when data is modified in the cache. Generally the fully qualified name of each server in the pool.
+$string['setservers_help'] = 'This is the list of servers that will be updated when data is modified in the cache. Generally the fully qualified name of each server in the pool.
 It **must** include the server listed in *Servers* above, even if by a different hostname.
 Servers should be defined one per line and consist of a server address and optionally a port.
 If no port is provided then the default port (11211) is used.
index 5b526cc..c5659ea 100644 (file)
@@ -7,7 +7,7 @@
     "require-dev": {
         "phpunit/phpunit": "6.5.*",
         "phpunit/dbUnit": "3.0.*",
-        "moodlehq/behat-extension": "3.36.0",
+        "moodlehq/behat-extension": "3.37.0",
         "mikey179/vfsStream": "^1.6"
     }
 }
index e8aaa2f..7179371 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "content-hash": "956ce0b653b805efb6a9a483f8c9a847",
+    "content-hash": "fafc7b770f55956d32d202645be66abf",
     "packages": [],
     "packages-dev": [
         {
         },
         {
             "name": "doctrine/instantiator",
-            "version": "1.0.5",
+            "version": "1.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/instantiator.git",
-                "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d"
+                "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d",
-                "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d",
+                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda",
+                "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3,<8.0-DEV"
+                "php": "^7.1"
             },
             "require-dev": {
                 "athletic/athletic": "~0.1.8",
                 "ext-pdo": "*",
                 "ext-phar": "*",
-                "phpunit/phpunit": "~4.0",
-                "squizlabs/php_codesniffer": "~2.0"
+                "phpunit/phpunit": "^6.2.3",
+                "squizlabs/php_codesniffer": "^3.0.2"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0.x-dev"
+                    "dev-master": "1.2.x-dev"
                 }
             },
             "autoload": {
                 "constructor",
                 "instantiate"
             ],
-            "time": "2015-06-14T21:17:01+00:00"
+            "time": "2017-07-22T11:58:36+00:00"
         },
         {
             "name": "fabpot/goutte",
-            "version": "v3.2.2",
+            "version": "v3.2.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/FriendsOfPHP/Goutte.git",
-                "reference": "395f61d7c2e15a813839769553a4de16fa3b3c96"
+                "reference": "3f0eaf0a40181359470651f1565b3e07e3dd31b8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/395f61d7c2e15a813839769553a4de16fa3b3c96",
-                "reference": "395f61d7c2e15a813839769553a4de16fa3b3c96",
+                "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/3f0eaf0a40181359470651f1565b3e07e3dd31b8",
+                "reference": "3f0eaf0a40181359470651f1565b3e07e3dd31b8",
                 "shasum": ""
             },
             "require": {
             "keywords": [
                 "scraper"
             ],
-            "time": "2017-11-19T08:45:40+00:00"
+            "time": "2018-06-29T15:13:57+00:00"
         },
         {
             "name": "guzzlehttp/guzzle",
         },
         {
             "name": "guzzlehttp/psr7",
-            "version": "1.4.2",
+            "version": "1.5.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/psr7.git",
-                "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c"
+                "reference": "9f83dded91781a01c63574e387eaa769be769115"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
-                "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
+                "url": "https://api.github.com/repos/guzzle/psr7/zipball/9f83dded91781a01c63574e387eaa769be769115",
+                "reference": "9f83dded91781a01c63574e387eaa769be769115",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.4.0",
-                "psr/http-message": "~1.0"
+                "psr/http-message": "~1.0",
+                "ralouphie/getallheaders": "^2.0.5"
             },
             "provide": {
                 "psr/http-message-implementation": "1.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "~4.0"
+                "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.4-dev"
+                    "dev-master": "1.5-dev"
                 }
             },
             "autoload": {
             "keywords": [
                 "http",
                 "message",
+                "psr-7",
                 "request",
                 "response",
                 "stream",
                 "uri",
                 "url"
             ],
-            "time": "2017-03-20T17:10:46+00:00"
+            "time": "2018-12-04T20:46:45+00:00"
         },
         {
             "name": "instaclick/php-webdriver",
         },
         {
             "name": "moodlehq/behat-extension",
-            "version": "v3.36.0",
+            "version": "v3.37.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/moodlehq/moodle-behat-extension.git",
         },
         {
             "name": "myclabs/deep-copy",
-            "version": "1.7.0",
+            "version": "1.8.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/myclabs/DeepCopy.git",
-                "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e"
+                "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e",
-                "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e",
+                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8",
+                "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.6 || ^7.0"
+                "php": "^7.1"
+            },
+            "replace": {
+                "myclabs/deep-copy": "self.version"
             },
             "require-dev": {
                 "doctrine/collections": "^1.0",
                 "doctrine/common": "^2.6",
-                "phpunit/phpunit": "^4.1"
+                "phpunit/phpunit": "^7.1"
             },
             "type": "library",
             "autoload": {
                 "object",
                 "object graph"
             ],
-            "time": "2017-10-19T19:58:43+00:00"
+            "time": "2018-06-11T23:09:50+00:00"
         },
         {
             "name": "phar-io/manifest",
         },
         {
             "name": "phpspec/prophecy",
-            "version": "1.7.6",
+            "version": "1.8.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpspec/prophecy.git",
-                "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712"
+                "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/33a7e3c4fda54e912ff6338c48823bd5c0f0b712",
-                "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712",
+                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06",
+                "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06",
                 "shasum": ""
             },
             "require": {
             },
             "require-dev": {
                 "phpspec/phpspec": "^2.5|^3.2",
-                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5"
+                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.7.x-dev"
+                    "dev-master": "1.8.x-dev"
                 }
             },
             "autoload": {
                 "spy",
                 "stub"
             ],
-            "time": "2018-04-18T13:57:24+00:00"
+            "time": "2018-08-05T17:53:17+00:00"
         },
         {
             "name": "phpunit/dbunit",
                 "testing",
                 "xunit"
             ],
+            "abandoned": true,
             "time": "2018-01-23T13:32:26+00:00"
         },
         {
         },
         {
             "name": "phpunit/phpunit",
-            "version": "6.5.8",
+            "version": "6.5.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "4f21a3c6b97c42952fd5c2837bb354ec0199b97b"
+                "reference": "0973426fb012359b2f18d3bd1e90ef1172839693"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4f21a3c6b97c42952fd5c2837bb354ec0199b97b",
-                "reference": "4f21a3c6b97c42952fd5c2837bb354ec0199b97b",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0973426fb012359b2f18d3bd1e90ef1172839693",
+                "reference": "0973426fb012359b2f18d3bd1e90ef1172839693",
                 "shasum": ""
             },
             "require": {
                 "phpunit/php-file-iterator": "^1.4.3",
                 "phpunit/php-text-template": "^1.2.1",
                 "phpunit/php-timer": "^1.0.9",
-                "phpunit/phpunit-mock-objects": "^5.0.5",
+                "phpunit/phpunit-mock-objects": "^5.0.9",
                 "sebastian/comparator": "^2.1",
                 "sebastian/diff": "^2.0",
                 "sebastian/environment": "^3.1",
                 "testing",
                 "xunit"
             ],
-            "time": "2018-04-10T11:38:34+00:00"
+            "time": "2018-09-08T15:10:43+00:00"
         },
         {
             "name": "phpunit/phpunit-mock-objects",
-            "version": "5.0.7",
+            "version": "5.0.10",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
-                "reference": "3eaf040f20154d27d6da59ca2c6e28ac8fd56dce"
+                "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/3eaf040f20154d27d6da59ca2c6e28ac8fd56dce",
-                "reference": "3eaf040f20154d27d6da59ca2c6e28ac8fd56dce",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/cd1cf05c553ecfec36b170070573e540b67d3f1f",
+                "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f",
                 "shasum": ""
             },
             "require": {
                 "phpunit/phpunit": "<6.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.5"
+                "phpunit/phpunit": "^6.5.11"
             },
             "suggest": {
                 "ext-soap": "*"
                 "mock",
                 "xunit"
             ],
-            "time": "2018-05-29T13:50:43+00:00"
+            "time": "2018-08-09T05:50:03+00:00"
         },
         {
             "name": "psr/container",
         },
         {
             "name": "psr/log",
-            "version": "1.0.2",
+            "version": "1.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/php-fig/log.git",
-                "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d"
+                "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d",
-                "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d",
+                "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
+                "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
                 "shasum": ""
             },
             "require": {
                 "psr",
                 "psr-3"
             ],
-            "time": "2016-10-10T12:19:37+00:00"
+            "time": "2018-11-20T15:27:04+00:00"
+        },
+        {
+            "name": "ralouphie/getallheaders",
+            "version": "2.0.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ralouphie/getallheaders.git",
+                "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/5601c8a83fbba7ef674a7369456d12f1e0d0eafa",
+                "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~3.7.0",
+                "satooshi/php-coveralls": ">=1.0"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "src/getallheaders.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Ralph Khattar",
+                    "email": "ralph.khattar@gmail.com"
+                }
+            ],
+            "description": "A polyfill for getallheaders.",
+            "time": "2016-02-11T07:05:27+00:00"
         },
         {
             "name": "sebastian/code-unit-reverse-lookup",
         },
         {
             "name": "symfony/browser-kit",
-            "version": "v3.4.11",
+            "version": "v4.2.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/browser-kit.git",
-                "reference": "840bb6f0d5b3701fd768b68adf7193c2d0f98f79"
+                "reference": "db7e59fec9c82d45e745eb500e6ede2d96f4a6e9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/840bb6f0d5b3701fd768b68adf7193c2d0f98f79",
-                "reference": "840bb6f0d5b3701fd768b68adf7193c2d0f98f79",
+                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/db7e59fec9c82d45e745eb500e6ede2d96f4a6e9",
+                "reference": "db7e59fec9c82d45e745eb500e6ede2d96f4a6e9",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9|>=7.0.8",
-                "symfony/dom-crawler": "~2.8|~3.0|~4.0"
+                "php": "^7.1.3",
+                "symfony/dom-crawler": "~3.4|~4.0"
             },
             "require-dev": {
-                "symfony/css-selector": "~2.8|~3.0|~4.0",
-                "symfony/process": "~2.8|~3.0|~4.0"
+                "symfony/css-selector": "~3.4|~4.0",
+                "symfony/process": "~3.4|~4.0"
             },
             "suggest": {
                 "symfony/process": ""
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.4-dev"
+                    "dev-master": "4.2-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony BrowserKit Component",
             "homepage": "https://symfony.com",
-            "time": "2018-03-19T22:32:39+00:00"
+            "time": "2018-11-26T11:49:31+00:00"
         },
         {
             "name": "symfony/class-loader",
-            "version": "v3.4.11",
+            "version": "v3.4.20",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/class-loader.git",
-                "reference": "e63c12699822bb3b667e7216ba07fbcc3a3e203e"
+                "reference": "420458095cf60025eb0841276717e0da7f75e50e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/class-loader/zipball/e63c12699822bb3b667e7216ba07fbcc3a3e203e",
-                "reference": "e63c12699822bb3b667e7216ba07fbcc3a3e203e",
+                "url": "https://api.github.com/repos/symfony/class-loader/zipball/420458095cf60025eb0841276717e0da7f75e50e",
+                "reference": "420458095cf60025eb0841276717e0da7f75e50e",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony ClassLoader Component",
             "homepage": "https://symfony.com",
-            "time": "2018-01-03T07:37:34+00:00"
+            "time": "2018-11-11T19:48:54+00:00"
         },
         {
             "name": "symfony/config",
-            "version": "v3.4.11",
+            "version": "v3.4.20",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/config.git",
-                "reference": "73e055cf2e6467715f187724a0347ea32079967c"
+                "reference": "8a660daeb65dedbe0b099529f65e61866c055081"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/config/zipball/73e055cf2e6467715f187724a0347ea32079967c",
-                "reference": "73e055cf2e6467715f187724a0347ea32079967c",
+                "url": "https://api.github.com/repos/symfony/config/zipball/8a660daeb65dedbe0b099529f65e61866c055081",
+                "reference": "8a660daeb65dedbe0b099529f65e61866c055081",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Config Component",
             "homepage": "https://symfony.com",
-            "time": "2018-05-14T16:49:53+00:00"
+            "time": "2018-11-26T10:17:44+00:00"
         },
         {
             "name": "symfony/console",
-            "version": "v3.3.17",
+            "version": "v3.3.18",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
         },
         {
             "name": "symfony/css-selector",
-            "version": "v3.4.11",
+            "version": "v3.4.20",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
-                "reference": "d2ce52290b648ae33b5301d09bc14ee378612914"
+                "reference": "345b9a48595d1ab9630db791dbc3e721bf0233e8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/css-selector/zipball/d2ce52290b648ae33b5301d09bc14ee378612914",
-                "reference": "d2ce52290b648ae33b5301d09bc14ee378612914",
+                "url": "https://api.github.com/repos/symfony/css-selector/zipball/345b9a48595d1ab9630db791dbc3e721bf0233e8",
+                "reference": "345b9a48595d1ab9630db791dbc3e721bf0233e8",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony CssSelector Component",
             "homepage": "https://symfony.com",
-            "time": "2018-05-16T12:49:49+00:00"
+            "time": "2018-11-11T19:48:54+00:00"
         },
         {
             "name": "symfony/debug",
-            "version": "v3.4.11",
+            "version": "v3.4.20",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/debug.git",
-                "reference": "b28fd73fefbac341f673f5efd707d539d6a19f68"
+                "reference": "a2233f555ddf55e5600f386fba7781cea1cb82d3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/debug/zipball/b28fd73fefbac341f673f5efd707d539d6a19f68",
-                "reference": "b28fd73fefbac341f673f5efd707d539d6a19f68",
+                "url": "https://api.github.com/repos/symfony/debug/zipball/a2233f555ddf55e5600f386fba7781cea1cb82d3",
+                "reference": "a2233f555ddf55e5600f386fba7781cea1cb82d3",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Debug Component",
             "homepage": "https://symfony.com",
-            "time": "2018-05-16T14:03:39+00:00"
+            "time": "2018-11-27T12:43:10+00:00"
         },
         {
             "name": "symfony/dependency-injection",
-            "version": "v3.3.17",
+            "version": "v3.3.18",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dependency-injection.git",
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v3.4.11",
+            "version": "v4.2.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "201b210fafcdd193c1e45b2994bf7133fb6263e8"
+                "reference": "7438a32108fdd555295f443605d6de2cce473159"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/201b210fafcdd193c1e45b2994bf7133fb6263e8",
-                "reference": "201b210fafcdd193c1e45b2994bf7133fb6263e8",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/7438a32108fdd555295f443605d6de2cce473159",
+                "reference": "7438a32108fdd555295f443605d6de2cce473159",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9|>=7.0.8",
+                "php": "^7.1.3",
                 "symfony/polyfill-ctype": "~1.8",
                 "symfony/polyfill-mbstring": "~1.0"
             },
             "require-dev": {
-                "symfony/css-selector": "~2.8|~3.0|~4.0"
+                "symfony/css-selector": "~3.4|~4.0"
             },
             "suggest": {
                 "symfony/css-selector": ""
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.4-dev"
+                    "dev-master": "4.2-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony DomCrawler Component",
             "homepage": "https://symfony.com",
-            "time": "2018-05-01T22:53:27+00:00"
+            "time": "2018-11-26T10:55:26+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v3.4.11",
+            "version": "v3.4.20",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "fdd5abcebd1061ec647089c6c41a07ed60af09f8"
+                "reference": "cc35e84adbb15c26ae6868e1efbf705a917be6b5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/fdd5abcebd1061ec647089c6c41a07ed60af09f8",
-                "reference": "fdd5abcebd1061ec647089c6c41a07ed60af09f8",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/cc35e84adbb15c26ae6868e1efbf705a917be6b5",
+                "reference": "cc35e84adbb15c26ae6868e1efbf705a917be6b5",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony EventDispatcher Component",
             "homepage": "https://symfony.com",
-            "time": "2018-04-06T07:35:25+00:00"
+            "time": "2018-11-30T18:07:24+00:00"
         },
         {
             "name": "symfony/filesystem",
-            "version": "v3.4.11",
+            "version": "v4.2.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "8e03ca3fa52a0f56b87506f38cf7bd3f9442b3a0"
+                "reference": "2f4c8b999b3b7cadb2a69390b01af70886753710"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/8e03ca3fa52a0f56b87506f38cf7bd3f9442b3a0",
-                "reference": "8e03ca3fa52a0f56b87506f38cf7bd3f9442b3a0",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/2f4c8b999b3b7cadb2a69390b01af70886753710",
+                "reference": "2f4c8b999b3b7cadb2a69390b01af70886753710",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5.9|>=7.0.8",
+                "php": "^7.1.3",
                 "symfony/polyfill-ctype": "~1.8"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.4-dev"
+                    "dev-master": "4.2-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "time": "2018-05-16T08:49:21+00:00"
+            "time": "2018-11-11T19:52:12+00:00"
         },
         {
             "name": "symfony/polyfill-ctype",
-            "version": "v1.8.0",
+            "version": "v1.10.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-ctype.git",
-                "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae"
+                "reference": "e3d826245268269cd66f8326bd8bc066687b4a19"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/7cc359f1b7b80fc25ed7796be7d96adc9b354bae",
-                "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae",
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19",
+                "reference": "e3d826245268269cd66f8326bd8bc066687b4a19",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.3.3"
             },
+            "suggest": {
+                "ext-ctype": "For best performance"
+            },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.8-dev"
+                    "dev-master": "1.9-dev"
                 }
             },
             "autoload": {
                 "polyfill",
                 "portable"
             ],
-            "time": "2018-04-30T19:57:29+00:00"
+            "time": "2018-08-06T14:22:27+00:00"
         },
         {
             "name": "symfony/polyfill-mbstring",
-            "version": "v1.8.0",
+            "version": "v1.10.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "3296adf6a6454a050679cde90f95350ad604b171"
+                "reference": "c79c051f5b3a46be09205c73b80b346e4153e494"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/3296adf6a6454a050679cde90f95350ad604b171",
-                "reference": "3296adf6a6454a050679cde90f95350ad604b171",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494",
+                "reference": "c79c051f5b3a46be09205c73b80b346e4153e494",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.8-dev"
+                    "dev-master": "1.9-dev"
                 }
             },
             "autoload": {
                 "portable",
                 "shim"
             ],
-            "time": "2018-04-26T10:06:28+00:00"
+            "time": "2018-09-21T13:07:52+00:00"
         },
         {
             "name": "symfony/process",
-            "version": "v2.8.41",
+            "version": "v2.8.49",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/process.git",
-                "reference": "713952f2ccbcc8342ecdbe1cb313d3e2da8aad28"
+                "reference": "c3591a09c78639822b0b290d44edb69bf9f05dc8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/process/zipball/713952f2ccbcc8342ecdbe1cb313d3e2da8aad28",
-                "reference": "713952f2ccbcc8342ecdbe1cb313d3e2da8aad28",
+                "url": "https://api.github.com/repos/symfony/process/zipball/c3591a09c78639822b0b290d44edb69bf9f05dc8",
+                "reference": "c3591a09c78639822b0b290d44edb69bf9f05dc8",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Process Component",
             "homepage": "https://symfony.com",
-            "time": "2018-05-15T21:17:45+00:00"
+            "time": "2018-11-11T11:18:13+00:00"
         },
         {
             "name": "symfony/translation",
-            "version": "v3.3.17",
+            "version": "v3.3.18",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
         },
         {
             "name": "symfony/yaml",
-            "version": "v3.3.17",
+            "version": "v3.3.18",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/yaml.git",
index 18cbe31..29f9b79 100644 (file)
@@ -545,6 +545,15 @@ $CFG->admin = 'admin';
 // on a shared file system that supports locking.
 //      $CFG->lock_file_root = $CFG->dataroot . '/lock';
 //
+//
+// Alternative task logging.
+// Since Moodle 3.7 the output of al scheduled and adhoc tasks is stored in the database and it is possible to use an
+// alternative task logging mechanism.
+// To set the alternative task logging mechanism in config.php you can use the following settings, providing the
+// alternative class name that will be auto-loaded.
+//
+//      $CFG->task_log_class = '\\local_mytasklogger\\logger';
+//
 // Moodle 2.9 allows administrators to customise the list of supported file types.
 // To add a new filetype or override the definition of an existing one, set the
 // customfiletypes variable like this:
index ad00ffb..4321d51 100644 (file)
@@ -2425,9 +2425,8 @@ class core_course_renderer extends plugin_renderer_base {
                     if (!empty($mycourseshtml)) {
                         $output .= $this->frontpage_part('skipmycourses', 'frontpage-course-list',
                             get_string('mycourses'), $mycourseshtml);
-                        break;
                     }
-                    // No "break" here. If there are no enrolled courses - continue to 'Available courses'.
+                    break;
 
                 case FRONTPAGEALLCOURSELIST:
                     $availablecourseshtml = $this->frontpage_available_courses();
index 9ba3261..ca2733c 100644 (file)
@@ -27,9 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_enrol_database_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index adc61cb..c286910 100644 (file)
@@ -27,9 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_enrol_flatfile_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index e86cfff..a4f9d25 100644 (file)
@@ -27,9 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_enrol_guest_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 857eb62..d89f296 100644 (file)
@@ -33,9 +33,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_enrol_imsenterprise_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 7b4db43..a346a99 100644 (file)
@@ -103,7 +103,7 @@ $string['phpldap_noextension'] = '<em>The PHP LDAP module does not seem to be pr
 $string['pluginname'] = 'LDAP enrolments';
 $string['pluginname_desc'] = '<p>You can use an LDAP server to control your enrolments. It is assumed your LDAP tree contains groups that map to the courses, and that each of those groups/courses will have membership entries to map to students.</p><p>It is assumed that courses are defined as groups in LDAP, with each group having multiple membership fields (<em>member</em> or <em>memberUid</em>) that contain a uniqueidentification of the user.</p><p>To use LDAP enrolment, your users <strong>must</strong> to have a valid  idnumber field. The LDAP groups must have that idnumber in the member fields for a user to be enrolled in the course. This will usually work well if you are already using LDAP Authentication.</p><p>Enrolments will be updated when the user logs in. You can also run a script to keep enrolments in synch. Look in <em>enrol/ldap/cli/sync.php</em>.</p><p>This plugin can also be set to automatically create new courses when new groups appear in LDAP.</p>';
 $string['pluginnotenabled'] = 'Plugin not enabled!';
-$string['role_mapping'] = '<p>For each role that you want to assign from LDAP, you need to specify the list of contexts where the role courses\'s groups are located. Separate different contexts with \';\'.</p><p>You also need to specify the attribute your LDAP server uses to hold the members of a group. Usually \'member\' or \'memberUid\'</p>';
+$string['role_mapping'] = '<p>For each role, you need to specify all LDAP contexts where the groups that represent the courses are located. Separate different contexts with a semicolon (;).</p><p>You also need to specify the attribute your LDAP server uses to hold the members of a group. This is usually \'member\' or \'memberUid\'.</p>';
 $string['role_mapping_attribute'] = 'LDAP member attribute for {$a}';
 $string['role_mapping_context'] = 'LDAP contexts for {$a}';
 $string['role_mapping_key'] = 'Map roles from LDAP ';
index cccd6ae..7b5736b 100644 (file)
@@ -41,210 +41,6 @@ function xmldb_enrol_lti_upgrade($oldversion) {
 
     $dbman = $DB->get_manager();
 
-    if ($oldversion < 2016052303) {
-
-        // Define table enrol_lti_lti2_consumer to be created.
-        $table = new xmldb_table('enrol_lti_lti2_consumer');
-
-        // Adding fields to table enrol_lti_lti2_consumer.
-        $table->add_field('id', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
-        $table->add_field('name', XMLDB_TYPE_CHAR, '50', null, XMLDB_NOTNULL, null, null);
-        $table->add_field('consumerkey256', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
-        $table->add_field('consumerkey', XMLDB_TYPE_TEXT, null, null, null, null, null);
-        $table->add_field('secret', XMLDB_TYPE_CHAR, '1024', null, XMLDB_NOTNULL, null, null);
-        $table->add_field('ltiversion', XMLDB_TYPE_CHAR, '10', null, null, null, null);
-        $table->add_field('consumername', XMLDB_TYPE_CHAR, '255', null, null, null, null);
-        $table->add_field('consumerversion', XMLDB_TYPE_CHAR, '255', null, null, null, null);
-        $table->add_field('consumerguid', XMLDB_TYPE_CHAR, '1024', null, null, null, null);
-        $table->add_field('profile', XMLDB_TYPE_TEXT, null, null, null, null, null);
-        $table->add_field('toolproxy', XMLDB_TYPE_TEXT, null, null, null, null, null);
-        $table->add_field('settings', XMLDB_TYPE_TEXT, null, null, null, null, null);
-        $table->add_field('protected', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, null);
-        $table->add_field('enabled', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, null);
-        $table->add_field('enablefrom', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
-        $table->add_field('enableuntil', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
-        $table->add_field('lastaccess', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
-        $table->add_field('created', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
-        $table->add_field('updated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
-
-        // Adding keys to table enrol_lti_lti2_consumer.
-        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
-
-        // Adding indexes to table enrol_lti_lti2_consumer.
-        $table->add_index('consumerkey256_uniq', XMLDB_INDEX_UNIQUE, array('consumerkey256'));
-
-        // Conditionally launch create table for enrol_lti_lti2_consumer.
-        if (!$dbman->table_exists($table)) {
-            $dbman->create_table($table);
-        }
-
-        // Define table enrol_lti_lti2_tool_proxy to be created.
-        $table = new xmldb_table('enrol_lti_lti2_tool_proxy');
-
-        // Adding fields to table enrol_lti_lti2_tool_proxy.
-        $table->add_field('id', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
-        $table->add_field('toolproxykey', XMLDB_TYPE_CHAR, '32', null, XMLDB_NOTNULL, null, null);
-        $table->add_field('consumerid', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null);
-        $table->add_field('toolproxy', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null);
-        $table->add_field('created', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
-        $table->add_field('updated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
-
-        // Adding keys to table enrol_lti_lti2_tool_proxy.
-        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
-        $table->add_key('toolproxykey_uniq', XMLDB_KEY_UNIQUE, array('toolproxykey'));
-        $table->add_key('consumerid', XMLDB_KEY_FOREIGN, array('consumerid'), 'enrol_lti_lti2_consumer', array('id'));
-
-        // Conditionally launch create table for enrol_lti_lti2_tool_proxy.
-        if (!$dbman->table_exists($table)) {
-            $dbman->create_table($table);
-        }
-
-        // Define table enrol_lti_lti2_context to be created.
-        $table = new xmldb_table('enrol_lti_lti2_context');
-
-        // Adding fields to table enrol_lti_lti2_context.
-        $table->add_field('id', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
-        $table->add_field('consumerid', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null);
-        $table->add_field('lticontextkey', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
-        $table->add_field('settings', XMLDB_TYPE_TEXT, null, null, null, null, null);
-        $table->add_field('created', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
-        $table->add_field('updated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
-
-        // Adding keys to table enrol_lti_lti2_context.
-        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
-        $table->add_key('consumerid', XMLDB_KEY_FOREIGN, array('consumerid'), 'enrol_lti_lti2_consumer', array('id'));
-
-        // Conditionally launch create table for enrol_lti_lti2_context.
-        if (!$dbman->table_exists($table)) {
-            $dbman->create_table($table);
-        }
-
-        // Define table enrol_lti_lti2_nonce to be created.
-        $table = new xmldb_table('enrol_lti_lti2_nonce');
-
-        // Adding fields to table enrol_lti_lti2_nonce.
-        $table->add_field('id', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
-        $table->add_field('consumerid', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null);
-        $table->add_field('value', XMLDB_TYPE_CHAR, '32', null, XMLDB_NOTNULL, null, null);
-        $table->add_field('expires', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
-
-        // Adding keys to table enrol_lti_lti2_nonce.
-        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
-        $table->add_key('consumerid', XMLDB_KEY_FOREIGN, array('consumerid'), 'enrol_lti_lti2_consumer', array('id'));
-
-        // Conditionally launch create table for enrol_lti_lti2_nonce.
-        if (!$dbman->table_exists($table)) {
-            $dbman->create_table($table);
-        }
-
-        // Define table enrol_lti_lti2_resource_link to be created.
-        $table = new xmldb_table('enrol_lti_lti2_resource_link');
-
-        // Adding fields to table enrol_lti_lti2_resource_link.
-        $table->add_field('id', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
-        $table->add_field('contextid', XMLDB_TYPE_INTEGER, '11', null, null, null, null);
-        $table->add_field('consumerid', XMLDB_TYPE_INTEGER, '11', null, null, null, null);
-        $table->add_field('ltiresourcelinkkey', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
-        $table->add_field('settings', XMLDB_TYPE_TEXT, null, null, null, null, null);
-        $table->add_field('primaryresourcelinkid', XMLDB_TYPE_INTEGER, '11', null, null, null, null);
-        $table->add_field('shareapproved', XMLDB_TYPE_INTEGER, '1', null, null, null, null);
-        $table->add_field('created', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
-        $table->add_field('updated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
-
-        // Adding keys to table enrol_lti_lti2_resource_link.
-        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
-        $table->add_key('contextid', XMLDB_KEY_FOREIGN, array('contextid'), 'enrol_lti_lti2_context', array('id'));
-        $table->add_key('primaryresourcelinkid', XMLDB_KEY_FOREIGN, array('primaryresourcelinkid'),
-            'enrol_lti_lti2_resource_link', array('id'));
-        $table->add_key('consumerid', XMLDB_KEY_FOREIGN, array('consumerid'), 'enrol_lti_lti2_consumer', array('id'));
-
-        // Conditionally launch create table for enrol_lti_lti2_resource_link.
-        if (!$dbman->table_exists($table)) {
-            $dbman->create_table($table);
-        }
-
-        // Define table enrol_lti_lti2_share_key to be created.
-        $table = new xmldb_table('enrol_lti_lti2_share_key');
-
-        // Adding fields to table enrol_lti_lti2_share_key.
-        $table->add_field('id', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
-        $table->add_field('sharekey', XMLDB_TYPE_CHAR, '32', null, XMLDB_NOTNULL, null, null);
-        $table->add_field('resourcelinkid', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null);
-        $table->add_field('autoapprove', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, null);
-        $table->add_field('expires', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
-
-        // Adding keys to table enrol_lti_lti2_share_key.
-        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
-        $table->add_key('sharekey', XMLDB_KEY_UNIQUE, array('sharekey'));
-        $table->add_key('resourcelinkid', XMLDB_KEY_FOREIGN_UNIQUE, array('resourcelinkid'),
-            'enrol_lti_lti2_resource_link', array('id'));
-
-        // Conditionally launch create table for enrol_lti_lti2_share_key.
-        if (!$dbman->table_exists($table)) {
-            $dbman->create_table($table);
-        }
-
-        // Define table enrol_lti_lti2_user_result to be created.
-        $table = new xmldb_table('enrol_lti_lti2_user_result');
-
-        // Adding fields to table enrol_lti_lti2_user_result.
-        $table->add_field('id', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
-        $table->add_field('resourcelinkid', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null);
-        $table->add_field('ltiuserkey', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
-        $table->add_field('ltiresultsourcedid', XMLDB_TYPE_CHAR, '1024', null, XMLDB_NOTNULL, null, null);
-        $table->add_field('created', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
-        $table->add_field('updated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
-
-        // Adding keys to table enrol_lti_lti2_user_result.
-        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
-        $table->add_key('resourcelinkid', XMLDB_KEY_FOREIGN, array('resourcelinkid'),
-            'enrol_lti_lti2_resource_link', array('id'));
-
-        // Conditionally launch create table for enrol_lti_lti2_user_result.
-        if (!$dbman->table_exists($table)) {
-            $dbman->create_table($table);
-        }
-
-        // Define table enrol_lti_tool_consumer_map to be created.
-        $table = new xmldb_table('enrol_lti_tool_consumer_map');
-
-        // Adding fields to table enrol_lti_tool_consumer_map.
-        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
-        $table->add_field('toolid', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null);
-        $table->add_field('consumerid', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null);
-
-        // Adding keys to table enrol_lti_tool_consumer_map.
-        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
-        $table->add_key('toolid', XMLDB_KEY_FOREIGN, array('toolid'), 'enrol_lti_tools', array('id'));
-        $table->add_key('consumerid', XMLDB_KEY_FOREIGN, array('consumerid'), 'enrol_lti_lti2_consumer', array('id'));
-
-        // Conditionally launch create table for enrol_lti_tool_consumer_map.
-        if (!$dbman->table_exists($table)) {
-            $dbman->create_table($table);
-        }
-
-        // Lti savepoint reached.
-        upgrade_plugin_savepoint(true, 2016052303, 'enrol', 'lti');
-    }
-
-    if ($oldversion < 2016052304) {
-
-        // Define field type to be added to enrol_lti_lti2_context.
-        $table = new xmldb_table('enrol_lti_lti2_context');
-        $field = new xmldb_field('type', XMLDB_TYPE_CHAR, '100', null, null, null, null, 'lticontextkey');
-
-        // Conditionally launch add field type.
-        if (!$dbman->field_exists($table, $field)) {
-            $dbman->add_field($table, $field);
-        }
-
-        // Lti savepoint reached.
-        upgrade_plugin_savepoint(true, 2016052304, 'enrol', 'lti');
-    }
-
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     if ($oldversion < 2017011300) {
 
         // Changing precision of field value on table enrol_lti_lti2_nonce to (64).
index 9d3b967..48dfc69 100644 (file)
@@ -27,9 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_enrol_manual_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index e42f78f..f6d9efb 100644 (file)
@@ -27,9 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_enrol_mnet_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 5732fe2..93ebd50 100644 (file)
@@ -47,9 +47,6 @@ function xmldb_enrol_paypal_upgrade($oldversion) {
 
     $dbman = $DB->get_manager();
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 6436e7c..cb255e8 100644 (file)
 defined('MOODLE_INTERNAL') || die();
 
 function xmldb_enrol_self_upgrade($oldversion) {
-    global $CFG, $DB;
-
-    if ($oldversion < 2016052301) {
-        // Get roles with manager archetype.
-        $managerroles = get_archetype_roles('manager');
-        if (!empty($managerroles)) {
-            // Remove wrong CAP_PROHIBIT from self:holdkey.
-            foreach ($managerroles as $role) {
-                $DB->execute("DELETE
-                                FROM {role_capabilities}
-                               WHERE roleid = ? AND capability = ? AND permission = ?",
-                        array($role->id, 'enrol/self:holdkey', CAP_PROHIBIT));
-            }
-        }
-        upgrade_plugin_savepoint(true, 2016052301, 'enrol', 'self');
-    }
-
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
+    global $CFG;
 
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
index 1b2a99b..10994e7 100644 (file)
@@ -33,7 +33,7 @@ $string['test_unoconvdoesnotexist'] = 'The unoconv path does not point to the un
 $string['test_unoconvdownload'] = 'Download the converted pdf test file.';
 $string['test_unoconvempty'] = 'The unoconv path is not set. Please review your path settings.';
 $string['test_unoconvisdir'] = 'The unoconv path points to a folder, please include the unoconv program in the path you specify';
-$string['test_unoconvnotestfile'] = 'The test document to be coverted into a PDF is missing';
+$string['test_unoconvnotestfile'] = 'The test document to be converted to PDF is missing.';
 $string['test_unoconvnotexecutable'] = 'The unoconv path points to a file that is not executable';
 $string['test_unoconvok'] = 'The unoconv path appears to be properly configured.';
 $string['test_unoconvversionnotsupported'] = 'The version of unoconv you have installed is not supported.';
index 754152c..e4bb953 100644 (file)
@@ -33,35 +33,6 @@ function xmldb_filter_mathjaxloader_upgrade($oldversion) {
 
     require_once($CFG->dirroot . '/filter/mathjaxloader/db/upgradelib.php');
 
-    if ($oldversion < 2016080200) {
-        // We are consolodating the two settings for http and https url into only the https
-        // setting. Since it is preferably to always load the secure resource.
-
-        $httpurl = get_config('filter_mathjaxloader', 'httpurl');
-        if ($httpurl !== 'http://cdn.mathjax.org/mathjax/2.6-latest/MathJax.js' &&
-            $httpurl !== 'http://cdn.mathjax.org/mathjax/2.6.1/MathJax.js') {
-            // If the http setting has been changed, we make the admin choose the https setting because
-            // it indicates some sort of custom setup. This will be supported by the release notes.
-            unset_config('httpsurl', 'filter_mathjaxloader');
-        }
-
-        // The seperate http setting has been removed. We always use the secure resource.
-        unset_config('httpurl', 'filter_mathjaxloader');
-
-        upgrade_plugin_savepoint(true, 2016080200, 'filter', 'mathjaxloader');
-    }
-
-    if ($oldversion < 2016102500) {
-        $httpsurl = get_config('filter_mathjaxloader', 'httpsurl');
-        if ($httpsurl === "https://cdn.mathjax.org/mathjax/2.6-latest/MathJax.js") {
-            set_config('httpsurl', 'https://cdn.mathjax.org/mathjax/2.7-latest/MathJax.js', 'filter_mathjaxloader');
-        }
-        upgrade_plugin_savepoint(true, 2016102500, 'filter', 'mathjaxloader');
-    }
-
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-    //
     if ($oldversion < 2017040300) {
 
         $httpsurl = get_config('filter_mathjaxloader', 'httpsurl');
index 09391c3..a8a43ac 100644 (file)
@@ -32,9 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_filter_mediaplugin_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index da0d43a..ec40486 100644 (file)
@@ -32,9 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_filter_tex_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 1965c9a..5fee948 100644 (file)
@@ -32,7 +32,7 @@ Feature: I need to export grades as text
     When I navigate to "Export > Plain text file" in the course gradebook
     And I expand all fieldsets
     And I click on "Course total" "checkbox"
-    And I set the field "Grade export decimal points" to "1"
+    And I set the field "Grade export decimal places" to "1"
     And I press "Download"
     Then I should see "Student,1"
     And I should see "80.0"
index d1ca5aa..38f6d78 100644 (file)
@@ -30,7 +30,7 @@ Feature: I need to export grades as xml
   Scenario: Export grades as text
     When I navigate to "Export > XML file" in the course gradebook
     And I expand all fieldsets
-    And I set the field "Grade export decimal points" to "1"
+    And I set the field "Grade export decimal places" to "1"
     And I press "Download"
     Then I should see "s1"
     And I should see "a1"
index e781e99..290350d 100644 (file)
@@ -37,9 +37,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_gradingform_guide_upgrade($oldversion) {
     global $DB;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index a48635c..e7e0bdf 100644 (file)
@@ -33,9 +33,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_gradingform_rubric_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 1aef803..5dbbfec 100644 (file)
 }
 
 .gradingform_rubric {
-    overflow: auto;
     padding-bottom: 1.5em;
-    max-width: 720px;
-    position: relative;
 }
 
 .gradingform_rubric.editor .criterion .controls,
@@ -89,6 +86,9 @@
 
 .gradingform_rubric .criteria {
     height: 100%;
+    display: flex;
+    width: 100%;
+    overflow: auto;
 }
 
 .gradingform_rubric .criterion {
     margin: 0;
     position: relative;
     float: right;
-}
\ No newline at end of file
+}
index 2cd5dbb..8d7d702 100644 (file)
@@ -34,13 +34,13 @@ $string['grader:view'] = 'View the grader report';
 $string['pluginname'] = 'Grader report';
 $string['preferences'] = 'Grader report preferences';
 $string['privacy:metadata:preference:grade_report_aggregationposition'] = 'Whether the category and course total columns are displayed first or last in the gradebook reports';
-$string['privacy:metadata:preference:grade_report_averagesdecimalpoints'] = 'The number of decimal points to display for each average or whether the overall decimal points setting for the category or grade item is used (inherit).';
+$string['privacy:metadata:preference:grade_report_averagesdecimalpoints'] = 'The number of decimal places to display for each average or whether the overall decimal places setting for the category or grade item is used (inherit).';
 $string['privacy:metadata:preference:grade_report_averagesdisplaytype'] = 'Whether the average (mean) is displayed as real grades, percentages or letters, or whether the display type for the category or grade item is used (inherit).';
 $string['privacy:metadata:preference:grade_report_enableajax'] = 'Whether to add a layer of AJAX functionality to the grader report, simplifying and speeding up common operations';
 $string['privacy:metadata:preference:grade_report_grader_collapsed_categories'] = 'List of gradebook categories to be collapsed';
 $string['privacy:metadata:preference:grade_report_meanselection'] = 'Whether cells with no grade should be included when calculating the average (mean) for each category or grade item';
 $string['privacy:metadata:preference:grade_report_quickgrading'] = 'Whether to display a text input box for each grade, allowing many grades to be edited at the same time';
-$string['privacy:metadata:preference:grade_report_rangesdecimalpoints'] = 'The number of decimal points to display for each range or whether the overall decimal points setting for the category or grade item is used (inherit)';
+$string['privacy:metadata:preference:grade_report_rangesdecimalpoints'] = 'The number of decimal places to display for each range or whether the overall decimal places setting for the category or grade item is used (inherit)';
 $string['privacy:metadata:preference:grade_report_rangesdisplaytype'] = 'Whether the range is displayed as real grades, percentages or letters, or whether the display type for the category or grade item is used (inherit)';
 $string['privacy:metadata:preference:grade_report_showactivityicons'] = 'Whether to show the activity icons next to activity names';
 $string['privacy:metadata:preference:grade_report_showanalysisicon'] = 'Whether to show grade analysis icon by default. If the activity module supports it, the grade analysis icon links to a page with more detailed explanation of the grade and how it was obtained.';
index a2d2496..bc436ed 100644 (file)
@@ -29,9 +29,6 @@
 function xmldb_gradereport_user_upgrade($oldversion) {
     global $CFG;
 
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index 0c1bb5d..b66ca45 100644 (file)
@@ -222,6 +222,7 @@ if ($editform->is_cancelled()) {
             $newgroup = new stdClass();
             $newgroup->courseid = $data->courseid;
             $newgroup->name     = $group['name'];
+            $newgroup->enablemessaging = $data->enablemessaging;
             $groupid = groups_create_group($newgroup);
             $createdgroups[] = $groupid;
             foreach($group['members'] as $user) {
index cb13808..31340b0 100644 (file)
@@ -42,7 +42,8 @@ class autogroup_form extends moodleform {
      * Form Definition
      */
     function definition() {
-        global $CFG, $COURSE;
+        global $USER, $COURSE;
+        $coursecontext = context_course::instance($COURSE->id);
 
         $mform =& $this->_form;
 
@@ -68,6 +69,12 @@ class autogroup_form extends moodleform {
         $mform->addRule('number', null, 'numeric', null, 'client');
         $mform->addRule('number', get_string('required'), 'required', null, 'client');
 
+        // Enable group messaging for the groups to be auto-created.
+        if (\core_message\api::can_create_group_conversation($USER->id, $coursecontext)) {
+            $mform->addElement('selectyesno', 'enablemessaging', get_string('enablemessaging', 'group'));
+            $mform->addHelpButton('enablemessaging', 'enablemessaging', 'group');
+        }
+
         $mform->addElement('header', 'groupmembershdr', get_string('groupmembers', 'group'));
         $mform->setExpanded('groupmembershdr', true);
 
index 41f8b2a..2f563a2 100644 (file)
@@ -83,7 +83,9 @@ if ($mform_post->is_cancelled()) {
             "groupidnumber" => 1,
             "description" => 1,
             "enrolmentkey" => 1,
-            "groupingname" => 1);
+            "groupingname" => 1,
+            "enablemessaging" => 1,
+        );
 
     // --- get header (field names) ---
     $header = explode($csv_delimiter, array_shift($rawlines));
index 8610678..54b4763 100644 (file)
@@ -57,6 +57,16 @@ Feature: Automatic creation of groups
     And I press "Submit"
     And the "groups" select box should contain "Group A (5)"
     And the "groups" select box should contain "Group B (5)"
+    # Check that group messaging is not enabled for the auto-created groups.
+    And I set the field "groups" to "Group A"
+    And I press "Edit group settings"
+    And I should see "No" in the "Group messaging" "select"
+    And I press "Cancel"
+    And I set the field "groups" to "Group B"
+    And I press "Edit group settings"
+    And I should see "No" in the "Group messaging" "select"
+    And I press "Cancel"
+    # Check groupings.
     And I follow "Groupings"
     And I should see "Grouping name"
     And I click on "Show groups in grouping" "link" in the "Grouping name" "table_row"
@@ -170,3 +180,20 @@ Feature: Automatic creation of groups
     And I set the field "Group/member count" to "11"
     And I press "Preview"
     And I should not see "Suspended Student 11"
+
+  @javascript
+  Scenario: Auto-create groups with group messaging
+    Given I set the following fields to these values:
+      | Naming scheme | Group @ |
+      | Auto create based on | Number of groups |
+      | Group/member count | 2 |
+      | Grouping of auto-created groups | No grouping |
+      | Group messaging | Yes |
+    And I press "Submit"
+    And I set the field "groups" to "Group A"
+    When I press "Edit group settings"
+    Then I should see "Yes" in the "Group messaging" "select"
+    And I press "Cancel"
+    And I set the field "groups" to "Group B"
+    And I press "Edit group settings"
+    And I should see "Yes" in the "Group messaging" "select"
index 95019f1..12ad501 100644 (file)
@@ -31,6 +31,17 @@ Feature: Importing of groups and groupings
     And I should see "group-id-1-duplicate"
     And I should see "group-noid-1"
     And I should see "group-noid-2"
+    # Group messaging should have been enabled for group-id-1.
+    And I set the field "groups" to "group-id-1"
+    And I press "Edit group settings"
+    And I should see "Yes" in the "Group messaging" "select"
+    And I press "Cancel"
+     # Group messaging should not have been enabled for group-id-2.
+    And I set the field "groups" to "group-id-2"
+    And I press "Edit group settings"
+    And I should see "No" in the "Group messaging" "select"
+    And I press "Cancel"
+    # Check groupings
     And I follow "Groupings"
     And I should see "Grouping-1"
     And I should see "Grouping-2"
index ec5fe44..0bf1fb4 100644 (file)
@@ -1,6 +1,6 @@
-groupname, description, groupidnumber,groupingname
-group-id-1, group-id-1, group-id-1,Grouping-1
-group-id-2, group-id-2, group-id-2,Grouping-2
-group-id-1-duplicate, Duplicate of group-id-1, group-id-1,Grouping-3
-group-noid-1, group-noid-1,,Grouping-3
-group-noid-2, group-noid-2,,Grouping-2
+groupname, description, groupidnumber,groupingname,enablemessaging
+group-id-1, group-id-1, group-id-1,Grouping-1,1
+group-id-2, group-id-2, group-id-2,Grouping-2,0
+group-id-1-duplicate, Duplicate of group-id-1, group-id-1,Grouping-3,0
+group-noid-1, group-noid-1,,Grouping-3,0
+group-noid-2, group-noid-2,,Grouping-2,0
index 61fa691..5b3bc0e 100644 (file)
@@ -30,6 +30,9 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+$string['cannotcreatedboninstall'] = '<p>Ne mogu stvoriti bazu podataka.</p>
+<p>Navedena baza podataka ne postoji i korisnik nema prava stvaranja baze podataka.</p>
+<p>Administrator treba provjeriti postavke baze podataka.</p>';
 $string['cannotcreatelangdir'] = 'Nije moguće stvoriti lang mapu';
 $string['cannotcreatetempdir'] = 'Nije moguće stvoriti privremenu (TEMP) mapu';
 $string['cannotdownloadcomponents'] = 'Nije moguće preuzimanje komponenti';
index 243344f..dbb7577 100644 (file)
@@ -280,6 +280,7 @@ $string['configmaxevents'] = 'Events to Lookahead';
 $string['configmessaging'] = 'If enabled, users can send messages to other users on the site.';
 $string['configmessagingallowemailoverride'] = 'Allow users to have email message notifications sent to an email address other than the email address in their profile';
 $string['configmessagingdeletereadnotificationsdelay'] = 'Read notifications can be deleted to save space. How long after a notification is read can it be deleted?';
+$string['configmessagingdeleteallnotificationsdelay'] = 'Read and unread notifications can be deleted to save space. How long after a notification is created can it be deleted?';
 $string['configmessagingallusers'] = 'If enabled, users can view the list of all users on the site when selecting someone to message, and their message preferences include the option to accept messages from anyone on the site. If disabled, users can only view the list of users in their courses, and they have just two options in message preferences - to accept messages from their contacts only, or their contacts and anyone in their courses.';
 $string['configminpassworddigits'] = 'Passwords must have at least these many digits.';
 $string['configminpasswordlength'] = 'Passwords must be at least these many characters long.';
@@ -769,6 +770,7 @@ $string['messaging'] = 'Enable messaging system';
 $string['messagingallowemailoverride'] = 'Notification email override';
 $string['messagingallusers'] = 'Allow site-wide messaging';
 $string['messagingdeletereadnotificationsdelay'] = 'Delete read notifications';
+$string['messagingdeleteallnotificationsdelay'] = 'Delete all notifications';
 $string['minpassworddigits'] = 'Digits';
 $string['minpasswordlength'] = 'Password length';
 $string['minpasswordlower'] = 'Lowercase letters';
@@ -1028,6 +1030,7 @@ $string['requestcategoryselection'] = 'Enable category selection';
 $string['restorecourse'] = 'Restore course';
 $string['restorernewroleid'] = 'Restorers\' role in courses';
 $string['restorernewroleid_help'] = 'If the user does not already have the permission to manage the newly restored course, the user is automatically assigned this role and enrolled if necessary. Select "None" if you do not want restorers to be able to manage every restored course.';
+$string['resultfilter'] = 'Filter by result';
 $string['reverseproxy'] = 'Reverse proxy';
 $string['riskconfig'] = 'Users could change site configuration and behaviour';
 $string['riskconfigshort'] = 'Configuration risk';
@@ -1162,6 +1165,34 @@ $string['tablesnosave'] = 'Changes in tables above are saved automatically.';
 $string['tabselectedtofront'] = 'On tables with tabs, should the row with the currently selected tab be placed at the front';
 $string['tabselectedtofronttext'] = 'Bring selected tab row to front';
 $string['testsiteupgradewarning'] = 'You are currently using the {$a} test site, to upgrade it properly use the command line interface tool';
+$string['task_scheduled_concurrency_limit'] = 'Scheduled task concurrency limit';
+$string['task_scheduled_concurrency_limit_desc'] = 'The number of scheduled task runners allowed to run concurrently. If the limit is high then the server may experience high load which affects performance. A setting of 0 will disable processing of scheduled tasks completely.';
+$string['task_scheduled_max_runtime'] = 'Scheduled task runner lifetime';
+$string['task_scheduled_max_runtime_desc'] = 'The age of a scheduled task runner before it is freed.';
+$string['task_adhoc_concurrency_limit'] = 'Adhoc task concurrency limit';
+$string['task_adhoc_concurrency_limit_desc'] = 'The number of adhoc task runners allowed to run concurrently. If the limit is high then scheduled tasks may not run regularly when there are lots of adhoc tasks. A setting of 0 will disable processing of adhoc tasks completely.';
+$string['task_adhoc_max_runtime'] = 'Adhoc task runner lifetime';
+$string['task_adhoc_max_runtime_desc'] = 'The age of an adhoc task runner before it is freed. A low duration is recommended as there is no limit to the number of adhoc tasks queued. If this number is too high and you have a large adhoc task queue then scheduled tasks may not be run regularly.';
+$string['task_logmode'] = 'When to log';
+$string['task_logmode_desc'] = 'You can choose when you wish task logging to take place. By default logs are always captured. You can disable logging entirely, or change to only log tasks which fail.';
+$string['task_logmode_none'] = 'Do not log anything';
+$string['task_logmode_all'] = 'Store the log output of all jobs';
+$string['task_logmode_failonly'] = 'Only store logs for jobs which fail';
+$string['task_logretention'] = 'Retention period';
+$string['task_logretention_desc'] = 'The maximum period that logs should be kept for. This setting interacts with the \'Retain runs\' setting: whichever is reached first will apply';
+$string['task_logretainruns'] = 'Retain runs';
+$string['task_logretainruns_desc'] = 'The number of runs of each task to retain. This setting interacts with the \'Retention period\' setting: whichever is reached first will apply.';
+$string['task_type:adhoc'] = 'Adhoc';
+$string['task_type:scheduled'] = 'Scheduled';
+$string['task_result:failed'] = 'Fail';
+$string['task_stats:dbreads'] = '{$a} reads';
+$string['task_stats:dbwrites'] = '{$a} writes';
+$string['task_starttime'] = 'Start time';
+$string['task_duration'] = 'Duration';
+$string['task_dbstats'] = 'Database';
+$string['task_result'] = 'Result';
+$string['tasktype'] = 'Type';
+$string['taskadmintitle'] = 'Tasks';
 $string['taskanalyticscleanup'] = 'Analytics cleanup';
 $string['taskautomatedbackup'] = 'Automated backups';
 $string['taskbackupcleanup'] = 'Clean backup tables and logs';
@@ -1174,6 +1205,7 @@ $string['taskcheckforupdates'] = 'Check for updates';
 $string['taskcompletionregular'] = 'Calculate regular completion data';
 $string['taskcompletiondaily'] = 'Completion mark as started';
 $string['taskcontextcleanup'] = 'Cleanup contexts';
+$string['tasklogging'] = 'Task log configuration';
 $string['taskcreatecontexts'] = 'Create missing contexts';
 $string['taskdeletecachetext'] = 'Delete old text cache records';
 $string['taskdeleteincompleteusers'] = 'Delete incomplete users';
@@ -1184,10 +1216,13 @@ $string['taskglobalsearchindex'] = 'Global search indexing';
 $string['taskglobalsearchoptimize'] = 'Global search index optimization';
 $string['taskgradecron'] = 'Background processing for gradebook';
 $string['tasklegacycron'] = 'Legacy cron processing for plugins';
+$string['tasklogcleanup'] = 'Cleanup of task logs';
+$string['tasklogs'] = 'Task logs';
 $string['taskmessagingcleanup'] = 'Background processing for messaging';
 $string['taskpasswordresetcleanup'] = 'Cleanup password reset attempts';
 $string['taskplagiarismcron'] = 'Background processing for legacy cron in plagiarism plugins';
 $string['taskportfoliocron'] = 'Background processing for portfolio plugins';
+$string['taskprocessing'] = 'Task processing';
 $string['taskquestioncron'] = 'Background processing for question engine';
 $string['taskrefreshsystemtokens'] = 'Refresh OAuth tokens for service accounts';
 $string['taskregistrationcron'] = 'Site registration';
index 95b9450..2317fb2 100644 (file)
@@ -430,7 +430,7 @@ $string['nologinas'] = 'You are not allowed to log in as that user';
 $string['nonmeaningfulcontent'] = 'Non meaningful content';
 $string['noparticipants'] = 'No participants found for this course';
 $string['noparticipatorycms'] = 'Sorry, but you have no participatory course modules to report on';
-$string['nopermissions'] = 'Sorry, but you do not currently have permissions to do that ({$a})';
+$string['nopermissions'] = 'Sorry, but you do not currently have permissions to do that ({$a}).';
 $string['nopermissiontocomment'] = 'You can\'t add comments';
 $string['nopermissiontodelentry'] = 'You can\'t delete other people\'s entries!';
 $string['nopermissiontoeditcomment'] = 'You can\'t edit other people\'s comments!';
@@ -441,7 +441,7 @@ $string['nopermissiontomanagegroup'] = 'You do not have the required permissions
 $string['nopermissiontorate'] = 'Rating of items not allowed!';
 $string['nopermissiontoshow'] = 'No permission to see this!';
 $string['nopermissiontounlock'] = 'No permission to unlock!';
-$string['nopermissiontoupdatecalendar'] = 'Sorry, but you do not currently have permissions to update calendar event';
+$string['nopermissiontoupdatecalendar'] = 'Sorry, but you do not currently have permissions to update a calendar event.';
 $string['nopermissiontoviewgrades'] = 'Can not view grades.';
 $string['nopermissiontoviewletergrade'] = 'Missing permission to view letter grades';
 $string['nopermissiontoviewpage'] = 'You are not allowed to look at this page';
index 7beca40..8bea3d3 100644 (file)
@@ -93,7 +93,7 @@ $string['autosort'] = 'Auto-sort';
 $string['availableidnumbers'] = 'Available ID numbers';
 $string['average'] = 'Average';
 $string['averagesdecimalpoints'] = 'Decimals in column averages';
-$string['averagesdecimalpoints_help'] = 'This setting determines the number of decimal points to display for each average or whether the overall decimal points setting for the category or grade item is used (inherit).';
+$string['averagesdecimalpoints_help'] = 'This setting determines the number of decimal places to display for each average or whether the overall decimal places setting for the category or grade item is used (inherit).';
 $string['averagesdisplaytype'] = 'Column averages display type';
 $string['averagesdisplaytype_help'] = 'This setting determines whether the average (mean) is displayed as real grades, percentages or letters, or whether the display type for the category or grade item is used (inherit).';
 $string['backupwithoutgradebook'] = 'Backup does not contain gradebook configuration';
@@ -142,8 +142,8 @@ $string['creatinggradebooksettings'] = 'Creating gradebook settings';
 $string['csv'] = 'CSV';
 $string['currentparentaggregation'] = 'Current parent aggregation';
 $string['curveto'] = 'Curve to';
-$string['decimalpoints'] = 'Overall decimal points';
-$string['decimalpoints_help'] = 'This setting determines the number of decimal points to display for each grade. It has no effect on grade calculations, which are made with an accuracy of 5 decimal places.';
+$string['decimalpoints'] = 'Overall decimal places';
+$string['decimalpoints_help'] = 'This setting determines the number of decimal places to display for each grade. It has no effect on grade calculations, which are made with an accuracy of 5 decimal places.';
 $string['default'] = 'Default';
 $string['defaultprev'] = 'Default ({$a})';
 $string['deletecategory'] = 'Delete category';
@@ -274,8 +274,8 @@ $string['gradeexport'] = 'Grade export';
 $string['gradeexportcolumntype'] = '{$a->name} ({$a->extra})';
 $string['gradeexportcustomprofilefields'] = 'Grade export custom profile fields';
 $string['gradeexportcustomprofilefields_desc'] = 'Include these custom profile fields in the grade export, separated by commas.';
-$string['gradeexportdecimalpoints'] = 'Grade export decimal points';
-$string['gradeexportdecimalpoints_desc'] = 'The number of decimal points to display for export. This can be overridden during export.';
+$string['gradeexportdecimalpoints'] = 'Grade export decimal places';
+$string['gradeexportdecimalpoints_desc'] = 'The number of decimal places to display for export. This can be overridden during export.';
 $string['gradeexportdisplaytype'] = 'Grade export display type';
 $string['gradeexportdisplaytype_desc'] = 'Grades can be shown as real grades, as percentages (in reference to the minimum and maximum grades) or as letters (A, B, C etc..) during export. This can be overridden during export.';
 $string['gradeexportdisplaytypes'] = 'Grade export display types';
@@ -666,7 +666,7 @@ $string['quickgrading_help'] = 'If enabled, when editing is turned on, a text in
 Note that when a grade is edited in the grader report, an overridden flag is set, meaning that the grade can no longer be changed from within the related activity.';
 $string['range'] = 'Range';
 $string['rangesdecimalpoints'] = 'Decimals shown in ranges';
-$string['rangesdecimalpoints_help'] = 'This setting determines the number of decimal points to display for each range or whether the overall decimal points setting for the category or grade item is used (inherit).';
+$string['rangesdecimalpoints_help'] = 'This setting determines the number of decimal places to display for each range or whether the overall decimal places setting for the category or grade item is used (inherit).';
 $string['rangesdisplaytype'] = 'Range display type';
 $string['rangesdisplaytype_help'] = 'This setting determines whether the range is displayed as real grades, percentages or letters, or whether the display type for the category or grade item is used (inherit).';
 $string['rank'] = 'Rank';
@@ -735,8 +735,8 @@ $string['showrange'] = 'Show ranges';
 $string['showrange_help'] = 'Whether to show a column for the range.';
 $string['showweight'] = 'Show weightings';
 $string['showweight_help'] = 'Whether to show a column for the grade weight.';
-$string['rangedecimals'] = 'Range decimal points';
-$string['rangedecimals_help'] = 'The number of decimal points to display for range.';
+$string['rangedecimals'] = 'Range decimal places';
+$string['rangedecimals_help'] = 'The number of decimal places to display for the range.';
 $string['showactivityicons'] = 'Show activity icons';
 $string['showactivityicons_help'] = 'If enabled, activity icons are shown next to activity names.';
 $string['showallhidden'] = 'Show hidden';
index 3b60116..e47c095 100644 (file)
@@ -203,9 +203,6 @@ $string['requiresconfiguration'] = 'Requires configuration';
 $string['searchforuser'] = 'Search for a user';
 $string['searchforuserorcourse'] = 'Search for a user or course';
 $string['searchmessages'] = 'Search messages';
-$string['searchnocontactsfound'] = 'No contacts found';
-$string['searchnomessagesfound'] = 'No messages found';
-$string['searchnononcontactsfound'] = 'No non contacts found';
 $string['searchcombined'] = 'Search people and messages';
 $string['seeall'] = 'See all';
 $string['selectmessagestodelete'] = 'Select messages to delete';
index 5fe9a09..9e42a43 100644 (file)
@@ -1588,6 +1588,9 @@ $string['privacy:metadata:task_adhoc'] = 'The status of adhoc tasks.';
 $string['privacy:metadata:task_adhoc:component'] = 'The component owning the task.';
 $string['privacy:metadata:task_adhoc:nextruntime'] = 'The earliest time to run this task.';
 $string['privacy:metadata:task_adhoc:userid'] = 'The user to run the task as.';
+$string['privacy:metadata:task_log'] = 'Log output for a log';
+$string['privacy:metadata:task_log:component'] = 'The component owning the task.';
+$string['privacy:metadata:task_log:userid'] = 'The user that the task belonged to.';
 $string['privacy:metadata:upgrade_log'] = 'The upgrade log.';
 $string['privacy:metadata:upgrade_log:backtrace'] = 'Any backtrace associated with this upgrade step.';
 $string['privacy:metadata:upgrade_log:details'] = 'Extra information relating to the upgrade.';
index 9d6dc4f..43131c2 100644 (file)
@@ -5434,6 +5434,9 @@ abstract class context extends stdClass implements IteratorAggregate {
             return array();
         }
 
+        // Preload the contexts to reduce DB calls.
+        context_helper::preload_contexts_by_id($contextids);
+
         $result = array();
         foreach ($contextids as $contextid) {
             $parent = context::instance_by_id($contextid, MUST_EXIST);
@@ -5870,6 +5873,34 @@ class context_helper extends context {
          context::preload_from_record($rec);
      }
 
+    /**
+     * Preload a set of contexts using their contextid.
+     *
+     * @param   array $contextids
+     */
+    public static function preload_contexts_by_id(array $contextids) {
+        global $DB;
+
+        // Determine which contexts are not already cached.
+        $tofetch = [];
+        foreach ($contextids as $contextid) {
+            if (!self::cache_get_by_id($contextid)) {
+                $tofetch[] = $contextid;
+            }
+        }
+
+        if (count($tofetch) > 1) {
+            // There are at least two to fetch.
+            // There is no point only fetching a single context as this would be no more efficient than calling the existing code.
+            list($insql, $inparams) = $DB->get_in_or_equal($tofetch, SQL_PARAMS_NAMED);
+            $ctxs = $DB->get_records_select('context', "id {$insql}", $inparams, '',
+                    \context_helper::get_preload_record_columns_sql('{context}'));
+            foreach ($ctxs as $ctx) {
+                self::preload_from_record($ctx);
+            }
+        }
+    }
+
     /**
      * Preload all contexts instances from course.
      *
index 661071e..096b837 100644 (file)
Binary files a/lib/amd/build/modal.min.js and b/lib/amd/build/modal.min.js differ
index d357c17..9e03a10 100644 (file)
Binary files a/lib/amd/build/str.min.js and b/lib/amd/build/str.min.js differ
index 3357ba4..b5be0b1 100644 (file)
Binary files a/lib/amd/build/templates.min.js and b/lib/amd/build/templates.min.js differ
index be98211..799b3dc 100644 (file)
@@ -723,7 +723,12 @@ define(['jquery', 'core/templates', 'core/notification', 'core/key_codes',
             // If the click wasn't inside the modal element then we should
             // hide the modal.
             if (!$(e.target).closest(SELECTORS.MODAL).length) {
-                this.hide();
+                // The check above fails to detect the click was inside the modal when the DOM tree is already changed.
+                // So, we check if we can still find the container element or not. If not, then the DOM tree is changed.
+                // It's best not to hide the modal in that case.
+                if ($(e.target).closest(SELECTORS.CONTAINER).length) {
+                    this.hide();
+                }
             }
         }.bind(this));
 
index eac5c54..286fb48 100644 (file)
@@ -176,6 +176,42 @@ define(['jquery', 'core/ajax', 'core/localstorage'], function($, ajax, storage)
             }
 
             return deferred.promise();
+        },
+        /**
+         * Add a list of strings to the caches.
+         *
+         * @method cache_strings
+         * @param {Object[]} strings Array of { key: key, component: component, lang: lang, value: value }
+         */
+         // eslint-disable-next-line camelcase
+        cache_strings: function(strings) {
+            var defaultLang = $('html').attr('lang').replace(/-/g, '_');
+            strings.forEach(function(string) {
+                var lang = !(lang in string) ? defaultLang : string.lang;
+                var key = string.key;
+                var component = string.component;
+                var value = string.value;
+                var cacheKey = ['core_str', key, component, lang].join('/');
+
+                // Check M.str caching.
+                if (!(component in M.str) || !(key in M.str[component])) {
+                    if (!(component in M.str)) {
+                        M.str[component] = {};
+                    }
+
+                    M.str[component][key] = value;
+                }
+
+                // Check local storage.
+                if (!storage.get(cacheKey)) {
+                    storage.set(cacheKey, value);
+                }
+
+                // Check the promises cache.
+                if (!(cacheKey in promiseCache)) {
+                    promiseCache[cacheKey] = $.Deferred().resolve(value).promise();
+                }
+            });
         }
     };
 });
index 514f210..748cea5 100644 (file)
@@ -59,6 +59,191 @@ define([
     /** @var {Object} iconSystem - Object extending core/iconsystem */
     var iconSystem = {};
 
+    /** @var {Object[]} loadTemplateBuffer - List of templates to be loaded */
+    var loadTemplateBuffer = [];
+
+    /** @var {Bool} isLoadingTemplates - Whether templates are currently being loaded */
+    var isLoadingTemplates = false;
+
+    /**
+     * Search the various caches for a template promise for the given search key.
+     * The search key should be in the format <theme>/<component>/<template> e.g. boost/core/modal.
+     *
+     * If the template is found in any of the caches it will populate the other caches with
+     * the same data as well.
+     *
+     * @param {String} searchKey The template search key in the format <theme>/<component>/<template> e.g. boost/core/modal
+     * @return {Object} jQuery promise resolved with the template source
+     */
+    var getTemplatePromiseFromCache = function(searchKey) {
+        // First try the cache of promises.
+        if (searchKey in templatePromises) {
+            return templatePromises[searchKey];
+        }
+
+        // Check the module cache.
+        if (searchKey in templateCache) {
+            // Add this to the promises cache for future.
+            templatePromises[searchKey] = $.Deferred().resolve(templateCache[searchKey]).promise();
+            return templatePromises[searchKey];
+        }
+
+        // Now try local storage.
+        var cached = storage.get('core_template/' + searchKey);
+        if (cached) {
+            // Add this to the module cache for future.
+            templateCache[searchKey] = cached;
+            // Add this to the promises cache for future.
+            templatePromises[searchKey] = $.Deferred().resolve(cached).promise();
+            return templatePromises[searchKey];
+        }
+
+        return null;
+    };
+
+    /**
+     * Take all of the templates waiting in the buffer and load them from the server
+     * or from the cache.
+     *
+     * All of the templates that need to be loaded from the server will be batched up
+     * and sent in a single network request.
+     */
+    var processLoadTemplateBuffer = function() {
+        if (!loadTemplateBuffer.length) {
+            return;
+        }
+
+        if (isLoadingTemplates) {
+            return;
+        }
+
+        isLoadingTemplates = true;
+        // Grab any templates waiting in the buffer.
+        var templatesToLoad = loadTemplateBuffer.slice();
+        // This will be resolved with the list of promises for the server request.
+        var serverRequestsDeferred = $.Deferred();
+        var requests = [];
+        // Get a list of promises for each of the templates we need to load.
+        var templatePromises = templatesToLoad.map(function(templateData) {
+            var component = templateData.component;
+            var name = templateData.name;
+            var searchKey = templateData.searchKey;
+            var theme = templateData.theme;
+            var templateDeferred = templateData.deferred;
+            var promise = null;
+
+            // Double check to see if this template happened to have landed in the
+            // cache as a dependency of an earlier template.
+            var cachedPromise = getTemplatePromiseFromCache(searchKey);
+            if (cachedPromise) {
+                // We've seen this template so immediately resolve the existing promise.
+                promise = cachedPromise;
+            } else {
+                // We haven't seen this template yet so we need to request it from
+                // the server.
+                requests.push({
+                    methodname: 'core_output_load_template_with_dependencies',
+                    args: {
+                        component: component,
+                        template: name,
+                        themename: theme
+                    }
+                });
+                // Remember the index in the requests list for this template so that
+                // we can get the appropriate promise back.
+                var index = requests.length - 1;
+
+                // The server deferred will be resolved with a list of all of the promises
+                // that were sent in the order that they were added to the requests array.
+                promise = serverRequestsDeferred.promise()
+                    .then(function(promises) {
+                        // The promise for this template will be the one that matches the index
+                        // for it's entry in the requests array.
+                        //
+                        // Make sure the promise is added to the promises cache for this template
+                        // search key so that we don't request it again.
+                        templatePromises[searchKey] = promises[index].then(function(response) {
+                            var templateSource = null;
+
+                            // Process all of the template dependencies for this template and add
+                            // them to the caches so that we don't request them again later.
+                            response.templates.forEach(function(data) {
+                                // Generate the search key for this template in the response so that we
+                                // can add it to the caches.
+                                var tempSearchKey = [theme, data.component, data.name].join('/');
+                                // Cache all of the dependent templates because we'll need them to render
+                                // the requested template.
+                                templateCache[tempSearchKey] = data.value;
+                                storage.set('core_template/' + tempSearchKey, data.value);
+
+                                if (data.component == component && data.name == name) {
+                                    // This is the original template that was requested so remember it to return.
+                                    templateSource = data.value;
+                                }
+                            });
+
+                            if (response.strings.length) {
+                                // If we have strings that the template needs then warm the string cache
+                                // with them now so that we don't need to re-fetch them.
+                                str.cache_strings(response.strings.map(function(data) {
+                                    return {
+                                        component: data.component,
+                                        key: data.name,
+                                        value: data.value
+                                    };
+                                }));
+                            }
+
+                            // Return the original template source that the user requested.
+                            return templateSource;
+                        });
+
+                        return templatePromises[searchKey];
+                    });
+            }
+
+            return promise
+                .then(function(source) {
+                    // When we've successfully loaded the template then resolve the deferred
+                    // in the buffer so that all of the calling code can proceed.
+                    return templateDeferred.resolve(source);
+                })
+                .catch(function(error) {
+                    // If there was an error loading the template then reject the deferred
+                    // in the buffer so that all of the calling code can proceed.
+                    templateDeferred.reject(error);
+                    // Rethrow for anyone else listening.
+                    throw error;
+                });
+        });
+
+        if (requests.length) {
+            // We have requests to send so resolve the deferred with the promises.
+            serverRequestsDeferred.resolve(ajax.call(requests, true, false));
+        } else {
+            // Nothing to load so we can resolve our deferred.
+            serverRequestsDeferred.resolve();
+        }
+
+        // Once we've finished loading all of the templates then recurse to process
+        // any templates that may have been added to the buffer in the time that we
+        // were fetching.
+        $.when.apply(null, templatePromises)
+            .then(function() {
+                // Remove the templates we've loaded from the buffer.
+                loadTemplateBuffer.splice(0, templatesToLoad.length);
+                isLoadingTemplates = false;
+                processLoadTemplateBuffer();
+                return;
+            })
+            .catch(function() {
+                // Remove the templates we've loaded from the buffer.
+                loadTemplateBuffer.splice(0, templatesToLoad.length);
+                isLoadingTemplates = false;
+                processLoadTemplateBuffer();
+            });
+    };
+
     /**
      * Constructor
      *
@@ -85,7 +270,7 @@ define([
     Renderer.prototype.currentThemeName = '';
 
     /**
-     * Load a template from the cache or local storage or ajax request.
+     * Load a template.
      *
      * @method getTemplate
      * @private
@@ -95,44 +280,44 @@ define([
      * @return {Promise} JQuery promise object resolved when the template has been fetched.
      */
     Renderer.prototype.getTemplate = function(templateName) {
-        var parts = templateName.split('/');
-        var component = parts.shift();
-        var name = parts.shift();
-
-        var searchKey = this.currentThemeName + '/' + templateName;
+        var currentTheme = this.currentThemeName;
+        var searchKey = currentTheme + '/' + templateName;
 
-        // First try request variables.
-        if (searchKey in templatePromises) {
-            return templatePromises[searchKey];
+        // If we haven't already seen this template then buffer it.
+        var cachedPromise = getTemplatePromiseFromCache(searchKey);
+        if (cachedPromise) {
+            return cachedPromise;
         }
 
-        // Now try local storage.
-        var cached = storage.get('core_template/' + searchKey);
-
-        if (cached) {
-            templateCache[searchKey] = cached;
-            templatePromises[searchKey] = $.Deferred().resolve(cached).promise();
-            return templatePromises[searchKey];
+        // Check the buffer to seee if this template has already been added.
+        var existingBufferRecords = loadTemplateBuffer.filter(function(record) {
+            return record.searchKey == searchKey;
+        });
+        if (existingBufferRecords.length) {
+            // This template is already in the buffer so just return the existing
+            // promise. No need to add it to the buffer again.
+            return existingBufferRecords[0].deferred.promise();
         }
 
-        // Oh well - load via ajax.
-        var promises = ajax.call([{
-            methodname: 'core_output_load_template',
-            args: {
-                component: component,
-                template: name,
-                themename: this.currentThemeName
-            }
-        }], true, false);
+        // This is the first time this has been requested so let's add it to the buffer
+        // to be loaded.
+        var parts = templateName.split('/');
+        var component = parts.shift();
+        var name = parts.shift();
+        var deferred = $.Deferred();
+
+        // Add this template to the buffer to be loaded.
+        loadTemplateBuffer.push({
+            component: component,
+            name: name,
+            theme: currentTheme,
+            searchKey: searchKey,
+            deferred: deferred
+        });
 
-        templatePromises[searchKey] = promises[0].then(
-            function(templateSource) {
-                templateCache[searchKey] = templateSource;
-                storage.set('core_template/' + searchKey, templateSource);
-                return templateSource;
-            }
-        );
-        return templatePromises[searchKey];
+        // We know there is at least one thing in the buffer so kick off a processing run.
+        processLoadTemplateBuffer();
+        return deferred.promise();
     };
 
     /**
index 2ff3f9f..493f0fd 100644 (file)
@@ -32,25 +32,6 @@ defined('MOODLE_INTERNAL') || die();
  */
 function xmldb_antivirus_clamav_upgrade($oldversion) {
 
-    if ($oldversion < 2016101700) {
-        // Remove setting that has been deprecated long time ago at MDL-44260.
-        unset_config('quarantinedir', 'antivirus_clamav');
-        upgrade_plugin_savepoint(true, 2016101700, 'antivirus', 'clamav');
-    }
-
-    if ($oldversion < 2016102600) {
-        // Make command line a default running method for now. We depend on this
-        // config variable in antivirus scan running, it should be defined.
-        if (!get_config('antivirus_clamav', 'runningmethod')) {
-            set_config('runningmethod', 'commandline', 'antivirus_clamav');
-        }
-
-        upgrade_plugin_savepoint(true, 2016102600, 'antivirus', 'clamav');
-    }
-
-    // Automatically generated Moodle v3.2.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
index af03103..ff53aab 100644 (file)
@@ -219,7 +219,7 @@ function behat_clean_init_config() {
         'wwwroot', 'dataroot', 'dirroot', 'admin', 'directorypermissions', 'filepermissions',
         'umaskpermissions', 'dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'prefix',
         'dboptions', 'proxyhost', 'proxyport', 'proxytype', 'proxyuser', 'proxypassword',
-        'proxybypass', 'theme', 'pathtogs', 'pathtophp', 'pathtodu', 'aspellpath', 'pathtodot', 'skiplangupgrade',
+        'proxybypass', 'pathtogs', 'pathtophp', 'pathtodu', 'aspellpath', 'pathtodot', 'skiplangupgrade',
         'altcacheconfigpath', 'pathtounoconv', 'alternative_file_system_class', 'pathtopython'
     ));
 
index 509ec44..cdca30c 100644 (file)
@@ -58,15 +58,6 @@ class external extends external_api {
             );
     }
 
-    /**
-     * Remove comments from mustache template.
-     * @param string $templatestr
-     * @return mixed
-     */
-    protected static function strip_template_comments($templatestr) {
-        return preg_replace('/(?={{!)(.*)(}})/sU', '', $templatestr);
-    }
-
     /**
      * Return a mustache template, and all the strings it requires.
      *
@@ -84,23 +75,14 @@ class external extends external_api {
                                                   'themename' => $themename,
                                                   'includecomments' => $includecomments));
 
-        $component = $params['component'];
-        $template = $params['template'];
-        $themename = $params['themename'];
-        $includecomments = $params['includecomments'];
-
-        $templatename = $component . '/' . $template;
-
+        $loader = new mustache_template_source_loader();
         // Will throw exceptions if the template does not exist.
-        $filename = mustache_template_finder::get_template_filepath($templatename, $themename);
-        $templatestr = file_get_contents($filename);
-
-        // Remove comments from template.
-        if (!$includecomments) {
-            $templatestr = self::strip_template_comments($templatestr);
-        }
-
-        return $templatestr;
+        return $loader->load(
+            $params['component'],
+            $params['template'],
+            $params['themename'],
+            $params['includecomments']
+        );
     }
 
     /**
@@ -112,6 +94,95 @@ class external extends external_api {
         return new external_value(PARAM_RAW, 'template');
     }
 
+    /**
+     * Returns description of load_template_with_dependencies() parameters.
+     *
+     * @return external_function_parameters
+     */
+    public static function load_template_with_dependencies_parameters() {
+        return new external_function_parameters([
+            'component' => new external_value(PARAM_COMPONENT, 'component containing the template'),
+            'template' => new external_value(PARAM_ALPHANUMEXT, 'name of the template'),
+            'themename' => new external_value(PARAM_ALPHANUMEXT, 'The current theme.'),
+            'includecomments' => new external_value(PARAM_BOOL, 'Include comments or not', VALUE_DEFAULT, false)
+        ]);
+    }
+
+    /**
+     * Return a mustache template, and all the child templates and strings it requires.
+     *
+     * @param string $component The component that holds the template.
+     * @param string $template The name of the template.
+     * @param string $themename The name of the current theme.
+     * @param bool $includecomments Whether to strip comments from the template source.
+     * @return string the template
+     */
+    public static function load_template_with_dependencies(
+        string $component,
+        string $template,
+        string $themename,
+        bool $includecomments = false
+    ) {
+        global $DB, $CFG, $PAGE;
+
+        $params = self::validate_parameters(
+            self::load_template_with_dependencies_parameters(),
+            [
+                'component' => $component,
+                'template' => $template,
+                'themename' => $themename,
+                'includecomments' => $includecomments
+            ]
+        );
+
+        $loader = new mustache_template_source_loader();
+        // Will throw exceptions if the template does not exist.
+        $dependencies = $loader->load_with_dependencies(
+            $params['component'],
+            $params['template'],
+            $params['themename'],
+            $params['includecomments']
+        );
+        $formatdependencies = function($dependency) {
+            $results = [];
+            foreach ($dependency as $dependencycomponent => $dependencyvalues) {
+                foreach ($dependencyvalues as $dependencyname => $dependencyvalue) {
+                    array_push($results, [
+                        'component' => $dependencycomponent,
+                        'name' => $dependencyname,
+                        'value' => $dependencyvalue
+                    ]);
+                }
+            }
+            return $results;
+        };
+
+        // Now we have to unpack the dependencies into a format that can be returned
+        // by external functions (because they don't support dynamic keys).
+        return [
+            'templates' => $formatdependencies($dependencies['templates']),
+            'strings' => $formatdependencies($dependencies['strings'])
+        ];
+    }
+
+    /**
+     * Returns description of load_template_with_dependencies() result value.
+     *
+     * @return external_description
+     */
+    public static function load_template_with_dependencies_returns() {
+        $resourcestructure = new external_single_structure([
+            'component' => new external_value(PARAM_COMPONENT, 'component containing the resource'),
+            'name' => new external_value(PARAM_TEXT, 'name of the resource'),
+            'value' => new external_value(PARAM_RAW, 'resource value')
+        ]);
+
+        return new external_single_structure([
+            'templates' => new external_multiple_structure($resourcestructure),
+            'strings' => new external_multiple_structure($resourcestructure)
+        ]);
+    }
+
     /**
      * Returns description of load_icon_map() parameters.
      *
index 1530895..e90efe8 100644 (file)
@@ -99,6 +99,7 @@ class icon_system_fontawesome extends icon_system_font {
             'core:e/document_properties' => 'fa-info',
             'core:e/emoticons' => 'fa-smile-o',
             'core:e/find_replace' => 'fa-search-plus',
+            'core:e/file-text' => 'fa-file-text',
             'core:e/forward' => 'fa-arrow-right',
             'core:e/fullpage' => 'fa-arrows-alt',
             'core:e/fullscreen' => 'fa-arrows-alt',
diff --git a/lib/classes/output/mustache_template_source_loader.php b/lib/classes/output/mustache_template_source_loader.php
new file mode 100644 (file)
index 0000000..6be1275
--- /dev/null
@@ -0,0 +1,342 @@
+<?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/>.
+
+/**
+ * Load template source strings.
+ *
+ * @package    core
+ * @category   output
+ * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\output;
+
+defined('MOODLE_INTERNAL') || die();
+
+use \Mustache_Tokenizer;
+
+/**
+ * Load template source strings.
+ *
+ * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mustache_template_source_loader {
+
+    /** @var $gettemplatesource Callback function to load the template source from full name */
+    private $gettemplatesource = null;
+
+    /**
+     * Constructor that takes a callback to allow the calling code to specify how to retrieve
+     * the source for a template name.
+     *
+     * If no callback is provided then default to the load from disk implementation.
+     *
+     * @param callable|null $gettemplatesource Callback to load template source by template name
+     */
+    public function __construct(callable $gettemplatesource = null) {
+        if ($gettemplatesource) {
+            // The calling code has specified a function for retrieving the template source
+            // code by name and theme.
+            $this->gettemplatesource = $gettemplatesource;
+        } else {
+            // By default we will pull the template from disk.
+            $this->gettemplatesource = function($component, $name, $themename) {
+                $fulltemplatename = $component . '/' . $name;
+                $filename = mustache_template_finder::get_template_filepath($fulltemplatename, $themename);
+                return file_get_contents($filename);
+            };
+        }
+    }
+
+    /**
+     * Remove comments from mustache template.
+     *
+     * @param string $templatestr
+     * @return string
+     */
+    protected function strip_template_comments($templatestr) : string {
+        return preg_replace('/(?={{!)(.*)(}})/sU', '', $templatestr);
+    }
+
+    /**
+     * Load the template source from the component and template name.
+     *
+     * @param string $component The moodle component (e.g. core_message)
+     * @param string $name The template name (e.g. message_drawer)
+     * @param string $themename The theme to load the template for (e.g. boost)
+     * @param bool $includecomments If the comments should be stripped from the source before returning
+     * @return string The template source
+     */
+    public function load(
+        string $component,
+        string $name,
+        string $themename,
+        bool $includecomments = false
+    ) : string {
+        // Get the template source from the callback.
+        $source = ($this->gettemplatesource)($component, $name, $themename);
+
+        // Remove comments from template.
+        if (!$includecomments) {
+            $source = $this->strip_template_comments($source);
+        }
+
+        return $source;
+    }
+
+    /**
+     * Load a template and some of the dependencies that will be needed in order to render
+     * the template.
+     *
+     * The current implementation will return all of the templates and all of the strings in
+     * each of those templates (excluding string substitutions).
+     *
+     * The return format is an array indexed with the dependency type (e.g. templates / strings) then
+     * the component (e.g. core_message), and then the id (e.g. message_drawer).
+     *
+     * For example:
+     * * We have 3 templates in core named foo, bar, and baz.
+     * * foo includes bar and bar includes baz.
+     * * foo uses the string 'home' from core
+     * * baz uses the string 'help' from core
+     *
+     * If we load the template foo this function would return:
+     * [
+     *      'templates' => [
+     *          'core' => [
+     *              'foo' => '... template source ...',
+     *              'bar' => '... template source ...',
+     *              'baz' => '... template source ...',
+     *          ]
+     *      ],
+     *      'strings' => [
+     *          'core' => [
+     *              'home' => 'Home',
+     *              'help' => 'Help'
+     *          ]
+     *      ]
+     * ]
+     *
+     * @param string $templatecomponent The moodle component (e.g. core_message)
+     * @param string $templatename The template name (e.g. message_drawer)
+     * @param string $themename The theme to load the template for (e.g. boost)
+     * @param bool $includecomments If the comments should be stripped from the source before returning
+     * @param array $seentemplates List of templates already processed / to be skipped.
+     * @param array $seenstrings List of strings already processed / to be skipped.
+     * @return array
+     */
+    public function load_with_dependencies(
+        string $templatecomponent,
+        string $templatename,
+        string $themename,
+        bool $includecomments = false,
+        array $seentemplates = [],
+        array $seenstrings = []
+    ) : array {
+        // Initialise the return values.
+        $templates = [];
+        $strings = [];
+        $templatecomponent = trim($templatecomponent);
+        $templatename = trim($templatename);
+        // Get the requested template source.
+        $templatesource = $this->load($templatecomponent, $templatename, $themename, $includecomments);
+        // This is a helper function to save a value in one of the result arrays (either $templates or $strings).
+        $save = function(array $results, array $seenlist, string $component, string $id, $value) {
+            if (!isset($results[$component])) {
+                // If the results list doesn't already contain this component then initialise it.
+                $results[$component] = [];
+            }
+
+            // Save the value.
+            $results[$component][$id] = $value;
+            // Record that this item has been processed.
+            array_push($seenlist, "$component/$id");
+            // Return the updated results and seen list.
+            return [$results, $seenlist];
+        };
+        // This is a helper function for processing a dependency. Does stuff like ignore duplicate processing,
+        // common result formatting etc.
+        $handler = function(array $dependency, array $ignorelist, callable $processcallback) {
+            foreach ($dependency as $component => $ids) {
+                foreach ($ids as $id) {
+                    $dependencyid = "$component/$id";
+                    if (array_search($dependencyid, $ignorelist) === false) {
+                        $processcallback($component, $id);
+                        // Add this to our ignore list now that we've processed it so that we don't
+                        // process it again.
+                        array_push($ignorelist, $dependencyid);
+                    }
+                }
+            }
+
+            return $ignorelist;
+        };
+
+        // Save this template as the first result in the $templates result array.
+        list($templates, $seentemplates) = $save($templates, $seentemplates, $templatecomponent, $templatename, $templatesource);
+
+        // Check the template for any dependencies that need to be loaded.
+        $dependencies = $this->scan_template_source_for_dependencies($templatesource);
+
+        // Load all of the lang strings that this template requires and add them to the
+        // returned values.
+        $seenstrings = $handler(
+            $dependencies['strings'],
+            $seenstrings,
+            // Include $strings and $seenstrings by reference so that their values can be updated
+            // outside of this anonymous function.
+            function($component, $id) use ($save, &$strings, &$seenstrings) {
+                $string = get_string($id, $component);
+                // Save the string in the $strings results array.
+                list($strings, $seenstrings) = $save($strings, $seenstrings, $component, $id, $string);
+            }
+        );
+
+        // Load any child templates that we've found in this template and add them to
+        // the return list of dependencies.
+        $seentemplates = $handler(
+            $dependencies['templates'],
+            $seentemplates,
+            // Include $strings, $seenstrings, $templates, and $seentemplates by reference so that their values can be updated
+            // outside of this anonymous function.
+            function($component, $id) use (
+                $themename,
+                $includecomments,
+                &$seentemplates,
+                &$seenstrings,
+                &$templates,
+                &$strings,
+                $save
+            ) {
+                // We haven't seen this template yet so load it and it's dependencies.
+                $subdependencies = $this->load_with_dependencies(
+                    $component,
+                    $id,
+                    $themename,
+                    $includecomments,
+                    $seentemplates,
+                    $seenstrings
+                );
+
+                foreach ($subdependencies['templates'] as $component => $ids) {
+                    foreach ($ids as $id => $value) {
+                        // Include the child themes in our results.
+                        list($templates, $seentemplates) = $save($templates, $seentemplates, $component, $id, $value);
+                    }
+                };
+
+                foreach ($subdependencies['strings'] as $component => $ids) {
+                    foreach ($ids as $id => $value) {
+                        // Include any strings that the child templates need in our results.
+                        list($strings, $seenstrings) = $save($strings, $seenstrings, $component, $id, $value);
+                    }
+                }
+            }
+        );
+
+        return [
+            'templates' => $templates,
+            'strings' => $strings
+        ];
+    }
+
+    /**
+     * Scan over a template source string and return a list of dependencies it requires.
+     * At the moment the list will only include other templates and strings.
+     *
+     * The return format is an array indexed with the dependency type (e.g. templates / strings) then
+     * the component (e.g. core_message) with it's value being an array of the items required
+     * in that component.
+     *
+     * For example:
+     * If we have a template foo that includes 2 templates, bar and baz, and also 2 strings
+     * 'home' and 'help' from the core component then the return value would look like:
+     *
+     * [
+     *      'templates' => [
+     *          'core' => ['foo', 'bar', 'baz']
+     *      ],
+     *      'strings' => [
+     *          'core' => ['home', 'help']
+     *      ]
+     * ]
+     *
+     * @param string $source The template source
+     * @return array
+     */
+    protected function scan_template_source_for_dependencies(string $source) : array {
+        $tokenizer = new Mustache_Tokenizer();
+        $tokens = $tokenizer->scan($source);
+        $templates = [];
+        $strings = [];
+        $addtodependencies = function($dependencies, $component, $id) {
+            $id = trim($id);
+            $component = trim($component);
+
+            if (!isset($dependencies[$component])) {
+                // Initialise the component if we haven't seen it before.
+                $dependencies[$component] = [];
+            }
+
+            // Add this id to the list of dependencies.
+            array_push($dependencies[$component], $id);
+
+            return $dependencies;
+        };
+
+        foreach ($tokens as $index => $token) {
+            $type = $token['type'];
+            $name = isset($token['name']) ? $token['name'] : null;
+
+            if ($name) {
+                switch ($type) {
+                    case Mustache_Tokenizer::T_PARTIAL:
+                        list($component, $id) = explode('/', $name);
+                        $templates = $addtodependencies($templates, $component, $id);
+                        break;
+                    case Mustache_Tokenizer::T_PARENT:
+                        list($component, $id) = explode('/', $name);
+                        $templates = $addtodependencies($templates, $component, $id);
+                        break;
+                    case Mustache_Tokenizer::T_SECTION:
+                        if ($name == 'str') {
+                            // The token that containts the string identifiers (key and component) should
+                            // immediately follow the #str token.
+                            $identifiertoken = isset($tokens[$index + 1]) ? $tokens[$index + 1] : null;
+
+                            if ($identifiertoken) {
+                                // The string identifier is the key and component comma separated.
+                                $identifierstring = $identifiertoken['value'];
+                                $parts = explode(',', $identifierstring);
+                                $id = $parts[0];
+                                // Default to 'core' for the component, if not specified.
+                                $component = isset($parts[1]) ? $parts[1] : 'core';
+                                $strings = $addtodependencies($strings, $component, $id);
+                            }
+                        }
+                        break;
+                }
+            }
+        }
+
+        return [
+            'templates' => $templates,
+            'strings' => $strings
+        ];
+    }
+}
index b9d1d67..bc9b8f8 100644 (file)
@@ -88,6 +88,13 @@ class provider implements
                 'userid'        => 'privacy:metadata:task_adhoc:userid',
             ], 'privacy:metadata:task_adhoc');
 
+        // The task_log table stores debugging data for tasks.
+        // These are cleaned regularly and intended purely for debugging.
+        $collection->add_database_table('task_log', [
+                'component'     => 'privacy:metadata:task_log:component',
+                'userid'        => 'privacy:metadata:task_log:userid',
+            ], 'privacy:metadata:task_log');
+
         // The events_queue includes information about pending events tasks.
         // These are stored for short periods whilst being processed into other locations.
         $collection->add_database_table('events_queue', [
index b1c71ad..9475c92 100644 (file)
@@ -48,6 +48,51 @@ class core_shutdown_manager {
         }
         self::$registered = true;
         register_shutdown_function(array('core_shutdown_manager', 'shutdown_handler'));
+
+        // Signal handlers should only be used when dealing with a CLI script.
+        // In the case of PHP called in a web server the server is the owning process and should handle the signal chain
+        // properly itself.
+        // The 'pcntl' extension is optional and not available on Windows.
+        if (CLI_SCRIPT && extension_loaded('pcntl') && function_exists('pcntl_async_signals')) {
+            // We capture and handle SIGINT (Ctrl+C) and SIGTERM (termination requested).
+            pcntl_async_signals(true);
+            pcntl_signal(SIGINT, ['core_shutdown_manager', 'signal_handler']);
+            pcntl_signal(SIGTERM, ['core_shutdown_manager', 'signal_handler']);
+        }
+    }
+
+    /**
+     * Signal handler for SIGINT, and SIGTERM.
+     *
+     * @param   int     $signo The signal being handled
+     */
+    public static function signal_handler($signo) {
+        // Note: There is no need to manually call the shutdown handler.
+        // The fact that we are calling exit() in this script means that the standard shutdown handling is performed
+        // anyway.
+        switch ($signo) {
+            case SIGTERM:
+                // Replicate native behaviour.
+                echo "Terminated: {$signo}\n";
+
+                // The standard exit code for SIGTERM is 143.
+                $exitcode = 143;
+                break;
+            case SIGINT:
+                // Replicate native behaviour.
+                echo "\n";
+
+                // The standard exit code for SIGINT (Ctrl+C) is 130.
+                $exitcode = 130;
+                break;
+            default:
+                // The signal handler was called with a signal it was not expecting.
+                // We should exit and complain.
+                echo "Warning: \core_shutdown_manager::signal_handler() was called with an unexpected signal ({$signo}).\n";
+                $exitcode = 1;
+        }
+
+        exit ($exitcode);
     }
 
     /**
diff --git a/lib/classes/task/database_logger.php b/lib/classes/task/database_logger.php
new file mode 100644 (file)
index 0000000..4aa4885
--- /dev/null
@@ -0,0 +1,168 @@
+<?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/>.
+
+/**
+ * Database logger for task logging.
+ *
+ * @package    core
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core\task;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Database logger for task logging.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class database_logger implements task_logger {
+
+    /** @var int Type constant for a scheduled task */
+    const TYPE_SCHEDULED = 0;
+
+    /** @var int Type constant for an adhoc task */
+    const TYPE_ADHOC = 1;
+
+    /**
+     * Whether the task is configured and ready to log.
+     *
+     * @return  bool
+     */
+    public static function is_configured() : bool {
+        return true;
+    }
+
+    /**
+     * Store the log for the specified task.
+     *
+     * @param   task_base   $task The task that the log belongs to.
+     * @param   string      $logpath The path to the log on disk
+     * @param   bool        $failed Whether the task failed
+     * @param   int         $dbreads The number of DB reads
+     * @param   int         $dbwrites The number of DB writes
+     * @param   float       $timestart The start time of the task
+     * @param   float       $timeend The end time of the task
+     */
+    public static function store_log_for_task(task_base $task, string $logpath, bool $failed,
+            int $dbreads, int $dbwrites, float $timestart, float $timeend) {
+        global $DB;
+
+        // Write this log to the database.
+        $logdata = (object) [
+            'type' => is_a($task, scheduled_task::class) ? self::TYPE_SCHEDULED : self::TYPE_ADHOC,
+            'component' => $task->get_component(),
+            'classname' => get_class($task),
+            'userid' => 0,
+            'timestart' => $timestart,
+            'timeend' => $timeend,
+            'dbreads' => $dbreads,
+            'dbwrites' => $dbwrites,
+            'result' => (int) $failed,
+            'output' => file_get_contents($logpath),
+        ];
+
+        if (is_a($task, adhoc_task::class) && $userid = $task->get_userid()) {
+            $logdata->userid = $userid;
+        }
+
+        $logdata->id = $DB->insert_record('task_log', $logdata);
+    }
+
+    /**
+     * Whether this task logger has a report available.
+     *
+     * @return  bool
+     */
+    public static function has_log_report() : bool {
+        return true;
+    }
+
+    /**
+     * Get any URL available for viewing relevant task log reports.
+     *
+     * @param   string      $classname The task class to fetch for
+     * @return  \moodle_url
+     */
+    public static function get_url_for_task_class(string $classname) : \moodle_url {
+        global $CFG;
+
+        return new \moodle_url("/{$CFG->admin}/tasklogs.php", [
+                'filter' => $classname,
+            ]);
+    }
+
+    /**
+     * Cleanup old task logs.
+     */
+    public static function cleanup() {
+        global $CFG, $DB;
+
+        // Delete logs older than the retention period.
+        $params = [
+            'retentionperiod' => time() - $CFG->task_logretention,
+        ];
+        $logids = $DB->get_fieldset_select('task_log', 'id', 'timestart < :retentionperiod', $params);
+        self::delete_task_logs($logids);
+
+        // Delete logs to retain a minimum number of logs.
+        $sql = "SELECT classname FROM {task_log} GROUP BY classname HAVING COUNT(classname) > :retaincount";
+        $params = [
+            'retaincount' => $CFG->task_logretainruns,
+        ];
+        $classes = $DB->get_fieldset_sql($sql, $params);
+
+        foreach ($classes as $classname) {
+            $notinsql = "";
+            $params = [
+                'classname' => $classname,
+            ];
+
+            $retaincount = (int) $CFG->task_logretainruns;
+            if ($retaincount) {
+                $keeplogs = $DB->get_records('task_log', [
+                        'classname' => $classname,
+                    ], 'timestart DESC', 'id', 0, $retaincount);
+
+                if ($keeplogs) {
+                    list($notinsql, $params) = $DB->get_in_or_equal(array_keys($keeplogs), SQL_PARAMS_NAMED, 'p', false);
+                    $params['classname'] = $classname;
+                    $notinsql = " AND id {$notinsql}";
+                }
+            }
+
+            $logids = $DB->get_fieldset_select('task_log', 'id', "classname = :classname {$notinsql}", $params);
+            self::delete_task_logs($logids);
+        }
+    }
+
+    /**
+     * Delete task logs for the specified logs.
+     *
+     * @param   array   $logids
+     */
+    public static function delete_task_logs(array $logids) {
+        global $DB;
+
+        if (empty($logids)) {
+            return;
+        }
+
+        $DB->delete_records_list('task_log', 'id', $logids);
+    }
+}
diff --git a/lib/classes/task/logmanager.php b/lib/classes/task/logmanager.php
new file mode 100644 (file)
index 0000000..8b812e1
--- /dev/null
@@ -0,0 +1,341 @@
+<?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/>.
+
+/**
+ * Task log manager.
+ *
+ * @package    core
+ * @category   task
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core\task;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Task log manager.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class logmanager {
+
+    /** @var int Do not log anything */
+    const MODE_NONE = 0;
+
+    /** @var int Log all tasks */
+    const MODE_ALL = 1;
+
+    /** @var int Only log fails */
+    const MODE_FAILONLY = 2;
+
+    /** @var int The default chunksize to use in ob_start */
+    const CHUNKSIZE = 1;
+
+    /**
+     * @var \core\task\task_base The task being logged.
+     */
+    protected static $task = null;
+
+    /**
+     * @var \stdClass Metadata about the current log
+     */
+    protected static $taskloginfo = null;
+
+    /**
+     * @var \resource The current filehandle used for logging
+     */
+    protected static $fh = null;
+
+    /**
+     * @var string The path to the log file
+     */
+    protected static $logpath = null;
+
+    /**
+     * @var bool Whether the task logger has been registered with the shutdown handler
+     */
+    protected static $tasklogregistered = false;
+
+    /**
+     * @var int The level of output buffering in place before starting.
+     */
+    protected static $oblevel = null;
+
+    /**
+     * Create a new task logger for the specified task, and prepare for logging.
+     *
+     * @param   \core\task\task_base    $task The task being run
+     */
+    public static function start_logging(task_base $task) {
+        global $DB;
+
+        if (!self::should_log()) {
+            return;
+        }
+
+        // We register a shutdown handler to ensure that logs causing any failures are correctly disposed of.
+        // Note: This must happen before the per-request directory is requested because the shutdown handler deletes the logfile.
+        if (!self::$tasklogregistered) {
+            \core_shutdown_manager::register_function(function() {
+                // These will only actually do anything if capturing is current active when the thread ended, which
+                // constitutes a failure.
+                \core\task\logmanager::finalise_log(true);
+            });
+
+            // Create a brand new per-request directory basedir.
+            get_request_storage_directory(true, true);
+
+            self::$tasklogregistered = true;
+        }
+
+        if (self::is_current_output_buffer()) {
+            // We cannot capture when we are already capturing.
+            throw new \coding_exception('Logging is already in progress for task "' . get_class(self::$task) . '". ' .
+                'Nested logging is not supported.');
+        }
+
+        // Store the initial data about the task and current state.
+        self::$task = $task;
+        self::$taskloginfo = (object) [
+            'dbread'    => $DB->perf_get_reads(),
+            'dbwrite'   => $DB->perf_get_writes(),
+            'timestart' => microtime(true),
+        ];
+
+        // For simplicity's sake we always store logs on disk and flush at the end.
+        self::$logpath = make_request_directory() . DIRECTORY_SEPARATOR . "task.log";
+        self::$fh = fopen(self::$logpath, 'w+');
+
+        // Note the level of the current output buffer.
+        // Note: You cannot use ob_get_level() as it will return `1` when the default output buffer is enabled.
+        if ($obstatus = ob_get_status()) {
+            self::$oblevel = $obstatus['level'];
+        } else {
+            self::$oblevel = null;
+        }
+
+        // Start capturing output.
+        ob_start([\core\task\logmanager::class, 'add_line'], self::CHUNKSIZE);
+    }
+
+    /**
+     * Whether logging is possible and should be happening.
+     *
+     * @return  bool
+     */
+    protected static function should_log() : bool {
+        global $CFG;
+
+        // Respect the config setting.
+        if (isset($CFG->task_logmode) && empty($CFG->task_logmode)) {
+            return false;
+        }
+
+        $loggerclass = self::get_logger_classname();
+        if (empty($loggerclass)) {
+            return false;
+        }
+
+        return $loggerclass::is_configured();
+    }
+
+    /**
+     * Return the name of the logging class to use.
+     *
+     * @return  string
+     */
+    public static function get_logger_classname() : string {
+        global $CFG;
+
+        if (!empty($CFG->task_log_class)) {
+            // Configuration is present to use an alternative task logging class.
+            return $CFG->task_log_class;
+        }
+
+        // Fall back on the default database logger.
+        return database_logger::class;
+    }
+
+    /**
+     * Whether this task logger has a report available.
+     *
+     * @return  bool
+     */
+    public static function has_log_report() : bool {
+        $loggerclass = self::get_logger_classname();
+
+        return $loggerclass::has_log_report();
+    }
+
+    /**
+     * Whether to use the standard settings form.
+     */
+    public static function uses_standard_settings() : bool {
+        $classname = self::get_logger_classname();
+        if (!class_exists($classname)) {
+            return false;
+        }
+
+        if (is_a($classname, database_logger::class, true)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Get any URL available for viewing relevant task log reports.
+     *
+     * @param   string      $classname The task class to fetch for
+     * @return  \moodle_url
+     */
+    public static function get_url_for_task_class(string $classname) : \moodle_url {
+        $loggerclass = self::get_logger_classname();
+
+        return $loggerclass::get_url_for_task_class($classname);
+    }
+
+    /**
+     * Whether we are the current log collector.
+     *
+     * @return  bool
+     */
+    protected static function is_current_output_buffer() : bool {
+        if (empty(self::$taskloginfo)) {
+            return false;
+        }
+
+        if ($ob = ob_get_status()) {
+            return 'core\\task\\logmanager::add_line' == $ob['name'];
+        }
+
+        return false;
+    }
+
+    /**
+     * Whether we are capturing at all.
+     *
+     * @return  bool
+     */
+    protected static function is_capturing() : bool {
+        $buffers = ob_get_status(true);
+        foreach ($buffers as $ob) {
+            if ('core\\task\\logmanager::add_line' == $ob['name']) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Finish writing for the current task.
+     *
+     * @param   bool    $failed
+     */
+    public static function finalise_log(bool $failed = false) {
+        global $CFG, $DB, $PERF;
+
+        if (!self::should_log()) {
+            return;
+        }
+
+        if (!self::is_capturing()) {
+            // Not capturing anything.
+            return;
+        }
+
+        // Ensure that all logs are closed.
+        $buffers = ob_get_status(true);
+        foreach (array_reverse($buffers) as $ob) {
+            if (null !== self::$oblevel) {
+                if ($ob['level'] <= self::$oblevel) {
+                    // Only close as far as the initial output buffer level.
+                    break;
+                }
+            }
+
+            // End and flush this buffer.
+            ob_end_flush();
+
+            if ('core\\task\\logmanager::add_line' == $ob['name']) {
+                break;
+            }
+        }
+        self::$oblevel = null;
+
+        // Flush any remaining buffer.
+        self::flush();
+
+        // Close and unset the FH.
+        fclose(self::$fh);
+        self::$fh = null;
+
+        if ($failed || empty($CFG->task_logmode) || self::MODE_ALL == $CFG->task_logmode) {
+            // Finalise the log.
+            $loggerclass = self::get_logger_classname();
+            $loggerclass::store_log_for_task(
+                self::$task,
+                self::$logpath,
+                $failed,
+                $DB->perf_get_reads() - self::$taskloginfo->dbread,
+                $DB->perf_get_writes() - self::$taskloginfo->dbwrite - $PERF->logwrites,
+                self::$taskloginfo->timestart,
+                microtime(true)
+            );
+        }
+
+        // Tidy up.
+        self::$logpath = null;
+        self::$taskloginfo = null;
+    }
+
+    /**
+     * Flush the current output buffer.
+     *
+     * This function will ensure that we are the current output buffer handler.
+     */
+    public static function flush() {
+        // We only call ob_flush if the current output buffer belongs to us.
+        if (self::is_current_output_buffer()) {
+            ob_flush();
+        }
+    }
+
+    /**
+     * Add a log record to the task log.
+     *
+     * @param   string  $log
+     * @return  string
+     */
+    public static function add_line(string $log) : string {
+        if (empty(self::$taskloginfo)) {
+            return $log;
+        }
+
+        if (empty(self::$fh)) {
+            return $log;
+        }
+
+        if (self::is_current_output_buffer()) {
+            fwrite(self::$fh, $log);
+        }
+
+        return $log;
+    }
+}