Merge branch 'MDL-62960-master' of git://github.com/rezaies/moodle
authorDamyon Wiese <damyon@moodle.com>
Tue, 16 Oct 2018 03:43:24 +0000 (11:43 +0800)
committerDamyon Wiese <damyon@moodle.com>
Tue, 16 Oct 2018 03:43:24 +0000 (11:43 +0800)
98 files changed:
admin/environment.xml
blocks/myoverview/amd/build/main.min.js [new file with mode: 0644]
blocks/myoverview/amd/build/paging_bar.min.js [deleted file]
blocks/myoverview/amd/build/paging_content.min.js [deleted file]
blocks/myoverview/amd/build/repository.min.js [new file with mode: 0644]
blocks/myoverview/amd/build/view.min.js [new file with mode: 0644]
blocks/myoverview/amd/build/view_nav.min.js [new file with mode: 0644]
blocks/myoverview/amd/src/main.js [new file with mode: 0644]
blocks/myoverview/amd/src/paging_bar.js [deleted file]
blocks/myoverview/amd/src/paging_content.js [deleted file]
blocks/myoverview/amd/src/repository.js [new file with mode: 0644]
blocks/myoverview/amd/src/view.js [new file with mode: 0644]
blocks/myoverview/amd/src/view_nav.js [new file with mode: 0644]
blocks/myoverview/classes/output/courses_view.php [deleted file]
blocks/myoverview/classes/output/main.php
blocks/myoverview/db/upgrade.php [deleted file]
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/templates/course-paging-content-item.mustache [deleted file]
blocks/myoverview/templates/course-paging-content.mustache [deleted file]
blocks/myoverview/templates/courses-view-by-status.mustache [deleted file]
blocks/myoverview/templates/courses-view-course-item.mustache [deleted file]
blocks/myoverview/templates/courses-view-nav-grouping-display-filter.mustache [deleted file]
blocks/myoverview/templates/courses-view.mustache
blocks/myoverview/templates/main.mustache
blocks/myoverview/templates/nav-display-selector.mustache [new file with mode: 0644]
blocks/myoverview/templates/nav-grouping-selector.mustache [new file with mode: 0644]
blocks/myoverview/templates/nav-sort-selector.mustache [new file with mode: 0644]
blocks/myoverview/templates/no-courses.mustache [moved from blocks/myoverview/templates/paging-bar-item.mustache with 57% similarity]
blocks/myoverview/templates/paging-bar.mustache [deleted file]
blocks/myoverview/templates/paging-content.mustache [deleted file]
blocks/myoverview/templates/placeholder-course.mustache [moved from blocks/myoverview/templates/paging-content-item.mustache with 60% similarity]
blocks/myoverview/templates/progress-bar.mustache [moved from theme/bootstrapbase/templates/block_myoverview/paging-content-item.mustache with 60% similarity]
blocks/myoverview/templates/progress-chart.mustache [deleted file]
blocks/myoverview/templates/view-cards.mustache [new file with mode: 0644]
blocks/myoverview/templates/view-list.mustache [new file with mode: 0644]
blocks/myoverview/templates/view-summary.mustache [new file with mode: 0644]
blocks/myoverview/tests/behat/block_myoverview_dashboard.feature
blocks/myoverview/tests/behat/block_myoverview_progress.feature
blocks/myoverview/version.php
blocks/timeline/amd/build/view_nav.min.js
blocks/timeline/amd/src/view_nav.js
blocks/timeline/block_timeline.php
blocks/timeline/classes/output/main.php
blocks/timeline/classes/privacy/provider.php
blocks/timeline/lang/en/block_timeline.php
blocks/timeline/lib.php [new file with mode: 0644]
blocks/timeline/templates/nav-day-filter.mustache
blocks/timeline/templates/nav-view-selector.mustache
blocks/timeline/templates/view.mustache
blocks/timeline/tests/behat/block_timeline_courses.feature
blocks/timeline/tests/behat/block_timeline_dates.feature
blocks/timeline/tests/privacy_test.php [new file with mode: 0644]
course/classes/external/course_summary_exporter.php
course/externallib.php
course/lib.php
course/tests/externallib_test.php
enrol/paypal/lib.php
enrol/tests/behat/role_visibility.feature
enrol/tests/enrollib_test.php
lang/en/admin.php
lib/editor/atto/plugins/media/lang/en/atto_media.php
lib/editor/atto/plugins/media/lib.php
lib/editor/atto/plugins/media/tests/behat/media.feature
lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-debug.js
lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-min.js
lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button.js
lib/editor/atto/plugins/media/yui/src/button/js/button.js
lib/enrollib.php
lib/environmentlib.php
mod/assign/feedback/editpdf/styles.css
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
question/format/aiken/format.php
question/format/aiken/lang/en/qformat_aiken.php
question/format/aiken/tests/aikenformat_test.php [new file with mode: 0644]
question/format/aiken/tests/fixtures/aiken_errors.txt [new file with mode: 0644]
question/type/multichoice/questiontype.php
question/type/multichoice/tests/questiontype_test.php
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/preset/default.scss
theme/boost/style/moodle.css
theme/boost/templates/columns2.mustache
theme/boost/templates/core/block.mustache
theme/bootstrapbase/less/moodle/blocks.less
theme/bootstrapbase/less/moodle/bs4-compat.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/block_myoverview/course-paging-content-item.mustache [deleted file]
theme/bootstrapbase/templates/block_myoverview/courses-view-course-item.mustache [deleted file]
theme/bootstrapbase/templates/block_myoverview/courses-view-nav-grouping-display-filter.mustache [deleted file]
theme/bootstrapbase/templates/block_myoverview/courses-view.mustache [deleted file]
theme/bootstrapbase/templates/block_myoverview/main.mustache [deleted file]
theme/bootstrapbase/templates/block_myoverview/paging-bar.mustache [deleted file]
theme/bootstrapbase/templates/block_timeline/nav-day-filter.mustache
theme/bootstrapbase/templates/block_timeline/nav-view-selector.mustache
theme/bootstrapbase/templates/block_timeline/view.mustache

index fbda3f3..40e66be 100644 (file)
       <VENDOR name="oracle" version="10.2" />
     </DATABASE>
     <PHP version="7.0.0" level="required">
+      <RESTRICT function="restrict_php_version_73" message="unsupportedphpversion73" />
     </PHP>
     <PCREUNICODE level="optional">
       <FEEDBACK>
       <VENDOR name="oracle" version="10.2" />
     </DATABASE>
     <PHP version="7.0.0" level="required">
+      <RESTRICT function="restrict_php_version_73" message="unsupportedphpversion73" />
     </PHP>
     <PCREUNICODE level="optional">
       <FEEDBACK>
diff --git a/blocks/myoverview/amd/build/main.min.js b/blocks/myoverview/amd/build/main.min.js
new file mode 100644 (file)
index 0000000..ce2592d
Binary files /dev/null and b/blocks/myoverview/amd/build/main.min.js differ
diff --git a/blocks/myoverview/amd/build/paging_bar.min.js b/blocks/myoverview/amd/build/paging_bar.min.js
deleted file mode 100644 (file)
index 40e8b6d..0000000
Binary files a/blocks/myoverview/amd/build/paging_bar.min.js and /dev/null differ
diff --git a/blocks/myoverview/amd/build/paging_content.min.js b/blocks/myoverview/amd/build/paging_content.min.js
deleted file mode 100644 (file)
index 97da2ca..0000000
Binary files a/blocks/myoverview/amd/build/paging_content.min.js and /dev/null differ
diff --git a/blocks/myoverview/amd/build/repository.min.js b/blocks/myoverview/amd/build/repository.min.js
new file mode 100644 (file)
index 0000000..ab27262
Binary files /dev/null and b/blocks/myoverview/amd/build/repository.min.js differ
diff --git a/blocks/myoverview/amd/build/view.min.js b/blocks/myoverview/amd/build/view.min.js
new file mode 100644 (file)
index 0000000..1d9b214
Binary files /dev/null and b/blocks/myoverview/amd/build/view.min.js differ
diff --git a/blocks/myoverview/amd/build/view_nav.min.js b/blocks/myoverview/amd/build/view_nav.min.js
new file mode 100644 (file)
index 0000000..978c770
Binary files /dev/null and b/blocks/myoverview/amd/build/view_nav.min.js differ
diff --git a/blocks/myoverview/amd/src/main.js b/blocks/myoverview/amd/src/main.js
new file mode 100644 (file)
index 0000000..e59e62e
--- /dev/null
@@ -0,0 +1,59 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Javascript to initialise the myoverview block.
+ *
+ * @package    block_myoverview
+ * @copyright  2018 Bas Brands <bas@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(
+[
+    'jquery',
+    'block_myoverview/view',
+    'block_myoverview/view_nav'
+],
+function(
+    $,
+    View,
+    ViewNav
+) {
+
+    var SELECTORS = {
+        COURSES_VIEW: '[data-region="courses-view"]',
+        COURSES_VIEW_CONTENT: '[data-region="course-view-content"]'
+    };
+
+    /**
+     * Initialise all of the modules for the overview block.
+     *
+     * @param {object} root The root element for the overview block.
+     */
+    var init = function(root) {
+        root = $(root);
+        var coursesViewRoot = root.find(SELECTORS.COURSES_VIEW);
+        var coursesViewContent = root.find(SELECTORS.COURSES_VIEW_CONTENT);
+        // Initialise the course navigation elements.
+        ViewNav.init(root, coursesViewRoot, coursesViewContent);
+        // Initialise the courses view modules.
+        View.init(coursesViewRoot, coursesViewContent);
+    };
+
+    return {
+        init: init
+    };
+});
diff --git a/blocks/myoverview/amd/src/paging_bar.js b/blocks/myoverview/amd/src/paging_bar.js
deleted file mode 100644 (file)
index e153e2d..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Javascript to load and render the paging bar.
- *
- * @module     block_myoverview/paging_bar
- * @package    block_myoverview
- * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-define(['jquery', 'core/custom_interaction_events'],
-        function($, CustomEvents) {
-
-    var SELECTORS = {
-        ROOT: '[data-region="paging-bar"]',
-        PAGE_ITEM: '[data-region="page-item"]',
-        ACTIVE_PAGE_ITEM: '[data-region="page-item"].active'
-    };
-
-    var EVENTS = {
-        PAGE_SELECTED: 'block_myoverview-paging-bar-page-selected',
-    };
-
-    /**
-     * Get the page element by number.
-     *
-     * @param {object} root The root element.
-     * @param {Number} pageNumber The page number.
-     * @returns {*}
-     */
-    var getPageByNumber = function(root, pageNumber) {
-        return root.find(SELECTORS.PAGE_ITEM + '[data-page-number="' + pageNumber + '"]');
-    };
-
-    /**
-     * Get the page number.
-     *
-     * @param {object} root The root element.
-     * @param {object} page The page.
-     * @returns {*} the page number
-     */
-    var getPageNumber = function(root, page) {
-        var pageNumber = page.attr('data-page-number');
-
-        if (pageNumber == 'first') {
-            pageNumber = 1;
-        } else if (pageNumber == 'last') {
-            pageNumber = root.attr('data-page-count');
-        }
-
-        return pageNumber;
-    };
-
-    /**
-     * Register event listeners for the module.
-     * @param {object} root The root element.
-     */
-    var registerEventListeners = function(root) {
-        root = $(root);
-        CustomEvents.define(root, [
-            CustomEvents.events.activate
-        ]);
-
-        root.on(CustomEvents.events.activate, SELECTORS.PAGE_ITEM, function(e, data) {
-            var page = $(e.target).closest(SELECTORS.PAGE_ITEM);
-            var activePage = root.find(SELECTORS.ACTIVE_PAGE_ITEM);
-            var pageNumber = getPageNumber(root, page);
-            var isSamePage = pageNumber == getPageNumber(root, activePage);
-
-            if (!isSamePage) {
-                root.find(SELECTORS.PAGE_ITEM).removeClass('active');
-                getPageByNumber(root, pageNumber).addClass('active');
-            }
-
-            root.trigger(EVENTS.PAGE_SELECTED, [{
-                pageNumber: pageNumber,
-                isSamePage: isSamePage,
-            }]);
-
-            data.originalEvent.preventDefault();
-        });
-    };
-
-    return {
-        registerEventListeners: registerEventListeners,
-        events: EVENTS,
-        rootSelector: SELECTORS.ROOT,
-    };
-});
diff --git a/blocks/myoverview/amd/src/paging_content.js b/blocks/myoverview/amd/src/paging_content.js
deleted file mode 100644 (file)
index 1e33dae..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-// 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/>.
-
-/**
- * Paging content module.
- *
- * @module     block_myoverview/paging_content
- * @package    block_myoverview
- * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-define(['jquery', 'core/templates', 'block_myoverview/paging_bar'],
-        function($, Templates, PagingBar) {
-
-    var SELECTORS = {
-        ROOT: '[data-region="paging-content"]',
-        PAGE_REGION: '[data-region="paging-content-item"]'
-    };
-
-    /**
-     * Constructor of the paging content module.
-     *
-     * @param {object} root
-     * @param {object} pagingBarElement
-     * @constructor
-     */
-    var PagingContent = function(root, pagingBarElement) {
-        this.root = $(root);
-        this.pagingBar = $(pagingBarElement);
-
-    };
-
-    PagingContent.rootSelector = SELECTORS.ROOT;
-
-    /**
-     * Load content and create page.
-     *
-     * @param {Number} pageNumber
-     * @returns {*|Promise}
-     */
-    PagingContent.prototype.createPage = function(pageNumber) {
-
-        return this.loadContent(pageNumber).then(function(html, js) {
-            Templates.appendNodeContents(this.root, html, js);
-        }.bind(this)).then(function() {
-                return this.findPage(pageNumber);
-            }.bind(this)
-        );
-    };
-
-    /**
-     * Find a page by the number.
-     *
-     * @param {Number} pageNumber The number of the page to be found.
-     * @returns {*} Page root
-     */
-    PagingContent.prototype.findPage = function(pageNumber) {
-        return this.root.find('[data-page="' + pageNumber + '"]');
-    };
-
-    /**
-     * Make a page visible.
-     *
-     * @param {Number} pageNumber The number of the page to be visible.
-     */
-    PagingContent.prototype.showPage = function(pageNumber) {
-
-        var existingPage = this.findPage(pageNumber);
-        this.root.find(SELECTORS.PAGE_REGION).addClass('hidden');
-
-        if (existingPage.length) {
-            existingPage.removeClass('hidden');
-        } else {
-            this.createPage(pageNumber).done(function(newPage) {
-                newPage.removeClass('hidden');
-            });
-        }
-    };
-
-    /**
-     * Event listeners.
-     */
-    PagingContent.prototype.registerEventListeners = function() {
-
-        this.pagingBar.on(PagingBar.events.PAGE_SELECTED, function(e, data) {
-            if (!data.isSamePage) {
-                this.showPage(data.pageNumber);
-            }
-        }.bind(this));
-    };
-
-    return PagingContent;
-});
diff --git a/blocks/myoverview/amd/src/repository.js b/blocks/myoverview/amd/src/repository.js
new file mode 100644 (file)
index 0000000..b4f7b5d
--- /dev/null
@@ -0,0 +1,53 @@
+// 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/>.
+
+/**
+ * A javascript module to retrieve enrolled coruses from the server.
+ *
+ * @package    block_myoverview
+ * @copyright  2018 Bas Brands <base@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['core/ajax'], function(Ajax) {
+
+    /**
+     * Retrieve a list of enrolled courses.
+     *
+     * Valid args are:
+     * string classification    future, inprogress, past
+     * int limit                number of records to retreive
+     * int Offset               offset for pagination
+     * int sort                 sort by lastaccess or name
+     *
+     * @method getEnrolledCoursesByTimeline
+     * @param {object} args The request arguments
+     * @return {promise} Resolved with an array of courses
+     */
+    var getEnrolledCoursesByTimeline = function(args) {
+
+        var request = {
+            methodname: 'core_course_get_enrolled_courses_by_timeline_classification',
+            args: args
+        };
+
+        var promise = Ajax.call([request])[0];
+
+        return promise;
+    };
+
+    return {
+        getEnrolledCoursesByTimeline: getEnrolledCoursesByTimeline
+    };
+});
diff --git a/blocks/myoverview/amd/src/view.js b/blocks/myoverview/amd/src/view.js
new file mode 100644 (file)
index 0000000..5f3f15a
--- /dev/null
@@ -0,0 +1,188 @@
+// 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/>.
+
+/**
+ * Manage the courses view for the overview block.
+ *
+ * @package    block_myoverview
+ * @copyright  2018 Bas Brands <bas@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(
+[
+    'jquery',
+    'core/notification',
+    'block_myoverview/repository',
+    'core/paged_content_factory',
+    'core/templates',
+],
+function(
+    $,
+    Notification,
+    Repository,
+    PagedContentFactory,
+    Templates
+) {
+
+    var TEMPLATES = {
+        COURSES_CARDS: 'block_myoverview/view-cards',
+        COURSES_LIST: 'block_myoverview/view-list',
+        COURSES_SUMMARY: 'block_myoverview/view-summary',
+        NOCOURSES: 'block_myoverview/no-courses'
+    };
+
+    var NUMCOURSES_PERPAGE = [12, 24];
+
+    var currentCourseList = [];
+
+    /**
+     * Get filter values from DOM.
+     *
+     * @param {object} root The root element for the courses view.
+     * @return {filters} Set filters.
+     */
+    var getFilterValues = function(root) {
+        var filters = {};
+        filters.display = root.attr('data-display');
+        filters.grouping = root.attr('data-grouping');
+        filters.sort = root.attr('data-sort');
+        return filters;
+    };
+
+    // We want the paged content controls below the paged content area
+    // and the controls should be ignored while data is loading.
+    var DEFAULT_PAGED_CONTENT_CONFIG = {
+        ignoreControlWhileLoading: true,
+        controlPlacementBottom: true,
+    };
+
+    /**
+     * Get enrolled courses from backend.
+     *
+     * @param {object} filters The filters for this view.
+     * @param {int} limit The number of courses to show.
+     * @param {int} pageNumber The pagenumber to view.
+     * @return {promise} Resolved with an array of courses.
+     */
+    var getMyCourses = function(filters, limit, pageNumber) {
+        return Repository.getEnrolledCoursesByTimeline({
+            offset:  pageNumber * limit,
+            limit: limit,
+            classification: filters.grouping,
+            sort: filters.sort
+        });
+    };
+
+    /**
+     * Render the dashboard courses.
+     *
+     * @param {object} root The root element for the courses view.
+     * @param {array} coursesData containing array of returned courses.
+     * @param {object} filters The filters for this view.
+     * @return {promise} jQuery promise resolved after rendering is complete.
+     */
+    var renderCourses = function(root, coursesData, filters) {
+
+        var currentTemplate = '';
+        if (filters.display == 'cards') {
+            currentTemplate = TEMPLATES.COURSES_CARDS;
+        } else if (filters.display == 'list') {
+            currentTemplate = TEMPLATES.COURSES_LIST;
+        } else {
+            currentTemplate = TEMPLATES.COURSES_SUMMARY;
+        }
+
+        if (coursesData.courses.length) {
+            return Templates.render(currentTemplate, {
+                courses: coursesData.courses
+            });
+        } else {
+            var nocoursesimg = root.attr('data-nocoursesimg');
+            return Templates.render(TEMPLATES.NOCOURSES, {
+                nocoursesimg: nocoursesimg
+            });
+        }
+    };
+
+    /**
+     * Intialise the courses list and cards views on page load.
+     *
+     * @param {object} root The root element for the courses view.
+     * @param {object} content The content element for the courses view.
+     */
+    var init = function(root, content) {
+
+        root = $(root);
+
+        var filters = getFilterValues(root);
+
+        var pagedContentPromise = PagedContentFactory.createWithLimit(
+            NUMCOURSES_PERPAGE,
+            function(pagesData, actions) {
+                var promises = [];
+
+                pagesData.forEach(function(pageData) {
+                    var pageNumber = pageData.pageNumber - 1;
+
+                    var pagePromise = getMyCourses(
+                        filters,
+                        pageData.limit,
+                        pageNumber
+                    ).then(function(coursesData) {
+                        if (coursesData.courses.length < pageData.limit) {
+                            actions.allItemsLoaded(pageData.pageNumber);
+                        }
+                        currentCourseList = coursesData;
+                        return renderCourses(root, coursesData, filters);
+                    })
+                    .catch(Notification.exception);
+
+                    promises.push(pagePromise);
+                });
+
+                return promises;
+            },
+            DEFAULT_PAGED_CONTENT_CONFIG
+        );
+
+        pagedContentPromise.then(function(html, js) {
+            return Templates.replaceNodeContents(content, html, js);
+        }).catch(Notification.exception);
+    };
+
+    /**
+     * Reset the courses views to their original
+     * state on first page load.
+     *
+     * This is called when configuration has changed for the event lists
+     * to cause them to reload their data.
+     *
+     * @param {object} root The root element for the timeline view.
+     * @param {object} content The content element for the timeline view.
+     */
+    var reset = function(root, content) {
+        var filters = getFilterValues(root);
+        renderCourses(root, currentCourseList, filters)
+            .then(function(html, js) {
+                return Templates.replaceNodeContents(content, html, js);
+            }).catch(Notification.exception);
+    };
+
+    return {
+        init: init,
+        reset: reset
+    };
+});
diff --git a/blocks/myoverview/amd/src/view_nav.js b/blocks/myoverview/amd/src/view_nav.js
new file mode 100644 (file)
index 0000000..42f5e33
--- /dev/null
@@ -0,0 +1,109 @@
+// 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/>.
+
+/**
+ * Manage the timeline view navigation for the overview block.
+ *
+ * @package    block_myoverview
+ * @copyright  2018 Bas Brands <bas@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(
+[
+    'jquery',
+    'core/custom_interaction_events',
+    'block_myoverview/view'
+],
+function(
+    $,
+    CustomEvents,
+    View
+) {
+
+    var SELECTORS = {
+        FILTERS: '[data-region="filter"]',
+        FILTER_OPTION: '[data-filter]',
+        DISPLAY_OPTION: '[data-display-option]'
+    };
+
+    /**
+     * Event listener for the Display filter (cards, list).
+     *
+     * @param {object} root The root element for the overview block
+     * @param {object} viewRoot The root element for displaying courses.
+     * @param {object} viewContent content The content element for the courses view.
+     */
+    var registerSelector = function(root, viewRoot, viewContent) {
+
+        var Selector = root.find(SELECTORS.FILTERS);
+
+        CustomEvents.define(Selector, [CustomEvents.events.activate]);
+        Selector.on(
+            CustomEvents.events.activate,
+            SELECTORS.FILTER_OPTION,
+            function(e, data) {
+                var option = $(e.target);
+
+                if (option.hasClass('active')) {
+                    // If it's already active then we don't need to do anything.
+                    return;
+                }
+
+                var attributename = 'data-' + option.attr('data-filter');
+                viewRoot.attr(attributename, option.attr('data-value'));
+
+                // Reset the views.
+                View.init(viewRoot, viewContent);
+
+                data.originalEvent.preventDefault();
+            }
+        );
+
+        CustomEvents.define(Selector, [CustomEvents.events.activate]);
+        Selector.on(
+            CustomEvents.events.activate,
+            SELECTORS.DISPLAY_OPTION,
+            function(e, data) {
+                var option = $(e.target);
+
+                if (option.hasClass('active')) {
+                    return;
+                }
+
+                viewRoot.attr('data-display', option.attr('data-value'));
+                View.reset(viewRoot, viewContent);
+                data.originalEvent.preventDefault();
+            }
+        );
+    };
+
+    /**
+     * Initialise the timeline view navigation by adding event listeners to
+     * the navigation elements.
+     *
+     * @param {object} root The root element for the myoverview block
+     * @param {object} viewRoot The root element for the myoverview block
+     * @param {object} viewContent The content element for the myoverview block
+     */
+    var init = function(root, viewRoot, viewContent) {
+        root = $(root);
+        registerSelector(root, viewRoot, viewContent);
+    };
+
+    return {
+        init: init
+    };
+});
diff --git a/blocks/myoverview/classes/output/courses_view.php b/blocks/myoverview/classes/output/courses_view.php
deleted file mode 100644 (file)
index 2c6027e..0000000
+++ /dev/null
@@ -1,189 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Class containing data for courses view in the myoverview block.
- *
- * @package    block_myoverview
- * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-namespace block_myoverview\output;
-defined('MOODLE_INTERNAL') || die();
-
-use renderable;
-use renderer_base;
-use templatable;
-use core_course\external\course_summary_exporter;
-
-/**
- * Class containing data for courses view in the myoverview block.
- *
- * @copyright  2017 Simey Lameze <simey@moodle.com>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class courses_view implements renderable, templatable {
-    /** Quantity of courses per page. */
-    const COURSES_PER_PAGE = 6;
-
-    /** @var array $courses List of courses the user is enrolled in. */
-    protected $courses = [];
-
-    /** @var array $coursesprogress List of progress percentage for each course. */
-    protected $coursesprogress = [];
-
-    /**
-     * The courses_view constructor.
-     *
-     * @param array $courses list of courses.
-     * @param array $coursesprogress list of courses progress.
-     */
-    public function __construct($courses, $coursesprogress) {
-        $this->courses = $courses;
-        $this->coursesprogress = $coursesprogress;
-    }
-
-    /**
-     * Export this data so it can be used as the context for a mustache template.
-     *
-     * @param \renderer_base $output
-     * @return array
-     */
-    public function export_for_template(renderer_base $output) {
-        global $CFG;
-        require_once($CFG->dirroot.'/course/lib.php');
-
-        // Build courses view data structure.
-        $coursesview = [
-            'hascourses' => !empty($this->courses)
-        ];
-
-        // How many courses we have per status?
-        $coursesbystatus = ['past' => 0, 'inprogress' => 0, 'future' => 0];
-        foreach ($this->courses as $course) {
-            $courseid = $course->id;
-            $context = \context_course::instance($courseid);
-            $exporter = new course_summary_exporter($course, [
-                'context' => $context
-            ]);
-            $exportedcourse = $exporter->export($output);
-            // Convert summary to plain text.
-            $exportedcourse->summary = content_to_text($exportedcourse->summary, $exportedcourse->summaryformat);
-
-            $course = new \core_course_list_element($course);
-            foreach ($course->get_course_overviewfiles() as $file) {
-                $isimage = $file->is_valid_image();
-                if ($isimage) {
-                    $url = file_encode_url("$CFG->wwwroot/pluginfile.php",
-                        '/'. $file->get_contextid(). '/'. $file->get_component(). '/'.
-                        $file->get_filearea(). $file->get_filepath(). $file->get_filename(), !$isimage);
-                    $exportedcourse->courseimage = $url;
-                    $exportedcourse->classes = 'courseimage';
-                    break;
-                }
-            }
-
-            $exportedcourse->color = $this->coursecolor($course->id);
-
-            if (!isset($exportedcourse->courseimage)) {
-                $pattern = new \core_geopattern();
-                $pattern->setColor($exportedcourse->color);
-                $pattern->patternbyid($courseid);
-                $exportedcourse->classes = 'coursepattern';
-                $exportedcourse->courseimage = $pattern->datauri();
-            }
-
-            // Include course visibility.
-            $exportedcourse->visible = (bool)$course->visible;
-
-            $courseprogress = null;
-
-            $classified = course_classify_for_timeline($course);
-
-            if (isset($this->coursesprogress[$courseid])) {
-                $courseprogress = $this->coursesprogress[$courseid]['progress'];
-                $exportedcourse->hasprogress = !is_null($courseprogress);
-                $exportedcourse->progress = $courseprogress;
-            }
-
-            if ($classified == COURSE_TIMELINE_PAST) {
-                // Courses that have already ended.
-                $pastpages = floor($coursesbystatus['past'] / $this::COURSES_PER_PAGE);
-
-                $coursesview['past']['pages'][$pastpages]['courses'][] = $exportedcourse;
-                $coursesview['past']['pages'][$pastpages]['active'] = ($pastpages == 0 ? true : false);
-                $coursesview['past']['pages'][$pastpages]['page'] = $pastpages + 1;
-                $coursesview['past']['haspages'] = true;
-                $coursesbystatus['past']++;
-            } else if ($classified == COURSE_TIMELINE_FUTURE) {
-                // Courses that have not started yet.
-                $futurepages = floor($coursesbystatus['future'] / $this::COURSES_PER_PAGE);
-
-                $coursesview['future']['pages'][$futurepages]['courses'][] = $exportedcourse;
-                $coursesview['future']['pages'][$futurepages]['active'] = ($futurepages == 0 ? true : false);
-                $coursesview['future']['pages'][$futurepages]['page'] = $futurepages + 1;
-                $coursesview['future']['haspages'] = true;
-                $coursesbystatus['future']++;
-            } else {
-                // Courses still in progress. Either their end date is not set, or the end date is not yet past the current date.
-                $inprogresspages = floor($coursesbystatus['inprogress'] / $this::COURSES_PER_PAGE);
-
-                $coursesview['inprogress']['pages'][$inprogresspages]['courses'][] = $exportedcourse;
-                $coursesview['inprogress']['pages'][$inprogresspages]['active'] = ($inprogresspages == 0 ? true : false);
-                $coursesview['inprogress']['pages'][$inprogresspages]['page'] = $inprogresspages + 1;
-                $coursesview['inprogress']['haspages'] = true;
-                $coursesbystatus['inprogress']++;
-            }
-        }
-
-        // Build courses view paging bar structure.
-        foreach ($coursesbystatus as $status => $total) {
-            $quantpages = ceil($total / $this::COURSES_PER_PAGE);
-
-            if ($quantpages) {
-                $coursesview[$status]['pagingbar']['disabled'] = ($quantpages <= 1);
-                $coursesview[$status]['pagingbar']['pagecount'] = $quantpages;
-                $coursesview[$status]['pagingbar']['first'] = ['page' => '&laquo;', 'url' => '#'];
-                $coursesview[$status]['pagingbar']['last'] = ['page' => '&raquo;', 'url' => '#'];
-                for ($page = 0; $page < $quantpages; $page++) {
-                    $coursesview[$status]['pagingbar']['pages'][$page] = [
-                        'number' => $page + 1,
-                        'page' => $page + 1,
-                        'url' => '#',
-                        'active' => ($page == 0 ? true : false)
-                    ];
-                }
-            }
-        }
-
-        return $coursesview;
-    }
-
-    /**
-     * Generate a semi-random color based on the courseid number (so it will always return
-     * the same color for a course)
-     *
-     * @param int $courseid
-     * @return string $color, hexvalue color code.
-     */
-    protected function coursecolor($courseid) {
-        // The colour palette is hardcoded for now. It would make sense to combine it with theme settings.
-        $basecolors = ['#81ecec', '#74b9ff', '#a29bfe', '#dfe6e9', '#00b894', '#0984e3', '#b2bec3', '#fdcb6e', '#fd79a8', '#6c5ce7'];
-
-        $color = $basecolors[$courseid % 10];
-        return $color;
-    }
-}
index 46834f3..c734468 100644 (file)
@@ -27,14 +27,13 @@ defined('MOODLE_INTERNAL') || die();
 use renderable;
 use renderer_base;
 use templatable;
-use core_completion\progress;
 
 require_once($CFG->libdir . '/completionlib.php');
 
 /**
  * Class containing data for my overview block.
  *
- * @copyright  2017 Simey Lameze <simey@moodle.com>
+ * @copyright  2018 Bas Brands <bas@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class main implements renderable, templatable {
@@ -45,38 +44,11 @@ class main implements renderable, templatable {
      * @return stdClass
      */
     public function export_for_template(renderer_base $output) {
-        global $USER;
 
-        $courses = enrol_get_my_courses('*');
-        $coursesprogress = [];
-
-        foreach ($courses as $course) {
-
-            $completion = new \completion_info($course);
-
-            // First, let's make sure completion is enabled.
-            if (!$completion->is_enabled()) {
-                continue;
-            }
-
-            $percentage = progress::get_course_progress_percentage($course);
-            if (!is_null($percentage)) {
-                $percentage = floor($percentage);
-            }
-
-            $coursesprogress[$course->id]['completed'] = $completion->is_course_complete($USER->id);
-            $coursesprogress[$course->id]['progress'] = $percentage;
-        }
-
-        $coursesview = new courses_view($courses, $coursesprogress);
         $nocoursesurl = $output->image_url('courses', 'block_myoverview')->out();
 
-        return [
-            'midnight' => usergetmidnight(time()),
-            'coursesview' => $coursesview->export_for_template($output),
-            'urls' => [
-                'nocourses' => $nocoursesurl,
-            ],
+        return (object) [
+            'nocoursesimg' => $nocoursesurl
         ];
     }
 }
diff --git a/blocks/myoverview/db/upgrade.php b/blocks/myoverview/db/upgrade.php
deleted file mode 100644 (file)
index b91cb96..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * This file keeps track of upgrades to the myoverview block
- *
- * @package block_myoverview
- * @copyright 2018 Ryan Wyllie <ryan@moodle.com>
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-/**
- * Upgrade code for the myoverview block.
- *
- * @param int $oldversion
- */
-function xmldb_block_myoverview_upgrade($oldversion) {
-    global $DB;
-
-    if ($oldversion < 2018092700) {
-        $DB->delete_records('user_preferences', ['name' => 'block_myoverview_last_tab']);
-        upgrade_block_savepoint(true, 2018092700, 'myoverview');
-    }
-
-    return true;
-}
index df3ae66..49fe21c 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['all'] = 'All';
+$string['aria:allcourses'] = 'All courses';
+$string['aria:card'] = 'Switch to card view';
+$string['aria:controls'] = 'Course overview controls';
+$string['aria:courseimage'] = 'Course image:';
+$string['aria:coursename'] = 'Course name:';
+$string['aria:coursesummary'] = 'Course summary text:';
+$string['aria:courseprogress'] = 'Course progress:';
+$string['aria:displaydropdown'] = 'Display dropdown';
+$string['aria:future'] = 'Show future courses';
+$string['aria:groupingdropdown'] = 'Grouping dropdown';
+$string['aria:inprogress'] = 'Show in courses in progress';
+$string['aria:lastaccessed'] = 'Sort courses by last accessed date';
+$string['aria:list'] = 'Switch to list view';
+$string['aria:title'] = 'Sort courses by title';
+$string['aria:past'] = 'Show past courses';
+$string['aria:summary'] = 'Switch to summary view';
+$string['aria:sortingdropdown'] = 'Sorting dropdown';
+$string['card'] = 'Card';
+$string['courseprogress'] = 'Course progress:';
+$string['complete'] = 'Complete';
+$string['favorite'] = 'Favorite';
 $string['future'] = 'Future';
+$string['future:aria'] = 'View future courses';
+$string['hidden'] = 'Hidden';
 $string['inprogress'] = 'In progress';
+$string['inprogress:aria'] = 'View in progress courses';
+$string['lastaccessed'] = 'Last accessed';
+$string['lastaccessed:aria'] = 'Sort course by lastaccessed';
+$string['list'] = 'List';
 $string['morecourses'] = 'More courses';
 $string['myoverview:addinstance'] = 'Add a new course overview block';
 $string['myoverview:myaddinstance'] = 'Add a new course overview block to Dashboard';
-$string['nocourses'] = 'No courses';
-$string['nocoursesinprogress'] = 'No in progress courses';
 $string['nocoursesfuture'] = 'No future courses';
+$string['nocoursesinprogress'] = 'No in progress courses';
+$string['nocourses'] = 'No courses';
 $string['nocoursespast'] = 'No past courses';
 $string['past'] = 'Past';
 $string['pluginname'] = 'Course overview';
-$string['viewcourse'] = 'View course';
-$string['viewcoursename'] = 'View course {$a}';
 $string['privacy:metadata'] = 'The myoverview block does not store any personal data.';
+$string['summary'] = 'Summary';
+$string['title'] = 'Title';
+$string['viewcoursename'] = 'View course {$a}';
+$string['viewcourse'] = 'View course';
+
diff --git a/blocks/myoverview/templates/course-paging-content-item.mustache b/blocks/myoverview/templates/course-paging-content-item.mustache
deleted file mode 100644 (file)
index bbaa637..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-{{!
-    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 block_myoverview/course-paging-content-item
-
-    This template renders each course block.
-
-    Example context (json):
-    {
-        "page": 1,
-        "active": true,
-        "courses": [
-            {
-                "fullnamedisplay": "course 1",
-                "viewurl": "https://www.google.com",
-                "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout."
-            },
-            {
-                "fullnamedisplay": "course 2",
-                "viewurl": "https://www.google.com",
-                "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout."
-            }
-        ]
-    }
-}}
-{{< block_myoverview/paging-content-item }}
-    {{$classes}}row card-deck{{/classes}}
-    {{$content}}
-        {{#courses}}
-            {{> block_myoverview/courses-view-course-item }}
-        {{/courses}}
-    {{/content}}
-{{/ block_myoverview/paging-content-item }}
diff --git a/blocks/myoverview/templates/course-paging-content.mustache b/blocks/myoverview/templates/course-paging-content.mustache
deleted file mode 100644 (file)
index 85ee437..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-{{!
-    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 block_myoverview/course-paging-content
-
-    This template renders the each course block containing a summary and calendar events.
-
-    Example context (json):
-    {
-        "pages": [
-            {
-                "page": 1,
-                "active": true,
-                "courses": [
-                    {
-                        "fullnamedisplay": "course 1",
-                        "viewurl": "https://www.google.com",
-                        "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout."
-                    },
-                    {
-                        "fullnamedisplay": "course 2",
-                        "viewurl": "https://www.google.com",
-                        "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout."
-                    }
-                ]
-            }
-        ]
-    }
-}}
-{{< block_myoverview/paging-content }}
-    {{$paging-content-item}}
-        {{> block_myoverview/course-paging-content-item }}
-    {{/paging-content-item}}
-{{/ block_myoverview/paging-content }}
diff --git a/blocks/myoverview/templates/courses-view-by-status.mustache b/blocks/myoverview/templates/courses-view-by-status.mustache
deleted file mode 100644 (file)
index 3360d55..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-{{!
-    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 block_myoverview/courses-view-by-status
-
-    This template renders the courses view for the myoverview block.
-
-    Example context (json):
-    {}
-}}
-<div id="{{$id}}courses-view-status-{{uniqid}}{{/id}}"
-     data-status="{{$status}}{{/status}}">
-
-    {{> block_myoverview/course-paging-content }}
-
-    <div class="text-xs-center text-center">
-        {{> block_myoverview/paging-bar }}
-    </div>
-</div>
-{{#js}}
-require(['jquery', 'block_myoverview/paging_bar', 'block_myoverview/paging_content'],
-    function($, PagingBar, PagingContent) {
-
-    var root = $('#{{$id}}courses-view-status-{{uniqid}}{{/id}}');
-    var pagingBarElement = root.find(PagingBar.rootSelector);
-    var pagingContentElement = root.find(PagingContent.rootSelector);
-
-    var content = new PagingContent(pagingContentElement, pagingBarElement);
-    content.registerEventListeners();
-});
-{{/js}}
diff --git a/blocks/myoverview/templates/courses-view-course-item.mustache b/blocks/myoverview/templates/courses-view-course-item.mustache
deleted file mode 100644 (file)
index db2034d..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-{{!
-    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 block_myoverview/courses-view-course-item
-
-    This template renders the course summary (view by courses) for the myoverview block.
-
-    Example context (json):
-    {
-        "fullnamedisplay": "course 3",
-        "viewurl": "https://www.google.com",
-        "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout."
-    }
-}}
-<div class="card mb-3 courses-view-course-item">
-    <a href="{{viewurl}}">
-        <div class="card-img-top myoverviewimg {{classes}}" style='background-image: url("{{{courseimage}}}");'>
-        </div>
-    </a>
-    <div class="card-body course-info-container" id="course-info-container-{{id}}">
-
-        <div class="media">
-            <div class="mr-2">
-                {{> block_myoverview/progress-chart}}
-            </div>
-            <div class="media-body">
-                <h4 class="h5"><a href="{{viewurl}}" class="{{^visible}}dimmed{{/visible}}">{{{fullnamedisplay}}}</a></h4>
-            </div>
-        </div>
-
-        <p class="text-muted">
-            {{#shortentext}} 140, {{summary}}{{/shortentext}}
-        </p>
-    </div>
-</div>
\ No newline at end of file
diff --git a/blocks/myoverview/templates/courses-view-nav-grouping-display-filter.mustache b/blocks/myoverview/templates/courses-view-nav-grouping-display-filter.mustache
deleted file mode 100644 (file)
index f7aa275..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-{{!
-    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 block_myoverview/courses-view-nav-grouping-display-filter
-
-    This template renders the main content area for the myoverview block.
-
-    Example context (json):
-    {}
-}}
-<div data-region="courses-grouping-display-filter" class="btn-group">
-    <button type="button" class="btn btn-outline-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-        {{#str}} inprogress, block_myoverview {{/str}}
-    </button>
-    <div class="dropdown-menu list-group hidden" data-show-active-item data-skip-active-class="true">
-        <a class="dropdown-item active" href="#myoverview_courses_view_in_progress" data-toggle="tab">
-            {{#str}} inprogress, block_myoverview {{/str}}
-        </a>
-        <a class="dropdown-item" href="#myoverview_courses_view_future" data-toggle="tab">
-            {{#str}} future, block_myoverview {{/str}}
-        </a>
-        <a class="dropdown-item" href="#myoverview_courses_view_past" data-toggle="tab">
-            {{#str}} past, block_myoverview {{/str}}
-        </a>
-    </div>
-</div>
index cb5b926..ab97aa4 100644 (file)
     This template renders the courses view for the myoverview block.
 
     Example context (json):
-    {}
+    {
+        "nocoursesimg": "https://moodlesite/theme/image.php/boost/block_myoverview/1535727318/courses"
+    }
 }}
-<div id="courses-view-{{uniqid}}" data-region="courses-view">
-    {{#hascourses}}
-    <div class="tab-content">
-        <div class="tab-pane active fade show" id="myoverview_courses_view_in_progress">
-            {{#inprogress}}
-                {{< block_myoverview/courses-view-by-status }}
-                    {{$id}}courses-view-in-progress{{/id}}
-                    {{$status}}1{{/status}}
-                    {{$pagingbarid}}pb-for-in-progress{{/pagingbarid}}
-                    {{$pagingcontentid}}pc-for-in-progress{{/pagingcontentid}}
-                {{/ block_myoverview/courses-view-by-status }}
-            {{/inprogress}}
-            {{^inprogress}}
-                <div class="justify-content-center text-center mt-5">
-                    <img class="empty-placeholder-image-lg"
-                         src="{{urls.nocourses}}"
-                         alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}"
-                         role="presentation">
-                    <p class="text-muted mt-3">{{#str}} nocoursesinprogress, block_myoverview {{/str}}</p>
-                </div>
-            {{/inprogress}}
+<div id="courses-view-{{uniqid}}"
+    data-region="courses-view"
+    data-display="cards"
+    data-grouping="all"
+    data-sort="fullname"
+    data-nocoursesimg="{{nocoursesimg}}">
+    <div data-region="course-view-content">
+        <div data-region="courses-loading-placeholder">
+            <div class="row card-deck">
+                {{> block_myoverview/placeholder-course }}
+                {{> block_myoverview/placeholder-course }}
+                {{> block_myoverview/placeholder-course }}
+                {{> block_myoverview/placeholder-course }}
+            </div>
         </div>
-        <div class="tab-pane fade" id="myoverview_courses_view_future">
-            {{#future}}
-                {{< block_myoverview/courses-view-by-status }}
-                    {{$id}}courses-view-future{{/id}}
-                    {{$status}}2{{/status}}
-                    {{$pagingbarid}}pb-for-future{{/pagingbarid}}
-                    {{$pagingcontentid}}pc-for-in-progress{{/pagingcontentid}}
-                {{/ block_myoverview/courses-view-by-status }}
-            {{/future}}
-            {{^future}}
-                <div class="justify-content-center text-center mt-5">
-                    <img class="empty-placeholder-image-lg"
-                         src="{{urls.nocourses}}"
-                         alt="{{#str}} nocoursesfuture, block_myoverview {{/str}}"
-                         role="presentation">
-                    <p class="text-muted mt-3">{{#str}} nocoursesfuture, block_myoverview {{/str}}</p>
-                </div>
-            {{/future}}
-        </div>
-        <div class="tab-pane fade" id="myoverview_courses_view_past">
-            {{#past}}
-                {{< block_myoverview/courses-view-by-status }}
-                    {{$id}}courses-view-past{{/id}}
-                    {{$status}}0{{/status}}
-                    {{$pagingbarid}}pb-for-past{{/pagingbarid}}
-                    {{$pagingcontentid}}pc-for-in-progress{{/pagingcontentid}}
-                {{/ block_myoverview/courses-view-by-status }}
-            {{/past}}
-            {{^past}}
-                <div class="justify-content-center text-center mt-5">
-                    <img class="empty-placeholder-image-lg"
-                         src="{{urls.nocourses}}"
-                         alt="{{#str}} nocoursespast, block_myoverview {{/str}}"
-                         role="presentation">
-                    <p class="text-muted mt-3">{{#str}} nocoursespast, block_myoverview {{/str}}</p>
-                </div>
-            {{/past}}
-        </div>
-    </div>
-    {{/hascourses}}
-    {{^hascourses}}
-    <div class="justify-content-center text-center mt-5">
-        <img class="empty-placeholder-image-lg"
-             src="{{urls.nocourses}}"
-             alt="{{#str}} nocourses, block_myoverview {{/str}}"
-             role="presentation">
-        <p class="text-muted mt-3">{{#str}} nocourses, block_myoverview {{/str}}</p>
     </div>
-    {{/hascourses}}
-</div>
\ No newline at end of file
+</div>
index fef8e9f..1d593e8 100644 (file)
     {}
 }}
 
-<div id="block-myoverview-{{uniqid}}" class="block-myoverview" data-region="myoverview">
-    <div class="container p-0 m-b-1">
-        <div class="row no-gutters">
-            {{#coursesview}}
-                {{#hascourses}}
-                    <div class="{{#viewingtimeline}}d-none{{/viewingtimeline}}" data-tab-content="courses">
-                        {{> block_myoverview/courses-view-nav-grouping-display-filter }}
-                    </div>
-                {{/hascourses}}
-            {{/coursesview}}
-        </div>
-    </div>
-    <div class="container p-0">
-        {{#coursesview}}
+<div id="block-myoverview-{{uniqid}}" class="block-myoverview" data-region="myoverview" role="navigation">
+
+            <div data-region="filter" class="d-flex m-b-1" aria-label="{{#str}} aria:controls, block_myoverview {{/str}}">
+                {{> block_myoverview/nav-grouping-selector }}
+
+                {{> block_myoverview/nav-sort-selector }}
+
+                {{> block_myoverview/nav-display-selector }}
+            </div>
+
+    <div class="container-fluid p-0">
         {{> block_myoverview/courses-view }}
-        {{/coursesview}}
     </div>
 </div>
+{{#js}}
+require(
+[
+    'jquery',
+    'block_myoverview/main',
+],
+function(
+    $,
+    Main
+) {
+    var root = $('#block-myoverview-{{uniqid}}');
+    Main.init(root);
+});
+{{/js}}
diff --git a/blocks/myoverview/templates/nav-display-selector.mustache b/blocks/myoverview/templates/nav-display-selector.mustache
new file mode 100644 (file)
index 0000000..e5661dc
--- /dev/null
@@ -0,0 +1,47 @@
+{{!
+    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 block_myoverview/nav-display-selector
+
+    This template renders display dropdown.
+
+    Example context (json):
+    {}
+}}
+<div class="dropdown">
+    <button id="displaydropdown" type="button" class="btn btn-outline-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
+    aria-label="{{#str}} aria:displaydropdown, block_myoverview {{/str}}">
+        <span class="d-sm-inline-block">{{#pix}} a/view_icon_active {{/pix}}</span>
+    </button>
+    <ul class="dropdown-menu" data-show-active-item aria-labelledby="displaydropdown">
+        <li>
+            <a class="dropdown-item active" href="#" data-display-option="display" data-value="cards" aria-label="{{#str}} aria:card, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
+            {{#str}} card, block_myoverview {{/str}}
+            </a>
+        </li>
+        <li>
+            <a class="dropdown-item" href="#" data-display-option="display" data-value="list" aria-label="{{#str}} aria:list, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
+            {{#str}} list, block_myoverview {{/str}}
+            </a>
+        </li>
+        <li>
+            <a class="dropdown-item" href="#" data-display-option="display" data-value="summary" aria-label="{{#str}} aria:summary, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
+            {{#str}} summary, block_myoverview {{/str}}
+            </a>
+        </li>
+    </ul>
+</div>
\ No newline at end of file
diff --git a/blocks/myoverview/templates/nav-grouping-selector.mustache b/blocks/myoverview/templates/nav-grouping-selector.mustache
new file mode 100644 (file)
index 0000000..ac57aa7
--- /dev/null
@@ -0,0 +1,51 @@
+{{!
+    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 block_myoverview/nav-grouping-selector
+
+    This template renders grouping dropdown.
+
+    Example context (json):
+    {}
+}}
+<div class="dropdown">
+    <button id="groupingdropdown" type="button" class="btn btn-outline-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="{{#str}} aria:groupingdropdown, block_myoverview {{/str}}">
+        <span class="d-sm-inline-block">{{#str}} all, block_myoverview {{/str}}</span>
+    </button>
+    <ul class="dropdown-menu" data-show-active-item aria-labelledby="groupingdropdown">
+        <li>
+            <a class="dropdown-item active" href="#" data-filter="grouping" data-value="all" aria-label="{{#str}} aria:allcourses, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
+                {{#str}} all, block_myoverview {{/str}}
+            </a>
+        </li>
+        <li>
+            <a class="dropdown-item" href="#" data-filter="grouping" data-value="inprogress" aria-label="{{#str}} aria:inprogress, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
+                {{#str}} inprogress, block_myoverview {{/str}}
+            </a>
+        </li>
+        <li>
+            <a class="dropdown-item" href="#" data-filter="grouping" data-value="future" aria-label="{{#str}} aria:future, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
+                {{#str}} future, block_myoverview {{/str}}
+            </a>
+        </li>
+        <li>
+            <a class="dropdown-item" href="#" data-filter="grouping" data-value="past" aria-label="{{#str}} aria:past, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
+                {{#str}} past, block_myoverview {{/str}}
+            </a>
+        </li>
+    </ul>
+</div>
diff --git a/blocks/myoverview/templates/nav-sort-selector.mustache b/blocks/myoverview/templates/nav-sort-selector.mustache
new file mode 100644 (file)
index 0000000..e647be9
--- /dev/null
@@ -0,0 +1,43 @@
+{{!
+    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 block_myoverview/nav-sort-selector
+
+    This template renders sorting dropdown.
+
+    Example context (json):
+    {}
+}}
+
+<div class="dropdown mr-1 ml-auto">
+    {{#str}} sortby, core {{/str}}
+    <button id="sortingdropdown" type="button" class="btn btn-outline-secondary dropdown-toggle" data-toggle="dropdown"  aria-haspopup="true" aria-expanded="false" aria-label="{{#str}} aria:sortingdropdown, block_myoverview {{/str}}">
+        <span class="d-sm-inline-block">{{#str}} title, block_myoverview {{/str}}</span>
+    </button>
+    <ul class="dropdown-menu" data-show-active-item aria-labelledby="sortingdropdown">
+        <li>
+            <a class="dropdown-item active" href="#" data-filter="sort" data-value="fullname" aria-label="{{#str}} aria:title, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
+                {{#str}} title, block_myoverview {{/str}}
+            </a>
+        </li>
+        <li>
+            <a class="dropdown-item" href="#" data-filter="sort" data-value="ul.timeaccess desc" aria-label="{{#str}} aria:lastaccessed, block_myoverview {{/str}}" aria-controls="courses-view-{{uniqid}}">
+                {{#str}} lastaccessed, block_myoverview {{/str}}
+            </a>
+        </li>
+    </ul>
+</div>
\ No newline at end of file
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template block_myoverview/paging-bar-item
+    @template block_myoverview/no-courses
 
-    This template renders a single item in the paging bar.
+    This template renders the no courses message.
 
     Example context (json):
     {
-        "url": "#",
-        "number": 1,
-        "page": "1",
-        "active": true
+        "nocoursesimg": "https://moodlesite/theme/image.php/boost/block_myoverview/1535727318/courses"
     }
 }}
-<li class="page-item {{#active}}active{{/active}} {{#disabled}}disabled{{/disabled}}"
-    data-region="page-item"
-    data-page-number="{{$pagenumber}}{{number}}{{/pagenumber}}">
-
-    <a href="{{url}}"
-       class="page-link"
-       data-region="page-link">
-        {{$item-content}}
-            {{{page}}}
-        {{/item-content}}
-    </a>
-</li>
+<div class="text-xs-center text-center m-t-3" data-region="empty-message">
+    <img class="empty-placeholder-image-lg"
+         src="{{nocoursesimg}}"
+         alt="{{#str}} nocourses, block_myoverview {{/str}}"
+         role="presentation">
+    <p class="text-muted mt-3">{{#str}} nocourses, block_myoverview {{/str}}</p>
+</div>
diff --git a/blocks/myoverview/templates/paging-bar.mustache b/blocks/myoverview/templates/paging-bar.mustache
deleted file mode 100644 (file)
index 71ffecf..0000000
+++ /dev/null
@@ -1,96 +0,0 @@
-{{!
-    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 block_myoverview/paging-bar
-
-    This template renders the bootstrap style paging bar.
-
-    Example context (json):
-    {
-        "pagingbar": {
-            "pagecount": 2,
-            "previous": {},
-            "next": {},
-            "first": {
-                "url": "#",
-                "page": "first"
-            },
-            "last": {
-                "url": "#",
-                "page": "last"
-            },
-            "pages": [
-                {
-                    "url": "#",
-                    "number": 1,
-                    "page": "1",
-                    "active": true
-                },
-                {
-                    "url": "#",
-                    "number": 2,
-                    "page": "2"
-                }
-            ]
-        }
-    }
-}}
-{{#pagingbar}}
-<nav aria-label="{{label}}"
-     id="{{$pagingbarid}}paging-bar-{{uniqid}}{{/pagingbarid}}"
-     data-region="paging-bar"
-     data-page-count="{{pagecount}}">
-
-    <ul class="pagination">
-        {{#previous}}
-            {{< block_myoverview/paging-bar-item }}
-                {{$item-content}}
-                    <span aria-hidden="true">&laquo;</span>
-                    <span class="sr-only">{{#str}}previous{{/str}}</span>
-                {{/item-content}}
-            {{/ block_myoverview/paging-bar-item }}
-        {{/previous}}
-        {{#first}}
-            {{< block_myoverview/paging-bar-item }}
-                {{$pagenumber}}first{{/pagenumber}}
-            {{/ block_myoverview/paging-bar-item }}
-        {{/first}}
-        {{#pages}}
-            {{> block_myoverview/paging-bar-item }}
-        {{/pages}}
-        {{#last}}
-            {{< block_myoverview/paging-bar-item }}
-                {{$pagenumber}}last{{/pagenumber}}
-            {{/ block_myoverview/paging-bar-item }}
-        {{/last}}
-        {{#next}}
-            {{< block_myoverview/paging-bar-item }}
-                {{$item-content}}
-                    <span aria-hidden="true">&raquo;</span>
-                    <span class="sr-only">{{#str}}next{{/str}}</span>
-                {{/item-content}}
-            {{/ block_myoverview/paging-bar-item }}
-        {{/next}}
-    </ul>
-</nav>
-{{#js}}
-require(['jquery', 'block_myoverview/paging_bar'], function($, PagingBar) {
-    var root = $('#{{$pagingbarid}}paging-bar-{{uniqid}}{{/pagingbarid}}');
-    PagingBar.registerEventListeners(root);
-});
-{{/js}}
-{{/pagingbar}}
diff --git a/blocks/myoverview/templates/paging-content.mustache b/blocks/myoverview/templates/paging-content.mustache
deleted file mode 100644 (file)
index 83a9cdd..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-{{!
-    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 block_myoverview/paging-content
-
-    This template renders each of the content regions for a paginated
-    content section.
-
-    Example context (json):
-    {
-        "pages": [
-            {
-                "active": true,
-                "page": 1,
-                "content": "<p>Some page content</p>"
-            },
-            {
-                "page": 2,
-                "content": "<p>Some page content</p>"
-            }
-        ]
-    }
-}}
-<div id="{{$pagingcontentid}}paging-content-{{uniqid}}{{/pagingcontentid}}" data-region="paging-content">
-    {{#pages}}
-        {{$paging-content-item}}
-            {{> block_myoverview/paging-content-item }}
-        {{/paging-content-item}}
-    {{/pages}}
-</div>
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template block_myoverview/paging-content-item
+    @template block_myoverview/placeholder-course
 
-    This template renders the content of a page. It is to be used with
-    the paging bar to toggle visibility of the content items.
+    This template renders an course card item loading placeholder for the myoverview block.
 
     Example context (json):
-    {
-        "active": true,
-        "page": 1,
-        "content": "<p>Some page content</p>"
-    }
+    {}
 }}
-<div data-region="paging-content-item"
-     data-page="{{page}}"
-     class="{{^active}}hidden{{/active}} {{$classes}}{{/classes}}">
-    {{$content}}
-        {{{content}}}
-    {{/content}}
+<div class="card course-card border-0">
+    <div class="card-img-top bg-pulse-grey w-100" style="height: 7rem">
+    </div>
+    <div class="card-body course-info-container">
+        <div class="bg-pulse-grey w-100 m-b-3" style="height: 1rem"></div>
+    </div>
+    <div class="bg-pulse-grey w-100" style="height: 3rem">
+    </div>
 </div>
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template block_myoverview/paging-content-item
+    @template block_myoverview/progress-bar
 
-    This template renders the content of a page. It is to be used with
-    the paging bar to toggle visibility of the content items.
+    This template renders a simple progress bar.
 
     Example context (json):
     {
-        "active": true,
-        "page": 1,
-        "content": "<p>Some page content</p>"
+        "progress": 50
     }
 }}
-<div data-region="paging-content-item"
-     data-page="{{page}}"
-     class="row-fluid {{^active}}hidden{{/active}} {{$classes}}{{/classes}}">
-    {{$content}}
-        {{{content}}}
-    {{/content}}
+
+<div class="progress">
+    <div class="progress-bar bar" role="progressbar" aria-valuenow="{{progress}}" style="width: {{progress}}%" aria-valuemin="0" aria-valuemax="100"></div>
+</div>
+<div class="small">
+    <span class="sr-only">{{#str}}aria:courseprogress, block_myoverview{{/str}}</span>
+    <strong>{{progress}}%</strong> {{#str}}complete, block_myoverview{{/str}}
 </div>
diff --git a/blocks/myoverview/templates/progress-chart.mustache b/blocks/myoverview/templates/progress-chart.mustache
deleted file mode 100644 (file)
index 18ff2a4..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-{{!
-    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 block_myoverview/progress-chart
-
-    This template renders a doughnut chart to show course progress.
-
-    Example context (json):
-    {
-        "hasprogress": true,
-        "progress": "60"
-    }
-}}
-<div class="progress-chart-container m-b-1">
-    {{#hasprogress}}
-    <div class="progress-doughnut">
-        <div class="progress-text {{#progress}}has-percent{{/progress}}">{{progress}}&#37;</div>
-        <div class="progress-indicator">
-            <svg xmlns="http://www.w3.org/2000/svg">
-                <g>
-                    <title aria-hidden="true">{{progress}}&#37;</title>
-                    <circle class="circle percent-{{progress}}"
-                            r="27.5"
-                            cx="35"
-                            cy="35"/>
-                </g>
-            </svg>
-        </div>
-    </div>
-    {{/hasprogress}}
-    {{^hasprogress}}
-    <div class="no-progress">
-        {{#pix}} i/course {{/pix}}
-    </div>
-    {{/hasprogress}}
-</div>
diff --git a/blocks/myoverview/templates/view-cards.mustache b/blocks/myoverview/templates/view-cards.mustache
new file mode 100644 (file)
index 0000000..8b0eaa3
--- /dev/null
@@ -0,0 +1,62 @@
+{{!
+    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 block_myoverview/view-cards
+
+    This template renders the cards view for the myoverview block.
+
+    Example context (json):
+    {
+        "courses": [
+            {
+                "name": "Assignment due 1",
+                "viewurl": "https://moodlesite/course/view.php?id=2",
+                "courseimage": "https://moodlesite/pluginfile/123/course/overviewfiles/123.jpg",
+                "fullname": "course 3",
+                "hasprogress": true,
+                "progress": 10
+            }
+        ]
+    }
+}}
+
+<div class="row card-deck" role="list">
+{{#courses}}
+    <div class="card course-card" role="listitem">
+        <a href="{{viewurl}}" tabindex="-1" role="presentation">
+            <div class="card-img-top myoverviewimg" style='background-image: url("{{{courseimage}}}");'>
+                <span class="sr-only">{{#str}}aria:courseimage, block_myoverview{{/str}}</span>
+            </div>
+        </a>
+        <div class="card-body course-info-container" id="course-info-container-{{id}}">
+            <div class="d-flex">
+                <div class="card-title">
+                    <a href="{{viewurl}}">
+                        <span class="sr-only">{{#str}}aria:coursename, block_myoverview{{/str}}</span>
+                        {{#shortentext}}140, {{{fullname}}} {{/shortentext}}
+                    </a>
+                </div>
+            </div>
+        </div>
+        {{#hasprogress}}
+        <div class="card-footer course-card-footer">
+            {{> block_myoverview/progress-bar}}
+        </div>
+        {{/hasprogress}}
+    </div>
+{{/courses}}
+</div>
\ No newline at end of file
diff --git a/blocks/myoverview/templates/view-list.mustache b/blocks/myoverview/templates/view-list.mustache
new file mode 100644 (file)
index 0000000..18e45ad
--- /dev/null
@@ -0,0 +1,55 @@
+{{!
+    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 block_myoverview/view-list
+
+    This template renders the list view for the myoverview block.
+
+    Example context (json):
+    {
+        "courses": [
+            {
+                "name": "Assignment due 1",
+                "viewurl": "https://moodlesite/course/view.php?id=2",
+                "courseimage": "https://moodlesite/pluginfile/123/course/overviewfiles/123.jpg",
+                "fullname": "course 3",
+                "hasprogress": true,
+                "progress": 10
+            }
+        ]
+    }
+}}
+
+<ul class="list-group" role="list">
+{{#courses}}
+    <li class="list-group-item course-listitem" role="listitem">
+        <div class="row-fluid">
+            <div class="{{#hasprogress}}col-6 span6{{/hasprogress}}{{^hasprogress}}col-12 span12{{/hasprogress}}">
+                <a href="{{viewurl}}">
+                    <span class="sr-only">{{#str}}aria:coursename, block_myoverview{{/str}}</span>
+                    {{{fullname}}}
+                </a>
+            </div>
+            {{#hasprogress}}
+            <div class="col-6 span6">
+                {{> block_myoverview/progress-bar}}
+            </div>
+            {{/hasprogress}}
+        </div>
+    </li>
+{{/courses}}
+</ul>
diff --git a/blocks/myoverview/templates/view-summary.mustache b/blocks/myoverview/templates/view-summary.mustache
new file mode 100644 (file)
index 0000000..8f9531c
--- /dev/null
@@ -0,0 +1,62 @@
+{{!
+    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 block_myoverview/view-summary
+
+    This template renders the list view for the myoverview block.
+
+    Example context (json):
+    {
+        "courses": [
+            {
+                "name": "Assignment due 1",
+                "viewurl": "https://moodlesite/course/view.php?id=2",
+                "courseimage": "https://moodlesite/pluginfile/123/course/overviewfiles/123.jpg",
+                "fullname": "course 3",
+                "summary": "This course is about assignments",
+                "hasprogress": true,
+                "progress": 10
+            }
+        ]
+    }
+}}
+<div role="list">
+{{#courses}}
+    <div class="course-summaryitem m-b-1 p-2" role="listitem">
+        <div class="row-fluid d-flex">
+            <a href="{{viewurl}}" class="col-sm-4 col-xl-3 span4" tabindex="-1" role="presentation">
+                <img src="{{{courseimage}}}" class="summaryimage img-fluid" alt="{{#str}}aria:courseimage, block_myoverview{{/str}}">
+            </a>
+            <div class="col-sm-8 col-xl-9 span8 align-self-stretch d-flex flex-column">
+                <a href="{{viewurl}}">
+                    <span class="sr-only">{{#str}}aria:coursename, block_myoverview{{/str}}</span>
+                    <h4>{{{fullname}}}</h4>
+                </a>
+                <div class="summary">
+                    <span class="sr-only">{{#str}}aria:coursesummary, block_myoverview{{/str}}</span>
+                    {{{summary}}}
+                </div>
+                <div class="ml-auto mt-auto w-50 p-t-1">
+                    {{#hasprogress}}
+                        {{> block_myoverview/progress-bar}}
+                    {{/hasprogress}}
+                </div>
+            </div>
+        </div>
+    </div>
+{{/courses}}
+</div>
\ No newline at end of file
index de37359..7607052 100644 (file)
@@ -7,30 +7,62 @@ Feature: The my overview block allows users to easily access their courses
   Background:
     Given the following "users" exist:
       | username | firstname | lastname | email                | idnumber |
-      | student1 | Student   | 1        | student1@example.com | S1       |
-      | student2 | Student   | 2        | student2@example.com | S2       |
+      | student1 | Student   | X        | student1@example.com | S1       |
     And the following "courses" exist:
       | fullname | shortname | category | startdate                   | enddate         |
       | Course 1 | C1        | 0        | ##1 month ago##             | ##15 days ago## |
       | Course 2 | C2        | 0        | ##yesterday##               | ##tomorrow## |
-      | Course 3 | C3        | 0        | ##first day of next month## | ##last day of next month## |
+      | Course 3 | C3        | 0        | ##yesterday##               | ##tomorrow## |
+      | Course 4 | C4        | 0        | ##yesterday##               | ##tomorrow## |
+      | Course 5 | C5        | 0        | ##first day of next month## | ##last day of next month## |
     And the following "course enrolments" exist:
       | user | course | role |
       | student1 | C1 | student |
       | student1 | C2 | student |
       | student1 | C3 | student |
+      | student1 | C4 | student |
+      | student1 | C5 | student |
 
-  Scenario: See the courses I am enrolled by their status on courses view
+  Scenario: View past courses
     Given I log in as "student1"
-    And I should see "Course 2" in the "Course overview" "block"
-    And I should not see "Course 1" in the "Course overview" "block"
-    And I click on "In progress" "button" in the "Course overview" "block"
-    And I click on "Future" "link" in the "Course overview" "block"
-    And I should see "Course 3" in the "Course overview" "block"
-    And I should not see "Course 1" in the "Course overview" "block"
-    And I click on "Future" "button" in the "Course overview" "block"
+    And I click on "All" "button" in the "Course overview" "block"
     When I click on "Past" "link" in the "Course overview" "block"
     Then I should see "Course 1" in the "Course overview" "block"
     And I should not see "Course 2" in the "Course overview" "block"
     And I should not see "Course 3" in the "Course overview" "block"
+    And I should not see "Course 4" in the "Course overview" "block"
+    And I should not see "Course 5" in the "Course overview" "block"
+    And I log out
+
+  Scenario: View future courses
+    Given I log in as "student1"
+    And I click on "All" "button" in the "Course overview" "block"
+    When I click on "Future" "link" in the "Course overview" "block"
+    Then I should see "Course 5" in the "Course overview" "block"
+    And I should not see "Course 1" in the "Course overview" "block"
+    And I should not see "Course 2" in the "Course overview" "block"
+    And I should not see "Course 3" in the "Course overview" "block"
+    And I should not see "Course 4" in the "Course overview" "block"
     And I log out
+
+  Scenario: View inprogress courses
+    Given I log in as "student1"
+    And I click on "All" "button" in the "Course overview" "block"
+    When I click on "In progress" "link" in the "Course overview" "block"
+    Then I should see "Course 2" in the "Course overview" "block"
+    Then I should see "Course 3" in the "Course overview" "block"
+    Then I should see "Course 4" in the "Course overview" "block"
+    And I should not see "Course 1" in the "Course overview" "block"
+    And I should not see "Course 5" in the "Course overview" "block"
+    And I log out
+
+  Scenario: View all courses
+    Given I log in as "student1"
+    And I click on "All" "button" in the "Course overview" "block"
+    When I click on "All" "link" in the "Course overview" "block"
+    Then I should see "Course 1" in the "Course overview" "block"
+    Then I should see "Course 2" in the "Course overview" "block"
+    Then I should see "Course 3" in the "Course overview" "block"
+    Then I should see "Course 4" in the "Course overview" "block"
+    Then I should see "Course 5" in the "Course overview" "block"
+    And I log out
\ No newline at end of file
index 5ed5656..06813f5 100644 (file)
@@ -20,6 +20,13 @@ Feature: Course overview block show users their progress on courses
       | teacher1 | C1 | editingteacher  |
       | student1 | C1 | student         |
 
+  Scenario: Course progress percentage should not be displayed if completion is not enabled
+    Given I log in as "student1"
+    And I click on "All" "button" in the "Course overview" "block"
+    When I click on "All" "link" in the "Course overview" "block"
+    Then I should not see "0%" in the "Course overview" "block"
+    And I log out
+
   Scenario: User complete activity and verify his progress
     Given I log in as "teacher1"
     And I am on "Course 1" course homepage with editing mode on
@@ -30,12 +37,13 @@ Feature: Course overview block show users their progress on courses
       | id_completionview   | 1                                                 |
     And I press "Save and return to course"
     And I log out
-    And I log in as "student1"
+    When I log in as "student1"
+    And I click on "All" "button" in the "Course overview" "block"
     Then I should see "Course 1" in the "Course overview" "block"
     And I should see "0%" in the "Course overview" "block"
     And I am on "Course 1" course homepage
     And I follow "Test choice 1"
     And I follow "Dashboard" in the user menu
-    And I should see "Course 1" in the "Course overview" "block"
+    And I click on "All" "button" in the "Course overview" "block"
     And I should see "100%" in the "Course overview" "block"
     And I log out
index 6b8960c..14b42c4 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2018092700;         // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2018100100;         // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2018050800;         // Requires this Moodle version.
 $plugin->component = 'block_myoverview'; // Full name of the plugin (used for diagnostics).
index c66ad2f..09fbfd9 100644 (file)
Binary files a/blocks/timeline/amd/build/view_nav.min.js and b/blocks/timeline/amd/build/view_nav.min.js differ
index 092dcbd..6fd32a6 100644 (file)
@@ -25,12 +25,16 @@ define(
 [
     'jquery',
     'core/custom_interaction_events',
-    'block_timeline/view'
+    'block_timeline/view',
+    'core/ajax',
+    'core/notification'
 ],
 function(
     $,
     CustomEvents,
-    View
+    View,
+    Ajax,
+    Notification
 ) {
 
     var SELECTORS = {
@@ -41,6 +45,29 @@ function(
         DATA_DAYS_LIMIT: '[data-days-limit]',
     };
 
+    /**
+     * Generic handler to persist user preferences
+     *
+     * @param {string} type The name of the attribute you're updating
+     * @param {string} value The value of the attribute you're updating
+     */
+    var updateUserPreferences = function(type, value) {
+        var request = {
+            methodname: 'core_user_update_user_preferences',
+            args: {
+                preferences: [
+                    {
+                        type: type,
+                        value: value
+                    }
+                ]
+            }
+        };
+
+        Ajax.call([request])[0]
+            .fail(Notification.exception);
+    };
+
     /**
      * Event listener for the day selector ("Next 7 days", "Next 30 days", etc).
      *
@@ -55,6 +82,11 @@ function(
             CustomEvents.events.activate,
             SELECTORS.TIMELINE_DAY_FILTER_OPTION,
             function(e, data) {
+                // Update the user preference
+                var filtername = $(e.currentTarget).data('filtername');
+                var type = 'block_timeline_user_filter_preference';
+                updateUserPreferences(type, filtername);
+
                 var option = $(e.target).closest(SELECTORS.TIMELINE_DAY_FILTER_OPTION);
 
                 if (option.hasClass('active')) {
@@ -94,11 +126,21 @@ function(
      * @param {object} timelineViewRoot The root element for the timeline view
      */
     var registerViewSelector = function(root, timelineViewRoot) {
+        var viewSelector = root.find(SELECTORS.TIMELINE_VIEW_SELECTOR);
+
         // Listen for when the user changes tab so that we can show the first set of courses
         // and load their events when they request the sort by courses view for the first time.
-        root.find(SELECTORS.TIMELINE_VIEW_SELECTOR).on('shown shown.bs.tab', function() {
+        viewSelector.on('shown shown.bs.tab', function() {
             View.shown(timelineViewRoot);
         });
+
+        // Event selector for user_sort
+        CustomEvents.define(viewSelector, [CustomEvents.events.activate]);
+        viewSelector.on(CustomEvents.events.activate, "[data-toggle='tab']", function(e) {
+            var filtername = $(e.currentTarget).data('filtername');
+            var type = 'block_timeline_user_sort_preference';
+            updateUserPreferences(type, filtername);
+        });
     };
 
     /**
index 5a24f9c..03e7230 100644 (file)
@@ -50,7 +50,10 @@ class block_timeline extends block_base {
             return $this->content;
         }
 
-        $renderable = new \block_timeline\output\main();
+        $sort = get_user_preferences('block_timeline_user_sort_preference');
+        $filter = get_user_preferences('block_timeline_user_filter_preference');
+
+        $renderable = new \block_timeline\output\main($sort, $filter);
         $renderer = $this->page->get_renderer('block_timeline');
 
         $this->content = (object) [
index b5e9609..218eb46 100644 (file)
@@ -30,6 +30,7 @@ use templatable;
 use core_course\external\course_summary_exporter;
 
 require_once($CFG->dirroot . '/course/lib.php');
+require_once($CFG->dirroot . '/blocks/timeline/lib.php');
 require_once($CFG->libdir . '/completionlib.php');
 
 /**
@@ -43,6 +44,86 @@ class main implements renderable, templatable {
     /** Number of courses to load per page */
     const COURSES_PER_PAGE = 2;
 
+    /**
+     * @var string The current filter preference
+     */
+    public $filter;
+
+    /**
+     * @var string The current sort/order preference
+     */
+    public $order;
+
+    /**
+     * main constructor.
+     *
+     * @param string $order Constant sort value from ../timeline/lib.php
+     * @param string $filter Constant sort value from ../timeline/lib.php
+     */
+    public function __construct($order, $filter) {
+        $this->order = $order ? $order : BLOCK_TIMELINE_SORT_BY_DATES;
+        $this->filter = $filter ? $filter : BLOCK_TIMELINE_FILTER_BY_7_DAYS;
+    }
+
+    /**
+     * Test the available filters with the current user preference and return an array with
+     * bool flags corresponding to which is active
+     *
+     * @return array
+     */
+    protected function get_filters_as_booleans() {
+        $filters = [
+            BLOCK_TIMELINE_FILTER_BY_NONE => false,
+            BLOCK_TIMELINE_FILTER_BY_OVERDUE => false,
+            BLOCK_TIMELINE_FILTER_BY_7_DAYS => false,
+            BLOCK_TIMELINE_FILTER_BY_30_DAYS => false,
+            BLOCK_TIMELINE_FILTER_BY_3_MONTHS => false,
+            BLOCK_TIMELINE_FILTER_BY_6_MONTHS => false
+        ];
+
+        // Set the selected filter to true.
+        $filters[$this->filter] = true;
+
+        return $filters;
+    }
+
+    /**
+     * Get the offset/limit values corresponding to $this->filter
+     * which are used to send through to the context as default values
+     *
+     * @return array
+     */
+    private function get_filter_offsets() {
+
+        $limit = false;
+        if (in_array($this->filter, [BLOCK_TIMELINE_FILTER_BY_NONE, BLOCK_TIMELINE_FILTER_BY_OVERDUE])) {
+            $offset = -14;
+            if ($this->filter == BLOCK_TIMELINE_FILTER_BY_OVERDUE) {
+                $limit = 0;
+            }
+        } else {
+            $offset = 0;
+            $limit = 7;
+
+            switch($this->filter) {
+                case BLOCK_TIMELINE_FILTER_BY_30_DAYS:
+                    $limit = 30;
+                    break;
+                case BLOCK_TIMELINE_FILTER_BY_3_MONTHS:
+                    $limit = 90;
+                    break;
+                case BLOCK_TIMELINE_FILTER_BY_6_MONTHS:
+                    $limit = 180;
+                    break;
+            }
+        }
+
+        return [
+            'daysoffset' => $offset,
+            'dayslimit' => $limit
+        ];
+    }
+
     /**
      * Export this data so it can be used as the context for a mustache template.
      *
@@ -69,13 +150,22 @@ class main implements renderable, templatable {
             return $exporter->export($output);
         }, $inprogresscourses);
 
-        return [
+        $filters = $this->get_filters_as_booleans();
+        $offsets = $this->get_filter_offsets();
+        $contextvariables = [
             'midnight' => usergetmidnight(time()),
             'coursepages' => [$formattedcourses],
             'urls' => [
                 'nocourses' => $nocoursesurl,
                 'noevents' => $noeventsurl
-            ]
+            ],
+            'sorttimelinedates' => $this->order == BLOCK_TIMELINE_SORT_BY_DATES,
+            'sorttimelinecourses' => $this->order == BLOCK_TIMELINE_SORT_BY_COURSES,
+            'selectedfilter' => $this->filter,
+            'hasdaysoffset' => true,
+            'hasdayslimit' => $offsets['dayslimit'] !== false ,
+            'nodayslimit' => $offsets['dayslimit'] === false ,
         ];
+        return array_merge($contextvariables, $filters, $offsets);
     }
 }
index 02ae526..3be0998 100644 (file)
@@ -25,6 +25,7 @@
 namespace block_timeline\privacy;
 
 defined('MOODLE_INTERNAL') || die();
+use \core_privacy\local\metadata\collection;
 
 /**
  * Privacy Subsystem for block_timeline.
@@ -32,15 +33,40 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class provider implements \core_privacy\local\metadata\null_provider {
+class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\user_preference_provider {
 
     /**
-     * Get the language string identifier with the component's language
-     * file to explain why this plugin stores no data.
+     * Returns meta-data information about the myoverview block.
      *
-     * @return  string
+     * @param  \core_privacy\local\metadata\collection $collection A collection of meta-data.
+     * @return \core_privacy\local\metadata\collection Return the collection of meta-data.
      */
-    public static function get_reason() : string {
-        return 'privacy:metadata';
+    public static function get_metadata(collection $collection) : collection {
+        $collection->add_user_preference('block_timeline_user_sort_preference', 'privacy:metadata:timelinesortpreference');
+        $collection->add_user_preference('block_timeline_user_filter_preference', 'privacy:metadata:timelinefilterpreference');
+        return $collection;
+    }
+
+    /**
+     * Export all user preferences for the myoverview block
+     *
+     * @param int $userid The userid of the user whose data is to be exported.
+     */
+    public static function export_user_preferences(int $userid) {
+        $preference = get_user_preferences('block_timeline_user_sort_preference', null, $userid);
+        if (isset($preference)) {
+            \core_privacy\local\request\writer::export_user_preference('block_timeline', 'block_timeline_user_sort_preference',
+                    get_string($preference, 'block_timeline'),
+                    get_string('privacy:metadata:timelinesortpreference', 'block_timeline')
+            );
+        }
+
+        $preference = get_user_preferences('block_timeline_user_filter_preference', null, $userid);
+        if (isset($preference)) {
+            \core_privacy\local\request\writer::export_user_preference('block_timeline', 'block_timeline_user_filter_preference',
+                    get_string($preference, 'block_timeline'),
+                    get_string('privacy:metadata:timelinefilterpreference', 'block_timeline')
+            );
+        }
     }
 }
index 70f961c..d474cc9 100644 (file)
@@ -41,9 +41,11 @@ $string['next7days'] = 'Next 7 days';
 $string['next3months'] = 'Next 3 months';
 $string['next6months'] = 'Next 6 months';
 $string['overdue'] = 'Overdue';
+$string['all'] = 'All';
 $string['pluginname'] = 'Timeline';
 $string['sortbycourses'] = 'Sort by courses';
 $string['sortbydates'] = 'Sort by dates';
 $string['timeline'] = 'Timeline';
 $string['viewcourse'] = 'View course';
-$string['privacy:metadata'] = 'The timeline block does not store any personal data.';
+$string['privacy:metadata:timelinesortpreference'] = 'The user sort preference for the timeline block.';
+$string['privacy:metadata:timelinefilterpreference'] = 'The user day filter preference for the timeline block.';
diff --git a/blocks/timeline/lib.php b/blocks/timeline/lib.php
new file mode 100644 (file)
index 0000000..26e8783
--- /dev/null
@@ -0,0 +1,70 @@
+<?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/>.
+
+/**
+ * Library functions for timeline
+ *
+ * @package   block_timeline
+ * @copyright 2018 Peter Dias
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Define constants to store the SORT user preference
+ */
+define('BLOCK_TIMELINE_SORT_BY_DATES', 'sortbydates');
+define('BLOCK_TIMELINE_SORT_BY_COURSES', 'sortbycourses');
+
+/**
+ * Define constants to store the FILTER user preference
+ */
+define('BLOCK_TIMELINE_FILTER_BY_NONE', 'all');
+define('BLOCK_TIMELINE_FILTER_BY_OVERDUE', 'overdue');
+define('BLOCK_TIMELINE_FILTER_BY_7_DAYS', 'next7days');
+define('BLOCK_TIMELINE_FILTER_BY_30_DAYS', 'next30days');
+define('BLOCK_TIMELINE_FILTER_BY_3_MONTHS', 'next3months');
+define('BLOCK_TIMELINE_FILTER_BY_6_MONTHS', 'next6months');
+
+/**
+ * Returns the name of the user preferences as well as the details this plugin uses.
+ *
+ * @return array
+ */
+function block_timeline_user_preferences() {
+    $preferences['block_timeline_user_sort_preference'] = array(
+        'null' => NULL_NOT_ALLOWED,
+        'default' => BLOCK_TIMELINE_SORT_BY_DATES,
+        'type' => PARAM_ALPHA,
+        'choices' => array(BLOCK_TIMELINE_SORT_BY_DATES, BLOCK_TIMELINE_SORT_BY_COURSES)
+    );
+
+    $preferences['block_timeline_user_filter_preference'] = array(
+        'null' => NULL_NOT_ALLOWED,
+        'default' => BLOCK_TIMELINE_FILTER_BY_30_DAYS,
+        'type' => PARAM_ALPHANUM,
+        'choices' => array(
+                BLOCK_TIMELINE_FILTER_BY_NONE,
+                BLOCK_TIMELINE_FILTER_BY_OVERDUE,
+                BLOCK_TIMELINE_FILTER_BY_7_DAYS,
+                BLOCK_TIMELINE_FILTER_BY_30_DAYS,
+                BLOCK_TIMELINE_FILTER_BY_3_MONTHS,
+                BLOCK_TIMELINE_FILTER_BY_6_MONTHS
+        )
+    );
+
+    return $preferences;
+}
index 90afd62..938763b 100644 (file)
     </button>
     <div role="menu" class="dropdown-menu" data-show-active-item>
         <a
-            class="dropdown-item"
+            class="dropdown-item {{#all}} active {{/all}}"
             href="#"
             data-from="-14"
+            data-filtername="all"
             aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} all, core {{/str}}{{/str}}"
         >
             {{#str}} all, core {{/str}}
         </a>
         <a
-            class="dropdown-item"
+            class="dropdown-item {{#overdue}} active {{/overdue}}"
             href="#"
             data-from="-14"
             data-to="0"
+            data-filtername="overdue"
             aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} overdue, block_timeline {{/str}}{{/str}}"
         >
             {{#str}} overdue, block_timeline {{/str}}
         <div class="dropdown-divider"></div>
         <h6 class="dropdown-header">{{#str}} duedate, block_timeline {{/str}}</h6>
         <a
-            class="dropdown-item"
+            class="dropdown-item {{#next7days}} active {{/next7days}}"
             href="#"
             data-from="0"
             data-to="7"
+            data-filtername="next7days"
             aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next7days, block_timeline {{/str}}{{/str}}"
         >
             {{#str}} next7days, block_timeline {{/str}}
         </a>
         <a
-            class="dropdown-item active"
+            class="dropdown-item {{#next30days}} active {{/next30days}}"
             href="#"
             data-from="0"
             data-to="30"
+            data-filtername="next30days"
             aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next30days, block_timeline {{/str}}{{/str}}"
         >
             {{#str}} next30days, block_timeline {{/str}}
         </a>
         <a
-            class="dropdown-item"
+            class="dropdown-item {{#next3months}} active {{/next3months}}"
             href="#"
             data-from="0"
             data-to="90"
+            data-filtername="next3months"
             aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next3months, block_timeline {{/str}}{{/str}}"
         >
             {{#str}} next3months, block_timeline {{/str}}
         </a>
         <a
-            class="dropdown-item"
+            class="dropdown-item {{#next6months}} active {{/next6months}}"
             href="#"
             data-from="0"
             data-to="180"
+            data-filtername="next6months"
             aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next6months, block_timeline {{/str}}{{/str}}"
         >
             {{#str}} next6months, block_timeline {{/str}}
index 50c2add..c7cbc1c 100644 (file)
     <button type="button" class="btn btn-outline-secondary dropdown-toggle icon-no-margin" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
         {{#pix}} i/filter {{/pix}}
         <span class="sr-only">
-            {{#str}} ariaviewselector, block_timeline{{/str}}
-            <span data-active-item-text>{{#str}} sortbydates, block_timeline {{/str}}</span>
+            {{#sorttimelinecourses}}<span data-active-item-text>{{/sorttimelinecourses}}{{#str}} ariaviewselector, block_timeline{{/str}}{{#sorttimelinecourses}}</span>{{/sorttimelinecourses}}
+            {{#sorttimelinedates}}<span data-active-item-text>{{/sorttimelinedates}}{{#str}} sortbydates, block_timeline {{/str}}{{#sorttimelinedates}}</span>{{/sorttimelinedates}}
         </span>
     </button>
-    <div role="menu" class="dropdown-menu dropdown-menu-right list-group hidden" data-show-active-item data-skip-active-class="true">
+    <div role="menu" class="dropdown-menu dropdown-menu-right list-group hidden" data-show-active-item data-skip-active-class="true" >
         <a
-            class="dropdown-item active"
+            class="dropdown-item {{#sorttimelinedates}}active{{/sorttimelinedates}}"
             href="#view_dates_{{uniqid}}"
             data-toggle="tab"
+            data-filtername="sortbydates"
             aria-label="{{#str}} ariaviewselectoroption, block_timeline, {{#str}} sortbydates, block_timeline {{/str}}{{/str}}"
         >
             {{#str}} sortbydates, block_timeline {{/str}}
         </a>
         <a
-            class="dropdown-item"
+            class="dropdown-item {{#sorttimelinecourses}}active{{/sorttimelinecourses}}"
             href="#view_courses_{{uniqid}}"
             data-toggle="tab"
+            data-filtername="sortbycourses"
             aria-label="{{#str}} ariaviewselectoroption, block_timeline, {{#str}} sortbycourses, block_timeline {{/str}}{{/str}}"
         >
             {{#str}} sortbycourses, block_timeline {{/str}}
index 73decee..acb5f78 100644 (file)
     This template renders the timeline view for the timeline block.
 
     Example context (json):
-    {}
+    {
+        "midnight": 1538954668,
+        "coursepages": [
+            {}
+        ],
+        "urls": {
+            "nocourses": "#",
+            "noevents": "#"
+        },
+        "sorttimelinedates": true,
+        "sorttimelinecourses": false,
+        "selectedfilter": "all",
+        "hasdaysoffset": true,
+        "hasdayslimit": false,
+        "nodayslimit": true,
+        "all": true,
+        "overdue": false,
+        "next7days": false,
+        "next30days": false,
+        "next3months": false,
+        "next6months": false,
+        "daysoffset": -14,
+        "dayslimit": false
+    }
 }}
 <div data-region="timeline-view">
     <div class="tab-content">
-        <div class="tab-pane active fade show" data-region="view-dates" id="view_dates_{{uniqid}}">
+        <div class="tab-pane {{#sorttimelinedates}}active show{{/sorttimelinedates}} fade" data-region="view-dates" id="view_dates_{{uniqid}}">
             {{> block_timeline/view-dates }}
         </div>
         <div
-            class="tab-pane fade"
+            class="tab-pane {{#sorttimelinecourses}}active show{{/sorttimelinecourses}} fade"
             data-region="view-courses"
             data-midnight="{{midnight}}"
             data-limit="2"
index 2da3916..d9a8a20 100644 (file)
@@ -70,3 +70,22 @@ Feature: The timeline block allows users to see upcoming courses
     And I should not see "Course 4" in the "Timeline" "block"
     And I should not see "Test choice 2 closes" in the "Timeline" "block"
     And I should not see "Test feedback 4 closes" in the "Timeline" "block"
+
+  Scenario: Persistent sort filter
+    Given I log in as "student1"
+    And I click on "Sort" "button" in the "Timeline" "block"
+    And I click on "Sort by dates" "link" in the "Timeline" "block"
+    And I click on "Sort" "button" in the "Timeline" "block"
+    And I click on "Sort by courses" "link" in the "Timeline" "block"
+    And I reload the page
+    Then I should see "Course 1" in the "Timeline" "block"
+    And I should see "Course 2" in the "Timeline" "block"
+    And I should see "More courses" in the "Timeline" "block"
+    And I should see "Test choice 1 closes" in the "Timeline" "block"
+    And I should see "Test feedback 1 closes" in the "Timeline" "block"
+    And I should not see "Course 3" in the "Timeline" "block"
+    And I should not see "Test choice 2 closes" in the "Timeline" "block"
+    And I should not see "Test choice 3 closes" in the "Timeline" "block"
+    And I should not see "Test feedback 2 closes" in the "Timeline" "block"
+    And I should not see "Test feedback 3 closes" in the "Timeline" "block"
+    And I should not see "Test assign 1 is due" in the "Timeline" "block"
index 22c04ec..29548a0 100644 (file)
@@ -86,3 +86,36 @@ Feature: The timeline block allows users to see upcoming activities
     And I should see "Test feedback 3 closes" in the "Timeline" "block"
     And I should see "Test feedback 2 closes" in the "Timeline" "block"
     And I should not see "Test choice 2 closes" in the "Timeline" "block"
+
+  Scenario: Persistent All in date view
+    Given I log in as "student1"
+    And I click on "Next 30 days" "button" in the "Timeline" "block"
+    When I click on "All" "link" in the "Timeline" "block"
+    And I reload the page
+    Then I should see "Test assign 1 is due" in the "Timeline" "block"
+    And I should see "Test feedback 1 closes" in the "Timeline" "block"
+    And I should see "Test choice 1 closes" in the "Timeline" "block"
+    And I should see "Test choice 3 closes" in the "Timeline" "block"
+    And I should see "Test feedback 3 closes" in the "Timeline" "block"
+    And I should not see "Test choice 2 closes" in the "Timeline" "block"
+    And I should not see "Test feedback 2 closes" in the "Timeline" "block"
+    And I click on "[data-region='paging-bar'] [data-control='next']" "css_element" in the "Timeline" "block"
+    And I should see "Test feedback 2 closes" in the "Timeline" "block"
+    And I should not see "Test assign 1 is due" in the "Timeline" "block"
+    And I should not see "Test feedback 1 closes" in the "Timeline" "block"
+    And I should not see "Test choice 1 closes" in the "Timeline" "block"
+    And I should not see "Test choice 3 closes" in the "Timeline" "block"
+    And I should not see "Test feedback 3 closes" in the "Timeline" "block"
+    And I should not see "Test choice 2 closes" in the "Timeline" "block"
+
+  Scenario: Persistent Overdue in date view
+    Given I log in as "student1"
+    And I click on "Next 30 days" "button" in the "Timeline" "block"
+    When I click on "Overdue" "link" in the "Timeline" "block"
+    And I reload the page
+    Then I should see "Test assign 1 is due" in the "Timeline" "block"
+    And I should not see "Test choice 2 closes" in the "Timeline" "block"
+    And I should not see "Test feedback 1 closes" in the "Timeline" "block"
+    And I should not see "Test choice 1 closes" in the "Timeline" "block"
+    And I should not see "Test choice 3 closes" in the "Timeline" "block"
+    And I should not see "Test feedback 3 closes" in the "Timeline" "block"
diff --git a/blocks/timeline/tests/privacy_test.php b/blocks/timeline/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..0aacf3e
--- /dev/null
@@ -0,0 +1,110 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for the block_timeline implementation of the privacy API.
+ *
+ * @package    block_timeline
+ * @category   test
+ * @copyright  2018 Peter Dias <peter@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use \core_privacy\local\request\writer;
+use \block_timeline\privacy\provider;
+
+/**
+ * Unit tests for the block_timeline implementation of the privacy API.
+ *
+ * @copyright  2018 Peter Dias <peter@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class block_timeline_privacy_testcase extends \core_privacy\tests\provider_testcase {
+
+    /**
+     * Ensure that export_user_preferences returns no data if the user has not visited the myoverview block.
+     */
+    public function test_export_user_preferences_no_pref() {
+        $this->resetAfterTest();
+
+        $user = $this->getDataGenerator()->create_user();
+        provider::export_user_preferences($user->id);
+        $writer = writer::with_context(\context_system::instance());
+        $this->assertFalse($writer->has_any_data());
+    }
+
+    /**
+     * Test that the preference courses is exported properly.
+     */
+    public function test_export_user_preferences_date_sort_preference() {
+        $this->resetAfterTest();
+
+        $user = $this->getDataGenerator()->create_user();
+        set_user_preference('block_timeline_user_sort_preference', 'sortbydates', $user);
+
+        provider::export_user_preferences($user->id);
+        $writer = writer::with_context(\context_system::instance());
+        $blockpreferences = $writer->get_user_preferences('block_timeline');
+        $this->assertEquals('Sort by dates', $blockpreferences->block_timeline_user_sort_preference->value);
+    }
+
+    /**
+     * Test that the preference timeline is exported properly.
+     */
+    public function test_export_user_preferences_course_sort_preference() {
+        $this->resetAfterTest();
+
+        $user = $this->getDataGenerator()->create_user();
+        set_user_preference('block_timeline_user_sort_preference', 'sortbycourses', $user);
+
+        provider::export_user_preferences($user->id);
+        $writer = writer::with_context(\context_system::instance());
+        $blockpreferences = $writer->get_user_preferences('block_timeline');
+        $this->assertEquals('Sort by courses', $blockpreferences->block_timeline_user_sort_preference->value);
+    }
+
+    /**
+     * Test that the preference timeline is exported properly.
+     */
+    public function test_export_user_preferences_7day_filter_preference() {
+        $this->resetAfterTest();
+
+        $user = $this->getDataGenerator()->create_user();
+        set_user_preference('block_timeline_user_filter_preference', 'next7days', $user);
+
+        provider::export_user_preferences($user->id);
+        $writer = writer::with_context(\context_system::instance());
+        $blockpreferences = $writer->get_user_preferences('block_timeline');
+        $this->assertEquals('Next 7 days', $blockpreferences->block_timeline_user_filter_preference->value);
+    }
+
+    /**
+     * Test that the preference timeline is exported properly.
+     */
+    public function test_export_user_preferences_all_filter_preference() {
+        $this->resetAfterTest();
+
+        $user = $this->getDataGenerator()->create_user();
+        set_user_preference('block_timeline_user_filter_preference', 'all', $user);
+
+        provider::export_user_preferences($user->id);
+        $writer = writer::with_context(\context_system::instance());
+        $blockpreferences = $writer->get_user_preferences('block_timeline');
+        $this->assertEquals('All', $blockpreferences->block_timeline_user_filter_preference->value);
+    }
+}
index 342296e..032c018 100644 (file)
@@ -41,9 +41,22 @@ class course_summary_exporter extends \core\external\exporter {
     }
 
     protected function get_other_values(renderer_base $output) {
+        $courseimage = self::get_course_image($this->data);
+        if (!$courseimage) {
+            $courseimage = self::get_course_pattern($this->data);
+        }
+        $progress = self::get_course_progress($this->data);
+        $hasprogress = false;
+        if ($progress === 0 || $progress > 0) {
+            $hasprogress = true;
+        }
+        $progress = floor($progress);
         return array(
             'fullnamedisplay' => get_course_display_name_for_list($this->data),
-            'viewurl' => (new moodle_url('/course/view.php', array('id' => $this->data->id)))->out(false)
+            'viewurl' => (new moodle_url('/course/view.php', array('id' => $this->data->id)))->out(false),
+            'courseimage' => $courseimage,
+            'progress' => $progress,
+            'hasprogress' => $hasprogress
         );
     }
 
@@ -96,7 +109,81 @@ class course_summary_exporter extends \core\external\exporter {
             ),
             'viewurl' => array(
                 'type' => PARAM_URL,
+            ),
+            'courseimage' => array(
+                'type' => PARAM_RAW,
+            ),
+            'progress' => array(
+                'type' => PARAM_INT,
+                'optional' => true
+            ),
+            'hasprogress' => array(
+                'type' => PARAM_BOOL
             )
         );
     }
+
+    /**
+     * Get the course image if added to course.
+     *
+     * @param object $course
+     * @return string url of course image
+     */
+    public static function get_course_image($course) {
+        global $CFG;
+        $courseinlist = new \core_course_list_element($course);
+        foreach ($courseinlist->get_course_overviewfiles() as $file) {
+            if ($file->is_valid_image()) {
+                $pathcomponents = [
+                    '/pluginfile.php',
+                    $file->get_contextid(),
+                    $file->get_component(),
+                    $file->get_filearea() . $file->get_filepath() . $file->get_filename()
+                ];
+                $path = implode('/', $pathcomponents);
+                return (new moodle_url($path))->out();
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Get the course pattern datauri.
+     *
+     * The datauri is an encoded svg that can be passed as a url.
+     * @param object $course
+     * @return string datauri
+     */
+    public static function get_course_pattern($course) {
+        $color = self::coursecolor($course->id);
+        $pattern = new \core_geopattern();
+        $pattern->setColor($color);
+        $pattern->patternbyid($course->id);
+        return $pattern->datauri();
+    }
+
+    /**
+     * Get the course progress percentage.
+     *
+     * @param object $course
+     * @return int progress
+     */
+    public static function get_course_progress($course) {
+        return \core_completion\progress::get_course_progress_percentage($course);
+    }
+
+    /**
+     * Get the course color.
+     *
+     * @param int $courseid
+     * @return string hex color code.
+     */
+    public static function coursecolor($courseid) {
+        // The colour palette is hardcoded for now. It would make sense to combine it with theme settings.
+        $basecolors = ['#81ecec', '#74b9ff', '#a29bfe', '#dfe6e9', '#00b894',
+            '#0984e3', '#b2bec3', '#fdcb6e', '#fd79a8', '#6c5ce7'];
+
+        $color = $basecolors[$courseid % 10];
+        return $color;
+    }
 }
index cb699fa..ea887ce 100644 (file)
@@ -58,6 +58,8 @@ class core_course_external extends external_api {
                                                 'The expected keys (value format) are:
                                                 excludemodules (bool) Do not return modules, return only the sections structure
                                                 excludecontents (bool) Do not return module contents (i.e: files inside a resource)
+                                                includestealthmodules (bool) Return stealth modules for students in a special
+                                                    section (with id -1)
                                                 sectionid (int) Return only this section
                                                 sectionnumber (int) Return only this section with number (order)
                                                 cmid (int) Return only this module information (among the whole sections structure)
@@ -98,6 +100,7 @@ class core_course_external extends external_api {
                     switch ($name) {
                         case 'excludemodules':
                         case 'excludecontents':
+                        case 'includestealthmodules':
                             $value = clean_param($option['value'], PARAM_BOOL);
                             $filters[$name] = $value;
                             break;
@@ -163,21 +166,12 @@ class core_course_external extends external_api {
             $modinfo = get_fast_modinfo($course);
             $sections = $modinfo->get_section_info_all();
             $coursenumsections = course_get_format($course)->get_last_section_number();
+            $stealthmodules = array();   // Array to keep all the modules available but not visible in a course section/topic.
 
             //for each sections (first displayed to last displayed)
             $modinfosections = $modinfo->get_sections();
             foreach ($sections as $key => $section) {
 
-                // Show the section if the user is permitted to access it, OR if it's not available
-                // but there is some available info text which explains the reason & should display.
-                $showsection = $section->uservisible ||
-                    ($section->visible && !$section->available &&
-                    !empty($section->availableinfo));
-
-                if (!$showsection) {
-                    continue;
-                }
-
                 // This becomes true when we are filtering and we found the value to filter with.
                 $sectionfound = false;
 
@@ -218,8 +212,8 @@ class core_course_external extends external_api {
 
                 $sectioncontents = array();
 
-                // For each module of the section (if it is visible).
-                if ($section->uservisible and empty($filters['excludemodules']) and !empty($modinfosections[$section->section])) {
+                // For each module of the section.
+                if (empty($filters['excludemodules']) and !empty($modinfosections[$section->section])) {
                     foreach ($modinfosections[$section->section] as $cmid) {
                         $cm = $modinfo->cms[$cmid];
 
@@ -312,8 +306,13 @@ class core_course_external extends external_api {
                             }
                         }
 
-                        //assign result to $sectioncontents
-                        $sectioncontents[] = $module;
+                        // Assign result to $sectioncontents, there is an exception,
+                        // stealth activities in non-visible sections for students go to a special section.
+                        if (!empty($filters['includestealthmodules']) && !$section->uservisible && $cm->is_stealth()) {
+                            $stealthmodules[] = $module;
+                        } else {
+                            $sectioncontents[] = $module;
+                        }
 
                         // If we just did a filtering, break the loop.
                         if ($modfound) {
@@ -325,13 +324,46 @@ class core_course_external extends external_api {
                 $sectionvalues['modules'] = $sectioncontents;
 
                 // assign result to $coursecontents
-                $coursecontents[] = $sectionvalues;
+                $coursecontents[$key] = $sectionvalues;
 
                 // Break the loop if we are filtering.
                 if ($sectionfound) {
                     break;
                 }
             }
+
+            // Now that we have iterated over all the sections and activities, check the visibility.
+            // We didn't this before to be able to retrieve stealth activities.
+            foreach ($coursecontents as $sectionnumber => $sectioncontents) {
+                $section = $sections[$sectionnumber];
+                // Show the section if the user is permitted to access it, OR if it's not available
+                // but there is some available info text which explains the reason & should display.
+                $showsection = $section->uservisible ||
+                    ($section->visible && !$section->available &&
+                    !empty($section->availableinfo));
+
+                if (!$showsection) {
+                    unset($coursecontents[$sectionnumber]);
+                    continue;
+                }
+
+                // Remove modules information if the section is not visible for the user.
+                if (!$section->uservisible) {
+                    $coursecontents[$sectionnumber]['modules'] = array();
+                }
+            }
+
+            // Include stealth modules in special section (without any info).
+            if (!empty($stealthmodules)) {
+                $coursecontents[] = array(
+                    'id' => -1,
+                    'name' => '',
+                    'summary' => '',
+                    'summaryformat' => FORMAT_MOODLE,
+                    'modules' => $stealthmodules
+                );
+            }
+
         }
         return $coursecontents;
     }
@@ -3019,6 +3051,7 @@ class core_course_external extends external_api {
      */
     public static function get_courses_by_field($field = '', $value = '') {
         global $DB, $CFG;
+        require_once($CFG->dirroot . '/course/lib.php');
         require_once($CFG->libdir . '/filterlib.php');
 
         $params = self::validate_parameters(self::get_courses_by_field_parameters(),
@@ -3622,6 +3655,8 @@ class core_course_external extends external_api {
         $sort = $params['sort'];
 
         switch($classification) {
+            case COURSE_TIMELINE_ALL:
+                break;
             case COURSE_TIMELINE_PAST:
                 break;
             case COURSE_TIMELINE_INPROGRESS:
index a20a219..cba9402 100644 (file)
@@ -55,6 +55,7 @@ define('FIRSTUSEDEXCELROW', 3);
 define('MOD_CLASS_ACTIVITY', 0);
 define('MOD_CLASS_RESOURCE', 1);
 
+define('COURSE_TIMELINE_ALL', 'all');
 define('COURSE_TIMELINE_PAST', 'past');
 define('COURSE_TIMELINE_INPROGRESS', 'inprogress');
 define('COURSE_TIMELINE_FUTURE', 'future');
@@ -4239,8 +4240,9 @@ function course_filter_courses_by_timeline_classification(
     int $limit = 0
 ) : array {
 
-    if (!in_array($classification, [COURSE_TIMELINE_PAST, COURSE_TIMELINE_INPROGRESS, COURSE_TIMELINE_FUTURE])) {
-        $message = 'Classification must be one of COURSE_TIMELINE_PAST, '
+    if (!in_array($classification,
+            [COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, COURSE_TIMELINE_INPROGRESS, COURSE_TIMELINE_FUTURE])) {
+        $message = 'Classification must be one of COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, '
             . 'COURSE_TIMELINE_INPROGRESS or COURSE_TIMELINE_FUTURE';
         throw new moodle_exception($message);
     }
@@ -4252,7 +4254,7 @@ function course_filter_courses_by_timeline_classification(
     foreach ($courses as $course) {
         $numberofcoursesprocessed++;
 
-        if ($classification == course_classify_for_timeline($course)) {
+        if ($classification == COURSE_TIMELINE_ALL || $classification == course_classify_for_timeline($course)) {
             $filteredcourses[] = $course;
             $filtermatches++;
         }
index c476088..e7759b0 100644 (file)
@@ -806,8 +806,11 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
      * @return array A list with the course object and course modules objects
      */
     private function prepare_get_course_contents_test() {
-        global $DB;
-        $course  = self::getDataGenerator()->create_course(['numsections' => 3]);
+        global $DB, $CFG;
+
+        $CFG->allowstealth = 1; // Allow stealth activities.
+
+        $course  = self::getDataGenerator()->create_course(['numsections' => 4]);
         $forumdescription = 'This is the forum description';
         $forum = $this->getDataGenerator()->create_module('forum',
             array('course' => $course->id, 'intro' => $forumdescription),
@@ -817,6 +820,8 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $datacm = get_coursemodule_from_instance('page', $data->id);
         $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id));
         $pagecm = get_coursemodule_from_instance('page', $page->id);
+        // This is an stealth page (set by visibleoncoursepage).
+        $pagestealth = $this->getDataGenerator()->create_module('page', array('course' => $course->id, 'visibleoncoursepage' => 0));
         $labeldescription = 'This is a very long label to test if more than 50 characters are returned.
                 So bla bla bla bla <b>bold bold bold</b> bla bla bla bla.';
         $label = $this->getDataGenerator()->create_module('label', array('course' => $course->id,
@@ -846,10 +851,18 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $conditions = array('course' => $course->id, 'section' => 2);
         $DB->set_field('course_sections', 'summary', 'Text with iframe <iframe src="https://moodle.org"></iframe>', $conditions);
 
-        // Add date availability condition not met for last section.
+        // Add date availability condition not met for section 3.
         $availability = '{"op":"&","c":[{"type":"date","d":">=","t":' . $tomorrow . '}],"showc":[true]}';
         $DB->set_field('course_sections', 'availability', $availability,
                 array('course' => $course->id, 'section' => 3));
+
+        // Create resource for last section.
+        $pageinhiddensection = $this->getDataGenerator()->create_module('page',
+            array('course' => $course->id, 'name' => 'Page in hidden section', 'section' => 4));
+        // Set not visible last section.
+        $DB->set_field('course_sections', 'visible', 0,
+                array('course' => $course->id, 'section' => 4));
+
         rebuild_course_cache($course->id, true);
 
         return array($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm);
@@ -863,6 +876,8 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
 
         list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
 
+        // We first run the test as admin.
+        $this->setAdminUser();
         $sections = core_course_external::get_course_contents($course->id, array());
         // We need to execute the return values cleaning process to simulate the web service server.
         $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
@@ -889,21 +904,18 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals(2, $testexecuted);
         $this->assertEquals(0, $sections[0]['section']);
 
-        // Check that the only return section has the 5 created modules.
-        $this->assertCount(4, $sections[0]['modules']);
+        $this->assertCount(5, $sections[0]['modules']);
         $this->assertCount(1, $sections[1]['modules']);
         $this->assertCount(1, $sections[2]['modules']);
-        $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
+        $this->assertCount(1, $sections[3]['modules']); // One module for the section with availability restrictions.
+        $this->assertCount(1, $sections[4]['modules']); // One module for the hidden section with a visible activity.
         $this->assertNotEmpty($sections[3]['availabilityinfo']);
         $this->assertEquals(1, $sections[1]['section']);
         $this->assertEquals(2, $sections[2]['section']);
         $this->assertEquals(3, $sections[3]['section']);
+        $this->assertEquals(4, $sections[4]['section']);
         $this->assertContains('<iframe', $sections[2]['summary']);
         $this->assertContains('</iframe>', $sections[2]['summary']);
-        // The module with the availability restriction met is returning contents.
-        $this->assertNotEmpty($sections[1]['modules'][0]['contents']);
-        // The module with the availability restriction not met is not returning contents.
-        $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]);
         $this->assertNotEmpty($sections[2]['modules'][0]['availabilityinfo']);
         try {
             $sections = core_course_external::get_course_contents($course->id,
@@ -915,6 +927,54 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
     }
 
 
+    /**
+     * Test get_course_contents as student
+     */
+    public function test_get_course_contents_student() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
+
+        $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
+        $user = self::getDataGenerator()->create_user();
+        self::getDataGenerator()->enrol_user($user->id, $course->id, $studentroleid);
+        $this->setUser($user);
+
+        $sections = core_course_external::get_course_contents($course->id, array());
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
+
+        $this->assertCount(4, $sections); // Nothing for the not visible section.
+        $this->assertCount(5, $sections[0]['modules']);
+        $this->assertCount(1, $sections[1]['modules']);
+        $this->assertCount(1, $sections[2]['modules']);
+        $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
+
+        $this->assertNotEmpty($sections[3]['availabilityinfo']);
+        $this->assertEquals(1, $sections[1]['section']);
+        $this->assertEquals(2, $sections[2]['section']);
+        $this->assertEquals(3, $sections[3]['section']);
+        // The module with the availability restriction met is returning contents.
+        $this->assertNotEmpty($sections[1]['modules'][0]['contents']);
+        // The module with the availability restriction not met is not returning contents.
+        $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]);
+
+        // Now include flag for returning stealth information (fake section).
+        $sections = core_course_external::get_course_contents($course->id,
+            array(array("name" => "includestealthmodules", "value" => 1)));
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
+
+        $this->assertCount(5, $sections); // Include fake section with stealth activities.
+        $this->assertCount(5, $sections[0]['modules']);
+        $this->assertCount(1, $sections[1]['modules']);
+        $this->assertCount(1, $sections[2]['modules']);
+        $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
+        $this->assertCount(1, $sections[4]['modules']); // One stealh module.
+        $this->assertEquals(-1, $sections[4]['id']);
+    }
+
     /**
      * Test get_course_contents excluding modules
      */
@@ -972,7 +1032,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
 
         $this->assertCount(1, $sections);
-        $this->assertCount(4, $sections[0]['modules']);
+        $this->assertCount(5, $sections[0]['modules']);
     }
 
     /**
@@ -2496,6 +2556,66 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
                 'expectedcourses' => ['cfuture', 'dfuture', 'efuture'],
                 'expectednextoffset' => 15
             ],
+            'all no limit or offset' => [
+                'coursedata' => $coursedata,
+                'classification' => 'all',
+                'limit' => 0,
+                'offset' => 0,
+                'expectedcourses' => [
+                    'afuture',
+                    'ainprogress',
+                    'apast',
+                    'bfuture',
+                    'binprogress',
+                    'bpast',
+                    'cfuture',
+                    'cinprogress',
+                    'cpast',
+                    'dfuture',
+                    'dinprogress',
+                    'dpast',
+                    'efuture',
+                    'einprogress',
+                    'epast'
+                ],
+                'expectednextoffset' => 15
+            ],
+            'all limit no offset' => [
+                'coursedata' => $coursedata,
+                'classification' => 'all',
+                'limit' => 5,
+                'offset' => 0,
+                'expectedcourses' => [
+                    'afuture',
+                    'ainprogress',
+                    'apast',
+                    'bfuture',
+                    'binprogress'
+                ],
+                'expectednextoffset' => 5
+            ],
+            'all limit and offset' => [
+                'coursedata' => $coursedata,
+                'classification' => 'all',
+                'limit' => 5,
+                'offset' => 5,
+                'expectedcourses' => [
+                    'bpast',
+                    'cfuture',
+                    'cinprogress',
+                    'cpast',
+                    'dfuture'
+                ],
+                'expectednextoffset' => 10
+            ],
+            'all offset past result set' => [
+                'coursedata' => $coursedata,
+                'classification' => 'all',
+                'limit' => 5,
+                'offset' => 50,
+                'expectedcourses' => [],
+                'expectednextoffset' => 50
+            ],
         ];
     }
 
index 21e77e9..64e2cb9 100644 (file)
@@ -37,7 +37,7 @@ class enrol_paypal_plugin extends enrol_plugin {
         // See https://www.paypal.com/cgi-bin/webscr?cmd=p/sell/mc/mc_intro-outside,
         // 3-character ISO-4217: https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_currency_codes
         $codes = array(
-            'AUD', 'BRL', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', 'HUF', 'ILS', 'JPY',
+            'AUD', 'BRL', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', 'HUF', 'ILS', 'INR', 'JPY',
             'MXN', 'MYR', 'NOK', 'NZD', 'PHP', 'PLN', 'RUB', 'SEK', 'SGD', 'THB', 'TRY', 'TWD', 'USD');
         $currencies = array();
         foreach ($codes as $c) {
index 9e42d55..794c17d 100644 (file)
@@ -21,7 +21,7 @@ Feature: Test role visibility for the participants page
 
   Scenario: Check the default roles are visible
     Given I log in as "manager1"
-    And I follow "Course 1"
+    And I am on "Course 1" course homepage
     When I navigate to "Users > Enrolled users" in current page administration
     Then "Learner 1" row "Roles" column of "participants" table should contain "Student"
     And "Teacher 1" row "Roles" column of "participants" table should contain "Teacher"
index 0cabb96..34b5b48 100644 (file)
@@ -792,4 +792,186 @@ class core_enrollib_testcase extends advanced_testcase {
         // There are still only two distinct users.
         $this->assertEquals(2, count_enrolled_users($context));
     }
+
+    /**
+     * Test cases for the test_enrol_get_my_courses_sort_by_last_access test.
+     */
+    public function get_enrol_get_my_courses_sort_by_last_access_test_cases() {
+        $now = time();
+
+        $enrolledcoursesdata = [
+            ['shortname' => 'a', 'lastaccess' => $now - 2],
+            ['shortname' => 'b', 'lastaccess' => $now - 1],
+            ['shortname' => 'c', 'lastaccess' => $now],
+            ['shortname' => 'd', 'lastaccess' => $now - 1],
+            ['shortname' => 'e']
+        ];
+        $unenrolledcoursesdata = [
+            ['shortname' => 'x', 'lastaccess' => $now - 2],
+            ['shortname' => 'y', 'lastaccess' => $now - 1],
+            ['shortname' => 'z', 'lastaccess' => $now]
+        ];
+
+        return [
+            'empty set' => [
+                'enrolledcoursesdata' => [],
+                'unenrolledcoursesdata' => $unenrolledcoursesdata,
+                'sort' => 'ul.timeaccess asc',
+                'limit' => 0,
+                'offset' => 0,
+                'expectedcourses' => []
+            ],
+            'ul.timeaccess asc, shortname asc no limit or offset' => [
+                'enrolledcoursesdata' => $enrolledcoursesdata,
+                'unenrolledcoursesdata' => $unenrolledcoursesdata,
+                'sort' => 'ul.timeaccess asc, shortname asc',
+                'limit' => 0,
+                'offset' => 0,
+                'expectedcourses' => ['e', 'a', 'b', 'd', 'c']
+            ],
+            'ul.timeaccess asc, shortname asc with limit no offset' => [
+                'enrolledcoursesdata' => $enrolledcoursesdata,
+                'unenrolledcoursesdata' => $unenrolledcoursesdata,
+                'sort' => 'ul.timeaccess asc, shortname asc',
+                'limit' => 2,
+                'offset' => 0,
+                'expectedcourses' => ['e', 'a']
+            ],
+            'ul.timeaccess asc, shortname asc with limit and offset' => [
+                'enrolledcoursesdata' => $enrolledcoursesdata,
+                'unenrolledcoursesdata' => $unenrolledcoursesdata,
+                'sort' => 'ul.timeaccess asc, shortname asc',
+                'limit' => 2,
+                'offset' => 2,
+                'expectedcourses' => ['b', 'd']
+            ],
+            'ul.timeaccess asc, shortname asc with limit and offset beyond end of data set' => [
+                'enrolledcoursesdata' => $enrolledcoursesdata,
+                'unenrolledcoursesdata' => $unenrolledcoursesdata,
+                'sort' => 'ul.timeaccess asc, shortname asc',
+                'limit' => 2,
+                'offset' => 4,
+                'expectedcourses' => ['c']
+            ],
+            'ul.timeaccess desc, shortname asc no limit or offset' => [
+                'enrolledcoursesdata' => $enrolledcoursesdata,
+                'unenrolledcoursesdata' => $unenrolledcoursesdata,
+                'sort' => 'ul.timeaccess desc, shortname asc',
+                'limit' => 0,
+                'offset' => 0,
+                'expectedcourses' => ['c', 'b', 'd', 'a', 'e']
+            ],
+            'ul.timeaccess desc, shortname desc, no limit or offset' => [
+                'enrolledcoursesdata' => $enrolledcoursesdata,
+                'unenrolledcoursesdata' => $unenrolledcoursesdata,
+                'sort' => 'ul.timeaccess desc, shortname desc',
+                'limit' => 0,
+                'offset' => 0,
+                'expectedcourses' => ['c', 'd', 'b', 'a', 'e']
+            ],
+            'ul.timeaccess asc, shortname desc, no limit or offset' => [
+                'enrolledcoursesdata' => $enrolledcoursesdata,
+                'unenrolledcoursesdata' => $unenrolledcoursesdata,
+                'sort' => 'ul.timeaccess asc, shortname desc',
+                'limit' => 0,
+                'offset' => 0,
+                'expectedcourses' => ['e', 'a', 'd', 'b', 'c']
+            ],
+            'shortname asc, no limit or offset' => [
+                'enrolledcoursesdata' => $enrolledcoursesdata,
+                'unenrolledcoursesdata' => $unenrolledcoursesdata,
+                'sort' => 'shortname asc',
+                'limit' => 0,
+                'offset' => 0,
+                'expectedcourses' => ['a', 'b', 'c', 'd', 'e']
+            ],
+            'shortname desc, no limit or offset' => [
+                'enrolledcoursesdata' => $enrolledcoursesdata,
+                'unenrolledcoursesdata' => $unenrolledcoursesdata,
+                'sort' => 'shortname desc',
+                'limit' => 0,
+                'offset' => 0,
+                'expectedcourses' => ['e', 'd', 'c', 'b', 'a']
+            ],
+        ];
+    }
+
+    /**
+     * Test the get_enrolled_courses_by_timeline_classification function.
+     *
+     * @dataProvider get_enrol_get_my_courses_sort_by_last_access_test_cases()
+     * @param array $enrolledcoursesdata Courses to create and enrol the user in
+     * @param array $unenrolledcoursesdata Courses to create nut not enrol the user in
+     * @param string $sort Sort string for the enrol function
+     * @param int $limit Maximum number of results
+     * @param int $offset Offset the courses result set by this amount
+     * @param array $expectedcourses Expected courses in result
+     */
+    public function test_enrol_get_my_courses_sort_by_last_access(
+        $enrolledcoursesdata,
+        $unenrolledcoursesdata,
+        $sort,
+        $limit,
+        $offset,
+        $expectedcourses
+    ) {
+        global $DB, $CFG;
+
+        $this->resetAfterTest();
+        $generator = $this->getDataGenerator();
+        $student = $generator->create_user();
+        $lastaccessrecords = [];
+
+        foreach ($enrolledcoursesdata as $coursedata) {
+            $lastaccess = null;
+
+            if (isset($coursedata['lastaccess'])) {
+                $lastaccess = $coursedata['lastaccess'];
+                unset($coursedata['lastaccess']);
+            }
+
+            $course = $generator->create_course($coursedata);
+            $generator->enrol_user($student->id, $course->id, 'student');
+
+            if (!is_null($lastaccess)) {
+                $lastaccessrecords[] = [
+                    'userid' => $student->id,
+                    'courseid' => $course->id,
+                    'timeaccess' => $lastaccess
+                ];
+            }
+        }
+
+        foreach ($unenrolledcoursesdata as $coursedata) {
+            $lastaccess = null;
+
+            if (isset($coursedata['lastaccess'])) {
+                $lastaccess = $coursedata['lastaccess'];
+                unset($coursedata['lastaccess']);
+            }
+
+            $course = $generator->create_course($coursedata);
+
+            if (!is_null($lastaccess)) {
+                $lastaccessrecords[] = [
+                    'userid' => $student->id,
+                    'courseid' => $course->id,
+                    'timeaccess' => $lastaccess
+                ];
+            }
+        }
+
+        if (!empty($lastaccessrecords)) {
+            $DB->insert_records('user_lastaccess', $lastaccessrecords);
+        }
+
+        $this->setUser($student);
+
+        $result = enrol_get_my_courses('shortname', $sort, $limit, [], false, $offset);
+        $actual = array_map(function($course) {
+            return $course->shortname;
+        }, array_values($result));
+
+        $this->assertEquals($expectedcourses, $actual);
+    }
 }
index 6824b16..ad1b1a9 100644 (file)
@@ -1208,6 +1208,7 @@ $string['unsupporteddbtablerowformat'] = 'Your database has tables using Antelop
 $string['unsupportedphpversion7'] = 'PHP version 7 is not supported.';
 $string['unsupportedphpversion71'] = 'PHP version 7.1 is not supported.';
 $string['unsupportedphpversion72'] = 'PHP version 7.2 is not supported.';
+$string['unsupportedphpversion73'] = 'PHP version 7.3 is not supported.';
 $string['unsuspenduser'] = 'Activate user account';
 $string['updateaccounts'] = 'Update existing accounts';
 $string['updatecomponent'] = 'Update component';
index 1d6c1c6..2e14642 100644 (file)
@@ -50,6 +50,7 @@ $string['descriptions_help'] = 'Audio descriptions may be used to provide a narr
 $string['descriptionssourcelabel'] = 'Description track URL';
 $string['displayoptions'] = 'Display options';
 $string['entername'] = 'Enter name';
+$string['entertitle'] = 'Enter title';
 $string['entersource'] = 'Source URL';
 $string['enterurl'] = 'Enter URL';
 $string['height'] = 'Height';
index 86a9d37..3e4b19d 100644 (file)
@@ -53,7 +53,7 @@ function atto_media_strings_for_js() {
                                           'descriptionssourcelabel',
                                           'displayoptions',
                                           'entername',
-                                          'entername',
+                                          'entertitle',
                                           'entersource',
                                           'enterurl',
                                           'height',
@@ -79,7 +79,7 @@ function atto_media_strings_for_js() {
                                           'videosourcelabel',
                                           'videowidth',
                                           'width'),
-                                    'atto_media');
+                                          'atto_media');
 }
 
 /**
index d6e42d3..b600418 100644 (file)
@@ -31,7 +31,7 @@ Feature: Add media to Atto
     When I click on "Save changes" "button"
     Then "//a[. = 'moodle-logo.webm']" "xpath_element" should exist
 
-  @javascript
+  @javascript @atto_media_video
   Scenario: Insert some media as a plain video
     Given I click on "Video" "link"
     And I click on "Browse repositories..." "button" in the "#id_summary_editor_video .atto_media_source.atto_media_media_source" "css_element"
@@ -46,7 +46,7 @@ Feature: Add media to Atto
     When I click on "Insert media" "button"
     Then "//video[descendant::source[contains(@src, 'moodle-logo.webm')]][descendant::source[contains(@src, 'moodle-logo.mp4')]]" "xpath_element" should exist
 
-  @javascript
+  @javascript @atto_media_video
   Scenario: Insert some media as a video with display settings
     Given I click on "Video" "link"
     And I click on "Browse repositories..." "button" in the "#id_summary_editor_video .atto_media_source.atto_media_media_source" "css_element"
@@ -61,11 +61,12 @@ Feature: Add media to Atto
     And I change window size to "large"
     And I set the field with xpath "//*[contains(concat(' ', normalize-space(@class), ' '), ' atto_media_width_entry ')]" to "420"
     And I set the field with xpath "//*[contains(concat(' ', normalize-space(@class), ' '), ' atto_media_height_entry ')]" to "69"
+    And I set the field "Enter title" to "VideoTitle"
     And I click on "Display options" "link"
     When I click on "Insert media" "button"
-    Then "//video[descendant::source[contains(@src, 'moodle-logo.webm')]][contains(@poster, 'moodle-logo.png')][@width=420][@height=69]" "xpath_element" should exist
+    Then "//video[descendant::source[contains(@src, 'moodle-logo.webm')]][contains(@poster, 'moodle-logo.png')][@width=420][@height=69][@title='VideoTitle']" "xpath_element" should exist
 
-  @javascript
+  @javascript @atto_media_video
   Scenario: Insert some media as a video with advanced settings
     Given I click on "Video" "link"
     And I click on "Browse repositories..." "button" in the "#id_summary_editor_video .atto_media_source.atto_media_media_source" "css_element"
@@ -80,7 +81,7 @@ Feature: Add media to Atto
     When I click on "Insert media" "button"
     Then "//video[descendant::source[contains(@src, 'moodle-logo.webm')]][@controls='true'][@loop='true'][@autoplay='true'][@autoplay='true']" "xpath_element" should exist
 
-  @javascript
+  @javascript @atto_media_video
   Scenario: Insert some media as a video with tracks
     Given I click on "Video" "link"
     And I change window size to "large"
@@ -164,3 +165,40 @@ Feature: Add media to Atto
     And I set the field with xpath "(//*[contains(concat(' ', normalize-space(@class), ' '), ' atto_media_track_default ')])[9]" to "1"
     When I click on "Insert media" "button"
     Then "//video[descendant::source[contains(@src, 'moodle-logo.webm')]][descendant::track[contains(@src, 'pretty-good-sv.vtt')][@kind='subtitles'][@label='Swedish'][@srclang='sv'][@default='true']][descendant::track[contains(@src, 'pretty-good-en.vtt')][@kind='subtitles'][@label='English'][@srclang='en'][not(@default)]][descendant::track[contains(@src, 'pretty-good-sv.vtt')][@kind='captions'][@label='Swedish'][@srclang='sv'][not(@default)]][descendant::track[contains(@src, 'pretty-good-en.vtt')][@kind='captions'][@label='English'][@srclang='en'][@default='true']][descendant::track[contains(@src, 'pretty-good-sv.vtt')][@kind='descriptions'][@label='Swedish'][@srclang='sv'][@default='true']][descendant::track[contains(@src, 'pretty-good-en.vtt')][@kind='descriptions'][@label='English'][@srclang='en'][not(@default)]][descendant::track[contains(@src, 'pretty-good-sv.vtt')][@kind='chapters'][@label='Swedish'][@srclang='sv'][not(@default)]][descendant::track[contains(@src, 'pretty-good-en.vtt')][@kind='chapters'][@label='English'][@srclang='en'][@default='true']][descendant::track[contains(@src, 'pretty-good-sv.vtt')][@kind='metadata'][@label='Swedish'][@srclang='sv'][@default='true']][descendant::track[contains(@src, 'pretty-good-en.vtt')][@kind='metadata'][@label='English'][@srclang='en'][not(@default)]]" "xpath_element" should exist
+
+  @javascript @atto_media_audio
+  Scenario: Insert some media as a plain audio
+    Given I click on "Audio" "link"
+    And I click on "Browse repositories..." "button" in the "#id_summary_editor_audio .atto_media_source.atto_media_media_source" "css_element"
+    And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
+    And I click on "moodle-logo.mp4" "link"
+    And I click on "Select this file" "button"
+    When I click on "Insert media" "button"
+    Then "//audio[descendant::source[contains(@src, 'moodle-logo.mp4')]]" "xpath_element" should exist
+
+  @javascript @atto_media_audio
+  Scenario: Insert some media as an audio with display settings
+    Given I click on "Audio" "link"
+    And I click on "Browse repositories..." "button" in the "#id_summary_editor_audio .atto_media_source.atto_media_media_source" "css_element"
+    And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
+    And I click on "moodle-logo.mp4" "link"
+    And I click on "Select this file" "button"
+    And I click on "Display options" "link" in the "#id_summary_editor_audio" "css_element"
+    And I set the field "audio_media-title-entry" to "AudioTitle"
+    When I click on "Insert media" "button"
+    Then "//audio[descendant::source[contains(@src, 'moodle-logo.mp4')]][@title='AudioTitle']" "xpath_element" should exist
+
+  @javascript @atto_media_audio
+  Scenario: Insert some media as an audio with advanced settings
+    Given I click on "Audio" "link"
+    And I click on "Browse repositories..." "button" in the "#id_summary_editor_audio .atto_media_source.atto_media_media_source" "css_element"
+    And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
+    And I click on "moodle-logo.mp4" "link"
+    And I click on "Select this file" "button"
+    And I click on "Advanced settings" "link" in the "#id_summary_editor_audio" "css_element"
+    And the field "audio_media-controls-toggle" matches value "1"
+    And I set the field "audio_media-autoplay-toggle" to "1"
+    And I set the field "audio_media-mute-toggle" to "1"
+    And I set the field "audio_media-loop-toggle" to "1"
+    When I click on "Insert media" "button"
+    Then "//audio[descendant::source[contains(@src, 'moodle-logo.mp4')]][@controls='true'][@loop='true'][@autoplay='true'][@autoplay='true']" "xpath_element" should exist
\ No newline at end of file
index 541685a..895858e 100644 (file)
Binary files a/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-debug.js and b/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-debug.js differ
index cfdd7c1..49dc812 100644 (file)
Binary files a/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-min.js and b/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-min.js differ
index 541685a..895858e 100644 (file)
Binary files a/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button.js and b/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button.js differ
index b406076..042db25 100644 (file)
@@ -49,6 +49,7 @@ var COMPONENTNAME = 'atto_media',
         TRACK_SOURCE: 'atto_media_track_source',
         DISPLAY_OPTIONS: 'atto_media_display_options',
         NAME_INPUT: 'atto_media_name_entry',
+        TITLE_INPUT: 'atto_media_title_entry',
         URL_INPUT: 'atto_media_url_entry',
         POSTER_SIZE: 'atto_media_poster_size',
         LINK_SIZE: 'atto_media_link_size',
@@ -80,6 +81,7 @@ var COMPONENTNAME = 'atto_media',
         TRACK_SOURCE: '.' + CSS.TRACK_SOURCE,
         DISPLAY_OPTIONS: '.' + CSS.DISPLAY_OPTIONS,
         NAME_INPUT: '.' + CSS.NAME_INPUT,
+        TITLE_INPUT: '.' + CSS.TITLE_INPUT,
         URL_INPUT: '.' + CSS.URL_INPUT,
         POSTER_SIZE: '.' + CSS.POSTER_SIZE,
         LINK_SIZE: '.' + CSS.LINK_SIZE,
@@ -160,14 +162,14 @@ var COMPONENTNAME = 'atto_media',
                     '<input name="mform_isexpanded_{{elementid}}_video-display-options" type="hidden">' +
                     '<legend class="ftoggler">{{get_string "displayoptions" component}}</legend>' +
                     '<div class="fcontainer">' +
-                        '{{> form_components.display_options}}' +
+                        '{{renderPartial "form_components.display_options" context=this id=CSS.VIDEO mediatype_video=true}}' +
                     '</div>' +
                 '</fieldset>' +
                 '<fieldset class="collapsible collapsed" id="{{elementid}}_video-advanced-settings">' +
                     '<input name="mform_isexpanded_{{elementid}}_video-advanced-settings" type="hidden">' +
                     '<legend class="ftoggler">{{get_string "advancedsettings" component}}</legend>' +
                     '<div class="fcontainer">' +
-                        '{{> form_components.advanced_settings}}' +
+                        '{{renderPartial "form_components.advanced_settings" context=this id=CSS.VIDEO}}' +
                     '</div>' +
                 '</fieldset>' +
                 '<fieldset class="collapsible collapsed" id="{{elementid}}_video-tracks">' +
@@ -180,11 +182,18 @@ var COMPONENTNAME = 'atto_media',
             AUDIO: '' +
                 '{{renderPartial "form_components.source" context=this id=CSS.MEDIA_SOURCE entersourcelabel="audiosourcelabel"' +
                     ' addcomponentlabel="addsource" multisource="true" addsourcehelp=helpStrings.addsource}}' +
+                '<fieldset class="collapsible collapsed" id="{{elementid}}_audio-display-options">' +
+                    '<input name="mform_isexpanded_{{elementid}}_audio-display-options" type="hidden">' +
+                    '<legend class="ftoggler">{{get_string "displayoptions" component}}</legend>' +
+                    '<div class="fcontainer">' +
+                        '{{renderPartial "form_components.display_options" context=this id=CSS.AUDIO}}' +
+                    '</div>' +
+                '</fieldset>' +
                 '<fieldset class="collapsible collapsed" id="{{elementid}}_audio-advanced-settings">' +
                     '<input name="mform_isexpanded_{{elementid}}_audio-advanced-settings" type="hidden">' +
                     '<legend class="ftoggler">{{get_string "advancedsettings" component}}</legend>' +
                     '<div class="fcontainer">' +
-                        '{{> form_components.advanced_settings}}' +
+                        '{{renderPartial "form_components.advanced_settings" context=this id=CSS.AUDIO}}' +
                     '</div>' +
                 '</fieldset>' +
                 '<fieldset class="collapsible collapsed" id="{{elementid}}_audio-tracks">' +
@@ -233,6 +242,13 @@ var COMPONENTNAME = 'atto_media',
                 '</div>',
             DISPLAY_OPTIONS: '' +
                 '<div class="{{CSS.DISPLAY_OPTIONS}}">' +
+                    '<div class="m-b-1">' +
+                        '<label for="{{id}}_media-title-entry">{{get_string "entertitle" component}}</label>' +
+                        '<input class="form-control fullwidth {{CSS.TITLE_INPUT}}" type="text" id="{{id}}_media-title-entry"' +
+                            'size="32"/>' +
+                    '</div>' +
+                    '<div class="clearfix"></div>' +
+                    '{{#mediatype_video}}' +
                     '<div class="m-b-1">' +
                         '<label>{{get_string "size" component}}</label>' +
                         '<div class="form-inline {{CSS.POSTER_SIZE}}">' +
@@ -245,26 +261,37 @@ var COMPONENTNAME = 'atto_media',
                     '</div>' +
                     '<div class="clearfix"></div>' +
                     '{{renderPartial "form_components.source" context=this id=CSS.POSTER_SOURCE entersourcelabel="poster"}}' +
+                    '{{/mediatype_video}}' +
                 '<div>',
             ADVANCED_SETTINGS: '' +
                 '<div class="{{CSS.ADVANCED_SETTINGS}}">' +
                     '<div class="form-check">' +
                         '<input type="checkbox" checked="true" class="form-check-input {{CSS.MEDIA_CONTROLS_TOGGLE}}"' +
-                        'id="media-controls-toggle"/>' +
-                        '<label class="form-check-label" for="media-controls-toggle">{{get_string "controls" component}}</label>' +
+                        'id="{{id}}_media-controls-toggle"/>' +
+                        '<label class="form-check-label" for="{{id}}_media-controls-toggle">' +
+                        '{{get_string "controls" component}}' +
+                        '</label>' +
                     '</div>' +
                     '<div class="form-check">' +
                         '<input type="checkbox" class="form-check-input {{CSS.MEDIA_AUTOPLAY_TOGGLE}}"' +
-                        'id="media-autoplay-toggle"/>' +
-                        '<label class="form-check-label" for="media-autoplay-toggle">{{get_string "autoplay" component}}</label>' +
+                        'id="{{id}}_media-autoplay-toggle"/>' +
+                        '<label class="form-check-label" for="{{id}}_media-autoplay-toggle">' +
+                        '{{get_string "autoplay" component}}' +
+                        '</label>' +
                     '</div>' +
                     '<div class="form-check">' +
-                        '<input type="checkbox" class="form-check-input {{CSS.MEDIA_MUTE_TOGGLE}}" id="media-mute-toggle"/>' +
-                        '<label class="form-check-label" for="media-mute-toggle">{{get_string "mute" component}}</label>' +
+                        '<input type="checkbox" class="form-check-input {{CSS.MEDIA_MUTE_TOGGLE}}" ' +
+                            'id="{{id}}_media-mute-toggle"/>' +
+                        '<label class="form-check-label" for="{{id}}_media-mute-toggle">' +
+                        '{{get_string "mute" component}}' +
+                        '</label>' +
                     '</div>' +
                     '<div class="form-check">' +
-                        '<input type="checkbox" class="form-check-input {{CSS.MEDIA_LOOP_TOGGLE}}" id="media-loop-toggle"/>' +
-                        '<label class="form-check-label" for="media-loop-toggle">{{get_string "loop" component}}</label>' +
+                        '<input type="checkbox" class="form-check-input {{CSS.MEDIA_LOOP_TOGGLE}}" ' +
+                            'id="{{id}}_media-loop-toggle"/>' +
+                        '<label class="form-check-label" for="{{id}}_media-loop-toggle">' +
+                        '{{get_string "loop" component}}' +
+                        '</label>' +
                     '</div>' +
                 '</div>',
             TRACK_TABS: '' +
@@ -366,6 +393,7 @@ var COMPONENTNAME = 'atto_media',
                     '{{#loop}}loop="true" {{/loop}}' +
                     '{{#muted}}muted="true" {{/muted}}' +
                     '{{#autoplay}}autoplay="true" {{/autoplay}}' +
+                    '{{#title}}title="{{../title}}" {{/title}}' +
                 '>' +
                     '{{#sources}}<source src="{{source}}">{{/sources}}' +
                     '{{#tracks}}' +
@@ -380,6 +408,7 @@ var COMPONENTNAME = 'atto_media',
                     '{{#loop}}loop="true" {{/loop}}' +
                     '{{#muted}}muted="true" {{/muted}}' +
                     '{{#autoplay}}autoplay="true" {{/autoplay}}' +
+                    '{{#title}}title="{{../title}}" {{/title}}' +
                 '>' +
                     '{{#sources}}<source src="{{source}}">{{/sources}}' +
                     '{{#tracks}}' +
@@ -645,17 +674,23 @@ Y.namespace('M.atto_media').Button = Y.Base.create('button', Y.M.editor_atto.Edi
         }, this);
 
         // Populate values.
-        tabPane.one(SELECTORS.POSTER_SOURCE + ' ' + SELECTORS.URL_INPUT).setAttribute('value', properties.poster);
-        tabPane.one(SELECTORS.WIDTH_INPUT).set('value', properties.width);
-        tabPane.one(SELECTORS.HEIGHT_INPUT).set('value', properties.height);
+        tabPane.one(SELECTORS.TITLE_INPUT).set('value', properties.title);
         tabPane.one(SELECTORS.MEDIA_CONTROLS_TOGGLE).set('checked', properties.controls);
         tabPane.one(SELECTORS.MEDIA_AUTOPLAY_TOGGLE).set('checked', properties.autoplay);
         tabPane.one(SELECTORS.MEDIA_MUTE_TOGGLE).set('checked', properties.muted);
         tabPane.one(SELECTORS.MEDIA_LOOP_TOGGLE).set('checked', properties.loop);
 
-        // Switch to the correct tab.
+        // Determine medium type.
         var mediumType = this._getMediumTypeFromTabPane(tabPane);
 
+        if (mediumType === 'video') {
+            // Populate values unique for video.
+            tabPane.one(SELECTORS.POSTER_SOURCE + ' ' + SELECTORS.URL_INPUT).setAttribute('value', properties.poster);
+            tabPane.one(SELECTORS.WIDTH_INPUT).set('value', properties.width);
+            tabPane.one(SELECTORS.HEIGHT_INPUT).set('value', properties.height);
+        }
+
+        // Switch to the correct tab.
         // Remove active class from all tabs + tab panes.
         tabPane.siblings('.active').removeClass('active');
         content.all('.root.nav-tabs .nav-item a').removeClass('active');
@@ -701,6 +736,7 @@ Y.namespace('M.atto_media').Button = Y.Base.create('button', Y.M.editor_atto.Edi
             type: medium.test('video') ? MEDIA_TYPES.VIDEO : MEDIA_TYPES.AUDIO,
             sources: medium.all('source').get('src'),
             poster: medium.getAttribute('poster'),
+            title: medium.getAttribute('title'),
             width: medium.getAttribute('width'),
             height: medium.getAttribute('height'),
             autoplay: boolAttr(medium, 'autoplay'),
@@ -945,7 +981,8 @@ Y.namespace('M.atto_media').Button = Y.Base.create('button', Y.M.editor_atto.Edi
             showControls: tab.one(SELECTORS.MEDIA_CONTROLS_TOGGLE).get('checked'),
             autoplay: tab.one(SELECTORS.MEDIA_AUTOPLAY_TOGGLE).get('checked'),
             muted: tab.one(SELECTORS.MEDIA_MUTE_TOGGLE).get('checked'),
-            loop: tab.one(SELECTORS.MEDIA_LOOP_TOGGLE).get('checked')
+            loop: tab.one(SELECTORS.MEDIA_LOOP_TOGGLE).get('checked'),
+            title: tab.one(SELECTORS.TITLE_INPUT).get('value') || false
         };
     }
 }, {
index fa20f65..5ddf497 100644 (file)
@@ -554,6 +554,8 @@ function enrol_add_course_navigation(navigation_node $coursenode, $course) {
  *
  * @param string|array $fields Extra fields to be returned (array or comma-separated list).
  * @param string|null $sort Comma separated list of fields to sort by, defaults to respecting navsortmycoursessort.
+ * Allowed prefixes for sort fields are: "ul" for the user_lastaccess table, "c" for the courses table,
+ * "ue" for the user_enrolments table.
  * @param int $limit max number of courses
  * @param array $courseids the list of course ids to filter by
  * @param bool $allaccessible Include courses user is not enrolled in, but can access
@@ -599,17 +601,32 @@ function enrol_get_my_courses($fields = null, $sort = null, $limit = 0, $coursei
 
     $orderby = "";
     $sort    = trim($sort);
+    $sorttimeaccess = false;
+    $allowedsortprefixes = array('c', 'ul', 'ue');
     if (!empty($sort)) {
         $rawsorts = explode(',', $sort);
         $sorts = array();
         foreach ($rawsorts as $rawsort) {
             $rawsort = trim($rawsort);
-            if (strpos($rawsort, 'c.') === 0) {
-                $rawsort = substr($rawsort, 2);
+            if (preg_match('/^ul\.(\S*)\s(asc|desc)/i', $rawsort, $matches)) {
+                if (strcasecmp($matches[2], 'asc') == 0) {
+                    $sorts[] = 'COALESCE(ul.' . $matches[1] . ', 0) ASC';
+                } else {
+                    $sorts[] = 'COALESCE(ul.' . $matches[1] . ', 0) DESC';
+                }
+                $sorttimeaccess = true;
+            } else if (strpos($rawsort, '.') !== false) {
+                $prefix = explode('.', $rawsort);
+                if (in_array($prefix[0], $allowedsortprefixes)) {
+                    $sorts[] = trim($rawsort);
+                } else {
+                    throw new coding_exception('Invalid $sort parameter in enrol_get_my_courses()');
+                }
+            } else {
+                $sorts[] = 'c.'.trim($rawsort);
             }
-            $sorts[] = trim($rawsort);
         }
-        $sort = 'c.'.implode(',c.', $sorts);
+        $sort = implode(',', $sorts);
         $orderby = "ORDER BY $sort";
     }
 
@@ -628,6 +645,9 @@ function enrol_get_my_courses($fields = null, $sort = null, $limit = 0, $coursei
     $params['contextlevel'] = CONTEXT_COURSE;
     $wheres = implode(" AND ", $wheres);
 
+    $timeaccessselect = "";
+    $timeaccessjoin = "";
+
     if (!empty($courseids)) {
         list($courseidssql, $courseidsparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
         $wheres = sprintf("%s AND c.id %s", $wheres, $courseidssql);
@@ -640,14 +660,20 @@ function enrol_get_my_courses($fields = null, $sort = null, $limit = 0, $coursei
         $courseidsql .= "
                 SELECT DISTINCT e.courseid
                   FROM {enrol} e
-                  JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid)
+                  JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = :userid1)
                  WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1
                        AND (ue.timeend = 0 OR ue.timeend > :now2)";
-        $params['userid'] = $USER->id;
+        $params['userid1'] = $USER->id;
         $params['active'] = ENROL_USER_ACTIVE;
         $params['enabled'] = ENROL_INSTANCE_ENABLED;
         $params['now1'] = round(time(), -2); // Improves db caching.
         $params['now2'] = $params['now1'];
+
+        if ($sorttimeaccess) {
+            $params['userid2'] = $USER->id;
+            $timeaccessselect = ', ul.timeaccess as lastaccessed';
+            $timeaccessjoin = "LEFT JOIN {user_lastaccess} ul ON (ul.courseid = c.id AND ul.userid = :userid2)";
+        }
     }
 
     // When including non-enrolled but accessible courses...
@@ -708,9 +734,10 @@ function enrol_get_my_courses($fields = null, $sort = null, $limit = 0, $coursei
 
     // Note: we can not use DISTINCT + text fields due to Oracle and MS limitations, that is why
     // we have the subselect there.
-    $sql = "SELECT $coursefields $ccselect
+    $sql = "SELECT $coursefields $ccselect $timeaccessselect
               FROM {course} c
               JOIN ($courseidsql) en ON (en.courseid = c.id)
+           $timeaccessjoin
            $ccjoin
              WHERE $wheres
           $orderby";
index 51ea48d..dbf2520 100644 (file)
@@ -1617,3 +1617,14 @@ function restrict_php_version_71(&$result) {
 function restrict_php_version_72(&$result) {
     return restrict_php_version($result, '7.2');
 }
+
+/**
+ * Check if the current PHP version is greater than or equal to
+ * PHP version 7.3.
+ *
+ * @param object $result an environment_results instance
+ * @return bool result of version check
+ */
+function restrict_php_version_73(&$result) {
+    return restrict_php_version($result, '7.3');
+}
index 1630264..844c943 100644 (file)
@@ -35,6 +35,7 @@
     position: absolute;
     overflow: auto;
     background-color: #ccc;
+    touch-action: none;
 }
 
 .assignfeedback_editpdf_widget {
@@ -388,6 +389,7 @@ ul.assignfeedback_editpdf_menu {
         position: relative;
         margin-bottom: 1em;
         top: 0;
+        max-height: 312px;
     }
 
     .assignfeedback_editpdf_widget .pageheader {
index dc22ae1..bea07f5 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js differ
index 827e3d2..bb0f77d 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js differ
index dc22ae1..bea07f5 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js differ
index 4320b85..878eb49 100644 (file)
@@ -773,6 +773,8 @@ EDITOR.prototype = {
         if (this.get('readonly')) {
             return;
         }
+        this.disable_touch_scroll();
+
         // Setup the tool buttons.
         Y.each(TOOLSELECTOR, function(selector, tool) {
             toolnode = this.get_dialogue_element(selector);
@@ -866,6 +868,7 @@ EDITOR.prototype = {
         if (tool !== "comment" && tool !== "select" && tool !== "drag" && tool !== "stamp") {
             this.lastannotationtool = tool;
         }
+
         this.refresh_button_state();
     },
 
@@ -1410,6 +1413,54 @@ EDITOR.prototype = {
         for (i = 0; i < this.drawables.length; i++) {
             this.drawables[i].scroll_update(x, y);
         }
+    },
+
+    /**
+     * Test the browser support for options objects on event listeners.
+     * @return Boolean
+     */
+    event_listener_options_supported: function() {
+        var passivesupported = false,
+            options,
+            testeventname = "testpassiveeventoptions";
+
+        // Options support testing example from:
+        // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
+
+        try {
+            options = Object.defineProperty({}, "passive", {
+                get: function() {
+                    passivesupported = true;
+                }
+            });
+
+            // We use an event name that is not likely to conflict with any real event.
+            document.addEventListener(testeventname, options, options);
+            // We remove the event listener as we have tested the options already.
+            document.removeEventListener(testeventname, options, options);
+        } catch(err) {
+            // It's already false.
+            passivesupported = false;
+        }
+        return passivesupported;
+    },
+
+    /**
+     * Disable Touch Move scrolling
+     */
+    disable_touch_scroll: function() {
+        if (this.event_listener_options_supported()) {
+            document.addEventListener('touchmove', this.stop_touch_scroll, {passive: false});
+        }
+    },
+
+    /**
+     * Stop Touch Scrolling
+     * @param {Object} e
+     */
+    stop_touch_scroll: function(e) {
+        e.stopPropagation();
+        e.preventDefault();
     }
 
 };
index 383327f..702b7e6 100644 (file)
@@ -59,39 +59,67 @@ class qformat_aiken extends qformat_default {
 
     public function readquestions($lines) {
         $questions = array();
-        $question = $this->defaultquestion();
+        $question = null;
         $endchar = chr(13);
+        $linenumber = 0;
         foreach ($lines as $line) {
             $stp = strpos($line, $endchar, 0);
             $newlines = explode($endchar, $line);
             $linescount = count($newlines);
             for ($i=0; $i < $linescount; $i++) {
+                $linenumber++;
                 $nowline = trim($newlines[$i]);
                 // Go through the array and build an object called $question
                 // When done, add $question to $questions.
                 if (strlen($nowline) < 2) {
                     continue;
                 }
-                if (preg_match('/^[A-Z][).][ \t]/', $nowline)) {
+                if (preg_match('/^[A-Z][).][ \t]?/', $nowline)) {
+                    if (is_null($question)) {
+                        // We have a response line, but we aren't currently in a question.
+                        $this->error(get_string('questionnotstarted', 'qformat_aiken', $linenumber));
+                        continue;
+                    }
+
                     // A choice. Trim off the label and space, then save.
                     $question->answer[] = $this->text_field(
                             htmlspecialchars(trim(substr($nowline, 2)), ENT_NOQUOTES));
                     $question->fraction[] = 0;
                     $question->feedback[] = $this->text_field('');
                 } else if (preg_match('/^ANSWER:/', $nowline)) {
+                    if (is_null($question)) {
+                        // We have an answer line, but we aren't currently in a question.
+                        $this->error(get_string('questionnotstarted', 'qformat_aiken', $linenumber));
+                        continue;
+                    }
+
                     // The line that indicates the correct answer. This question is finised.
                     $ans = trim(substr($nowline, strpos($nowline, ':') + 1));
                     $ans = substr($ans, 0, 1);
                     // We want to map A to 0, B to 1, etc.
                     $rightans = ord($ans) - ord('A');
+
+                    if (count($question->answer) < 2) {
+                        // The multichoice question requires at least 2 answers, or there will be a failure later.
+                        $this->error(get_string('questionmissinganswers', 'qformat_aiken', $linenumber), '', $question->name);
+                        $question = null;
+                        continue;
+                    }
+
                     $question->fraction[$rightans] = 1;
                     $questions[] = $question;
 
-                    // Clear array for next question set.
-                    $question = $this->defaultquestion();
+                    // Clear variable for next question set.
+                    $question = null;
                     continue;
                 } else {
                     // Must be the first line of a new question, since no recognised prefix.
+                    if (!is_null($question)) {
+                        // In this case, there was already an open question that we didn't complete. It is being discarded.
+                        $this->error(get_string('questionnotcomplete', 'qformat_aiken', $linenumber), '', $question->name);
+                    }
+
+                    $question = $this->defaultquestion();
                     $question->qtype = 'multichoice';
                     $question->name = $this->create_default_question_name($nowline, get_string('questionname', 'question'));
                     $question->questiontext = htmlspecialchars(trim($nowline), ENT_NOQUOTES);
index 44cf96f..052a3cb 100644 (file)
@@ -26,3 +26,6 @@ $string['pluginname'] = 'Aiken format';
 $string['pluginname_help'] = 'This is a simple format for importing multiple choice questions from a text file.';
 $string['pluginname_link'] = 'qformat/aiken';
 $string['privacy:metadata'] = 'The Aiken question format plugin does not store any personal data.';
+$string['questionmissinganswers'] = 'Question must have at least 2 answers on line {$a}';
+$string['questionnotcomplete'] = 'Question not completed before next question start on line {$a}';
+$string['questionnotstarted'] = 'Question not started on line {$a}';
diff --git a/question/format/aiken/tests/aikenformat_test.php b/question/format/aiken/tests/aikenformat_test.php
new file mode 100644 (file)
index 0000000..6f1408d
--- /dev/null
@@ -0,0 +1,87 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for the Moodle Aiken format.
+ *
+ * @package    qformat_aiken
+ * @copyright  2018 Eric Merrill (eric.a.merrill@gmail.com)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/questionlib.php');
+require_once($CFG->dirroot . '/question/format.php');
+require_once($CFG->dirroot . '/question/format/aiken/format.php');
+require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
+
+
+/**
+ * Unit tests for the matching question definition class.
+ *
+ * @copyright  2018 Eric Merrill (eric.a.merrill@gmail.com)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class aikenformat_test extends question_testcase {
+    public function test_readquestions() {
+        global $CFG;
+
+        $lines = file($CFG->dirroot.'/question/format/aiken/tests/fixtures/aiken_errors.txt');
+        $importer = new qformat_aiken($lines);
+
+        // The importer echos some errors, so we need to capture and check that.
+        ob_start();
+        $questions = $importer->readquestions($lines);
+        $output = ob_get_contents();
+        ob_end_clean();
+
+        // Check that there were some expected errors.
+        $this->assertContains('Error importing question A question with too few answers', $output);
+        $this->assertContains('Question must have at least 2 answers on line 3', $output);
+        $this->assertContains('Question not started on line 5', $output);
+        $this->assertContains('Question not started on line 7', $output);
+        $this->assertContains('Error importing question A question started but not finished', $output);
+        $this->assertContains('Question not completed before next question start on line 18', $output);
+
+        // There are two expected questions.
+        $this->assertCount(2, $questions);
+
+        $q1 = null;
+        $q2 = null;
+        foreach ($questions as $question) {
+            if ($question->name === 'A good question') {
+                $q1 = $question;
+            } else if ($question->name === 'A second good question') {
+                $q2 = $question;
+            }
+        }
+
+        // Check the first good question.
+        $this->assertCount(2, $q1->answer);
+        $this->assertEquals(1, $q1->fraction[0]);
+        $this->assertEquals('Correct', $q1->answer[0]['text']);
+        $this->assertEquals('Incorrect', $q1->answer[1]['text']);
+
+        // Check the second good question.
+        $this->assertCount(2, $q2->answer);
+        $this->assertEquals(1, $q2->fraction[1]);
+        $this->assertEquals('Incorrect (No space)', $q2->answer[0]['text']);
+        $this->assertEquals('Correct (No space)', $q2->answer[1]['text']);
+    }
+}
diff --git a/question/format/aiken/tests/fixtures/aiken_errors.txt b/question/format/aiken/tests/fixtures/aiken_errors.txt
new file mode 100644 (file)
index 0000000..92e6138
--- /dev/null
@@ -0,0 +1,21 @@
+A question with too few answers
+A) Only answer
+ANSWER: A
+
+A) Question not started
+
+ANSWER: Question not started
+
+A good question
+A) Correct
+B) Incorrect
+ANSWER: A
+
+A question started but not finished
+A) Correct-ish
+B) Incorrect-ish
+
+A second good question
+A)Incorrect (No space)
+B)Correct (No space)
+ANSWER: B
\ No newline at end of file
index c90db2c..60bd721 100644 (file)
@@ -39,11 +39,51 @@ require_once($CFG->libdir . '/questionlib.php');
 class qtype_multichoice extends question_type {
     public function get_question_options($question) {
         global $DB, $OUTPUT;
-        $question->options = $DB->get_record('qtype_multichoice_options',
-                array('questionid' => $question->id), '*', MUST_EXIST);
+
+        $question->options = $DB->get_record('qtype_multichoice_options', ['questionid' => $question->id]);
+
+        if ($question->options === false) {
+            // If this has happened, then we have a problem.
+            // For the user to be able to edit or delete this question, we need options.
+            debugging("Question ID {$question->id} was missing an options record. Using default.", DEBUG_DEVELOPER);
+
+            $question->options = $this->create_default_options($question);
+        }
+
         parent::get_question_options($question);
     }
 
+    /**
+     * Create a default options object for the provided question.
+     *
+     * @param object $question The queston we are working with.
+     * @return object The options object.
+     */
+    protected function create_default_options($question) {
+        // Create a default question options record.
+        $options = new stdClass();
+        $options->questionid = $question->id;
+
+        // Get the default strings and just set the format.
+        $options->correctfeedback = get_string('correctfeedbackdefault', 'question');
+        $options->correctfeedbackformat = FORMAT_HTML;
+        $options->partiallycorrectfeedback = get_string('partiallycorrectfeedbackdefault', 'question');;
+        $options->partiallycorrectfeedbackformat = FORMAT_HTML;
+        $options->incorrectfeedback = get_string('incorrectfeedbackdefault', 'question');
+        $options->incorrectfeedbackformat = FORMAT_HTML;
+
+        $config = get_config('qtype_multichoice');
+        $options->single = $config->answerhowmany;
+        if (isset($question->layout)) {
+            $options->layout = $question->layout;
+        }
+        $options->answernumbering = $config->answernumbering;
+        $options->shuffleanswers = $config->shuffleanswers;
+        $options->shownumcorrect = 1;
+
+        return $options;
+    }
+
     public function save_question_options($question) {
         global $DB;
         $context = $question->context;
@@ -60,7 +100,7 @@ class qtype_multichoice extends question_type {
             }
         }
         if ($answercount < 2) { // Check there are at lest 2 answers for multiple choice.
-            $result->notice = get_string('notenoughanswers', 'qtype_multichoice', '2');
+            $result->error = get_string('notenoughanswers', 'qtype_multichoice', '2');
             return $result;
         }
 
index fa93d26..4c9f198 100644 (file)
@@ -163,4 +163,77 @@ class qtype_multichoice_test extends advanced_testcase {
             }
         }
     }
+
+    /**
+     * Test to make sure that loading of question options works, including in an error case.
+     */
+    public function test_get_question_options() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        // Create a complete, in DB question to use.
+        $questiondata = test_question_maker::get_question_data('multichoice', 'two_of_four');
+        $formdata = test_question_maker::get_question_form_data('multichoice', 'two_of_four');
+
+        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
+        $cat = $generator->create_question_category(array());
+
+        $formdata->category = "{$cat->id},{$cat->contextid}";
+        qtype_multichoice_edit_form::mock_submit((array)$formdata);
+
+        $form = qtype_multichoice_test_helper::get_question_editing_form($cat, $questiondata);
+
+        $this->assertTrue($form->is_validated());
+
+        $fromform = $form->get_data();
+
+        $returnedfromsave = $this->qtype->save_question($questiondata, $fromform);
+
+        // Now get just the raw DB record.
+        $question = $DB->get_record('question', ['id' => $returnedfromsave->id], '*', MUST_EXIST);
+
+        // Load it.
+        $this->qtype->get_question_options($question);
+        $this->assertDebuggingNotCalled();
+        $this->assertInstanceOf(stdClass::class, $question->options);
+
+        $options = $question->options;
+        $this->assertEquals($question->id, $options->questionid);
+        $this->assertEquals(0, $options->single);
+
+        $this->assertCount(4, $options->answers);
+
+        // Now we are going to delete the options record.
+        $DB->delete_records('qtype_multichoice_options', ['questionid' => $question->id]);
+
+        // Now see what happens.
+        $question = $DB->get_record('question', ['id' => $returnedfromsave->id], '*', MUST_EXIST);
+        $this->qtype->get_question_options($question);
+
+        $this->assertDebuggingCalled('Question ID '.$question->id.' was missing an options record. Using default.');
+        $this->assertInstanceOf(stdClass::class, $question->options);
+        $options = $question->options;
+        $this->assertEquals($question->id, $options->questionid);
+        $this->assertCount(4, $options->answers);
+
+        $this->assertEquals(get_string('correctfeedbackdefault', 'question'), $options->correctfeedback);
+        $this->assertEquals(FORMAT_HTML, $options->correctfeedbackformat);
+
+        // We no longer know how many answers, so it just has to guess with the default value.
+        $this->assertEquals(get_config('qtype_multichoice', 'answerhowmany'), $options->single);
+
+        // And finally we try again with no answer either.
+        $DB->delete_records('question_answers', ['question' => $question->id]);
+
+        $question = $DB->get_record('question', ['id' => $returnedfromsave->id], '*', MUST_EXIST);
+        $this->qtype->get_question_options($question);
+
+        $this->assertDebuggingCalled('Question ID '.$question->id.' was missing an options record. Using default.');
+        $this->assertInstanceOf(stdClass::class, $question->options);
+        $options = $question->options;
+        $this->assertEquals($question->id, $options->questionid);
+        $this->assertCount(0, $options->answers);
+    }
 }
index 4ab424a..1d5b33f 100644 (file)
@@ -63,6 +63,9 @@ $blocks-plus-gutter: $blocks-column-width + ( $grid-gutter-width / 2 );
     width: calc(100% - #{$blocks-plus-gutter});
     @include media-breakpoint-down(lg) {
         width: 100%;
+        /* MDL-63102 - Remove extra space at bottom.
+        If modifying make sure block-region is horizontally stacked when in full screen */
+        display: block;
     }
 }
 
@@ -72,113 +75,94 @@ $blocks-plus-gutter: $blocks-column-width + ( $grid-gutter-width / 2 );
     }
 }
 
-$chart-size: 70px;
-$doughnut-border-size: 15px;
-$doughnut-dasharray: 173;
-$doughnut-empty-colour: $gray-lighter;
-$doughnut-fill-colour: $brand-warning;
-
-.progress-chart-container {
-    height: $chart-size;
-    width: $chart-size;
-
-    .progress-doughnut {
-        position: relative;
-        height: $chart-size;
-        width: $chart-size;
-        background-clip: padding-box;
-        border: $doughnut-border-size solid $doughnut-empty-colour;
-        border-radius: 50%;
-        box-sizing: border-box;
-
-        .progress-text {
-            position: absolute;
-            top: 50%;
-            /*rtl:ignore*/
-            left: 50%;
-            transform: translate(-50%, -50%);
-            color: $doughnut-empty-colour;
-
-            &.has-percent {
-                color: $doughnut-fill-colour;
-            }
-        }
+$card-gutter : $card-deck-margin * 2;
 
-        .progress-indicator {
-            position: absolute;
-            top: ($doughnut-border-size * -1);
-            left: ($doughnut-border-size * -1);
-            height: $chart-size;
-            width: $chart-size;
-
-            svg {
-                position: relative;
-                height: 100%;
-                width: 100%;
-
-                .circle {
-                    stroke-width: $doughnut-border-size;
-                    stroke: $doughnut-fill-colour;
-                    fill: none;
-                    stroke-dasharray: $doughnut-dasharray;
-                    stroke-dashoffset: $doughnut-dasharray;
-                    transform: rotate(-90deg);
-                    transform-origin: center center;
-
-                    @for $i from 1 through 100 {
-                        &.percent-#{$i} {
-                            stroke-dashoffset: $doughnut-dasharray - ($i / 100 * $doughnut-dasharray);
-                        }
-                    }
-                }
-            }
+.block_myoverview {
+    .empty-placeholder-image-lg {
+        height: 125px;
+    }
+    .course-card {
+        margin-bottom: $card-gutter;
+        flex-basis: 100%;
+    }
+    .course-info-container {
+        padding: 0.8rem;
+    }
+    .course-card-footer {
+        padding: 0.8rem;
+    }
+    .progress {
+        height: 0.5rem;
+    }
+    .myoverviewimg {
+        height: 7rem;
+        background-position: center;
+        background-size: cover;
+    }
+    .course-summaryitem {
+        border: $border-width solid $border-color;
+        background-color: $body-bg;
+    }
+    .summary img {
+        max-width: 100%;
+    }
+    @include media-breakpoint-down(sm) {
+        .summaryimage {
+            max-height: 7rem;
         }
     }
+}
 
-    .no-progress {
-        height: $chart-size;
-        width: $chart-size;
-        background-color: $doughnut-empty-colour;
-        border-radius: 50%;
-        position: relative;
-
-        .icon {
-            position: absolute;
-            top: 50%;
-            /*rtl:ignore*/
-            left: 50%;
-            margin: 0;
-            transform: translate(-63%, -50%);
-            color: #fff;
-            height: ($chart-size / 2) + 10px;
-            width: ($chart-size / 2) + 10px;
-            font-size: ($chart-size / 2) + 10px;
+#region-main {
+    .block_myoverview {
+        .course-card {
+            flex-grow: 0;
+            flex-shrink: 0;
+            flex-basis: calc(50% - #{$card-gutter});
+        }
+        @include media-breakpoint-up(sm) {
+            .course-card {
+                flex-basis: calc(50% - #{$card-gutter});
+            }
+        }
+        @include media-breakpoint-up(md) {
+            .course-card {
+                flex-basis: calc(33.33% - #{$card-gutter});
+            }
+        }
+        @include media-breakpoint-up(lg) {
+            .course-card {
+                flex-basis: calc(25% - #{$card-gutter});
+            }
+        }
+        @include media-breakpoint-up(xl) {
+            .course-card {
+                flex-basis: calc(20% - #{$card-gutter});
+            }
         }
     }
 }
 
-.block_myoverview {
-    .empty-placeholder-image-sm {
-        height: 50px;
+#region-main.has-blocks .block_myoverview {
+    @include media-breakpoint-up(lg) {
+        .course-card {
+            flex-basis: calc(33.33% - #{$card-gutter});
+        }
     }
-
-    .empty-placeholder-image-lg {
-        height: 125px;
+    @include media-breakpoint-up(xl) {
+        .course-card {
+            flex-basis: calc(25% - #{$card-gutter});
+        }
     }
 }
 
-.card-deck {
-    .card {
-        $card-gutter : $card-deck-margin * 2;
-        flex-grow: 0;
-        flex-shrink: 0;
+body.drawer-open-left #region-main.has-blocks .block_myoverview {
+    .course-card {
         flex-basis: calc(50% - #{$card-gutter});
     }
-    .myoverviewimg {
-        height: 150px;
-        &.courseimage {
-            background-position: center;
-            background-size: cover;
+    @include media-breakpoint-up(lg) {
+        .course-card {
+            flex-basis: calc(33.33% - #{$card-gutter});
         }
     }
 }
index b22908e..7fc00a8 100644 (file)
@@ -148,6 +148,13 @@ a.autolink.glossary:hover {
     cursor: pointer;
 }
 
+.pagelayout-mydashboard #region-main {
+    border: 0;
+    padding: 0;
+    background-color: transparent;
+    margin-top: -1px;
+}
+
 .collapsibleregioncaption img {
     vertical-align: middle;
 }
@@ -2141,4 +2148,4 @@ div.editor_atto_toolbar button .icon {
     .dir-ltr-hide {
         display: none;
     }
-}
\ No newline at end of file
+}
index cd04cd5..d11f963 100644 (file)
@@ -58,6 +58,8 @@ $breadcrumb-divider-rtl: "/" !default;
 // Alerts
 $alert-border-width:                0 !default;
 
+$card-group-margin: .25rem;
+
 $theme-colors: (
     primary: $primary,
     secondary: $gray-200,
index b430c6e..2059e48 100644 (file)
@@ -5576,25 +5576,25 @@ tbody.collapse.show {
   display: flex;
   flex-direction: column; }
   .card-deck .card, .card-deck #page-enrol-users #filterform, #page-enrol-users .card-deck #filterform, .card-deck .que .history, .que .card-deck .history, .card-deck .userprofile .profile_tree section, .userprofile .profile_tree .card-deck section, .card-deck .groupinfobox, .card-deck .well {
-    margin-bottom: 15px; }
+    margin-bottom: 0.25rem; }
   @media (min-width: 576px) {
     .card-deck {
       flex-flow: row wrap;
-      margin-right: -15px;
-      margin-left: -15px; }
+      margin-right: -0.25rem;
+      margin-left: -0.25rem; }
       .card-deck .card, .card-deck #page-enrol-users #filterform, #page-enrol-users .card-deck #filterform, .card-deck .que .history, .que .card-deck .history, .card-deck .userprofile .profile_tree section, .userprofile .profile_tree .card-deck section, .card-deck .groupinfobox, .card-deck .well {
         display: flex;
         flex: 1 0 0%;
         flex-direction: column;
-        margin-right: 15px;
+        margin-right: 0.25rem;
         margin-bottom: 0;
-        margin-left: 15px; } }
+        margin-left: 0.25rem; } }
 
 .card-group {
   display: flex;
   flex-direction: column; }
   .card-group > .card, #page-enrol-users .card-group > #filterform, .que .card-group > .history, .userprofile .profile_tree .card-group > section, .card-group > .groupinfobox, .card-group > .well {
-    margin-bottom: 15px; }
+    margin-bottom: 0.25rem; }
   @media (min-width: 576px) {
     .card-group {
       flex-flow: row wrap; }
@@ -8882,6 +8882,12 @@ a.autolink.glossary:hover {
 .pagelayout-mydashboard.jsenabled .collapsibleregioncaption {
   cursor: pointer; }
 
+.pagelayout-mydashboard #region-main {
+  border: 0;
+  padding: 0;
+  background-color: transparent;
+  margin-top: -1px; }
+
 .collapsibleregioncaption img {
   vertical-align: middle; }
 
@@ -11119,284 +11125,82 @@ div.editor_atto_toolbar button .icon {
   @media (max-width: 1199.98px) {
     #region-main-settings-menu.has-blocks,
     #region-main.has-blocks {
-      width: 100%; } }
+      width: 100%;
+      /* MDL-63102 - Remove extra space at bottom.
+        If modifying make sure block-region is horizontally stacked when in full screen */
+      display: block; } }
 
 @media (max-width: 1199.98px) {
   [data-region="blocks-column"] {
     width: 100%; } }
 
-.progress-chart-container {
-  height: 70px;
-  width: 70px; }
-  .progress-chart-container .progress-doughnut {
-    position: relative;
-    height: 70px;
-    width: 70px;
-    background-clip: padding-box;
-    border: 15px solid #dee2e6;
-    border-radius: 50%;
-    box-sizing: border-box; }
-    .progress-chart-container .progress-doughnut .progress-text {
-      position: absolute;
-      top: 50%;
-      /*rtl:ignore*/
-      left: 50%;
-      transform: translate(-50%, -50%);
-      color: #dee2e6; }
-      .progress-chart-container .progress-doughnut .progress-text.has-percent {
-        color: #ff7518; }
-    .progress-chart-container .progress-doughnut .progress-indicator {
-      position: absolute;
-      top: -15px;
-      left: -15px;
-      height: 70px;
-      width: 70px; }
-      .progress-chart-container .progress-doughnut .progress-indicator svg {
-        position: relative;
-        height: 100%;
-        width: 100%; }
-        .progress-chart-container .progress-doughnut .progress-indicator svg .circle {
-          stroke-width: 15px;
-          stroke: #ff7518;
-          fill: none;
-          stroke-dasharray: 173;
-          stroke-dashoffset: 173;
-          transform: rotate(-90deg);
-          transform-origin: center center; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-1 {
-            stroke-dashoffset: 171.27; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-2 {
-            stroke-dashoffset: 169.54; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-3 {
-            stroke-dashoffset: 167.81; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-4 {
-            stroke-dashoffset: 166.08; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-5 {
-            stroke-dashoffset: 164.35; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-6 {
-            stroke-dashoffset: 162.62; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-7 {
-            stroke-dashoffset: 160.89; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-8 {
-            stroke-dashoffset: 159.16; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-9 {
-            stroke-dashoffset: 157.43; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-10 {
-            stroke-dashoffset: 155.7; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-11 {
-            stroke-dashoffset: 153.97; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-12 {
-            stroke-dashoffset: 152.24; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-13 {
-            stroke-dashoffset: 150.51; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-14 {
-            stroke-dashoffset: 148.78; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-15 {
-            stroke-dashoffset: 147.05; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-16 {
-            stroke-dashoffset: 145.32; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-17 {
-            stroke-dashoffset: 143.59; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-18 {
-            stroke-dashoffset: 141.86; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-19 {
-            stroke-dashoffset: 140.13; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-20 {
-            stroke-dashoffset: 138.4; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-21 {
-            stroke-dashoffset: 136.67; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-22 {
-            stroke-dashoffset: 134.94; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-23 {
-            stroke-dashoffset: 133.21; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-24 {
-            stroke-dashoffset: 131.48; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-25 {
-            stroke-dashoffset: 129.75; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-26 {
-            stroke-dashoffset: 128.02; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-27 {
-            stroke-dashoffset: 126.29; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-28 {
-            stroke-dashoffset: 124.56; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-29 {
-            stroke-dashoffset: 122.83; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-30 {
-            stroke-dashoffset: 121.1; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-31 {
-            stroke-dashoffset: 119.37; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-32 {
-            stroke-dashoffset: 117.64; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-33 {
-            stroke-dashoffset: 115.91; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-34 {
-            stroke-dashoffset: 114.18; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-35 {
-            stroke-dashoffset: 112.45; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-36 {
-            stroke-dashoffset: 110.72; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-37 {
-            stroke-dashoffset: 108.99; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-38 {
-            stroke-dashoffset: 107.26; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-39 {
-            stroke-dashoffset: 105.53; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-40 {
-            stroke-dashoffset: 103.8; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-41 {
-            stroke-dashoffset: 102.07; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-42 {
-            stroke-dashoffset: 100.34; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-43 {
-            stroke-dashoffset: 98.61; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-44 {
-            stroke-dashoffset: 96.88; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-45 {
-            stroke-dashoffset: 95.15; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-46 {
-            stroke-dashoffset: 93.42; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-47 {
-            stroke-dashoffset: 91.69; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-48 {
-            stroke-dashoffset: 89.96; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-49 {
-            stroke-dashoffset: 88.23; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-50 {
-            stroke-dashoffset: 86.5; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-51 {
-            stroke-dashoffset: 84.77; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-52 {
-            stroke-dashoffset: 83.04; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-53 {
-            stroke-dashoffset: 81.31; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-54 {
-            stroke-dashoffset: 79.58; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-55 {
-            stroke-dashoffset: 77.85; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-56 {
-            stroke-dashoffset: 76.12; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-57 {
-            stroke-dashoffset: 74.39; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-58 {
-            stroke-dashoffset: 72.66; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-59 {
-            stroke-dashoffset: 70.93; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-60 {
-            stroke-dashoffset: 69.2; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-61 {
-            stroke-dashoffset: 67.47; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-62 {
-            stroke-dashoffset: 65.74; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-63 {
-            stroke-dashoffset: 64.01; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-64 {
-            stroke-dashoffset: 62.28; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-65 {
-            stroke-dashoffset: 60.55; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-66 {
-            stroke-dashoffset: 58.82; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-67 {
-            stroke-dashoffset: 57.09; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-68 {
-            stroke-dashoffset: 55.36; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-69 {
-            stroke-dashoffset: 53.63; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-70 {
-            stroke-dashoffset: 51.9; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-71 {
-            stroke-dashoffset: 50.17; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-72 {
-            stroke-dashoffset: 48.44; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-73 {
-            stroke-dashoffset: 46.71; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-74 {
-            stroke-dashoffset: 44.98; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-75 {
-            stroke-dashoffset: 43.25; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-76 {
-            stroke-dashoffset: 41.52; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-77 {
-            stroke-dashoffset: 39.79; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-78 {
-            stroke-dashoffset: 38.06; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-79 {
-            stroke-dashoffset: 36.33; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-80 {
-            stroke-dashoffset: 34.6; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-81 {
-            stroke-dashoffset: 32.87; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-82 {
-            stroke-dashoffset: 31.14; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-83 {
-            stroke-dashoffset: 29.41; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-84 {
-            stroke-dashoffset: 27.68; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-85 {
-            stroke-dashoffset: 25.95; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-86 {
-            stroke-dashoffset: 24.22; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-87 {
-            stroke-dashoffset: 22.49; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-88 {
-            stroke-dashoffset: 20.76; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-89 {
-            stroke-dashoffset: 19.03; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-90 {
-            stroke-dashoffset: 17.3; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-91 {
-            stroke-dashoffset: 15.57; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-92 {
-            stroke-dashoffset: 13.84; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-93 {
-            stroke-dashoffset: 12.11; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-94 {
-            stroke-dashoffset: 10.38; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-95 {
-            stroke-dashoffset: 8.65; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-96 {
-            stroke-dashoffset: 6.92; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-97 {
-            stroke-dashoffset: 5.19; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-98 {
-            stroke-dashoffset: 3.46; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-99 {
-            stroke-dashoffset: 1.73; }
-          .progress-chart-container .progress-doughnut .progress-indicator svg .circle.percent-100 {
-            stroke-dashoffset: 0; }
-  .progress-chart-container .no-progress {
-    height: 70px;
-    width: 70px;
-    background-color: #dee2e6;
-    border-radius: 50%;
-    position: relative; }
-    .progress-chart-container .no-progress .icon {
-      position: absolute;
-      top: 50%;
-      /*rtl:ignore*/
-      left: 50%;
-      margin: 0;
-      transform: translate(-63%, -50%);
-      color: #fff;
-      height: 45px;
-      width: 45px;
-      font-size: 45px; }
-
-.block_myoverview .empty-placeholder-image-sm {
-  height: 50px; }
-
 .block_myoverview .empty-placeholder-image-lg {
   height: 125px; }
 
-.card-deck .card, .card-deck #page-enrol-users #filterform, #page-enrol-users .card-deck #filterform, .card-deck .que .history, .que .card-deck .history, .card-deck .userprofile .profile_tree section, .userprofile .profile_tree .card-deck section, .card-deck .groupinfobox, .card-deck .well {
+.block_myoverview .course-card {
+  margin-bottom: 0.5rem;
+  flex-basis: 100%; }
+
+.block_myoverview .course-info-container {
+  padding: 0.8rem; }
+
+.block_myoverview .course-card-footer {
+  padding: 0.8rem; }
+
+.block_myoverview .progress {
+  height: 0.5rem; }
+
+.block_myoverview .myoverviewimg {
+  height: 7rem;
+  background-position: center;
+  background-size: cover; }
+
+.block_myoverview .course-summaryitem {
+  border: 1px solid #dee2e6;
+  background-color: #fff; }
+
+.block_myoverview .summary img {
+  max-width: 100%; }
+
+@media (max-width: 767.98px) {
+  .block_myoverview .summaryimage {
+    max-height: 7rem; } }
+
+#region-main .block_myoverview .course-card {
   flex-grow: 0;
   flex-shrink: 0;
-  flex-basis: calc(50% - 30px); }
+  flex-basis: calc(50% - 0.5rem); }
 
-.card-deck .myoverviewimg {
-  height: 150px; }
-  .card-deck .myoverviewimg.courseimage {
-    background-position: center;
-    background-size: cover; }
+@media (min-width: 576px) {
+  #region-main .block_myoverview .course-card {
+    flex-basis: calc(50% - 0.5rem); } }
+
+@media (min-width: 768px) {
+  #region-main .block_myoverview .course-card {
+    flex-basis: calc(33.33% - 0.5rem); } }
+
+@media (min-width: 992px) {
+  #region-main .block_myoverview .course-card {
+    flex-basis: calc(25% - 0.5rem); } }
+
+@media (min-width: 1200px) {
+  #region-main .block_myoverview .course-card {
+    flex-basis: calc(20% - 0.5rem); } }
+
+@media (min-width: 992px) {
+  #region-main.has-blocks .block_myoverview .course-card {
+    flex-basis: calc(33.33% - 0.5rem); } }
+
+@media (min-width: 1200px) {
+  #region-main.has-blocks .block_myoverview .course-card {
+    flex-basis: calc(25% - 0.5rem); } }
+
+body.drawer-open-left #region-main.has-blocks .block_myoverview .course-card {
+  flex-basis: calc(50% - 0.5rem); }
+
+@media (min-width: 992px) {
+  body.drawer-open-left #region-main.has-blocks .block_myoverview .course-card {
+    flex-basis: calc(33.33% - 0.5rem); } }
 
 .block_settings .block_tree [aria-expanded="true"],
 .block_settings .block_tree [aria-expanded="true"].emptybranch,
index 7047055..4ff79b6 100644 (file)
@@ -68,7 +68,7 @@
                     <div> {{{ output.region_main_settings_menu }}} </div>
                 </div>
                 {{/hasregionmainsettingsmenu}}
-                <section id="region-main" {{#hasblocks}}class="has-blocks mb-3"{{/hasblocks}}>
+                <section id="region-main" {{#hasblocks}}class="has-blocks mb-2"{{/hasblocks}}>
 
                     {{#hasregionmainsettingsmenu}}
                         <div class="region_main_settings_menu_proxy"></div>
index 8eb1abb..7123185 100644 (file)
@@ -20,7 +20,7 @@
 
 {{! Start Block Container }}
 <section id="{{id}}"
-     class="{{#hidden}}hidden{{/hidden}} block block_{{type}} {{#hascontrols}}block_with_controls{{/hascontrols}} card mb-3"
+     class="{{#hidden}}hidden{{/hidden}} block block_{{type}} {{#hascontrols}}block_with_controls{{/hascontrols}} card mb-2"
      role="{{ariarole}}"
      data-block="{{type}}"
      {{#arialabel}}
index 79229ac..936abbc 100644 (file)
 }
 
 .block_myoverview {
-    background-color: transparent;
-
-    .row-fluid [class*="span"] {
-        margin-left: 0;
-    }
-
-    .empty-placeholder-image-sm {
-        height: 50px;
+    *,
+    *::before,
+    *::after {
+        box-sizing: border-box;
     }
-
     .empty-placeholder-image-lg {
         height: 125px;
     }
-
-    .courses-view-course-item {
-        height: 220px;
-        overflow-y: hidden;
+    .course-card {
+        .border-radius(@baseBorderRadius);
+        margin-bottom: 0.5rem;
+        flex-grow: 0;
+        flex-shrink: 0;
+        flex-basis: ~"calc(50% - 0.6rem)";
+    }
+    .course-info-container {
+        flex: 1 1 auto;
+        padding: 0.8rem;
+    }
+    .course-card-footer {
+        padding: 0.8rem;
+        background-color: @wellBackground;
+        border-top: 1px solid darken(@wellBackground, 7%);
+        .border-bottom-radius(@baseBorderRadius);
+    }
+    .progress {
+        height: 0.5rem;
+        margin-bottom: 0;
+    }
+    .myoverviewimg {
+        height: 7rem;
+        background-position: center;
+        background-size: cover;
+        .border-top-radius(@baseBorderRadius);
+    }
+    .list-group {
+        margin: 0;
+    }
+    .course-listitem {
+        display: block;
+        padding: 0.75rem 1.25rem;
+        margin-bottom: 0.5rem;
+        background-color: @white;
+        border: 1px solid @tableBorder;
+        .border-radius(@baseBorderRadius);
+    }
+    .course-summaryitem {
+        padding: 0.5rem;
+        background-color: @white;
+        border: 1px solid @tableBorder;
+        .border-radius(@baseBorderRadius);
+    }
+    .summary img {
+        max-width: 100%;
     }
 
-    h4 {
-        font-weight: normal;
+    @media (max-width: 576px) {
+        .summaryimage {
+            max-height: 7rem;
+        }
     }
 
-    .well {
-        background-color: @white;
-        box-shadow: none;
-        margin: 0 5px 10px;
+    @media (min-width: 576px) {
+        .card-deck {
+            display: flex;
+            flex-flow: row wrap;
+            margin-right: -.25rem;
+            margin-left: -.25rem;
+        }
+        .course-card {
+            display: flex;
+            flex-direction: column;
+            margin-right: 0.25rem;
+            margin-left: 0.25rem;
+        }
     }
-    .myoverviewimg {
-        height: 150px;
-        &.courseimage {
-            background-position: center;
-            background-size: cover;
+
+    @media (min-width: 1200px) {
+        .course-card {
+            flex-basis: ~"calc(33% - 0.5rem)";
         }
     }
-}
+}
\ No newline at end of file
index 3fd60ca..a2a2334 100644 (file)
 .w-25 {
     width: 25%;
 }
+
+.d-flex {
+    display: flex !important;
+}
+
+.flex-column {
+    flex-direction: column !important;
+}
+
+.align-self-stretch {
+    align-self: stretch;
+}
+
+.ml-auto {
+    margin-left: auto;
+}
+
+.mt-auto {
+    margin-top: auto;
+}
index 79dd22b..25b62ed 100644 (file)
@@ -16536,36 +16536,100 @@ body {
   height: 35px;
   width: 35px;
 }
-.block_myoverview {
-  background-color: transparent;
-}
-.block_myoverview .row-fluid [class*="span"] {
-  margin-left: 0;
-}
-.block_myoverview .empty-placeholder-image-sm {
-  height: 50px;
+.block_myoverview *,
+.block_myoverview *::before,
+.block_myoverview *::after {
+  box-sizing: border-box;
 }
 .block_myoverview .empty-placeholder-image-lg {
   height: 125px;
 }
-.block_myoverview .courses-view-course-item {
-  height: 220px;
-  overflow-y: hidden;
+.block_myoverview .course-card {
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+  margin-bottom: 0.5rem;
+  flex-grow: 0;
+  flex-shrink: 0;
+  flex-basis: calc(50% - 0.6rem);
 }
-.block_myoverview h4 {
-  font-weight: normal;
+.block_myoverview .course-info-container {
+  flex: 1 1 auto;
+  padding: 0.8rem;
 }
-.block_myoverview .well {
-  background-color: #fff;
-  box-shadow: none;
-  margin: 0 5px 10px;
+.block_myoverview .course-card-footer {
+  padding: 0.8rem;
+  background-color: #f5f5f5;
+  border-top: 1px solid #e3e3e3;
+  -webkit-border-bottom-right-radius: 4px;
+  -moz-border-radius-bottomright: 4px;
+  border-bottom-right-radius: 4px;
+  -webkit-border-bottom-left-radius: 4px;
+  -moz-border-radius-bottomleft: 4px;
+  border-bottom-left-radius: 4px;
 }
-.block_myoverview .myoverviewimg {
-  height: 150px;
+.block_myoverview .progress {
+  height: 0.5rem;
+  margin-bottom: 0;
 }
-.block_myoverview .myoverviewimg.courseimage {
+.block_myoverview .myoverviewimg {
+  height: 7rem;
   background-position: center;
   background-size: cover;
+  -webkit-border-top-right-radius: 4px;
+  -moz-border-radius-topright: 4px;
+  border-top-right-radius: 4px;
+  -webkit-border-top-left-radius: 4px;
+  -moz-border-radius-topleft: 4px;
+  border-top-left-radius: 4px;
+}
+.block_myoverview .list-group {
+  margin: 0;
+}
+.block_myoverview .course-listitem {
+  display: block;
+  padding: 0.75rem 1.25rem;
+  margin-bottom: 0.5rem;
+  background-color: #fff;
+  border: 1px solid #ddd;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+}
+.block_myoverview .course-summaryitem {
+  padding: 0.5rem;
+  background-color: #fff;
+  border: 1px solid #ddd;
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  border-radius: 4px;
+}
+.block_myoverview .summary img {
+  max-width: 100%;
+}
+@media (max-width: 576px) {
+  .block_myoverview .summaryimage {
+    max-height: 7rem;
+  }
+}
+@media (min-width: 576px) {
+  .block_myoverview .card-deck {
+    display: flex;
+    flex-flow: row wrap;
+    margin-right: -0.25rem;
+    margin-left: -0.25rem;
+  }
+  .block_myoverview .course-card {
+    display: flex;
+    flex-direction: column;
+    margin-right: 0.25rem;
+    margin-left: 0.25rem;
+  }
+}
+@media (min-width: 1200px) {
+  .block_myoverview .course-card {
+    flex-basis: calc(33% - 0.5rem);
+  }
 }
 /**
  * Moodle forms HTML isn't changeable via renderers (yet?) so this
@@ -22107,3 +22171,18 @@ ul.indented-list {
 .w-25 {
   width: 25%;
 }
+.d-flex {
+  display: flex !important;
+}
+.flex-column {
+  flex-direction: column !important;
+}
+.align-self-stretch {
+  align-self: stretch;
+}
+.ml-auto {
+  margin-left: auto;
+}
+.mt-auto {
+  margin-top: auto;
+}
diff --git a/theme/bootstrapbase/templates/block_myoverview/course-paging-content-item.mustache b/theme/bootstrapbase/templates/block_myoverview/course-paging-content-item.mustache
deleted file mode 100644 (file)
index 75fe6c8..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-{{!
-    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 block_myoverview/course-paging-content-item
-
-    This template renders each course block.
-
-    Example context (json):
-    {
-        "page": 1,
-        "active": true,
-        "courses": [
-            {
-                "fullnamedisplay": "course 1",
-                "viewurl": "https://www.google.com",
-                "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout."
-            },
-            {
-                "fullnamedisplay": "course 2",
-                "viewurl": "https://www.google.com",
-                "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout."
-            }
-        ]
-    }
-}}
-{{< block_myoverview/paging-content-item }}
-    {{$content}}
-        {{#courses}}
-            {{> block_myoverview/courses-view-course-item }}
-        {{/courses}}
-        <div class="clearfix"></div>
-    {{/content}}
-{{/ block_myoverview/paging-content-item }}
diff --git a/theme/bootstrapbase/templates/block_myoverview/courses-view-course-item.mustache b/theme/bootstrapbase/templates/block_myoverview/courses-view-course-item.mustache
deleted file mode 100644 (file)
index d9fd70e..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-{{!
-    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 block_myoverview/course-view-course-item
-
-    This template renders the course summary (view by courses) for the myoverview block.
-
-    Example context (json):
-    {
-        "shortname": "course 3",
-        "viewurl": "https://www.google.com",
-        "summary": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout."
-    }
-}}
-<div class="span6">
-    <div class="well well-small">
-        <a href="{{viewurl}}">
-            <div class="myoverviewimg m-b-1 {{classes}}" style='background-image: url("{{{courseimage}}}");'>
-            </div>
-        </a>
-        <div class="course-info-container" id="course-info-container-{{id}}">
-            <div class="media">
-                <div class="pull-left">
-                    <div class="media-object">
-                        {{> block_myoverview/progress-chart}}
-                    </div>
-                </div>
-                <div class="media-body">
-                    <h4 class="media-heading">
-                        <a href="{{viewurl}}" class="{{^visible}}dimmed{{/visible}}">{{{fullnamedisplay}}}</a>
-                    </h4>
-                </div>
-            </div>
-            <p class="text-muted">
-                {{#shortentext}} 140, {{{summary}}}{{/shortentext}}
-            </p>
-        </div>
-    </div>
-</div>
diff --git a/theme/bootstrapbase/templates/block_myoverview/courses-view-nav-grouping-display-filter.mustache b/theme/bootstrapbase/templates/block_myoverview/courses-view-nav-grouping-display-filter.mustache
deleted file mode 100644 (file)
index 0cd929c..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-{{!
-    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 block_myoverview/courses-view-nav-grouping-display-filter
-
-    This template renders the main content area for the myoverview block.
-
-    Example context (json):
-    {}
-}}
-<div data-region="courses-grouping-display-filter" class="btn-group">
-    <button type="button" class="btn btn-outline-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-        <span data-active-item-text>{{#str}} inprogress, block_myoverview {{/str}}</span>
-        <span data-region="caret" class="caret"></span>
-    </button>
-    <ul class="dropdown-menu" data-show-active-item>
-        <li class="dropdown-item active" data-target="#myoverview_courses_view_in_progress" data-toggle="tab">
-            <a tabindex="-1" href="#">{{#str}} inprogress, block_myoverview {{/str}}</a>
-        </li>
-        <li class="dropdown-item" data-target="#myoverview_courses_view_future" data-toggle="tab">
-            <a tabindex="-1" href="#">{{#str}} future, block_myoverview {{/str}}</a>
-        </li>
-        <li class="dropdown-item" data-target="#myoverview_courses_view_past" data-toggle="tab">
-            <a tabindex="-1" href="#">{{#str}} past, block_myoverview {{/str}}</a>
-        </li>
-    </ul>
-</div>
diff --git a/theme/bootstrapbase/templates/block_myoverview/courses-view.mustache b/theme/bootstrapbase/templates/block_myoverview/courses-view.mustache
deleted file mode 100644 (file)
index 5bfc78d..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-{{!
-    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 block_myoverview/courses-view
-
-    This template renders the courses view for the myoverview block.
-
-    Example context (json):
-    {}
-}}
-<div id="courses-view-{{uniqid}}" data-region="courses-view">
-    {{#hascourses}}
-    <div class="tab-content">
-        <div class="tab-pane active fade in" id="myoverview_courses_view_in_progress">
-            {{#inprogress}}
-                {{< block_myoverview/courses-view-by-status }}
-                    {{$id}}courses-view-in-progress{{/id}}
-                    {{$status}}1{{/status}}
-                    {{$pagingbarid}}pb-for-in-progress{{/pagingbarid}}
-                    {{$pagingcontentid}}pc-for-in-progress{{/pagingcontentid}}
-                {{/ block_myoverview/courses-view-by-status }}
-            {{/inprogress}}
-            {{^inprogress}}
-                <div class="text-xs-center text-center m-t-3">
-                    <img class="empty-placeholder-image-lg"
-                         src="{{urls.nocourses}}"
-                         alt="{{#str}} nocoursesinprogress, block_myoverview {{/str}}"
-                         role="presentation">
-                    <p class="text-muted m-t-1">{{#str}} nocoursesinprogress, block_myoverview {{/str}}</p>
-                </div>
-            {{/inprogress}}
-        </div>
-        <div class="tab-pane fade" id="myoverview_courses_view_future">
-            {{#future}}
-                {{< block_myoverview/courses-view-by-status }}
-                    {{$id}}courses-view-future{{/id}}
-                    {{$status}}2{{/status}}
-                    {{$pagingbarid}}pb-for-future{{/pagingbarid}}
-                    {{$pagingcontentid}}pc-for-in-progress{{/pagingcontentid}}
-                {{/ block_myoverview/courses-view-by-status }}
-            {{/future}}
-            {{^future}}
-                <div class="text-xs-center text-center m-t-3">
-                    <img class="empty-placeholder-image-lg"
-                         src="{{urls.nocourses}}"
-                         alt="{{#str}} nocoursesfuture, block_myoverview {{/str}}"
-                         role="presentation">
-                    <p class="text-muted m-t-1">{{#str}} nocoursesfuture, block_myoverview {{/str}}</p>
-                </div>
-            {{/future}}
-        </div>
-        <div class="tab-pane fade" id="myoverview_courses_view_past">
-            {{#past}}
-                {{< block_myoverview/courses-view-by-status }}
-                    {{$id}}courses-view-past{{/id}}
-                    {{$status}}0{{/status}}
-                    {{$pagingbarid}}pb-for-past{{/pagingbarid}}
-                    {{$pagingcontentid}}pc-for-in-progress{{/pagingcontentid}}
-                {{/ block_myoverview/courses-view-by-status }}
-            {{/past}}
-            {{^past}}
-                <div class="text-xs-center text-center m-t-3">
-                    <img class="empty-placeholder-image-lg"
-                         src="{{urls.nocourses}}"
-                         alt="{{#str}} nocoursespast, block_myoverview {{/str}}"
-                         role="presentation">
-                    <p class="text-muted m-t-1">{{#str}} nocoursespast, block_myoverview {{/str}}</p>
-                </div>
-            {{/past}}
-        </div>
-    </div>
-    {{/hascourses}}
-    {{^hascourses}}
-    <div class="text-xs-center text-center m-t-3">
-        <img class="empty-placeholder-image-lg"
-             src="{{urls.nocourses}}"
-             alt="{{#str}} nocourses, block_myoverview {{/str}}">
-        <p class="text-muted m-t-1">{{#str}} nocourses, block_myoverview {{/str}}</p>
-    </div>
-    {{/hascourses}}
-</div>
-{{#js}}
-require(['jquery', 'core/custom_interaction_events'], function($, customEvents) {
-    var root = $('#courses-view-{{uniqid}}');
-    customEvents.define(root, [customEvents.events.activate]);
-    root.on(customEvents.events.activate, '[data-toggle="btns"] > .btn', function(e) {
-        root.find('.btn.active').removeClass('active');
-        $(e.target).closest('.btn').addClass('active');
-    });
-});
-{{/js}}
diff --git a/theme/bootstrapbase/templates/block_myoverview/main.mustache b/theme/bootstrapbase/templates/block_myoverview/main.mustache
deleted file mode 100644 (file)
index 3a8a005..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-{{!
-    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 block_myoverview/main
-
-    This template renders the main content area for the myoverview block.
-
-    Example context (json):
-    {}
-}}
-
-<div id="block-myoverview-{{uniqid}}" class="block-myoverview" data-region="myoverview">
-    <div class="container-fluid p-0 m-b-1">
-        <div class="row-fluid no-gutters">
-            {{#coursesview}}
-                {{#hascourses}}
-                    <div class="{{#viewingtimeline}}d-none{{/viewingtimeline}}" data-tab-content="courses">
-                        {{> block_myoverview/courses-view-nav-grouping-display-filter }}
-                    </div>
-                {{/hascourses}}
-            {{/coursesview}}
-        </div>
-    </div>
-    <div class="container-fluid p-0">
-        {{#coursesview}}
-        {{> block_myoverview/courses-view }}
-        {{/coursesview}}
-    </div>
-</div>
diff --git a/theme/bootstrapbase/templates/block_myoverview/paging-bar.mustache b/theme/bootstrapbase/templates/block_myoverview/paging-bar.mustache
deleted file mode 100644 (file)
index 96d44d1..0000000
+++ /dev/null
@@ -1,97 +0,0 @@
-{{!
-    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 block_myoverview/paging-bar
-
-    This template renders the bootstrap style paging bar.
-
-    Example context (json):
-    {
-        "pagingbar": {
-            "pagecount": 2,
-            "previous": {},
-            "next": {},
-            "first": {
-                "url": "#",
-                "page": "first"
-            },
-            "last": {
-                "url": "#",
-                "page": "last"
-            },
-            "pages": [
-                {
-                    "url": "#",
-                    "number": 1,
-                    "page": "1",
-                    "active": true
-                },
-                {
-                    "url": "#",
-                    "number": 2,
-                    "page": "2"
-                }
-            ]
-        }
-    }
-}}
-{{#pagingbar}}
-<div aria-label="{{label}}"
-     id="{{$pagingbarid}}paging-bar-{{uniqid}}{{/pagingbarid}}"
-     class="pagination"
-     data-region="paging-bar"
-     data-page-count="{{pagecount}}">
-
-    <ul>
-        {{#previous}}
-            {{< block_myoverview/paging-bar-item }}
-                {{$item-content}}
-                    <span aria-hidden="true">&laquo;</span>
-                    <span class="sr-only">{{#str}}previous{{/str}}</span>
-                {{/item-content}}
-            {{/ block_myoverview/paging-bar-item }}
-        {{/previous}}
-        {{#first}}
-            {{< block_myoverview/paging-bar-item }}
-                {{$pagenumber}}first{{/pagenumber}}
-            {{/ block_myoverview/paging-bar-item }}
-        {{/first}}
-        {{#pages}}
-            {{> block_myoverview/paging-bar-item }}
-        {{/pages}}
-        {{#last}}
-            {{< block_myoverview/paging-bar-item }}
-                {{$pagenumber}}last{{/pagenumber}}
-            {{/ block_myoverview/paging-bar-item }}
-        {{/last}}
-        {{#next}}
-            {{< block_myoverview/paging-bar-item }}
-                {{$item-content}}
-                    <span aria-hidden="true">&raquo;</span>
-                    <span class="sr-only">{{#str}}next{{/str}}</span>
-                {{/item-content}}
-            {{/ block_myoverview/paging-bar-item }}
-        {{/next}}
-    </ul>
-</div>
-{{#js}}
-require(['jquery', 'block_myoverview/paging_bar'], function($, PagingBar) {
-    var root = $('#{{$pagingbarid}}paging-bar-{{uniqid}}{{/pagingbarid}}');
-    PagingBar.registerEventListeners(root);
-});
-{{/js}}
-{{/pagingbar}}
index 85a1183..e792b0d 100644 (file)
         <span data-region="caret" class="caret"></span>
     </button>
     <ul role="menu" class="dropdown-menu" data-show-active-item>
-        <li class="dropdown-item" data-from="-14">
+        <li class="dropdown-item {{#all}} active {{/all}}" data-from="-14" data-filtername="all">
             <a href="#" aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} all, core {{/str}}{{/str}}">
                 {{#str}} all, core {{/str}}
             </a>
         </li>
-        <li class="dropdown-item" data-from="-14" data-to="0">
+        <li class="dropdown-item {{#overdue}} active {{/overdue}}" data-from="-14" data-to="0" data-filtername="overdue">
             <a href="#" aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} overdue, block_timeline {{/str}}{{/str}}">
                 {{#str}} overdue, block_timeline {{/str}}
             </a>
         </li>
         <li class="divider"></li>
-        <li class="dropdown-item" data-from="0" data-to="7">
+        <li class="dropdown-item {{#next7days}} active {{/next7days}}" data-from="0" data-to="7" data-filtername="next7days">
             <a href="#" aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next7days, block_timeline {{/str}}{{/str}}">
                 {{#str}} next7days, block_timeline {{/str}}
             </a>
         </li>
-        <li class="dropdown-item active" data-from="0" data-to="30">
+        <li class="dropdown-item {{#next30days}} active {{/next30days}}" data-from="0" data-to="30" data-filtername="next30days">
             <a href="#" aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next30days, block_timeline {{/str}}{{/str}}">
                 {{#str}} next30days, block_timeline {{/str}}
             </a>
         </li>
-        <li class="dropdown-item" data-from="0" data-to="90">
+        <li class="dropdown-item {{#next3months}} active {{/next3months}}" data-from="0" data-to="90" data-filtername="next3months">
             <a href="#" aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next3months, block_timeline {{/str}}{{/str}}">
                 {{#str}} next3months, block_timeline {{/str}}
             </a>
         </li>
-        <li class="dropdown-item" data-from="0" data-to="180">
+        <li class="dropdown-item {{#next6months}} active {{/next6months}}" data-from="0" data-to="180" data-filtername="next6months">
             <a href="#" aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next6months, block_timeline {{/str}}{{/str}}">
                 {{#str}} next6months, block_timeline {{/str}}
             </a>
index 4caeda9..9339cb4 100644 (file)
         <span data-region="caret" class="caret"></span>
     </button>
     <ul role="menu" class="dropdown-menu dropdown-menu-right" data-show-active-item>
-        <li class="dropdown-item active" data-target="#view_dates_{{uniqid}}" data-toggle="tab">
+        <li class="dropdown-item {{#sorttimelinedates}}active{{/sorttimelinedates}}" data-target="#view_dates_{{uniqid}}" data-toggle="tab" data-filtername="sortbydates">
             <a href="#" aria-label="{{#str}} ariaviewselectoroption, block_timeline, {{#str}} sortbydates, block_timeline {{/str}}{{/str}}">
                 {{#str}} sortbydates, block_timeline {{/str}}
             </a>
         </li>
-        <li class="dropdown-item" data-target="#view_courses_{{uniqid}}" data-toggle="tab">
+        <li class="dropdown-item {{#sorttimelinecourses}}active{{/sorttimelinecourses}}" data-target="#view_courses_{{uniqid}}" data-toggle="tab" data-filtername="sortbycourses">
             <a href="#" aria-label="{{#str}} ariaviewselectoroption, block_timeline, {{#str}} sortbycourses, block_timeline {{/str}}{{/str}}">
                 {{#str}} sortbycourses, block_timeline {{/str}}
             </a>
index 7d67b03..352631d 100644 (file)
     This template renders the timeline view for the timeline block.
 
     Example context (json):
-    {}
+    {
+        "midnight": 1538954668,
+        "coursepages": [
+            {}
+        ],
+        "urls": {
+            "nocourses": "#",
+            "noevents": "#"
+        },
+        "sorttimelinedates": true,
+        "sorttimelinecourses": false,
+        "selectedfilter": "all",
+        "hasdaysoffset": true,
+        "hasdayslimit": false,
+        "nodayslimit": true,
+        "all": true,
+        "overdue": false,
+        "next7days": false,
+        "next30days": false,
+        "next3months": false,
+        "next6months": false,
+        "daysoffset": -14,
+        "dayslimit": false
+    }
 }}
 <div data-region="timeline-view">
     <div class="tab-content" style="overflow: visible">
-        <div class="tab-pane active fade in" data-region="view-dates" id="view_dates_{{uniqid}}">
+        <div class="tab-pane {{#sorttimelinedates}}active in{{/sorttimelinedates}} fade" data-region="view-dates" id="view_dates_{{uniqid}}">
             {{> block_timeline/view-dates }}
         </div>
         <div
-            class="tab-pane fade"
+            class="tab-pane {{#sorttimelinecourses}}active in{{/sorttimelinecourses}} fade"
             data-region="view-courses"
             data-midnight="{{midnight}}"
             data-limit="2"