MDL-63044 block_timeline: add timeline block
authorRyan Wyllie <ryan@moodle.com>
Mon, 27 Aug 2018 08:53:44 +0000 (16:53 +0800)
committerRyan Wyllie <ryan@moodle.com>
Thu, 27 Sep 2018 05:10:45 +0000 (13:10 +0800)
55 files changed:
admin/tool/usertours/tests/behat/create_tour.feature
blocks/timeline/amd/build/calendar_events_repository.min.js [new file with mode: 0644]
blocks/timeline/amd/build/event_list.min.js [new file with mode: 0644]
blocks/timeline/amd/build/main.min.js [new file with mode: 0644]
blocks/timeline/amd/build/paging_bar.min.js [new file with mode: 0644]
blocks/timeline/amd/build/paging_content.min.js [new file with mode: 0644]
blocks/timeline/amd/build/view.min.js [new file with mode: 0644]
blocks/timeline/amd/build/view_courses.min.js [new file with mode: 0644]
blocks/timeline/amd/build/view_dates.min.js [new file with mode: 0644]
blocks/timeline/amd/build/view_nav.min.js [new file with mode: 0644]
blocks/timeline/amd/src/calendar_events_repository.js [new file with mode: 0644]
blocks/timeline/amd/src/event_list.js [new file with mode: 0644]
blocks/timeline/amd/src/main.js [new file with mode: 0644]
blocks/timeline/amd/src/view.js [new file with mode: 0644]
blocks/timeline/amd/src/view_courses.js [new file with mode: 0644]
blocks/timeline/amd/src/view_dates.js [new file with mode: 0644]
blocks/timeline/amd/src/view_nav.js [new file with mode: 0644]
blocks/timeline/block_timeline.php [new file with mode: 0644]
blocks/timeline/classes/output/main.php [new file with mode: 0644]
blocks/timeline/classes/output/renderer.php [new file with mode: 0644]
blocks/timeline/classes/privacy/provider.php [new file with mode: 0644]
blocks/timeline/db/access.php [new file with mode: 0644]
blocks/timeline/db/install.php [new file with mode: 0644]
blocks/timeline/lang/en/block_timeline.php [new file with mode: 0644]
blocks/timeline/pix/activities.svg [new file with mode: 0644]
blocks/timeline/pix/courses.svg [new file with mode: 0644]
blocks/timeline/templates/course-item-loading-placeholder.mustache [new file with mode: 0644]
blocks/timeline/templates/course-item.mustache [new file with mode: 0644]
blocks/timeline/templates/course-items.mustache [new file with mode: 0644]
blocks/timeline/templates/event-list-content.mustache [new file with mode: 0644]
blocks/timeline/templates/event-list-item.mustache [new file with mode: 0644]
blocks/timeline/templates/event-list-items.mustache [new file with mode: 0644]
blocks/timeline/templates/event-list.mustache [new file with mode: 0644]
blocks/timeline/templates/main.mustache [new file with mode: 0644]
blocks/timeline/templates/nav-day-filter.mustache [new file with mode: 0644]
blocks/timeline/templates/nav-view-selector.mustache [new file with mode: 0644]
blocks/timeline/templates/placeholder-event-list-item.mustache [new file with mode: 0644]
blocks/timeline/templates/view-courses.mustache [new file with mode: 0644]
blocks/timeline/templates/view-dates.mustache [new file with mode: 0644]
blocks/timeline/templates/view.mustache [new file with mode: 0644]
blocks/timeline/tests/behat/block_timeline_courses.feature [new file with mode: 0644]
blocks/timeline/tests/behat/block_timeline_dates.feature [new file with mode: 0644]
blocks/timeline/version.php [new file with mode: 0644]
blocks/upgrade.txt
lib/blocklib.php
lib/classes/plugin_manager.php
theme/bootstrapbase/templates/block_timeline/course-item-loading-placeholder.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_timeline/event-list-item.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_timeline/event-list-items.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_timeline/event-list.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_timeline/main.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_timeline/nav-day-filter.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_timeline/nav-view-selector.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_timeline/placeholder-event-list-item.mustache [new file with mode: 0644]
theme/bootstrapbase/templates/block_timeline/view.mustache [new file with mode: 0644]

index 33c7db7..09f99ff 100644 (file)
@@ -27,16 +27,16 @@ Feature: Add a new user tour
       | Selector                    | .usermenu            | User menu         | This is your personal user menu. You'll find your personal preferences and your user profile here. |
     When I am on homepage
     Then I should see "Welcome to your personal learning space. We'd like to give you a quick tour to show you some of the areas you may find helpful"
-    And I press "Next"
+    And I click on "Next" "button" in the "[data-role='flexitour-step']" "css_element"
     And I should see "This area shows you what's happening in some of your courses"
     And I should not see "This is the Calendar. All of your assignments and due dates can be found here"
-    And I press "Next"
+    And I click on "Next" "button" in the "[data-role='flexitour-step']" "css_element"
     And I should see "This is the Calendar. All of your assignments and due dates can be found here"
     And I should not see "This area shows you what's happening in some of your courses"
-    And I press "Prev"
+    And I click on "Prev" "button" in the "[data-role='flexitour-step']" "css_element"
     And I should not see "This is the Calendar. All of your assignments and due dates can be found here"
     And I should see "This area shows you what's happening in some of your courses"
-    And I press "End tour"
+    And I click on "End tour" "button" in the "[data-role='flexitour-step']" "css_element"
     And I should not see "This area shows you what's happening in some of your courses"
     And I am on homepage
     And I should not see "Welcome to your personal learning space. We'd like to give you a quick tour to show you some of the areas you may find helpful"
diff --git a/blocks/timeline/amd/build/calendar_events_repository.min.js b/blocks/timeline/amd/build/calendar_events_repository.min.js
new file mode 100644 (file)
index 0000000..5c6e35a
Binary files /dev/null and b/blocks/timeline/amd/build/calendar_events_repository.min.js differ
diff --git a/blocks/timeline/amd/build/event_list.min.js b/blocks/timeline/amd/build/event_list.min.js
new file mode 100644 (file)
index 0000000..cd209c5
Binary files /dev/null and b/blocks/timeline/amd/build/event_list.min.js differ
diff --git a/blocks/timeline/amd/build/main.min.js b/blocks/timeline/amd/build/main.min.js
new file mode 100644 (file)
index 0000000..4b8ded9
Binary files /dev/null and b/blocks/timeline/amd/build/main.min.js differ
diff --git a/blocks/timeline/amd/build/paging_bar.min.js b/blocks/timeline/amd/build/paging_bar.min.js
new file mode 100644 (file)
index 0000000..40e8b6d
Binary files /dev/null and b/blocks/timeline/amd/build/paging_bar.min.js differ
diff --git a/blocks/timeline/amd/build/paging_content.min.js b/blocks/timeline/amd/build/paging_content.min.js
new file mode 100644 (file)
index 0000000..97da2ca
Binary files /dev/null and b/blocks/timeline/amd/build/paging_content.min.js differ
diff --git a/blocks/timeline/amd/build/view.min.js b/blocks/timeline/amd/build/view.min.js
new file mode 100644 (file)
index 0000000..e374dc3
Binary files /dev/null and b/blocks/timeline/amd/build/view.min.js differ
diff --git a/blocks/timeline/amd/build/view_courses.min.js b/blocks/timeline/amd/build/view_courses.min.js
new file mode 100644 (file)
index 0000000..918fdd1
Binary files /dev/null and b/blocks/timeline/amd/build/view_courses.min.js differ
diff --git a/blocks/timeline/amd/build/view_dates.min.js b/blocks/timeline/amd/build/view_dates.min.js
new file mode 100644 (file)
index 0000000..b29cb60
Binary files /dev/null and b/blocks/timeline/amd/build/view_dates.min.js differ
diff --git a/blocks/timeline/amd/build/view_nav.min.js b/blocks/timeline/amd/build/view_nav.min.js
new file mode 100644 (file)
index 0000000..c66ad2f
Binary files /dev/null and b/blocks/timeline/amd/build/view_nav.min.js differ
diff --git a/blocks/timeline/amd/src/calendar_events_repository.js b/blocks/timeline/amd/src/calendar_events_repository.js
new file mode 100644 (file)
index 0000000..ace7909
--- /dev/null
@@ -0,0 +1,166 @@
+// 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 calendar events from the server.
+ *
+ * @module     block_timeline/calendar_events_repository
+ * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notification) {
+
+    var DEFAULT_LIMIT = 20;
+
+    /**
+     * Retrieve a list of calendar events for the logged in user for the
+     * given course.
+     *
+     * Valid args are:
+     * int courseid     Only get events for this course
+     * int starttime    Only get events after this time
+     * int endtime      Only get events before this time
+     * int limit        Limit the number of results returned
+     * int aftereventid Offset the result set from the given id
+     *
+     * @method queryByCourse
+     * @param {object} args The request arguments
+     * @return {promise} Resolved with an array of the calendar events
+     */
+    var queryByCourse = function(args) {
+        if (!args.hasOwnProperty('limit')) {
+            args.limit = DEFAULT_LIMIT;
+        }
+
+        args.limitnum = args.limit;
+        delete args.limit;
+
+        if (args.hasOwnProperty('starttime')) {
+            args.timesortfrom = args.starttime;
+            delete args.starttime;
+        }
+
+        if (args.hasOwnProperty('endtime')) {
+            args.timesortto = args.endtime;
+            delete args.endtime;
+        }
+
+        var request = {
+            methodname: 'core_calendar_get_action_events_by_course',
+            args: args
+        };
+
+        var promise = Ajax.call([request])[0];
+
+        promise.fail(Notification.exception);
+
+        return promise;
+    };
+
+    /**
+     * Retrieve a list of calendar events for the given courses for the
+     * logged in user.
+     *
+     * Valid args are:
+     * array courseids    Get events for these courses
+     * int   starttime    Only get events after this time
+     * int   endtime      Only get events before this time
+     * int   limit        Limit the number of results returned
+     *
+     * @method queryByCourses
+     * @param {object} args The request arguments
+     * @return {promise} Resolved with an array of the calendar events
+     */
+    var queryByCourses = function(args) {
+        if (!args.hasOwnProperty('limit')) {
+            // This is intentionally smaller than the default limit.
+            args.limit = 10;
+        }
+
+        args.limitnum = args.limit;
+        delete args.limit;
+
+        if (args.hasOwnProperty('starttime')) {
+            args.timesortfrom = args.starttime;
+            delete args.starttime;
+        }
+
+        if (args.hasOwnProperty('endtime')) {
+            args.timesortto = args.endtime;
+            delete args.endtime;
+        }
+
+        var request = {
+            methodname: 'core_calendar_get_action_events_by_courses',
+            args: args
+        };
+
+        var promise = Ajax.call([request])[0];
+
+        promise.fail(Notification.exception);
+
+        return promise;
+    };
+
+    /**
+     * Retrieve a list of calendar events for the logged in user after the given
+     * time.
+     *
+     * Valid args are:
+     * int starttime    Only get events after this time
+     * int endtime      Only get events before this time
+     * int limit        Limit the number of results returned
+     * int aftereventid Offset the result set from the given id
+     *
+     * @method queryByTime
+     * @param {object} args The request arguments
+     * @return {promise} Resolved with an array of the calendar events
+     */
+    var queryByTime = function(args) {
+        if (!args.hasOwnProperty('limit')) {
+            args.limit = DEFAULT_LIMIT;
+        }
+
+        args.limitnum = args.limit;
+        delete args.limit;
+
+        if (args.hasOwnProperty('starttime')) {
+            args.timesortfrom = args.starttime;
+            delete args.starttime;
+        }
+
+        if (args.hasOwnProperty('endtime')) {
+            args.timesortto = args.endtime;
+            delete args.endtime;
+        }
+
+        var request = {
+            methodname: 'core_calendar_get_action_events_by_timesort',
+            args: args
+        };
+
+        var promise = Ajax.call([request])[0];
+
+        promise.fail(Notification.exception);
+
+        return promise;
+    };
+
+    return {
+        queryByTime: queryByTime,
+        queryByCourse: queryByCourse,
+        queryByCourses: queryByCourses,
+    };
+});
diff --git a/blocks/timeline/amd/src/event_list.js b/blocks/timeline/amd/src/event_list.js
new file mode 100644 (file)
index 0000000..dfd1517
--- /dev/null
@@ -0,0 +1,465 @@
+// 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 list of calendar events for a
+ * given day range.
+ *
+ * @module     block_timeline/event_list
+ * @package    block_timeline
+ * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(
+[
+    'jquery',
+    'core/notification',
+    'core/templates',
+    'core/paged_content_factory',
+    'core/str',
+    'core/user_date',
+    'block_timeline/calendar_events_repository'
+],
+function(
+    $,
+    Notification,
+    Templates,
+    PagedContentFactory,
+    Str,
+    UserDate,
+    CalendarEventsRepository
+) {
+
+    var SECONDS_IN_DAY = 60 * 60 * 24;
+
+    var SELECTORS = {
+        EMPTY_MESSAGE: '[data-region="empty-message"]',
+        ROOT: '[data-region="event-list-container"]',
+        EVENT_LIST_CONTENT: '[data-region="event-list-content"]',
+        EVENT_LIST_LOADING_PLACEHOLDER: '[data-region="event-list-loading-placeholder"]',
+    };
+
+    var TEMPLATES = {
+        EVENT_LIST_CONTENT: 'block_timeline/event-list-content'
+    };
+
+    // 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,
+        ariaLabels: {
+            itemsperpagecomponents: 'ariaeventlistpagelimit, block_timeline',
+        }
+    };
+
+    /**
+     * Hide the content area and display the empty content message.
+     *
+     * @param {object} root The container element
+     */
+    var hideContent = function(root) {
+        root.find(SELECTORS.EVENT_LIST_CONTENT).addClass('hidden');
+        root.find(SELECTORS.EMPTY_MESSAGE).removeClass('hidden');
+    };
+
+    /**
+     * Show the content area and hide the empty content message.
+     *
+     * @param {object} root The container element
+     */
+    var showContent = function(root) {
+        root.find(SELECTORS.EVENT_LIST_CONTENT).removeClass('hidden');
+        root.find(SELECTORS.EMPTY_MESSAGE).addClass('hidden');
+    };
+
+    /**
+     * Empty the content area.
+     *
+     * @param {object} root The container element
+     */
+    var emptyContent = function(root) {
+        root.find(SELECTORS.EVENT_LIST_CONTENT).empty();
+    };
+
+    /**
+     * Construct the template context from a list of calendar events. The events
+     * are grouped by which day they are on. The day is calculated from the user's
+     * midnight timestamp to ensure that the calculation is timezone agnostic.
+     *
+     * The return data structure will look like:
+     * {
+     *      eventsbyday: [
+     *          {
+     *              dayTimestamp: 1533744000,
+     *              events: [
+     *                  { ...event 1 data... },
+     *                  { ...event 2 data... }
+     *              ]
+     *          },
+     *          {
+     *              dayTimestamp: 1533830400,
+     *              events: [
+     *                  { ...event 3 data... },
+     *                  { ...event 4 data... }
+     *              ]
+     *          }
+     *      ]
+     * }
+     *
+     * Each day timestamp is the day's midnight in the user's timezone.
+     *
+     * @param {array} calendarEvents List of calendar events
+     * @param {Number} midnight A timestamp representing midnight in the user's timezone
+     * @return {object}
+     */
+    var buildTemplateContext = function(calendarEvents, midnight) {
+        var eventsByDay = {};
+        var templateContext = {
+            eventsbyday: []
+        };
+
+        calendarEvents.forEach(function(calendarEvent) {
+            var dayTimestamp = UserDate.getUserMidnightForTimestamp(calendarEvent.timesort, midnight);
+            if (eventsByDay[dayTimestamp]) {
+                eventsByDay[dayTimestamp].push(calendarEvent);
+            } else {
+                eventsByDay[dayTimestamp] = [calendarEvent];
+            }
+        });
+
+        Object.keys(eventsByDay).forEach(function(dayTimestamp) {
+            var events = eventsByDay[dayTimestamp];
+            templateContext.eventsbyday.push({
+                past: dayTimestamp < midnight,
+                dayTimestamp: dayTimestamp,
+                events: events
+            });
+        });
+
+        return templateContext;
+    };
+
+    /**
+     * Render the HTML for the given calendar events.
+     *
+     * @param {array} calendarEvents  A list of calendar events
+     * @param {Number} midnight A timestamp representing midnight for the user
+     * @return {promise} Resolved with HTML and JS strings.
+     */
+    var render = function(calendarEvents, midnight) {
+        var templateContext = buildTemplateContext(calendarEvents, midnight);
+        var templateName = TEMPLATES.EVENT_LIST_CONTENT;
+
+        return Templates.render(templateName, templateContext);
+    };
+
+    /**
+     * Retrieve a list of calendar events from the server for the given
+     * constraints.
+     *
+     * @param {Number} midnight The user's midnight time in unix timestamp.
+     * @param {Number} limit Limit the result set to this number of items
+     * @param {Number} daysOffset How many days (from midnight) to offset the results from
+     * @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to
+     * @param {int|falsey} lastId The ID of the last seen event (if any)
+     * @param {int|undefined} courseId Course ID to restrict events to
+     * @return {promise} A jquery promise
+     */
+    var load = function(midnight, limit, daysOffset, daysLimit, lastId, courseId) {
+        var startTime = midnight + (daysOffset * SECONDS_IN_DAY);
+        var endTime = daysLimit != undefined ? midnight + (daysLimit * SECONDS_IN_DAY) : false;
+
+        var args = {
+            starttime: startTime,
+            limit: limit,
+        };
+
+        if (lastId) {
+            args.aftereventid = lastId;
+        }
+
+        if (endTime) {
+            args.endtime = endTime;
+        }
+
+        if (courseId) {
+            // If we have a course id then we only want events from that course.
+            args.courseid = courseId;
+            return CalendarEventsRepository.queryByCourse(args);
+        } else {
+            // Otherwise we want events from any course.
+            return CalendarEventsRepository.queryByTime(args);
+        }
+    };
+
+    /**
+     * Handle a single page request from the paged content. Uses the given page data to request
+     * the events from the server.
+     *
+     * Checks the given preloadedPages before sending a request to the server to make sure we
+     * don't load data unnecessarily.
+     *
+     * @param {object} pageData A single page data (see core/paged_content_pages for more info).
+     * @param {object} actions Paged content actions (see core/paged_content_pages for more info).
+     * @param {Number} midnight The user's midnight time in unix timestamp.
+     * @param {object} lastIds The last event ID for each loaded page. Page number is key, id is value.
+     * @param {object} preloadedPages An object of preloaded page data. Page number as key, data promise as value.
+     * @param {int|undefined} courseId Course ID to restrict events to
+     * @param {Number} daysOffset How many days (from midnight) to offset the results from
+     * @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to
+     * @return {object} jQuery promise resolved with calendar events.
+     */
+    var loadEventsFromPageData = function(
+        pageData,
+        actions,
+        midnight,
+        lastIds,
+        preloadedPages,
+        courseId,
+        daysOffset,
+        daysLimit
+    ) {
+        var pageNumber = pageData.pageNumber;
+        var limit = pageData.limit;
+        var lastPageNumber = pageNumber;
+
+        // This is here to protect us if, for some reason, the pages
+        // are loaded out of order somehow and we don't have a reference
+        // to the previous page. In that case, scan back to find the most
+        // recent page we've seen.
+        while (!lastIds.hasOwnProperty(lastPageNumber)) {
+            lastPageNumber--;
+        }
+        // Use the last id of the most recent page.
+        var lastId = lastIds[lastPageNumber];
+        var eventsPromise = null;
+
+        if (preloadedPages && preloadedPages.hasOwnProperty(pageNumber)) {
+            // This page has been preloaded so use that rather than load the values
+            // again.
+            eventsPromise = preloadedPages[pageNumber];
+        } else {
+            // Load one more than the given limit so that we can tell if there
+            // is more content to load after this.
+            eventsPromise = load(midnight, limit + 1, daysOffset, daysLimit, lastId, courseId);
+        }
+
+        return eventsPromise.then(function(result) {
+            if (!result.events.length) {
+                // If we didn't get any events back then tell the paged content
+                // that we're done loading.
+                actions.allItemsLoaded(pageNumber);
+                return [];
+            }
+
+            var calendarEvents = result.events;
+            // We expect to receive limit + 1 events back from the server.
+            // Any less means there are no more events to load.
+            var loadedAll = calendarEvents.length <= limit;
+
+            if (loadedAll) {
+                // Tell the pagination that everything is loaded.
+                actions.allItemsLoaded(pageNumber);
+            } else {
+                // Remove the last element from the array because it isn't
+                // needed in this result set.
+                calendarEvents.pop();
+            }
+
+            return calendarEvents;
+        });
+    };
+
+    /**
+     * Use the paged content factory to create a paged content element for showing
+     * the event list. We only provide a page limit to the factory because we don't
+     * know exactly how many pages we'll need. This creates a paging bar with just
+     * next/previous buttons.
+     *
+     * This function specifies the callback for loading the event data that the user
+     * is requesting.
+     *
+     * @param {int|array} pageLimit A single limit or list of limits as options for the paged content
+     * @param {object} preloadedPages An object of preloaded page data. Page number as key, data promise as value.
+     * @param {Number} midnight The user's midnight time in unix timestamp.
+     * @param {object} firstLoad A jQuery promise to be resolved after the first set of data is loaded.
+     * @param {int|undefined} courseId Course ID to restrict events to
+     * @param {Number} daysOffset How many days (from midnight) to offset the results from
+     * @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to
+     * @param {string} paginationAriaLabel String to set as the aria label for the pagination bar.
+     * @return {object} jQuery promise.
+     */
+    var createPagedContent = function(
+        pageLimit,
+        preloadedPages,
+        midnight,
+        firstLoad,
+        courseId,
+        daysOffset,
+        daysLimit,
+        paginationAriaLabel
+    ) {
+        // Remember the last event id we loaded on each page because we can't
+        // use the offset value since the backend can skip events if the user doesn't
+        // have the capability to see them. Instead we load the next page of events
+        // based on the last seen event id.
+        var lastIds = {'1': 0};
+        var hasContent = false;
+        var config = $.extend({}, DEFAULT_PAGED_CONTENT_CONFIG);
+
+        return Str.get_string(
+                'ariaeventlistpagelimit',
+                'block_timeline',
+                $.isArray(pageLimit) ? pageLimit[0] : pageLimit
+            )
+            .then(function(string) {
+                config.ariaLabels.itemsperpage = string;
+                config.ariaLabels.paginationnav = paginationAriaLabel;
+                return string;
+            })
+            .then(function() {
+                return PagedContentFactory.createWithLimit(
+                    pageLimit,
+                    function(pagesData, actions) {
+                        var promises = [];
+
+                        pagesData.forEach(function(pageData) {
+                            var pageNumber = pageData.pageNumber;
+                            // Load the page data.
+                            var pagePromise = loadEventsFromPageData(
+                                pageData,
+                                actions,
+                                midnight,
+                                lastIds,
+                                preloadedPages,
+                                courseId,
+                                daysOffset,
+                                daysLimit
+                            ).then(function(calendarEvents) {
+                                if (calendarEvents.length) {
+                                    // Remember that we've loaded content.
+                                    hasContent = true;
+                                    // Remember the last id we've seen.
+                                    var lastEventId = calendarEvents[calendarEvents.length - 1].id;
+                                    // Record the id that the next page will need to start from.
+                                    lastIds[pageNumber + 1] = lastEventId;
+                                    // Get the HTML and JS for these calendar events.
+                                    return render(calendarEvents, midnight);
+                                } else {
+                                    return calendarEvents;
+                                }
+                            })
+                            .catch(Notification.exception);
+
+                            promises.push(pagePromise);
+                        });
+
+                        $.when.apply($, promises).then(function() {
+                            // Tell the calling code that the first page has been loaded
+                            // and whether it contains any content.
+                            firstLoad.resolve(hasContent);
+                            return;
+                        })
+                        .catch(function() {
+                            firstLoad.resolve(hasContent);
+                        });
+
+                        return promises;
+                    },
+                    config
+                );
+            });
+    };
+
+    /**
+     * Create a paged content region for the calendar events in the given root element.
+     * The content of the root element are replaced with a new paged content section
+     * each time this function is called.
+     *
+     * This function will be called each time the offset or limit values are changed to
+     * reload the event list region.
+     *
+     * @param {object} root The event list container element
+     * @param {int|array} pageLimit A single limit or list of limits as options for the paged content
+     * @param {object} preloadedPages An object of preloaded page data. Page number as key, data promise as value.
+     * @param {string} paginationAriaLabel String to set as the aria label for the pagination bar.
+     */
+    var init = function(root, pageLimit, preloadedPages, paginationAriaLabel) {
+        root = $(root);
+
+        // Create a promise that will be resolved once the first set of page
+        // data has been loaded. This ensures that the loading placeholder isn't
+        // hidden until we have all of the data back to prevent the page elements
+        // jumping around.
+        var firstLoad = $.Deferred();
+        var eventListContent = root.find(SELECTORS.EVENT_LIST_CONTENT);
+        var loadingPlaceholder = root.find(SELECTORS.EVENT_LIST_LOADING_PLACEHOLDER);
+        var courseId = root.attr('data-course-id');
+        var daysOffset = parseInt(root.attr('data-days-offset'), 10);
+        var daysLimit = root.attr('data-days-limit');
+        var midnight = parseInt(root.attr('data-midnight'), 10);
+
+        // Make sure the content area and loading placeholder is visible.
+        // This is because the init function can be called to re-initialise
+        // an existing event list area.
+        emptyContent(root);
+        showContent(root);
+        loadingPlaceholder.removeClass('hidden');
+
+        // Days limit isn't mandatory.
+        if (daysLimit != undefined) {
+            daysLimit = parseInt(daysLimit, 10);
+        }
+
+        // Created the paged content element.
+        createPagedContent(pageLimit, preloadedPages, midnight, firstLoad, courseId, daysOffset, daysLimit, paginationAriaLabel)
+            .then(function(html, js) {
+                html = $(html);
+                // Hide the content for now.
+                html.addClass('hidden');
+                // Replace existing elements with the newly created paged content.
+                // If we're reinitialising an existing event list this will replace
+                // the old event list (including removing any event handlers).
+                Templates.replaceNodeContents(eventListContent, html, js);
+
+                firstLoad.then(function(hasContent) {
+                    // Prevent changing page elements too much by only showing the content
+                    // once we've loaded some data for the first time. This allows our
+                    // fancy loading placeholder to shine.
+                    html.removeClass('hidden');
+                    loadingPlaceholder.addClass('hidden');
+
+                    if (!hasContent) {
+                        // If we didn't get any data then show the empty data message.
+                        hideContent(root);
+                    }
+
+                    return hasContent;
+                })
+                .catch(function() {
+                    return false;
+                });
+
+                return html;
+            })
+            .catch(Notification.exception);
+    };
+
+    return {
+        init: init,
+        rootSelector: SELECTORS.ROOT,
+    };
+});
diff --git a/blocks/timeline/amd/src/main.js b/blocks/timeline/amd/src/main.js
new file mode 100644 (file)
index 0000000..334248b
--- /dev/null
@@ -0,0 +1,57 @@
+// 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 timeline block.
+ *
+ * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(
+[
+    'jquery',
+    'block_timeline/view_nav',
+    'block_timeline/view'
+],
+function(
+    $,
+    ViewNav,
+    View
+) {
+
+    var SELECTORS = {
+        TIMELINE_VIEW: '[data-region="timeline-view"]'
+    };
+
+    /**
+     * Initialise all of the modules for the timeline block.
+     *
+     * @param {object} root The root element for the timeline block.
+     */
+    var init = function(root) {
+        root = $(root);
+        var viewRoot = root.find(SELECTORS.TIMELINE_VIEW);
+
+        // Initialise the timeline navigation elements.
+        ViewNav.init(root, viewRoot);
+        // Initialise the timeline view modules.
+        View.init(viewRoot);
+    };
+
+    return {
+        init: init
+    };
+});
diff --git a/blocks/timeline/amd/src/view.js b/blocks/timeline/amd/src/view.js
new file mode 100644 (file)
index 0000000..89430a7
--- /dev/null
@@ -0,0 +1,97 @@
+// 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 for the timeline block.
+ *
+ * @package    block_timeline
+ * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(
+[
+    'jquery',
+    'block_timeline/view_dates',
+    'block_timeline/view_courses',
+],
+function(
+    $,
+    ViewDates,
+    ViewCourses
+) {
+
+    var SELECTORS = {
+        TIMELINE_DATES_VIEW: '[data-region="view-dates"]',
+        TIMELINE_COURSES_VIEW: '[data-region="view-courses"]',
+    };
+
+    /**
+     * Intialise the timeline dates and courses views on page load.
+     * This function should only be called once per page load because
+     * it can cause event listeners to be added to the page.
+     *
+     * @param {object} root The root element for the timeline view.
+     */
+    var init = function(root) {
+        root = $(root);
+        var datesViewRoot = root.find(SELECTORS.TIMELINE_DATES_VIEW);
+        var coursesViewRoot = root.find(SELECTORS.TIMELINE_COURSES_VIEW);
+
+        ViewDates.init(datesViewRoot);
+        ViewCourses.init(coursesViewRoot);
+    };
+
+    /**
+     * Reset the timeline dates and 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.
+     */
+    var reset = function(root) {
+        var datesViewRoot = root.find(SELECTORS.TIMELINE_DATES_VIEW);
+        var coursesViewRoot = root.find(SELECTORS.TIMELINE_COURSES_VIEW);
+        ViewDates.reset(datesViewRoot);
+        ViewCourses.reset(coursesViewRoot);
+    };
+
+    /**
+     * Tell the timeline dates or courses view that it has been displayed.
+     *
+     * This is called each time one of the views is displayed and is used to
+     * lazy load the data within it on first load.
+     *
+     * @param {object} root The root element for the timeline view.
+     */
+    var shown = function(root) {
+        var datesViewRoot = root.find(SELECTORS.TIMELINE_DATES_VIEW);
+        var coursesViewRoot = root.find(SELECTORS.TIMELINE_COURSES_VIEW);
+
+        if (datesViewRoot.hasClass('active')) {
+            ViewDates.shown(datesViewRoot);
+        } else {
+            ViewCourses.shown(coursesViewRoot);
+        }
+    };
+
+    return {
+        init: init,
+        reset: reset,
+        shown: shown,
+    };
+});
diff --git a/blocks/timeline/amd/src/view_courses.js b/blocks/timeline/amd/src/view_courses.js
new file mode 100644 (file)
index 0000000..4749c92
--- /dev/null
@@ -0,0 +1,606 @@
+// 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 courses view for the timeline block.
+ *
+ * @package    block_timeline
+ * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(
+[
+    'jquery',
+    'core/notification',
+    'core/custom_interaction_events',
+    'core/str',
+    'core/templates',
+    'block_timeline/event_list',
+    'core_course/repository',
+    'block_timeline/calendar_events_repository'
+],
+function(
+    $,
+    Notification,
+    CustomEvents,
+    Str,
+    Templates,
+    EventList,
+    CourseRepository,
+    EventsRepository
+) {
+
+    var SELECTORS = {
+        MORE_COURSES_BUTTON: '[data-action="more-courses"]',
+        MORE_COURSES_BUTTON_CONTAINER: '[data-region="more-courses-button-container"]',
+        NO_COURSES_EMPTY_MESSAGE: '[data-region="no-courses-empty-message"]',
+        COURSES_LIST: '[data-region="courses-list"]',
+        COURSE_ITEMS_LOADING_PLACEHOLDER: '[data-region="course-items-loading-placeholder"]',
+        COURSE_EVENTS_CONTAINER: '[data-region="course-events-container"]',
+        COURSE_NAME: '[data-region="course-name"]',
+        LOADING_ICON: '.loading-icon'
+    };
+
+    var TEMPLATES = {
+        COURSE_ITEMS: 'block_timeline/course-items',
+        LOADING_ICON: 'core/loading'
+    };
+
+    var COURSE_CLASSIFICATION = 'inprogress';
+    var COURSE_SORT = 'fullname asc';
+    var COURSE_EVENT_LIMIT = 5;
+    var COURSE_LIMIT = 2;
+    var SECONDS_IN_DAY = 60 * 60 * 24;
+
+    /**
+     * Hide the loading placeholder elements.
+     *
+     * @param {object} root The rool element.
+     */
+    var hideLoadingPlaceholder = function(root) {
+        root.find(SELECTORS.COURSE_ITEMS_LOADING_PLACEHOLDER).addClass('hidden');
+    };
+
+    /**
+     * Hide the "more courses" button.
+     *
+     * @param {object} root The rool element.
+     */
+    var hideMoreCoursesButton = function(root) {
+        root.find(SELECTORS.MORE_COURSES_BUTTON_CONTAINER).addClass('hidden');
+    };
+
+    /**
+     * Show the "more courses" button.
+     *
+     * @param {object} root The rool element.
+     */
+    var showMoreCoursesButton = function(root) {
+        root.find(SELECTORS.MORE_COURSES_BUTTON_CONTAINER).removeClass('hidden');
+    };
+
+    /**
+     * Disable the "more courses" button and show the loading spinner.
+     *
+     * @param {object} root The rool element.
+     */
+    var enableMoreCoursesButtonLoading = function(root) {
+        var button = root.find(SELECTORS.MORE_COURSES_BUTTON);
+        button.prop('disabled', true);
+        Templates.render(TEMPLATES.LOADING_ICON, {})
+            .then(function(html) {
+                button.append(html);
+                return html;
+            })
+            .catch(function() {
+                // It's not important if this false so just do so silently.
+                return false;
+            });
+    };
+
+    /**
+     * Enable the "more courses" button and remove the loading spinner.
+     *
+     * @param {object} root The rool element.
+     */
+    var disableMoreCoursesButtonLoading = function(root) {
+        var button = root.find(SELECTORS.MORE_COURSES_BUTTON);
+        button.prop('disabled', false);
+        button.find(SELECTORS.LOADING_ICON).remove();
+    };
+
+    /**
+     * Display the message for when there are no courses available.
+     *
+     * @param {object} root The rool element.
+     */
+    var showNoCoursesEmptyMessage = function(root) {
+        root.find(SELECTORS.NO_COURSES_EMPTY_MESSAGE).removeClass('hidden');
+    };
+
+    /**
+     * Render the course items HTML to the page.
+     *
+     * @param {object} root The rool element.
+     * @param {string} html The course items HTML to render.
+     */
+    var renderCourseItemsHTML = function(root, html) {
+        var container = root.find(SELECTORS.COURSES_LIST);
+        Templates.appendNodeContents(container, html, '');
+    };
+
+    /**
+     * Check if any courses have been loaded.
+     *
+     * @param {object} root The rool element.
+     * @return {bool}
+     */
+    var hasLoadedCourses = function(root) {
+        return root.find(SELECTORS.COURSE_EVENTS_CONTAINER).length > 0;
+    };
+
+    /**
+     * Return the offset value for fetching courses.
+     *
+     * @param {object} root The rool element.
+     * @return {Number}
+     */
+    var getOffset = function(root) {
+        return parseInt(root.attr('data-offset'), 10);
+    };
+
+    /**
+     * Set the offset value for fetching courses.
+     *
+     * @param {object} root The rool element.
+     * @param {Number} offset Offset value.
+     */
+    var setOffset = function(root, offset) {
+        root.attr('data-offset', offset);
+    };
+
+    /**
+     * Return the limit value for fetching courses.
+     *
+     * @param {object} root The rool element.
+     * @return {Number}
+     */
+    var getLimit = function(root) {
+        return parseInt(root.attr('data-limit'), 10);
+    };
+
+    /**
+     * Return the days offset value for fetching events.
+     *
+     * @param {object} root The rool element.
+     * @return {Number}
+     */
+    var getDaysOffset = function(root) {
+        return parseInt(root.attr('data-days-offset'), 10);
+    };
+
+    /**
+     * Return the days limit value for fetching events. The days
+     * limit is optional so undefined will be returned if it isn't
+     * set.
+     *
+     * @param {object} root The rool element.
+     * @return {int|undefined}
+     */
+    var getDaysLimit = function(root) {
+        var daysLimit = root.attr('data-days-limit');
+        return daysLimit != undefined ? parseInt(daysLimit, 10) : undefined;
+    };
+
+    /**
+     * Return the timestamp for the user's midnight.
+     *
+     * @param {object} root The rool element.
+     * @return {Number}
+     */
+    var getMidnight = function(root) {
+        return parseInt(root.attr('data-midnight'), 10);
+    };
+
+    /**
+     * Return the start time for fetching events. This is calculated
+     * based on the user's midnight value so that timezones are
+     * preserved.
+     *
+     * @param {object} root The rool element.
+     * @return {Number}
+     */
+    var getStartTime = function(root) {
+        var midnight = getMidnight(root);
+        var daysOffset = getDaysOffset(root);
+        return midnight + (daysOffset * SECONDS_IN_DAY);
+    };
+
+    /**
+     * Return the end time for fetching events. This is calculated
+     * based on the user's midnight value so that timezones are
+     * preserved.
+     *
+     * @param {object} root The rool element.
+     * @return {Number}
+     */
+    var getEndTime = function(root) {
+        var midnight = getMidnight(root);
+        var daysLimit = getDaysLimit(root);
+        return daysLimit != undefined ? midnight + (daysLimit * SECONDS_IN_DAY) : false;
+    };
+
+    /**
+     * Get a list of events for the given course ids. Returns a promise that will
+     * be resolved with the events.
+     *
+     * @param {array} courseIds The list of course ids to fetch events for.
+     * @param {Number} startTime Timestamp to fetch events from.
+     * @param {Number} limit Limit to the number of events (this applies per course, not total)
+     * @param {Number} endTime Timestamp to fetch events to.
+     * @return {object} jQuery promise.
+     */
+    var getEventsForCourseIds = function(courseIds, startTime, limit, endTime) {
+        var args = {
+            courseids: courseIds,
+            starttime: startTime,
+            limit: limit
+        };
+
+        if (endTime) {
+            args.endtime = endTime;
+        }
+
+        return EventsRepository.queryByCourses(args);
+    };
+
+    /**
+     * Get the last time the events were reloaded.
+     *
+     * @param {object} root The rool element.
+     * @return {Number}
+     */
+    var getEventReloadTime = function(root) {
+        return root.data('last-event-load-time');
+    };
+
+    /**
+     * Set the last time the events were reloaded.
+     *
+     * @param {object} root The rool element.
+     * @param {Number} time Timestamp in milliseconds.
+     */
+    var setEventReloadTime = function(root, time) {
+        root.data('last-event-load-time', time);
+    };
+
+    /**
+     * Check if events have begun reloading since the given
+     * time.
+     *
+     * @param {object} root The rool element.
+     * @param {Number} time Timestamp in milliseconds.
+     * @return {bool}
+     */
+    var hasReloadedEventsSince = function(root, time) {
+        return getEventReloadTime(root) > time;
+    };
+
+    /**
+     * Send a request to the server to load the events for the courses.
+     *
+     * @param {array} courses List of course objects.
+     * @param {Number} startTime Timestamp to load events after.
+     * @param {int|undefined} endTime Timestamp to load events up until.
+     * @return {object} jQuery promise resolved with the events.
+     */
+    var loadEventsForCourses = function(courses, startTime, endTime) {
+        var courseIds = courses.map(function(course) {
+            return course.id;
+        });
+
+        return getEventsForCourseIds(courseIds, startTime, COURSE_EVENT_LIMIT + 1, endTime);
+    };
+
+    /**
+     * Render the courses in the DOM once the server has returned the courses.
+     *
+     * @param {array} courses List of course objects.
+     * @param {object} root The root element
+     * @param {Number} midnight The midnight timestamp in the user's timezone.
+     * @param {Number} daysOffset Number of days from today to offset the events.
+     * @param {Number} daysLimit Number of days from today to limit the events to.
+     * @param {string} noEventsURL URL for the image to display for no events.
+     * @return {object} jQuery promise resolved after rendering is complete.
+     */
+    var updateDisplayFromCourses = function(courses, root, midnight, daysOffset, daysLimit, noEventsURL) {
+        // Render the courses template.
+        return Templates.render(TEMPLATES.COURSE_ITEMS, {
+            courses: courses,
+            midnight: midnight,
+            hasdaysoffset: true,
+            hasdayslimit: daysLimit != undefined,
+            daysoffset: daysOffset,
+            dayslimit: daysLimit,
+            nodayslimit: daysLimit == undefined,
+            urls: {
+                noevents: noEventsURL
+            }
+        }).then(function(html) {
+            hideLoadingPlaceholder(root);
+
+            if (html) {
+                // Template rendering is complete and we have the HTML so we can
+                // add it to the DOM.
+                renderCourseItemsHTML(root, html);
+            } else {
+                if (!hasLoadedCourses(root)) {
+                    // There were no courses to render so show the empty placeholder
+                    // message for the user to tell them.
+                    showNoCoursesEmptyMessage(root);
+                }
+            }
+
+            return html;
+        })
+        .then(function(html) {
+            if (courses.length < COURSE_LIMIT) {
+                // We know there aren't any more courses because we got back less
+                // than we asked for so hide the button to request more.
+                hideMoreCoursesButton(root);
+            } else {
+                // Make sure the button is visible if there are more courses to load.
+                showMoreCoursesButton(root);
+            }
+
+            return html;
+        })
+        .catch(function() {
+            hideLoadingPlaceholder(root);
+        });
+    };
+
+    /**
+     * Find all of the visible course blocks and initialise the event
+     * list module to being loading the events for the course block.
+     *
+     * @param {object} root The root element for the timeline courses view.
+     * @return {object} jQuery promise resolved with courses and events.
+     */
+    var loadMoreCourses = function(root) {
+        var offset = getOffset(root);
+        var limit = getLimit(root);
+
+        // Start loading the next set of courses.
+        return CourseRepository.getEnrolledCoursesByTimelineClassification(
+            COURSE_CLASSIFICATION,
+            limit,
+            offset,
+            COURSE_SORT
+        ).then(function(result) {
+            var startEventLoadingTime = Date.now();
+            var courses = result.courses;
+            var nextOffset = result.nextoffset;
+            var daysOffset = getDaysOffset(root);
+            var daysLimit = getDaysLimit(root);
+            var midnight = getMidnight(root);
+            var startTime = getStartTime(root);
+            var endTime = getEndTime(root);
+            var noEventsURL = root.attr('data-no-events-url');
+            // Record the next offset if we want to request more courses.
+            setOffset(root, nextOffset);
+            // Load the events for these courses.
+            var eventsPromise = loadEventsForCourses(courses, startTime, endTime);
+            // Render the courses in the DOM.
+            var renderPromise = updateDisplayFromCourses(courses, root, midnight, daysOffset, daysLimit, noEventsURL);
+
+            return $.when(eventsPromise, renderPromise)
+                .then(function(eventsByCourse) {
+                    if (hasReloadedEventsSince(root, startEventLoadingTime)) {
+                        // All of the events are being reloaded so ignore our results.
+                        return eventsByCourse;
+                    }
+
+                    // When we've got all of the courses and events we can render the events in the
+                    // correct course event list.
+                    courses.forEach(function(course) {
+                        var courseId = course.id;
+                        var events = [];
+                        var containerSelector = '[data-region="course-events-container"][data-course-id="' + courseId + '"]';
+                        var courseEventsContainer = root.find(containerSelector);
+                        var eventListRoot = courseEventsContainer.find(EventList.rootSelector);
+                        var courseGroups = eventsByCourse.groupedbycourse.filter(function(group) {
+                            return group.courseid == courseId;
+                        });
+
+                        if (courseGroups.length) {
+                            // Get the events for this course.
+                            events = courseGroups[0].events;
+                        }
+
+                        // Create a preloaded page to pass to the event list because we've already
+                        // loaded the first page of events.
+                        var pageOnePreload = $.Deferred().resolve({events: events}).promise();
+                        // Initialise the event list pagination area for this course.
+                        Str.get_string('ariaeventlistpaginationnavcourses', 'block_timeline', course.fullnamedisplay)
+                            .then(function(string) {
+                                EventList.init(eventListRoot, COURSE_EVENT_LIMIT, {'1': pageOnePreload}, string);
+                                return string;
+                            })
+                            .catch(function() {
+                                // An error is ok, just render with the default string.
+                                EventList.init(eventListRoot, COURSE_EVENT_LIMIT, {'1': pageOnePreload});
+                            });
+                    });
+
+                    return eventsByCourse;
+                });
+        }).catch(Notification.exception);
+    };
+
+    /**
+     * Reload the events for all of the visible courses. These events will be loaded
+     * in a single request to the server.
+     *
+     * @param {object} root The root element.
+     * @return {object} jQuery promise resolved with courses and events.
+     */
+    var reloadCourseEvents = function(root) {
+        var startReloadTime = Date.now();
+        var startTime = getStartTime(root);
+        var endTime = getEndTime(root);
+        var courseEventsContainers = root.find(SELECTORS.COURSE_EVENTS_CONTAINER);
+        var courseIds = courseEventsContainers.map(function() {
+            return $(this).attr('data-course-id');
+        }).get();
+
+        // Record when we started our request.
+        setEventReloadTime(root, startReloadTime);
+
+        // Load all of the events for the given courses.
+        return getEventsForCourseIds(courseIds, startTime, COURSE_EVENT_LIMIT + 1, endTime)
+            .then(function(eventsByCourse) {
+                if (hasReloadedEventsSince(root, startReloadTime)) {
+                    // A new reload has begun so ignore our results.
+                    return eventsByCourse;
+                }
+
+                courseEventsContainers.each(function(index, container) {
+                    container = $(container);
+                    var courseId = container.attr('data-course-id');
+                    var courseName = container.find(SELECTORS.COURSE_NAME).text();
+                    var eventListContainer = container.find(EventList.rootSelector);
+                    var pageDeferred = $.Deferred();
+                    var events = [];
+                    var courseGroups = eventsByCourse.groupedbycourse.filter(function(group) {
+                        return group.courseid == courseId;
+                    });
+
+                    if (courseGroups.length) {
+                        // Get the events just for this course.
+                        events = courseGroups[0].events;
+                    }
+
+                    pageDeferred.resolve({events: events});
+
+                    // Re-initialise the events list with the preloaded events we just got from
+                    // the server.
+                    Str.get_string('ariaeventlistpaginationnavcourses', 'block_timeline', courseName)
+                        .then(function(string) {
+                            EventList.init(eventListContainer, COURSE_EVENT_LIMIT, {'1': pageDeferred.promise()}, string);
+                            return string;
+                        })
+                        .catch(function() {
+                            // Ignore a failure to load the string. Just render with the default string.
+                            EventList.init(eventListContainer, COURSE_EVENT_LIMIT, {'1': pageDeferred.promise()});
+                        });
+                });
+
+                return eventsByCourse;
+            }).catch(Notification.exception);
+    };
+
+    /**
+     * Add event listeners to load more courses for the courses view.
+     *
+     * @param {object} root The root element for the timeline courses view.
+     */
+    var registerEventListeners = function(root) {
+        CustomEvents.define(root, [CustomEvents.events.activate]);
+        // Show more courses and load their events when the user clicks the "more courses"
+        // button.
+        root.on(CustomEvents.events.activate, SELECTORS.MORE_COURSES_BUTTON, function(e, data) {
+            enableMoreCoursesButtonLoading(root);
+            loadMoreCourses(root)
+                .then(function() {
+                    disableMoreCoursesButtonLoading(root);
+                    return;
+                })
+                .catch(function() {
+                    disableMoreCoursesButtonLoading(root);
+                });
+
+            if (data) {
+                data.originalEvent.preventDefault();
+                data.originalEvent.stopPropagation();
+            }
+            e.stopPropagation();
+        });
+    };
+
+    /**
+     * Initialise the timeline courses view. Begin loading the events
+     * if this view is active. Add the relevant event listeners.
+     *
+     * This function should only be called once per page load because it
+     * is adding event listeners to the page.
+     *
+     * @param {object} root The root element for the timeline courses view.
+     */
+    var init = function(root) {
+        root = $(root);
+
+        setEventReloadTime(root, Date.now());
+
+        if (root.hasClass('active')) {
+            // Only load if this is active otherwise it will be lazy loaded later.
+            loadMoreCourses(root);
+            root.attr('data-seen', true);
+        }
+
+        registerEventListeners(root);
+    };
+
+    /**
+     * Reset the element back to it's initial state. Begin loading the events again
+     * if this view is active.
+     *
+     * @param {object} root The root element for the timeline courses view.
+     */
+    var reset = function(root) {
+        root.removeAttr('data-seen');
+        if (root.hasClass('active')) {
+            shown(root);
+        }
+    };
+
+    /**
+     * If this is the first time this view has been displayed then begin loading
+     * the events.
+     *
+     * @param {object} root The root element for the timeline courses view.
+     */
+    var shown = function(root) {
+        if (!root.attr('data-seen')) {
+            if (hasLoadedCourses(root)) {
+                // This isn't the first time this view is shown so just reload the
+                // events for the courses we've already loaded.
+                reloadCourseEvents(root);
+            } else {
+                // We haven't loaded any courses yet so do that now.
+                loadMoreCourses(root);
+            }
+
+            root.attr('data-seen', true);
+        }
+    };
+
+    return {
+        init: init,
+        reset: reset,
+        shown: shown
+    };
+});
diff --git a/blocks/timeline/amd/src/view_dates.js b/blocks/timeline/amd/src/view_dates.js
new file mode 100644 (file)
index 0000000..b18d21a
--- /dev/null
@@ -0,0 +1,103 @@
+// 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 dates view for the timeline block.
+ *
+ * @package    block_timeline
+ * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(
+[
+    'jquery',
+    'core/str',
+    'block_timeline/event_list'
+],
+function(
+    $,
+    Str,
+    EventList
+) {
+
+    var SELECTORS = {
+        EVENT_LIST_CONTAINER: '[data-region="event-list-container"]',
+    };
+
+    /**
+     * Initialise the event list and being loading the events.
+     *
+     * @param {object} root The root element for the timeline dates view.
+     */
+    var load = function(root) {
+        var eventListContainer = root.find(SELECTORS.EVENT_LIST_CONTAINER);
+        Str.get_string('ariaeventlistpaginationnavdates', 'block_timeline')
+            .then(function(string) {
+                EventList.init(eventListContainer, [5, 10, 25], {}, string);
+                return string;
+            })
+            .catch(function() {
+                // Ignore if we can't load the string. Still init the event list.
+                EventList.init(eventListContainer, [5, 10, 25]);
+            });
+    };
+
+    /**
+     * Initialise the timeline dates view. Begin loading the events
+     * if this view is active.
+     *
+     * @param {object} root The root element for the timeline courses view.
+     */
+    var init = function(root) {
+        root = $(root);
+        if (root.hasClass('active')) {
+            load(root);
+            root.attr('data-seen', true);
+        }
+    };
+
+    /**
+     * Reset the view back to it's initial state. If this view is active then
+     * beging loading the events.
+     *
+     * @param {object} root The root element for the timeline courses view.
+     */
+    var reset = function(root) {
+        root.removeAttr('data-seen');
+        if (root.hasClass('active')) {
+            load(root);
+            root.attr('data-seen', true);
+        }
+    };
+
+    /**
+     * Load the events if this is the first time the view is displayed.
+     *
+     * @param {object} root The root element for the timeline courses view.
+     */
+    var shown = function(root) {
+        if (!root.attr('data-seen')) {
+            load(root);
+            root.attr('data-seen', true);
+        }
+    };
+
+    return {
+        init: init,
+        reset: reset,
+        shown: shown
+    };
+});
diff --git a/blocks/timeline/amd/src/view_nav.js b/blocks/timeline/amd/src/view_nav.js
new file mode 100644 (file)
index 0000000..092dcbd
--- /dev/null
@@ -0,0 +1,120 @@
+// 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 timeline block.
+ *
+ * @package    block_timeline
+ * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(
+[
+    'jquery',
+    'core/custom_interaction_events',
+    'block_timeline/view'
+],
+function(
+    $,
+    CustomEvents,
+    View
+) {
+
+    var SELECTORS = {
+        TIMELINE_DAY_FILTER: '[data-region="day-filter"]',
+        TIMELINE_DAY_FILTER_OPTION: '[data-from]',
+        TIMELINE_VIEW_SELECTOR: '[data-region="view-selector"]',
+        DATA_DAYS_OFFSET: '[data-days-offset]',
+        DATA_DAYS_LIMIT: '[data-days-limit]',
+    };
+
+    /**
+     * Event listener for the day selector ("Next 7 days", "Next 30 days", etc).
+     *
+     * @param {object} root The root element for the timeline block
+     * @param {object} timelineViewRoot The root element for the timeline view
+     */
+    var registerTimelineDaySelector = function(root, timelineViewRoot) {
+        var timelineDaySelectorContainer = root.find(SELECTORS.TIMELINE_DAY_FILTER);
+
+        CustomEvents.define(timelineDaySelectorContainer, [CustomEvents.events.activate]);
+        timelineDaySelectorContainer.on(
+            CustomEvents.events.activate,
+            SELECTORS.TIMELINE_DAY_FILTER_OPTION,
+            function(e, data) {
+                var option = $(e.target).closest(SELECTORS.TIMELINE_DAY_FILTER_OPTION);
+
+                if (option.hasClass('active')) {
+                    // If it's already active then we don't need to do anything.
+                    return;
+                }
+
+                var daysOffset = option.attr('data-from');
+                var daysLimit = option.attr('data-to');
+                var elementsWithDaysOffset = root.find(SELECTORS.DATA_DAYS_OFFSET);
+
+                elementsWithDaysOffset.attr('data-days-offset', daysOffset);
+
+                if (daysLimit != undefined) {
+                    elementsWithDaysOffset.attr('data-days-limit', daysLimit);
+                } else {
+                    elementsWithDaysOffset.removeAttr('data-days-limit');
+                }
+
+                // Reset the views to reinitialise the event lists now that we've
+                // updated the day limits.
+                View.reset(timelineViewRoot);
+
+                data.originalEvent.preventDefault();
+            }
+        );
+    };
+
+    /**
+     * Event listener for the "sort" button in the timeline navigation that allows for
+     * changing between the timeline dates and courses views.
+     *
+     * On a view change we tell the timeline view module that the view has been shown
+     * so that it can handle how to display the appropriate view.
+     *
+     * @param {object} root The root element for the timeline block
+     * @param {object} timelineViewRoot The root element for the timeline view
+     */
+    var registerViewSelector = function(root, timelineViewRoot) {
+        // 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() {
+            View.shown(timelineViewRoot);
+        });
+    };
+
+    /**
+     * Initialise the timeline view navigation by adding event listeners to
+     * the navigation elements.
+     *
+     * @param {object} root The root element for the timeline block
+     * @param {object} timelineViewRoot The root element for the timeline view
+     */
+    var init = function(root, timelineViewRoot) {
+        root = $(root);
+        registerTimelineDaySelector(root, timelineViewRoot);
+        registerViewSelector(root, timelineViewRoot);
+    };
+
+    return {
+        init: init
+    };
+});
diff --git a/blocks/timeline/block_timeline.php b/blocks/timeline/block_timeline.php
new file mode 100644 (file)
index 0000000..5a24f9c
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains the class for the Timeline block.
+ *
+ * @package    block_timeline
+ * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Timeline block class.
+ *
+ * @package    block_timeline
+ * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class block_timeline extends block_base {
+
+    /**
+     * Init.
+     */
+    public function init() {
+        $this->title = get_string('pluginname', 'block_timeline');
+    }
+
+    /**
+     * Returns the contents.
+     *
+     * @return stdClass contents of block
+     */
+    public function get_content() {
+        if (isset($this->content)) {
+            return $this->content;
+        }
+
+        $renderable = new \block_timeline\output\main();
+        $renderer = $this->page->get_renderer('block_timeline');
+
+        $this->content = (object) [
+            'text' => $renderer->render($renderable),
+            'footer' => ''
+        ];
+
+        return $this->content;
+    }
+
+    /**
+     * Locations where block can be displayed.
+     *
+     * @return array
+     */
+    public function applicable_formats() {
+        return array('my' => true);
+    }
+}
diff --git a/blocks/timeline/classes/output/main.php b/blocks/timeline/classes/output/main.php
new file mode 100644 (file)
index 0000000..b5e9609
--- /dev/null
@@ -0,0 +1,81 @@
+<?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 timeline block.
+ *
+ * @package    block_timeline
+ * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace block_timeline\output;
+defined('MOODLE_INTERNAL') || die();
+
+use renderable;
+use renderer_base;
+use templatable;
+use core_course\external\course_summary_exporter;
+
+require_once($CFG->dirroot . '/course/lib.php');
+require_once($CFG->libdir . '/completionlib.php');
+
+/**
+ * Class containing data for timeline block.
+ *
+ * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class main implements renderable, templatable {
+
+    /** Number of courses to load per page */
+    const COURSES_PER_PAGE = 2;
+
+    /**
+     * Export this data so it can be used as the context for a mustache template.
+     *
+     * @param \renderer_base $output
+     * @return stdClass
+     */
+    public function export_for_template(renderer_base $output) {
+
+        $nocoursesurl = $output->image_url('courses', 'block_timeline')->out();
+        $noeventsurl = $output->image_url('activities', 'block_timeline')->out();
+
+        $requiredproperties = course_summary_exporter::define_properties();
+        $fields = join(',', array_keys($requiredproperties));
+        $courses = course_get_enrolled_courses_for_logged_in_user(0, 0, null, $fields);
+        list($inprogresscourses, $processedcount) = course_filter_courses_by_timeline_classification(
+            $courses,
+            COURSE_TIMELINE_INPROGRESS,
+            self::COURSES_PER_PAGE
+        );
+        $formattedcourses = array_map(function($course) use ($output) {
+            \context_helper::preload_from_record($course);
+            $context = \context_course::instance($course->id);
+            $exporter = new course_summary_exporter($course, ['context' => $context]);
+            return $exporter->export($output);
+        }, $inprogresscourses);
+
+        return [
+            'midnight' => usergetmidnight(time()),
+            'coursepages' => [$formattedcourses],
+            'urls' => [
+                'nocourses' => $nocoursesurl,
+                'noevents' => $noeventsurl
+            ]
+        ];
+    }
+}
diff --git a/blocks/timeline/classes/output/renderer.php b/blocks/timeline/classes/output/renderer.php
new file mode 100644 (file)
index 0000000..dbe6bb7
--- /dev/null
@@ -0,0 +1,48 @@
+<?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/>.
+
+/**
+ * Timeline block rendrer.
+ *
+ * @package    block_timeline
+ * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace block_timeline\output;
+defined('MOODLE_INTERNAL') || die;
+
+use plugin_renderer_base;
+use renderable;
+
+/**
+ * Timeline block renderer.
+ *
+ * @package    block_timeline
+ * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class renderer extends plugin_renderer_base {
+
+    /**
+     * Return the main content for the block timeline.
+     *
+     * @param main $main The main renderable
+     * @return string HTML string
+     */
+    public function render_main(main $main) {
+        return $this->render_from_template('block_timeline/main', $main->export_for_template($this));
+    }
+}
diff --git a/blocks/timeline/classes/privacy/provider.php b/blocks/timeline/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..02ae526
--- /dev/null
@@ -0,0 +1,46 @@
+<?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/>.
+
+/**
+ * Privacy Subsystem implementation for block_timeline.
+ *
+ * @package    block_timeline
+ * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace block_timeline\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for block_timeline.
+ *
+ * @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 {
+
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
diff --git a/blocks/timeline/db/access.php b/blocks/timeline/db/access.php
new file mode 100644 (file)
index 0000000..6bd70a2
--- /dev/null
@@ -0,0 +1,50 @@
+<?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/>.
+
+/**
+ * Capabilities for the timeline block.
+ *
+ * @package    block_timeline
+ * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$capabilities = array(
+
+    'block/timeline:myaddinstance' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_SYSTEM,
+        'archetypes' => array(
+            'user' => CAP_ALLOW
+        ),
+
+        'clonepermissionsfrom' => 'moodle/my:manageblocks'
+    ),
+
+    'block/timeline:addinstance' => array(
+        'riskbitmask' => RISK_SPAM | RISK_XSS,
+
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_BLOCK,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW
+        ),
+
+        'clonepermissionsfrom' => 'moodle/site:manageblocks'
+    )
+);
diff --git a/blocks/timeline/db/install.php b/blocks/timeline/db/install.php
new file mode 100644 (file)
index 0000000..c5a3d65
--- /dev/null
@@ -0,0 +1,108 @@
+<?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/>.
+
+/**
+ * Timeline block installation.
+ *
+ * @package    block_timeline
+ * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+ /**
+  * Add the timeline block to the dashboard for all users by default
+  * when it is installed.
+  */
+function xmldb_block_timeline_install() {
+    global $DB;
+
+    if ($DB->count_records('block_instances') < 1) {
+        // Only add the timeline block if it's being installed on an existing site.
+        // For new sites it will be added by blocks_add_default_system_blocks().
+        return;
+    }
+
+    if ($defaultmypage = $DB->get_record('my_pages', array('userid' => null, 'name' => '__default', 'private' => 1))) {
+        $subpagepattern = $defaultmypage->id;
+    } else {
+        $subpagepattern = null;
+    }
+
+    $page = new moodle_page();
+    $systemcontext = context_system::instance();
+    $page->set_context($systemcontext);
+    // Add the block to the default /my.
+    $page->blocks->add_region(BLOCK_POS_RIGHT);
+    $page->blocks->add_block('timeline', BLOCK_POS_RIGHT, 0, false, 'my-index', $subpagepattern);
+
+    // Now we need to find all users that have viewed their dashboard because it'll have
+    // made duplicates of the default block_instances for them so they won't see the new
+    // timeline block without the admin resetting all of the dashboards.
+    //
+    // Instead we'll just add the timeline block to their dashboards here. We will only
+    // add the timeline block if they still have the myoverview block.
+    $sql = "SELECT parentcontextid, subpagepattern
+            FROM {block_instances}
+            WHERE pagetypepattern = 'my-index'
+            AND blockname = 'myoverview'
+            AND parentcontextid != ?";
+    $params = [$systemcontext->id];
+    $existingrecords = $DB->get_recordset_sql($sql, $params);
+    $blockinstances = [];
+    $seencontexts = [];
+    $now = time();
+
+    foreach ($existingrecords as $existingrecord) {
+        $parentcontextid = $existingrecord->parentcontextid;
+        if (isset($seencontexts[$parentcontextid])) {
+            // If we've seen this context already then skip it because we don't want
+            // to add duplicate timeline blocks to the same context. This happens
+            // if something funny is going on with the subpagepattern.
+            continue;
+        } else {
+            $seencontexts[$parentcontextid] = true;
+        }
+
+        $blockinstances[] = [
+            'blockname' => 'timeline',
+            'parentcontextid' => $parentcontextid,
+            'showinsubcontexts' => false,
+            'pagetypepattern' => 'my-index',
+            'subpagepattern' => $existingrecord->subpagepattern,
+            'defaultregion' => BLOCK_POS_RIGHT,
+            'defaultweight' => 0,
+            'configdata' => '',
+            'timecreated' => $now,
+            'timemodified' => $now,
+        ];
+
+        if (count($blockinstances) >= 1000) {
+            // Insert after every 1000 records so that the memory usage doesn't
+            // get out of control.
+            $DB->insert_records('block_instances', $blockinstances);
+            $blockinstances = [];
+        }
+    }
+
+    $existingrecords->close();
+
+    if (!empty($blockinstances)) {
+        // Insert what ever is left over.
+        $DB->insert_records('block_instances', $blockinstances);
+    }
+}
diff --git a/blocks/timeline/lang/en/block_timeline.php b/blocks/timeline/lang/en/block_timeline.php
new file mode 100644 (file)
index 0000000..70f961c
--- /dev/null
@@ -0,0 +1,49 @@
+<?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/>.
+
+/**
+ * Lang strings for the timeline block.
+ *
+ * @package    block_timeline
+ * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['ariadayfilter'] = 'Filter timeline items';
+$string['ariadayfilteroption'] = '{$a} filter option';
+$string['ariaeventlistitem'] = '{$a->name} activity in {$a->course} is due on {$a->date}';
+$string['ariaeventlistpagelimit'] = 'Show {$a} activities per page';
+$string['ariaeventlistpaginationnavdates'] = 'Timeline activities pagination';
+$string['ariaeventlistpaginationnavcourses'] = 'Timeline activities for course {$a} pagination';
+$string['ariaviewselector'] = 'Sort timeline items';
+$string['ariaviewselectoroption'] = '{$a} sort option';
+$string['duedate'] = 'Due date';
+$string['morecourses'] = 'More courses';
+$string['timeline:addinstance'] = 'Add a new timeline block';
+$string['timeline:myaddinstance'] = 'Add a new timeline block to Dashboard';
+$string['nocoursesinprogress'] = 'No in progress courses';
+$string['noevents'] = 'No upcoming activities due';
+$string['next30days'] = 'Next 30 days';
+$string['next7days'] = 'Next 7 days';
+$string['next3months'] = 'Next 3 months';
+$string['next6months'] = 'Next 6 months';
+$string['overdue'] = 'Overdue';
+$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.';
diff --git a/blocks/timeline/pix/activities.svg b/blocks/timeline/pix/activities.svg
new file mode 100644 (file)
index 0000000..ed7546a
--- /dev/null
@@ -0,0 +1,41 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="157 -1509 148 125" preserveAspectRatio="xMinYMid meet">
+  <defs>
+    <style>
+      .cls-1 {
+        clip-path: url(#clip-Activities);
+      }
+
+      .cls-2 {
+        fill: #eee;
+      }
+
+      .cls-3 {
+        fill: #c4c8cc;
+      }
+
+      .cls-4 {
+        fill: #fff;
+      }
+    </style>
+    <clipPath id="clip-Activities">
+      <rect x="157" y="-1509" width="148" height="125"/>
+    </clipPath>
+  </defs>
+  <g id="Activities" class="cls-1">
+    <g id="Group_42" data-name="Group 42" transform="translate(-268 -1985)">
+      <ellipse id="Ellipse_37" data-name="Ellipse 37" class="cls-2" cx="74" cy="14.785" rx="74" ry="14.785" transform="translate(425 571.43)"/>
+      <rect id="Rectangle_80" data-name="Rectangle 80" class="cls-3" width="94.182" height="110.215" transform="translate(451.909 476)"/>
+      <g id="Group_41" data-name="Group 41" transform="translate(467.043 493)">
+        <rect id="Rectangle_81" data-name="Rectangle 81" class="cls-4" width="44.456" height="5.625" transform="translate(21.16 0.549)"/>
+        <rect id="Rectangle_82" data-name="Rectangle 82" class="cls-4" width="33.342" height="5.625" transform="translate(21.16 11.652)"/>
+        <rect id="Rectangle_83" data-name="Rectangle 83" class="cls-4" width="44.456" height="5.625" transform="translate(21.16 30.772)"/>
+        <rect id="Rectangle_84" data-name="Rectangle 84" class="cls-4" width="33.342" height="5.625" transform="translate(21.16 41.875)"/>
+        <rect id="Rectangle_85" data-name="Rectangle 85" class="cls-4" width="44.456" height="5.625" transform="translate(21.16 61.291)"/>
+        <rect id="Rectangle_86" data-name="Rectangle 86" class="cls-4" width="33.342" height="5.625" transform="translate(21.16 72.393)"/>
+        <ellipse id="Ellipse_38" data-name="Ellipse 38" class="cls-4" cx="7.007" cy="7" rx="7.007" ry="7" transform="translate(0 0)"/>
+        <ellipse id="Ellipse_39" data-name="Ellipse 39" class="cls-4" cx="7.007" cy="7" rx="7.007" ry="7" transform="translate(0 31)"/>
+        <ellipse id="Ellipse_40" data-name="Ellipse 40" class="cls-4" cx="7.007" cy="7" rx="7.007" ry="7" transform="translate(0 61)"/>
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/blocks/timeline/pix/courses.svg b/blocks/timeline/pix/courses.svg
new file mode 100644 (file)
index 0000000..75e59fc
--- /dev/null
@@ -0,0 +1,52 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="157 -1305 148 125" preserveAspectRatio="xMinYMid meet">
+  <defs>
+    <style>
+      .cls-1 {
+        clip-path: url(#clip-Courses);
+      }
+
+      .cls-2 {
+        fill: #eee;
+      }
+
+      .cls-3 {
+        fill: #c4c8cc;
+      }
+
+      .cls-4 {
+        fill: #fff;
+      }
+    </style>
+    <clipPath id="clip-Courses">
+      <rect x="157" y="-1305" width="148" height="125"/>
+    </clipPath>
+  </defs>
+  <g id="Courses" class="cls-1">
+    <g id="Group_44" data-name="Group 44" transform="translate(-268 -1781)">
+      <ellipse id="Ellipse_41" data-name="Ellipse 41" class="cls-2" cx="74" cy="14.785" rx="74" ry="14.785" transform="translate(425 571.43)"/>
+      <rect id="Rectangle_87" data-name="Rectangle 87" class="cls-3" width="95.097" height="110.215" transform="translate(451.909 476)"/>
+      <g id="Group_43" data-name="Group 43" transform="translate(464.04 494)">
+        <rect id="Rectangle_88" data-name="Rectangle 88" class="cls-4" width="31.043" height="34" transform="translate(0)"/>
+        <rect id="Rectangle_89" data-name="Rectangle 89" class="cls-4" width="31.043" height="34" transform="translate(0 42)"/>
+        <rect id="Rectangle_90" data-name="Rectangle 90" class="cls-4" width="31.067" height="34" transform="translate(39.005)"/>
+        <rect id="Rectangle_91" data-name="Rectangle 91" class="cls-4" width="31.067" height="34" transform="translate(39.005 42)"/>
+        <rect id="Rectangle_92" data-name="Rectangle 92" class="cls-3" width="23.023" height="3.18" transform="translate(3.081 16.549)"/>
+        <rect id="Rectangle_93" data-name="Rectangle 93" class="cls-3" width="23.023" height="3.18" transform="translate(3.081 58.549)"/>
+        <rect id="Rectangle_94" data-name="Rectangle 94" class="cls-3" width="23.023" height="3.18" transform="translate(43.122 16.549)"/>
+        <rect id="Rectangle_95" data-name="Rectangle 95" class="cls-3" width="23.023" height="3.18" transform="translate(43.122 58.549)"/>
+        <rect id="Rectangle_96" data-name="Rectangle 96" class="cls-3" width="14.014" height="3.18" transform="translate(3.081 21.825)"/>
+        <rect id="Rectangle_97" data-name="Rectangle 97" class="cls-3" width="18.845" height="3.18" transform="translate(3.081 26.825)"/>
+        <rect id="Rectangle_98" data-name="Rectangle 98" class="cls-3" width="14.014" height="3.18" transform="translate(3.081 63.825)"/>
+        <rect id="Rectangle_99" data-name="Rectangle 99" class="cls-3" width="18.845" height="3.18" transform="translate(3.081 68.825)"/>
+        <rect id="Rectangle_100" data-name="Rectangle 100" class="cls-3" width="14.014" height="3.18" transform="translate(43.122 21.825)"/>
+        <rect id="Rectangle_101" data-name="Rectangle 101" class="cls-3" width="18.845" height="3.18" transform="translate(43.122 26.825)"/>
+        <rect id="Rectangle_102" data-name="Rectangle 102" class="cls-3" width="14.014" height="3.18" transform="translate(43.122 63.825)"/>
+        <rect id="Rectangle_103" data-name="Rectangle 103" class="cls-3" width="18.845" height="3.18" transform="translate(43.122 68.825)"/>
+        <ellipse id="Ellipse_42" data-name="Ellipse 42" class="cls-3" cx="5.658" cy="5.652" rx="5.658" ry="5.652" transform="translate(3.003 3.55)"/>
+        <ellipse id="Ellipse_43" data-name="Ellipse 43" class="cls-3" cx="5.658" cy="5.652" rx="5.658" ry="5.652" transform="translate(3.003 45.55)"/>
+        <ellipse id="Ellipse_44" data-name="Ellipse 44" class="cls-3" cx="5.658" cy="5.652" rx="5.658" ry="5.652" transform="translate(43.044 3.55)"/>
+        <ellipse id="Ellipse_45" data-name="Ellipse 45" class="cls-3" cx="5.658" cy="5.652" rx="5.658" ry="5.652" transform="translate(43.044 45.55)"/>
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/blocks/timeline/templates/course-item-loading-placeholder.mustache b/blocks/timeline/templates/course-item-loading-placeholder.mustache
new file mode 100644 (file)
index 0000000..d2c8168
--- /dev/null
@@ -0,0 +1,40 @@
+{{!
+    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_timeline/course-item-loading-placeholder
+
+    This template renders the each course block containing a summary and calendar events.
+
+    Example context (json):
+    {}
+}}
+<li class="list-group-item mt-3 p-0 border-0">
+    <div class="w-50 bg-pulse-grey mb-2" style="height: 20px"></div>
+    <div>
+        <ul class="pl-0 list-group list-group-flush">
+            {{> block_timeline/placeholder-event-list-item }}
+            {{> block_timeline/placeholder-event-list-item }}
+            {{> block_timeline/placeholder-event-list-item }}
+            {{> block_timeline/placeholder-event-list-item }}
+            {{> block_timeline/placeholder-event-list-item }}
+        </ul>
+        <div class="pt-3 d-flex justify-content-between">
+            <div class="w-25 bg-pulse-grey" style="height: 35px"></div>
+            <div class="w-25 bg-pulse-grey" style="height: 35px"></div>
+        </div>
+    </div>
+</li>
diff --git a/blocks/timeline/templates/course-item.mustache b/blocks/timeline/templates/course-item.mustache
new file mode 100644 (file)
index 0000000..dafb42a
--- /dev/null
@@ -0,0 +1,36 @@
+{{!
+    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_timeline/course-item
+
+    This template renders the each course block containing a summary and calendar events.
+
+    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."
+    }
+}}
+<li class="list-group-item mt-3 p-0 border-0">
+    <div data-region="course-events-container" id="course-events-container-{{id}}" data-course-id="{{id}}">
+        <h4 class="h5"><a href="{{viewurl}}" data-region="course-name">{{{fullnamedisplay}}}</a></h4>
+        {{< block_timeline/event-list }}
+            {{$courseid}}{{id}}{{/courseid}}
+        {{/ block_timeline/event-list }}
+    </div>
+</li>
diff --git a/blocks/timeline/templates/course-items.mustache b/blocks/timeline/templates/course-items.mustache
new file mode 100644 (file)
index 0000000..444a130
--- /dev/null
@@ -0,0 +1,31 @@
+{{!
+    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_timeline/course-items
+
+    This template renders the each course block containing a summary and calendar events.
+
+    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."
+    }
+}}
+{{#courses}}
+    {{> block_timeline/course-item }}
+{{/courses}}
diff --git a/blocks/timeline/templates/event-list-content.mustache b/blocks/timeline/templates/event-list-content.mustache
new file mode 100644 (file)
index 0000000..b8df729
--- /dev/null
@@ -0,0 +1,71 @@
+{{!
+    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_timeline/event-list-content
+
+    This template renders a group of event list items for the timeline block.
+
+    Example context (json):
+    {
+        "events": [
+            {
+                "name": "Assignment due 1",
+                "url": "https://www.google.com",
+                "timesort": 1490320388,
+                "course": {
+                    "fullnamedisplay": "Course 1"
+                },
+                "action": {
+                    "name": "Submit assignment",
+                    "url": "https://www.google.com",
+                    "itemcount": 1,
+                    "actionable": true
+                },
+                "icon": {
+                    "key": "icon",
+                    "component": "mod_assign",
+                    "alttext": "Assignment icon"
+                }
+            },
+            {
+                "name": "Assignment due 2",
+                "url": "https://www.google.com",
+                "timesort": 1490320388,
+                "course": {
+                    "fullnamedisplay": "Course 1"
+                },
+                "action": {
+                    "name": "Submit assignment",
+                    "url": "https://www.google.com",
+                    "itemcount": 1,
+                    "actionable": true
+                },
+                "icon": {
+                    "key": "icon",
+                    "component": "mod_assign",
+                    "alttext": "Assignment icon"
+                }
+            }
+        ]
+    }
+}}
+<div class="border-bottom pb-2">
+    {{#eventsbyday}}
+        <h5 class="h6 mt-3 mb-0 {{#past}}text-danger{{/past}}">{{#userdate}} {{dayTimestamp}}, {{#str}} strftimedayshort, core_langconfig {{/str}}  {{/userdate}}</h5>
+        {{> block_timeline/event-list-items }}
+    {{/eventsbyday}}
+</div>
\ No newline at end of file
diff --git a/blocks/timeline/templates/event-list-item.mustache b/blocks/timeline/templates/event-list-item.mustache
new file mode 100644 (file)
index 0000000..40fe0dc
--- /dev/null
@@ -0,0 +1,63 @@
+{{!
+    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_timeline/event-list-item
+
+    This template renders an event list item for the timeline block.
+
+    Example context (json):
+    {
+        "name": "Assignment due 1",
+        "url": "https://www.google.com",
+        "timesort": 1490320388,
+        "course": {
+            "fullnamedisplay": "Course 1"
+        },
+        "action": {
+            "name": "Submit assignment",
+            "url": "https://www.google.com",
+            "itemcount": 1,
+            "showitemcount": true,
+            "actionable": true
+        },
+        "icon": {
+            "key": "icon",
+            "component": "mod_assign",
+            "alttext": "Assignment icon"
+        }
+    }
+}}
+<a
+    class="list-group-item list-group-item-action flex-column py-2 pl-0 pr-0 border-0"
+    href="{{{action.url}}}"
+    title="{{name}}"
+    data-region="event-list-item"
+    aria-label='{{#str}} ariaeventlistitem, block_timeline, { "name": "{{name}}", "course": "{{course.fullnamedisplay}}", "date": "{{#userdate}} {{timesort}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}}" } {{/str}}'
+>
+    <div class="d-flex">
+        <div class="icon-size-4 d-flex align-self-center">
+            {{#icon}}{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}{{/icon}}
+        </div>
+        <div class="w-100 event-name-container text-truncate line-height-3">
+            <h6 class="event-name text-truncate mb-0">{{{name}}}</h6>
+            <small class="text-muted text-truncate m-b-0">{{{course.fullnamedisplay}}}</small>
+        </div>
+        <small class="text-right text-nowrap ml-1">
+            {{#userdate}} {{timesort}}, {{#str}} strftimetime24, core_langconfig {{/str}} {{/userdate}}
+        </small>
+    </div>
+</a>
diff --git a/blocks/timeline/templates/event-list-items.mustache b/blocks/timeline/templates/event-list-items.mustache
new file mode 100644 (file)
index 0000000..27f6b42
--- /dev/null
@@ -0,0 +1,70 @@
+{{!
+    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_timeline/event-list-items
+
+    This template renders a group of event list items for the timeline block.
+
+    Example context (json):
+    {
+        "events": [
+            {
+                "name": "Assignment due 1",
+                "url": "https://www.google.com",
+                "timesort": 1490320388,
+                "course": {
+                    "fullnamedisplay": "Course 1"
+                },
+                "action": {
+                    "name": "Submit assignment",
+                    "url": "https://www.google.com",
+                    "itemcount": 1,
+                    "actionable": true
+                },
+                "icon": {
+                    "key": "icon",
+                    "component": "mod_assign",
+                    "alttext": "Assignment icon"
+                }
+            },
+            {
+                "name": "Assignment due 2",
+                "url": "https://www.google.com",
+                "timesort": 1490320388,
+                "course": {
+                    "fullnamedisplay": "Course 1"
+                },
+                "action": {
+                    "name": "Submit assignment",
+                    "url": "https://www.google.com",
+                    "itemcount": 1,
+                    "actionable": true
+                },
+                "icon": {
+                    "key": "icon",
+                    "component": "mod_assign",
+                    "alttext": "Assignment icon"
+                }
+            }
+        ]
+    }
+}}
+<div class="pl-0 list-group list-group-flush">
+{{#events}}
+    {{> block_timeline/event-list-item }}
+{{/events}}
+</div>
diff --git a/blocks/timeline/templates/event-list.mustache b/blocks/timeline/templates/event-list.mustache
new file mode 100644 (file)
index 0000000..9a640c4
--- /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_timeline/event-list
+
+    This template renders a list of events for the timeline block.
+
+    Example context (json):
+    {
+    }
+}}
+<div data-region="event-list-container"
+     data-days-offset="{{$daysoffset}}{{#hasdaysoffset}}{{daysoffset}}{{/hasdaysoffset}}{{^hasdaysoffset}}0{{/hasdaysoffset}}{{/daysoffset}}"
+     {{^nodayslimit}}data-days-limit="{{$dayslimit}}{{#hasdayslimit}}{{dayslimit}}{{/hasdayslimit}}{{^hasdayslimit}}30{{/hasdayslimit}}{{/dayslimit}}"{{/nodayslimit}}
+     data-course-id="{{$courseid}}{{/courseid}}"
+     data-midnight="{{midnight}}"
+>
+    <div data-region="event-list-loading-placeholder">
+        <ul class="pl-0 list-group list-group-flush">
+            {{> block_timeline/placeholder-event-list-item }}
+            {{> block_timeline/placeholder-event-list-item }}
+            {{> block_timeline/placeholder-event-list-item }}
+            {{> block_timeline/placeholder-event-list-item }}
+            {{> block_timeline/placeholder-event-list-item }}
+        </ul>
+        <div class="pt-3 d-flex justify-content-between">
+            <div class="w-25 bg-pulse-grey" style="height: 35px"></div>
+            <div class="w-25 bg-pulse-grey" style="height: 35px"></div>
+        </div>
+    </div>
+    <div data-region="event-list-content"></div>
+    <div class="hidden text-xs-center text-center mt-3" data-region="empty-message">
+        <img
+            src="{{urls.noevents}}"
+            alt="{{#str}} noevents, block_timeline {{/str}}"
+            role="presentation"
+            style="height: 70px; width: 70px"
+        >
+        <p class="text-muted m-t-1">{{#str}} noevents, block_timeline {{/str}}</p>
+    </div>
+</div>
diff --git a/blocks/timeline/templates/main.mustache b/blocks/timeline/templates/main.mustache
new file mode 100644 (file)
index 0000000..021cf47
--- /dev/null
@@ -0,0 +1,54 @@
+{{!
+    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_timeline/main
+
+    This template renders the main content area for the timeline block.
+
+    Example context (json):
+    {}
+}}
+
+<div id="block-timeline-{{uniqid}}" class="block-timeline" data-region="timeline">
+    <div class="container p-0 pb-3 border-bottom">
+        <div class="row no-gutters">
+            <div class="col-sm d-flex justify-content-start">
+                {{> block_timeline/nav-day-filter }}
+            </div>
+            <div class="col-sm d-flex justify-content-end">
+                {{> block_timeline/nav-view-selector }}
+            </div>
+        </div>
+    </div>
+    <div class="container p-0">
+        {{> block_timeline/view }}
+    </div>
+</div>
+{{#js}}
+require(
+[
+    'jquery',
+    'block_timeline/main',
+],
+function(
+    $,
+    Main
+) {
+    var root = $('#block-timeline-{{uniqid}}');
+    Main.init(root);
+});
+{{/js}}
diff --git a/blocks/timeline/templates/nav-day-filter.mustache b/blocks/timeline/templates/nav-day-filter.mustache
new file mode 100644 (file)
index 0000000..90afd62
--- /dev/null
@@ -0,0 +1,90 @@
+{{!
+    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_timeline/nav-day-filter
+
+    This template renders the day range selector for the timeline view.
+
+    Example context (json):
+    {}
+}}
+<div data-region="day-filter" class="dropdown">
+    <button type="button" class="btn btn-outline-secondary dropdown-toggle icon-no-margin" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+        {{#pix}} i/duration {{/pix}}
+        <span class="sr-only">
+            {{#str}} ariadayfilter, block_timeline {{/str}}
+            <span data-active-item-text>{{#str}} next30days, block_timeline {{/str}}</span>
+        </span>
+    </button>
+    <div role="menu" class="dropdown-menu" data-show-active-item>
+        <a
+            class="dropdown-item"
+            href="#"
+            data-from="-14"
+            aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} all, core {{/str}}{{/str}}"
+        >
+            {{#str}} all, core {{/str}}
+        </a>
+        <a
+            class="dropdown-item"
+            href="#"
+            data-from="-14"
+            data-to="0"
+            aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} overdue, block_timeline {{/str}}{{/str}}"
+        >
+            {{#str}} overdue, block_timeline {{/str}}
+        </a>
+        <div class="dropdown-divider"></div>
+        <h6 class="dropdown-header">{{#str}} duedate, block_timeline {{/str}}</h6>
+        <a
+            class="dropdown-item"
+            href="#"
+            data-from="0"
+            data-to="7"
+            aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next7days, block_timeline {{/str}}{{/str}}"
+        >
+            {{#str}} next7days, block_timeline {{/str}}
+        </a>
+        <a
+            class="dropdown-item active"
+            href="#"
+            data-from="0"
+            data-to="30"
+            aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next30days, block_timeline {{/str}}{{/str}}"
+        >
+            {{#str}} next30days, block_timeline {{/str}}
+        </a>
+        <a
+            class="dropdown-item"
+            href="#"
+            data-from="0"
+            data-to="90"
+            aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next3months, block_timeline {{/str}}{{/str}}"
+        >
+            {{#str}} next3months, block_timeline {{/str}}
+        </a>
+        <a
+            class="dropdown-item"
+            href="#"
+            data-from="0"
+            data-to="180"
+            aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next6months, block_timeline {{/str}}{{/str}}"
+        >
+            {{#str}} next6months, block_timeline {{/str}}
+        </a>
+    </div>
+</div>
diff --git a/blocks/timeline/templates/nav-view-selector.mustache b/blocks/timeline/templates/nav-view-selector.mustache
new file mode 100644 (file)
index 0000000..50c2add
--- /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_timeline/nav-view-selector
+
+    This template renders the timeline sort selector.
+
+    Example context (json):
+    {}
+}}
+<div data-region="view-selector" class="btn-group">
+    <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>
+        </span>
+    </button>
+    <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"
+            href="#view_dates_{{uniqid}}"
+            data-toggle="tab"
+            aria-label="{{#str}} ariaviewselectoroption, block_timeline, {{#str}} sortbydates, block_timeline {{/str}}{{/str}}"
+        >
+            {{#str}} sortbydates, block_timeline {{/str}}
+        </a>
+        <a
+            class="dropdown-item"
+            href="#view_courses_{{uniqid}}"
+            data-toggle="tab"
+            aria-label="{{#str}} ariaviewselectoroption, block_timeline, {{#str}} sortbycourses, block_timeline {{/str}}{{/str}}"
+        >
+            {{#str}} sortbycourses, block_timeline {{/str}}
+        </a>
+    </div>
+</div>
diff --git a/blocks/timeline/templates/placeholder-event-list-item.mustache b/blocks/timeline/templates/placeholder-event-list-item.mustache
new file mode 100644 (file)
index 0000000..ad01fc0
--- /dev/null
@@ -0,0 +1,42 @@
+{{!
+    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_timeline/event-list-item
+
+    This template renders an event list item loading placeholder for the timeline block.
+
+    Example context (json):
+    {}
+}}
+<li class="list-group-item pl-0 pr-0">
+    <div class="row">
+        <div class="col-8 pr-0">
+            <div class="d-flex flex-row align-items-center" style="height: 32px">
+                <div class="bg-pulse-grey rounded-circle" style="height: 32px; width: 32px;"></div>
+                <div style="flex: 1" class="pl-2">
+                    <div class="bg-pulse-grey w-100" style="height: 15px;"></div>
+                    <div class="bg-pulse-grey w-75 mt-1" style="height: 10px;"></div>
+                </div>
+            </div>
+        </div>
+        <div class="col-4 pr-3">
+            <div class="d-flex flex-row justify-content-end" style="height: 32px; padding-top: 2px">
+                <div class="bg-pulse-grey w-75" style="height: 15px;"></div>
+            </div>
+        </div>
+    </div>
+</li>
diff --git a/blocks/timeline/templates/view-courses.mustache b/blocks/timeline/templates/view-courses.mustache
new file mode 100644 (file)
index 0000000..d4d1799
--- /dev/null
@@ -0,0 +1,49 @@
+{{!
+    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_timeline/view-courses
+
+    This template renders the timeline view by courses for the timeline block.
+
+    Example context (json):
+    {}
+}}
+<div data-region="course-items-loading-placeholder">
+    <ul class="list-group unstyled">
+        {{> block_timeline/course-item-loading-placeholder }}
+        {{> block_timeline/course-item-loading-placeholder }}
+    </ul>
+    <div class="bg-pulse-grey m-t-1" style="width: 100px; height: 30px; margin-left: auto; margin-right: auto"></div>
+</div>
+<ul class="list-group unstyled" data-region="courses-list"></ul>
+<div class="hidden text-xs-center text-center pt-3" data-region="more-courses-button-container">
+    <button type="button" class="btn btn-secondary" data-action="more-courses">
+        {{#str}} morecourses, block_timeline {{/str}}
+        <span class="hidden" data-region="loading-icon-container">
+            {{> core/loading }}
+        </span>
+    </button>
+</div>
+<div class="hidden text-xs-center text-center mt-3" data-region="no-courses-empty-message">
+    <img
+        src="{{urls.noevents}}"
+        alt="{{#str}} nocoursesinprogress, block_timeline {{/str}}"
+        role="presentation"
+        style="height: 70px; width: 70px"
+    >
+    <p class="text-muted m-t-1">{{#str}} nocoursesinprogress, block_timeline {{/str}}</p>
+</div>
diff --git a/blocks/timeline/templates/view-dates.mustache b/blocks/timeline/templates/view-dates.mustache
new file mode 100644 (file)
index 0000000..0b571f0
--- /dev/null
@@ -0,0 +1,27 @@
+{{!
+    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_timeline/view-dates
+
+    This template renders the timeline view by dates for the timeline block.
+
+    Example context (json):
+    {}
+}}
+<div data-region="timeline-view-dates">
+    {{> block_timeline/event-list }}
+</div>
diff --git a/blocks/timeline/templates/view.mustache b/blocks/timeline/templates/view.mustache
new file mode 100644 (file)
index 0000000..73decee
--- /dev/null
@@ -0,0 +1,44 @@
+{{!
+    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_timeline/view
+
+    This template renders the timeline view for the timeline block.
+
+    Example context (json):
+    {}
+}}
+<div data-region="timeline-view">
+    <div class="tab-content">
+        <div class="tab-pane active fade show" data-region="view-dates" id="view_dates_{{uniqid}}">
+            {{> block_timeline/view-dates }}
+        </div>
+        <div
+            class="tab-pane fade"
+            data-region="view-courses"
+            data-midnight="{{midnight}}"
+            data-limit="2"
+            data-offset="0"
+            data-days-limit="30"
+            data-days-offset="0"
+            data-no-events-url="{{urls.noevents}}"
+            id="view_courses_{{uniqid}}"
+        >
+            {{> block_timeline/view-courses }}
+        </div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/blocks/timeline/tests/behat/block_timeline_courses.feature b/blocks/timeline/tests/behat/block_timeline_courses.feature
new file mode 100644 (file)
index 0000000..f937684
--- /dev/null
@@ -0,0 +1,72 @@
+@block @block_timeline @javascript
+Feature: The timeline block allows users to see upcoming activities
+  In order to enable the timeline block
+  As a student
+  I can add the timeline block to my dashboard
+
+  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       |
+    And the following "courses" exist:
+      | fullname | shortname | category | startdate                   | enddate         |
+      | Course 1 | C1        | 0        | ##yesterday##               | ##tomorrow## |
+      | Course 2 | C2        | 0        | ##yesterday##               | ##tomorrow## |
+      | Course 3 | C3        | 0        | ##yesterday##               | ##tomorrow## |
+      | Course 4 | C4        | 0        | ##first day of next month## | ##last day of next month## |
+    And the following "activities" exist:
+      | activity | course | idnumber  | name            | intro                   | timeopen      | timeclose     |
+      | choice   | C2     | choice1   | Test choice 1   | Test choice description | ##yesterday## | ##tomorrow##  |
+      | choice   | C1     | choice2   | Test choice 2   | Test choice description | ##1 month ago## | ##15 days ago##  |
+      | choice   | C3     | choice3   | Test choice 3   | Test choice description | ##first day of +5 months## | ##last day of +5 months##  |
+      | feedback | C2     | feedback1 | Test feedback 1 | Test feedback description | ##yesterday## | ##tomorrow##  |
+      | feedback | C1     | feedback2 | Test feedback 2 | Test feedback description | ##first day of +10 months## | ##last day of +10 months##  |
+      | feedback | C3     | feedback3 | Test feedback 3 | Test feedback description | ##first day of +5 months## | ##last day of +5 months## |
+      | feedback | C4     | feedback4 | Test feedback 4 | Test feedback description | ##yesterday## | ##tomorrow## |
+    And the following "activities" exist:
+      | activity | course | idnumber  | name            | intro                   | timeopen        | duedate     |
+      | assign   | C1     | assign1   | Test assign 1   | Test assign description | ##1 month ago## | ##yesterday##  |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student1 | C2 | student |
+      | student1 | C3 | student |
+      | student1 | C4 | student |
+
+  Scenario: Next 30 days in course view
+    Given I log in as "student1"
+    And I click on "Sort" "button" in the "Timeline" "block"
+    When I click on "Sort by courses" "link" in the "Timeline" "block"
+    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"
+
+  Scenario: All in course view
+    Given I log in as "student1"
+    And I click on "Next 30 days" "button" in the "Timeline" "block"
+    And I click on "All" "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"
+    When I click on "More courses" "button" in the "Timeline" "block"
+    Then I should see "Course 3" in the "Timeline" "block"
+    And I should see "Course 2" in the "Timeline" "block"
+    And I should see "Course 1" 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 1 closes" in the "Timeline" "block"
+    And I should see "Test feedback 2 closes" in the "Timeline" "block"
+    And I should see "Test feedback 3 closes" in the "Timeline" "block"
+    And I should see "Test assign 1 is due" in the "Timeline" "block"
+    And I should not see "More courses" in the "Timeline" "block"
+    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"
diff --git a/blocks/timeline/tests/behat/block_timeline_dates.feature b/blocks/timeline/tests/behat/block_timeline_dates.feature
new file mode 100644 (file)
index 0000000..6aaf269
--- /dev/null
@@ -0,0 +1,88 @@
+@block @block_timeline @javascript
+Feature: The timeline block allows users to see upcoming activities
+  In order to enable the timeline block
+  As a student
+  I can add the timeline block to my dashboard
+
+  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       |
+    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## |
+    And the following "activities" exist:
+      | activity | course | idnumber  | name            | intro                   | timeopen      | timeclose     |
+      | choice   | C2     | choice1   | Test choice 1   | Test choice description | ##yesterday## | ##tomorrow##  |
+      | choice   | C1     | choice2   | Test choice 2   | Test choice description | ##1 month ago## | ##15 days ago##  |
+      | choice   | C3     | choice3   | Test choice 3   | Test choice description | ##first day of +5 months## | ##last day of +5 months##  |
+      | feedback | C2     | feedback1 | Test feedback 1 | Test feedback description | ##yesterday## | ##tomorrow##  |
+      | feedback | C1     | feedback2 | Test feedback 2 | Test feedback description | ##first day of +10 months## | ##last day of +10 months##  |
+      | feedback | C3     | feedback3 | Test feedback 3 | Test feedback description | ##first day of +5 months## | ##last day of +5 months## |
+    And the following "activities" exist:
+      | activity | course | idnumber  | name            | intro                   | timeopen        | duedate     |
+      | assign   | C1     | assign1   | Test assign 1   | Test assign description | ##1 month ago## | ##yesterday##  |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student1 | C2 | student |
+      | student1 | C3 | student |
+
+  Scenario: Next 7 days 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 "Next 7 days" "link" in the "Timeline" "block"
+    Then 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 "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 3 closes" in the "Timeline" "block"
+    And I should not see "Test assign 1 is due" in the "Timeline" "block"
+
+  Scenario: 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"
+    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"
+
+  Scenario: 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"
+    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: All in date view no next
+    Given I log in as "student1"
+    And I click on "Next 30 days" "button" in the "Timeline" "block"
+    And I click on "All" "link" in the "Timeline" "block"
+    And I click on "5" "button" in the "Timeline" "block"
+    When I click on "25" "link" in the "Timeline" "block"
+    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 see "Test feedback 2 closes" in the "Timeline" "block"
+    And I should not see "Test choice 2 closes" in the "Timeline" "block"
diff --git a/blocks/timeline/version.php b/blocks/timeline/version.php
new file mode 100644 (file)
index 0000000..64de7f3
--- /dev/null
@@ -0,0 +1,29 @@
+<?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/>.
+
+/**
+ * Version details for the timeline block.
+ *
+ * @package    block_timeline
+ * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version   = 2018083100;         // The current plugin version (Date: YYYYMMDDXX).
+$plugin->requires  = 2018082400;         // Requires this Moodle version.
+$plugin->component = 'block_timeline'; // Full name of the plugin (used for diagnostics).
index 7ef8f0a..1234de4 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in /blocks/* - activity modules,
 information provided here is intended especially for developers.
 
+=== 3.6 ===
+
+* The timeline view from block_myoverview has been split out into block_timeline.
+
 === 3.4 ===
 
 * The block_instances table now contains fields timecreated and timemodified. If third-party code
index c8fa245..bf42e6c 100644 (file)
@@ -2583,7 +2583,7 @@ function blocks_add_default_system_blocks() {
         $subpagepattern = null;
     }
 
-    $newblocks = array('private_files', 'online_users', 'badges', 'calendar_month', 'calendar_upcoming');
+    $newblocks = array('timeline', 'private_files', 'online_users', 'badges', 'calendar_month', 'calendar_upcoming');
     $newcontent = array('lp', 'myoverview');
     $page->blocks->add_blocks(array(BLOCK_POS_RIGHT => $newblocks, 'content' => $newcontent), 'my-index', $subpagepattern);
 }
index e319dc3..ca16fd4 100644 (file)
@@ -1721,7 +1721,7 @@ class core_plugin_manager {
                 'private_files', 'quiz_results', 'recent_activity',
                 'rss_client', 'search_forums', 'section_links',
                 'selfcompletion', 'settings', 'site_main_menu',
-                'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
+                'social_activities', 'tag_flickr', 'tag_youtube', 'tags', 'timeline'
             ),
 
             'booktool' => array(
diff --git a/theme/bootstrapbase/templates/block_timeline/course-item-loading-placeholder.mustache b/theme/bootstrapbase/templates/block_timeline/course-item-loading-placeholder.mustache
new file mode 100644 (file)
index 0000000..096ec11
--- /dev/null
@@ -0,0 +1,44 @@
+{{!
+    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_timeline/course-item-loading-placeholder
+
+    This template renders the each course block containing a summary and calendar events.
+
+    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."
+    }
+}}
+<li class="m-t-1">
+    <div class="bg-pulse-grey" style="height: 25px; width: 50%;"></div>
+    <div class="m-t-1">
+        <ul class="media-list">
+            {{> block_timeline/placeholder-event-list-item }}
+            {{> block_timeline/placeholder-event-list-item }}
+            {{> block_timeline/placeholder-event-list-item }}
+            {{> block_timeline/placeholder-event-list-item }}
+            {{> block_timeline/placeholder-event-list-item }}
+        </ul>
+        <div class="m-t-1">
+            <div class="bg-pulse-grey pull-right" style="height: 35px; width: 25%;"></div>
+            <div class="bg-pulse-grey" style="height: 35px; width: 25%;"></div>
+        </div>
+    </div>
+</li>
diff --git a/theme/bootstrapbase/templates/block_timeline/event-list-item.mustache b/theme/bootstrapbase/templates/block_timeline/event-list-item.mustache
new file mode 100644 (file)
index 0000000..af008b1
--- /dev/null
@@ -0,0 +1,63 @@
+{{!
+    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_timeline/event-list-item
+
+    This template renders an event list item for the timeline block.
+
+    Example context (json):
+    {
+        "name": "Assignment due 1",
+        "url": "https://www.google.com",
+        "timesort": 1490320388,
+        "course": {
+            "fullnamedisplay": "Course 1"
+        },
+        "action": {
+            "name": "Submit assignment",
+            "url": "https://www.google.com",
+            "itemcount": 1,
+            "showitemcount": true,
+            "actionable": true
+        },
+        "icon": {
+            "key": "icon",
+            "component": "mod_assign",
+            "alttext": "Assignment icon"
+        }
+    }
+}}
+<li class="media">
+    <a
+        class="media"
+        href="{{{action.url}}}"
+        title="{{name}}"
+        data-region="event-list-item"
+        aria-label='{{#str}} ariaeventlistitem, block_timeline, { "name": "{{name}}", "course": "{{course.fullnamedisplay}}", "date": "{{#userdate}} {{timesort}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}}" } {{/str}}'
+    >
+        <div class="icon-size-4 pull-left">
+            {{#icon}}{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}{{/icon}}
+        </div>
+        <small class="text-right text-nowrap pull-right">
+            {{#userdate}} {{timesort}}, {{#str}} strftimetime24, core_langconfig {{/str}} {{/userdate}}
+        </small>
+        <div class="text-truncate line-height-3 media-body">
+            <h6 class="text-truncate m-0">{{{name}}}</h6>
+            <small class="text-muted text-truncate">{{{course.fullnamedisplay}}}</small>
+        </div>
+    </a>
+</li>
diff --git a/theme/bootstrapbase/templates/block_timeline/event-list-items.mustache b/theme/bootstrapbase/templates/block_timeline/event-list-items.mustache
new file mode 100644 (file)
index 0000000..59da36e
--- /dev/null
@@ -0,0 +1,70 @@
+{{!
+    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_timeline/event-list-items
+
+    This template renders a group of event list items for the timeline block.
+
+    Example context (json):
+    {
+        "events": [
+            {
+                "name": "Assignment due 1",
+                "url": "https://www.google.com",
+                "timesort": 1490320388,
+                "course": {
+                    "fullnamedisplay": "Course 1"
+                },
+                "action": {
+                    "name": "Submit assignment",
+                    "url": "https://www.google.com",
+                    "itemcount": 1,
+                    "actionable": true
+                },
+                "icon": {
+                    "key": "icon",
+                    "component": "mod_assign",
+                    "alttext": "Assignment icon"
+                }
+            },
+            {
+                "name": "Assignment due 2",
+                "url": "https://www.google.com",
+                "timesort": 1490320388,
+                "course": {
+                    "fullnamedisplay": "Course 1"
+                },
+                "action": {
+                    "name": "Submit assignment",
+                    "url": "https://www.google.com",
+                    "itemcount": 1,
+                    "actionable": true
+                },
+                "icon": {
+                    "key": "icon",
+                    "component": "mod_assign",
+                    "alttext": "Assignment icon"
+                }
+            }
+        ]
+    }
+}}
+<ul class="media-list">
+{{#events}}
+    {{> block_timeline/event-list-item }}
+{{/events}}
+</ul>
diff --git a/theme/bootstrapbase/templates/block_timeline/event-list.mustache b/theme/bootstrapbase/templates/block_timeline/event-list.mustache
new file mode 100644 (file)
index 0000000..12801c5
--- /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_timeline/event-list
+
+    This template renders a list of events for the timeline block.
+
+    Example context (json):
+    {
+    }
+}}
+<div data-region="event-list-container"
+     data-days-offset="{{$daysoffset}}{{#hasdaysoffset}}{{daysoffset}}{{/hasdaysoffset}}{{^hasdaysoffset}}0{{/hasdaysoffset}}{{/daysoffset}}"
+     {{^nodayslimit}}data-days-limit="{{$dayslimit}}{{#hasdayslimit}}{{dayslimit}}{{/hasdayslimit}}{{^hasdayslimit}}30{{/hasdayslimit}}{{/dayslimit}}"{{/nodayslimit}}
+     data-course-id="{{$courseid}}{{/courseid}}"
+     data-midnight="{{midnight}}"
+>
+    <div data-region="event-list-loading-placeholder">
+        <ul class="media-list m-t-1">
+            {{> block_timeline/placeholder-event-list-item }}
+            {{> block_timeline/placeholder-event-list-item }}
+            {{> block_timeline/placeholder-event-list-item }}
+            {{> block_timeline/placeholder-event-list-item }}
+            {{> block_timeline/placeholder-event-list-item }}
+        </ul>
+        <div class="m-t-1">
+            <div class="bg-pulse-grey pull-right" style="height: 35px; width: 25%;"></div>
+            <div class="bg-pulse-grey" style="height: 35px; width: 25%;"></div>
+        </div>
+    </div>
+    <div data-region="event-list-content"></div>
+    <div class="hidden text-xs-center text-center mt-3" data-region="empty-message">
+        <img
+            src="{{urls.noevents}}"
+            alt="{{#str}} noevents, block_timeline {{/str}}"
+            role="presentation"
+            style="height: 70px; width: 70px"
+        >
+        <p class="text-muted m-t-1">{{#str}} noevents, block_timeline {{/str}}</p>
+    </div>
+</div>
diff --git a/theme/bootstrapbase/templates/block_timeline/main.mustache b/theme/bootstrapbase/templates/block_timeline/main.mustache
new file mode 100644 (file)
index 0000000..7e82e6f
--- /dev/null
@@ -0,0 +1,50 @@
+{{!
+    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_timeline/main
+
+    This template renders the main content area for the timeline block.
+
+    Example context (json):
+    {}
+}}
+
+<div id="block-timeline-{{uniqid}}" class="block-timeline" data-region="timeline">
+    <div class="container-fluid p-0 pb-3 border-bottom">
+        <div class="row-fluid no-gutters">
+            {{> block_timeline/nav-day-filter }}
+            {{> block_timeline/nav-view-selector }}
+        </div>
+    </div>
+    <div class="container-fluid p-0">
+        {{> block_timeline/view }}
+    </div>
+</div>
+{{#js}}
+require(
+[
+    'jquery',
+    'block_timeline/main',
+],
+function(
+    $,
+    Main
+) {
+    var root = $('#block-timeline-{{uniqid}}');
+    Main.init(root);
+});
+{{/js}}
diff --git a/theme/bootstrapbase/templates/block_timeline/nav-day-filter.mustache b/theme/bootstrapbase/templates/block_timeline/nav-day-filter.mustache
new file mode 100644 (file)
index 0000000..85a1183
--- /dev/null
@@ -0,0 +1,67 @@
+{{!
+    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_timeline/nav-day-filter
+
+    This template renders the day range selector for the timeline view.
+
+    Example context (json):
+    {}
+}}
+<div data-region="day-filter" class="btn-group">
+    <button type="button" class="btn dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+        {{#pix}} i/duration {{/pix}}
+        <span class="sr-only">
+            {{#str}} ariadayfilter, block_timeline {{/str}}
+            <span data-active-item-text>{{#str}} next30days, block_timeline {{/str}}</span>
+        </span>
+        <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">
+            <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">
+            <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">
+            <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">
+            <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">
+            <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">
+            <a href="#" aria-label="{{#str}} ariadayfilteroption, block_timeline, {{#str}} next6months, block_timeline {{/str}}{{/str}}">
+                {{#str}} next6months, block_timeline {{/str}}
+            </a>
+        </li>
+    </ul>
+</div>
diff --git a/theme/bootstrapbase/templates/block_timeline/nav-view-selector.mustache b/theme/bootstrapbase/templates/block_timeline/nav-view-selector.mustache
new file mode 100644 (file)
index 0000000..4caeda9
--- /dev/null
@@ -0,0 +1,46 @@
+{{!
+    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_timeline/nav-view-selector
+
+    This template renders the timeline sort selector.
+
+    Example context (json):
+    {}
+}}
+<div data-region="view-selector" class="btn-group pull-right">
+    <button type="button" class="btn dropdown-toggle" 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>
+        </span>
+        <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">
+            <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">
+            <a href="#" aria-label="{{#str}} ariaviewselectoroption, block_timeline, {{#str}} sortbycourses, block_timeline {{/str}}{{/str}}">
+                {{#str}} sortbycourses, block_timeline {{/str}}
+            </a>
+        </li>
+    </ul>
+</div>
diff --git a/theme/bootstrapbase/templates/block_timeline/placeholder-event-list-item.mustache b/theme/bootstrapbase/templates/block_timeline/placeholder-event-list-item.mustache
new file mode 100644 (file)
index 0000000..3e98db1
--- /dev/null
@@ -0,0 +1,31 @@
+{{!
+    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_timeline/event-list-item
+
+    This template renders an event list item loading placeholder for the timeline block.
+
+    Example context (json):
+    {}
+}}
+<li class="media" style="height: 32px">
+    <div class="bg-pulse-grey pull-left" style="height: 32px; width: 32px; border-radius: 50%"></div>
+    <div class="media-body">
+        <div class="bg-pulse-grey" style="height: 15px;"></div>
+        <div class="bg-pulse-grey" style="height: 10px; margin-top: 7px; width: 75%"></div>
+    </div>
+</li>
diff --git a/theme/bootstrapbase/templates/block_timeline/view.mustache b/theme/bootstrapbase/templates/block_timeline/view.mustache
new file mode 100644 (file)
index 0000000..7d67b03
--- /dev/null
@@ -0,0 +1,44 @@
+{{!
+    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_timeline/view
+
+    This template renders the timeline view for the timeline block.
+
+    Example context (json):
+    {}
+}}
+<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}}">
+            {{> block_timeline/view-dates }}
+        </div>
+        <div
+            class="tab-pane fade"
+            data-region="view-courses"
+            data-midnight="{{midnight}}"
+            data-limit="2"
+            data-offset="0"
+            data-days-limit="30"
+            data-days-offset="0"
+            data-no-events-url="{{urls.noevents}}"
+            id="view_courses_{{uniqid}}"
+        >
+            {{> block_timeline/view-courses }}
+        </div>
+    </div>
+</div>
\ No newline at end of file