diff --git a/blocks/timeline/amd/build/event_list.min.js b/blocks/timeline/amd/build/event_list.min.js
index 185c3cd79af..3fbed128f91 100644
--- a/blocks/timeline/amd/build/event_list.min.js
+++ b/blocks/timeline/amd/build/event_list.min.js
@@ -1,2 +1,2 @@
-define ("block_timeline/event_list",["jquery","core/notification","core/templates","core/paged_content_factory","core/str","core/user_date","block_timeline/calendar_events_repository"],function(a,b,c,d,e,f,g){var h={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\"]"},i={EVENT_LIST_CONTENT:"block_timeline/event-list-content"},j={ignoreControlWhileLoading:!0,controlPlacementBottom:!0,ariaLabels:{itemsperpagecomponents:"ariaeventlistpagelimit, block_timeline"}},k=function(a){a.find(h.EVENT_LIST_CONTENT).addClass("hidden");a.find(h.EMPTY_MESSAGE).removeClass("hidden")},l=function(a){a.find(h.EVENT_LIST_CONTENT).removeClass("hidden");a.find(h.EMPTY_MESSAGE).addClass("hidden")},m=function(a){a.find(h.EVENT_LIST_CONTENT).empty()},n=function(a,b){var c={},d={eventsbyday:[]};a.forEach(function(a){var b=a.timeusermidnight;if(c[b]){c[b].push(a)}else{c[b]=[a]}});Object.keys(c).forEach(function(a){var e=c[a];d.eventsbyday.push({past:a<b,dayTimestamp:a,events:e})});return d},o=function(a,b){var d=n(a,b),e=i.EVENT_LIST_CONTENT;return c.render(e,d)},p=function(a,b,c,d,e,f){var h=d!=void 0?a+d*86400:!1,i={starttime:a+c*86400,limit:b};if(e){i.aftereventid=e}if(h){i.endtime=h}if(f){i.courseid=f;return g.queryByCourse(i)}else{return g.queryByTime(i)}},q=function(a,b,c,d,e,g,h,i){var j=a.pageNumber,k=a.limit,l=j;while(!d.hasOwnProperty(l)){l--}var m=d[l],n=null;if(e&&e.hasOwnProperty(j)){n=e[j]}else{n=p(c,k+1,h,i,m,g)}return n.then(function(a){if(!a.events.length){b.allItemsLoaded(j);return[]}var d=a.events.filter(function(a){if("open"==a.eventtype||"opensubmission"==a.eventtype){var b=f.getUserMidnightForTimestamp(a.timesort,c);return b>c}return!0}),e=d.length<=k;if(e){b.allItemsLoaded(j)}else{d.pop()}return d})},r=function(c,f,g,h,i,k,l,m,n){var p={1:0},r=!1,s=a.extend({},j,n);return e.get_string("ariaeventlistpagelimit","block_timeline",a.isArray(c)?c[0].value:c).then(function(a){s.ariaLabels.itemsperpage=a;s.ariaLabels.paginationnav=m;return a}).then(function(){return d.createWithLimit(c,function(c,d){var e=[];c.forEach(function(a){var c=a.pageNumber,h=q(a,d,g,p,f,i,k,l).then(function(a){if(a.length){r=!0;var b=a[a.length-1].id;p[c+1]=b;return o(a,g)}else{return a}}).catch(b.exception);e.push(h)});a.when.apply(a,e).then(function(){h.resolve(r)}).catch(function(){h.resolve(r)});return e},s)})};return{init:function init(d,e,f,g,i){d=a(d);var j=a.Deferred(),n=d.find(h.EVENT_LIST_CONTENT),o=d.find(h.EVENT_LIST_LOADING_PLACEHOLDER),p=d.attr("data-course-id"),q=parseInt(d.attr("data-days-offset"),10),s=d.attr("data-days-limit"),t=parseInt(d.attr("data-midnight"),10);m(d);l(d);o.removeClass("hidden");if(s!=void 0){s=parseInt(s,10)}return r(e,f,t,j,p,q,s,g,i).then(function(b,e){b=a(b);b.addClass("hidden");c.replaceNodeContents(n,b,e);j.then(function(a){b.removeClass("hidden");o.addClass("hidden");if(!a){k(d)}return a}).catch(function(){return!1});return b}).catch(b.exception)},rootSelector:h.ROOT}});
+define ("block_timeline/event_list",["jquery","core/notification","core/templates","core/paged_content_factory","core/str","core/user_date","block_timeline/calendar_events_repository"],function(a,b,c,d,e,f,g){var h=!1,i={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\"]"},j={EVENT_LIST_CONTENT:"block_timeline/event-list-content"},k={ignoreControlWhileLoading:!0,controlPlacementBottom:!0,ariaLabels:{itemsperpagecomponents:"ariaeventlistpagelimit, block_timeline"}},l=function(a){a.find(i.EVENT_LIST_CONTENT).addClass("hidden");a.find(i.EMPTY_MESSAGE).removeClass("hidden")},m=function(a){a.find(i.EVENT_LIST_CONTENT).removeClass("hidden");a.find(i.EMPTY_MESSAGE).addClass("hidden")},n=function(a){a.find(i.EVENT_LIST_CONTENT).empty()},o=function(a,b){var c={},d={courseview:h,eventsbyday:[]};a.forEach(function(a){var b=a.timeusermidnight;if(c[b]){c[b].push(a)}else{c[b]=[a]}});Object.keys(c).forEach(function(a){var e=c[a];d.eventsbyday.push({past:a<b,dayTimestamp:a,events:e})});return d},p=function(a,b){var d=o(a,b),e=j.EVENT_LIST_CONTENT;return c.render(e,d)},q=function(a,b,c,d,e,f){var h=d!=void 0?a+d*86400:!1,i={starttime:a+c*86400,limit:b};if(e){i.aftereventid=e}if(h){i.endtime=h}if(f){i.courseid=f;return g.queryByCourse(i)}else{return g.queryByTime(i)}},r=function(a,b,c,d,e,g,h,i){var j=a.pageNumber,k=a.limit,l=j;while(!d.hasOwnProperty(l)){l--}var m=d[l],n=null;if(e&&e.hasOwnProperty(j)){n=e[j]}else{n=q(c,k+1,h,i,m,g)}return n.then(function(a){if(!a.events.length){b.allItemsLoaded(j);return[]}var d=a.events.filter(function(a){if("open"==a.eventtype||"opensubmission"==a.eventtype){var b=f.getUserMidnightForTimestamp(a.timesort,c);return b>c}return!0}),e=d.length<=k;if(e){b.allItemsLoaded(j)}else{d.pop()}return d})},s=function(c,f,g,h,i,j,l,m,n){var o={1:0},q=!1,s=a.extend({},k,n);return e.get_string("ariaeventlistpagelimit","block_timeline",a.isArray(c)?c[0].value:c).then(function(a){s.ariaLabels.itemsperpage=a;s.ariaLabels.paginationnav=m;return a}).then(function(){return d.createWithLimit(c,function(c,d){var e=[];c.forEach(function(a){var c=a.pageNumber,h=r(a,d,g,o,f,i,j,l).then(function(a){if(a.length){q=!0;var b=a[a.length-1].id;o[c+1]=b;return p(a,g)}else{return a}}).catch(b.exception);e.push(h)});a.when.apply(a,e).then(function(){h.resolve(q)}).catch(function(){h.resolve(q)});return e},s)})};return{init:function init(d,e,f,g){var j=4<arguments.length&&arguments[4]!==void 0?arguments[4]:{};d=a(d);h=!!j.courseview;var k=a.Deferred(),o=d.find(i.EVENT_LIST_CONTENT),p=d.find(i.EVENT_LIST_LOADING_PLACEHOLDER),q=d.attr("data-course-id"),r=parseInt(d.attr("data-days-offset"),10),t=d.attr("data-days-limit"),u=parseInt(d.attr("data-midnight"),10);n(d);m(d);p.removeClass("hidden");if(t!=void 0){t=parseInt(t,10)}return s(e,f,u,k,q,r,t,g,j).then(function(b,e){b=a(b);b.addClass("hidden");c.replaceNodeContents(o,b,e);k.then(function(a){b.removeClass("hidden");p.addClass("hidden");if(!a){l(d)}return a}).catch(function(){return!1});return b}).catch(b.exception)},rootSelector:i.ROOT}});
 //# sourceMappingURL=event_list.min.js.map
diff --git a/blocks/timeline/amd/build/event_list.min.js.map b/blocks/timeline/amd/build/event_list.min.js.map
index be868aefb00..0bd718d9ab2 100644
--- a/blocks/timeline/amd/build/event_list.min.js.map
+++ b/blocks/timeline/amd/build/event_list.min.js.map
@@ -1 +1 @@
-{"version":3,"sources":["../src/event_list.js"],"names":["define","$","Notification","Templates","PagedContentFactory","Str","UserDate","CalendarEventsRepository","SELECTORS","EMPTY_MESSAGE","ROOT","EVENT_LIST_CONTENT","EVENT_LIST_LOADING_PLACEHOLDER","TEMPLATES","DEFAULT_PAGED_CONTENT_CONFIG","ignoreControlWhileLoading","controlPlacementBottom","ariaLabels","itemsperpagecomponents","hideContent","root","find","addClass","removeClass","showContent","emptyContent","empty","buildTemplateContext","calendarEvents","midnight","eventsByDay","templateContext","eventsbyday","forEach","calendarEvent","dayTimestamp","timeusermidnight","push","Object","keys","events","past","render","templateName","load","limit","daysOffset","daysLimit","lastId","courseId","endTime","args","starttime","aftereventid","endtime","courseid","queryByCourse","queryByTime","loadEventsFromPageData","pageData","actions","lastIds","preloadedPages","pageNumber","lastPageNumber","hasOwnProperty","eventsPromise","then","result","length","allItemsLoaded","filter","event","eventtype","getUserMidnightForTimestamp","timesort","loadedAll","pop","createPagedContent","pageLimit","firstLoad","paginationAriaLabel","additionalConfig","hasContent","config","extend","get_string","isArray","value","string","itemsperpage","paginationnav","createWithLimit","pagesData","promises","pagePromise","lastEventId","id","catch","exception","when","apply","resolve","init","Deferred","eventListContent","loadingPlaceholder","attr","parseInt","html","js","replaceNodeContents","rootSelector"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Javascript to load and render the list of calendar events for a\n * given day range.\n *\n * @module     block_timeline/event_list\n * @copyright  2016 Ryan Wyllie <ryan@moodle.com>\n * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(\n[\n    'jquery',\n    'core/notification',\n    'core/templates',\n    'core/paged_content_factory',\n    'core/str',\n    'core/user_date',\n    'block_timeline/calendar_events_repository'\n],\nfunction(\n    $,\n    Notification,\n    Templates,\n    PagedContentFactory,\n    Str,\n    UserDate,\n    CalendarEventsRepository\n) {\n\n    var SECONDS_IN_DAY = 60 * 60 * 24;\n\n    var SELECTORS = {\n        EMPTY_MESSAGE: '[data-region=\"empty-message\"]',\n        ROOT: '[data-region=\"event-list-container\"]',\n        EVENT_LIST_CONTENT: '[data-region=\"event-list-content\"]',\n        EVENT_LIST_LOADING_PLACEHOLDER: '[data-region=\"event-list-loading-placeholder\"]',\n    };\n\n    var TEMPLATES = {\n        EVENT_LIST_CONTENT: 'block_timeline/event-list-content'\n    };\n\n    // We want the paged content controls below the paged content area\n    // and the controls should be ignored while data is loading.\n    var DEFAULT_PAGED_CONTENT_CONFIG = {\n        ignoreControlWhileLoading: true,\n        controlPlacementBottom: true,\n        ariaLabels: {\n            itemsperpagecomponents: 'ariaeventlistpagelimit, block_timeline',\n        }\n    };\n\n    /**\n     * Hide the content area and display the empty content message.\n     *\n     * @param {object} root The container element\n     */\n    var hideContent = function(root) {\n        root.find(SELECTORS.EVENT_LIST_CONTENT).addClass('hidden');\n        root.find(SELECTORS.EMPTY_MESSAGE).removeClass('hidden');\n    };\n\n    /**\n     * Show the content area and hide the empty content message.\n     *\n     * @param {object} root The container element\n     */\n    var showContent = function(root) {\n        root.find(SELECTORS.EVENT_LIST_CONTENT).removeClass('hidden');\n        root.find(SELECTORS.EMPTY_MESSAGE).addClass('hidden');\n    };\n\n    /**\n     * Empty the content area.\n     *\n     * @param {object} root The container element\n     */\n    var emptyContent = function(root) {\n        root.find(SELECTORS.EVENT_LIST_CONTENT).empty();\n    };\n\n    /**\n     * Construct the template context from a list of calendar events. The events\n     * are grouped by which day they are on. The day is calculated from the user's\n     * midnight timestamp to ensure that the calculation is timezone agnostic.\n     *\n     * The return data structure will look like:\n     * {\n     *      eventsbyday: [\n     *          {\n     *              dayTimestamp: 1533744000,\n     *              events: [\n     *                  { ...event 1 data... },\n     *                  { ...event 2 data... }\n     *              ]\n     *          },\n     *          {\n     *              dayTimestamp: 1533830400,\n     *              events: [\n     *                  { ...event 3 data... },\n     *                  { ...event 4 data... }\n     *              ]\n     *          }\n     *      ]\n     * }\n     *\n     * Each day timestamp is the day's midnight in the user's timezone.\n     *\n     * @param {array} calendarEvents List of calendar events\n     * @param {Number} midnight A timestamp representing midnight in the user's timezone\n     * @return {object}\n     */\n    var buildTemplateContext = function(calendarEvents, midnight) {\n        var eventsByDay = {};\n        var templateContext = {\n            eventsbyday: []\n        };\n\n        calendarEvents.forEach(function(calendarEvent) {\n            var dayTimestamp = calendarEvent.timeusermidnight;\n            if (eventsByDay[dayTimestamp]) {\n                eventsByDay[dayTimestamp].push(calendarEvent);\n            } else {\n                eventsByDay[dayTimestamp] = [calendarEvent];\n            }\n        });\n\n        Object.keys(eventsByDay).forEach(function(dayTimestamp) {\n            var events = eventsByDay[dayTimestamp];\n            templateContext.eventsbyday.push({\n                past: dayTimestamp < midnight,\n                dayTimestamp: dayTimestamp,\n                events: events\n            });\n        });\n\n        return templateContext;\n    };\n\n    /**\n     * Render the HTML for the given calendar events.\n     *\n     * @param {array} calendarEvents  A list of calendar events\n     * @param {Number} midnight A timestamp representing midnight for the user\n     * @return {promise} Resolved with HTML and JS strings.\n     */\n    var render = function(calendarEvents, midnight) {\n        var templateContext = buildTemplateContext(calendarEvents, midnight);\n        var templateName = TEMPLATES.EVENT_LIST_CONTENT;\n\n        return Templates.render(templateName, templateContext);\n    };\n\n    /**\n     * Retrieve a list of calendar events from the server for the given\n     * constraints.\n     *\n     * @param {Number} midnight The user's midnight time in unix timestamp.\n     * @param {Number} limit Limit the result set to this number of items\n     * @param {Number} daysOffset How many days (from midnight) to offset the results from\n     * @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to\n     * @param {int|false} lastId The ID of the last seen event (if any)\n     * @param {int|undefined} courseId Course ID to restrict events to\n     * @return {Promise} A jquery promise\n     */\n    var load = function(midnight, limit, daysOffset, daysLimit, lastId, courseId) {\n        var startTime = midnight + (daysOffset * SECONDS_IN_DAY);\n        var endTime = daysLimit != undefined ? midnight + (daysLimit * SECONDS_IN_DAY) : false;\n\n        var args = {\n            starttime: startTime,\n            limit: limit,\n        };\n\n        if (lastId) {\n            args.aftereventid = lastId;\n        }\n\n        if (endTime) {\n            args.endtime = endTime;\n        }\n\n        if (courseId) {\n            // If we have a course id then we only want events from that course.\n            args.courseid = courseId;\n            return CalendarEventsRepository.queryByCourse(args);\n        } else {\n            // Otherwise we want events from any course.\n            return CalendarEventsRepository.queryByTime(args);\n        }\n    };\n\n    /**\n     * Handle a single page request from the paged content. Uses the given page data to request\n     * the events from the server.\n     *\n     * Checks the given preloadedPages before sending a request to the server to make sure we\n     * don't load data unnecessarily.\n     *\n     * @param {object} pageData A single page data (see core/paged_content_pages for more info).\n     * @param {object} actions Paged content actions (see core/paged_content_pages for more info).\n     * @param {Number} midnight The user's midnight time in unix timestamp.\n     * @param {object} lastIds The last event ID for each loaded page. Page number is key, id is value.\n     * @param {object} preloadedPages An object of preloaded page data. Page number as key, data promise as value.\n     * @param {int|undefined} courseId Course ID to restrict events to\n     * @param {Number} daysOffset How many days (from midnight) to offset the results from\n     * @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to\n     * @return {object} jQuery promise resolved with calendar events.\n     */\n    var loadEventsFromPageData = function(\n        pageData,\n        actions,\n        midnight,\n        lastIds,\n        preloadedPages,\n        courseId,\n        daysOffset,\n        daysLimit\n    ) {\n        var pageNumber = pageData.pageNumber;\n        var limit = pageData.limit;\n        var lastPageNumber = pageNumber;\n\n        // This is here to protect us if, for some reason, the pages\n        // are loaded out of order somehow and we don't have a reference\n        // to the previous page. In that case, scan back to find the most\n        // recent page we've seen.\n        while (!lastIds.hasOwnProperty(lastPageNumber)) {\n            lastPageNumber--;\n        }\n        // Use the last id of the most recent page.\n        var lastId = lastIds[lastPageNumber];\n        var eventsPromise = null;\n\n        if (preloadedPages && preloadedPages.hasOwnProperty(pageNumber)) {\n            // This page has been preloaded so use that rather than load the values\n            // again.\n            eventsPromise = preloadedPages[pageNumber];\n        } else {\n            // Load one more than the given limit so that we can tell if there\n            // is more content to load after this.\n            eventsPromise = load(midnight, limit + 1, daysOffset, daysLimit, lastId, courseId);\n        }\n\n        return eventsPromise.then(function(result) {\n            if (!result.events.length) {\n                // If we didn't get any events back then tell the paged content\n                // that we're done loading.\n                actions.allItemsLoaded(pageNumber);\n                return [];\n            }\n\n            var calendarEvents = result.events.filter(function(event) {\n                if (event.eventtype == \"open\" || event.eventtype == \"opensubmission\") {\n                    var dayTimestamp = UserDate.getUserMidnightForTimestamp(event.timesort, midnight);\n                    return dayTimestamp > midnight;\n                }\n                return true;\n            });\n            // We expect to receive limit + 1 events back from the server.\n            // Any less means there are no more events to load.\n            var loadedAll = calendarEvents.length <= limit;\n\n            if (loadedAll) {\n                // Tell the pagination that everything is loaded.\n                actions.allItemsLoaded(pageNumber);\n            } else {\n                // Remove the last element from the array because it isn't\n                // needed in this result set.\n                calendarEvents.pop();\n            }\n\n            return calendarEvents;\n        });\n    };\n\n    /**\n     * Use the paged content factory to create a paged content element for showing\n     * the event list. We only provide a page limit to the factory because we don't\n     * know exactly how many pages we'll need. This creates a paging bar with just\n     * next/previous buttons.\n     *\n     * This function specifies the callback for loading the event data that the user\n     * is requesting.\n     *\n     * @param {int|array} pageLimit A single limit or list of limits as options for the paged content\n     * @param {object} preloadedPages An object of preloaded page data. Page number as key, data promise as value.\n     * @param {Number} midnight The user's midnight time in unix timestamp.\n     * @param {object} firstLoad A jQuery promise to be resolved after the first set of data is loaded.\n     * @param {int|undefined} courseId Course ID to restrict events to\n     * @param {Number} daysOffset How many days (from midnight) to offset the results from\n     * @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to\n     * @param {string} paginationAriaLabel String to set as the aria label for the pagination bar.\n     * @param {object} additionalConfig Additional config options to pass to pagedContentFactory\n     * @return {object} jQuery promise.\n     */\n    var createPagedContent = function(\n        pageLimit,\n        preloadedPages,\n        midnight,\n        firstLoad,\n        courseId,\n        daysOffset,\n        daysLimit,\n        paginationAriaLabel,\n        additionalConfig\n    ) {\n        // Remember the last event id we loaded on each page because we can't\n        // use the offset value since the backend can skip events if the user doesn't\n        // have the capability to see them. Instead we load the next page of events\n        // based on the last seen event id.\n        var lastIds = {'1': 0};\n        var hasContent = false;\n        var config = $.extend({}, DEFAULT_PAGED_CONTENT_CONFIG, additionalConfig);\n\n        return Str.get_string(\n                'ariaeventlistpagelimit',\n                'block_timeline',\n                $.isArray(pageLimit) ? pageLimit[0].value : pageLimit\n            )\n            .then(function(string) {\n                config.ariaLabels.itemsperpage = string;\n                config.ariaLabels.paginationnav = paginationAriaLabel;\n                return string;\n            })\n            .then(function() {\n                return PagedContentFactory.createWithLimit(\n                    pageLimit,\n                    function(pagesData, actions) {\n                        var promises = [];\n\n                        pagesData.forEach(function(pageData) {\n                            var pageNumber = pageData.pageNumber;\n                            // Load the page data.\n                            var pagePromise = loadEventsFromPageData(\n                                pageData,\n                                actions,\n                                midnight,\n                                lastIds,\n                                preloadedPages,\n                                courseId,\n                                daysOffset,\n                                daysLimit\n                            ).then(function(calendarEvents) {\n                                if (calendarEvents.length) {\n                                    // Remember that we've loaded content.\n                                    hasContent = true;\n                                    // Remember the last id we've seen.\n                                    var lastEventId = calendarEvents[calendarEvents.length - 1].id;\n                                    // Record the id that the next page will need to start from.\n                                    lastIds[pageNumber + 1] = lastEventId;\n                                    // Get the HTML and JS for these calendar events.\n                                    return render(calendarEvents, midnight);\n                                } else {\n                                    return calendarEvents;\n                                }\n                            })\n                            .catch(Notification.exception);\n\n                            promises.push(pagePromise);\n                        });\n\n                        $.when.apply($, promises).then(function() {\n                            // Tell the calling code that the first page has been loaded\n                            // and whether it contains any content.\n                            firstLoad.resolve(hasContent);\n                            return;\n                        })\n                        .catch(function() {\n                            firstLoad.resolve(hasContent);\n                        });\n\n                        return promises;\n                    },\n                    config\n                );\n            });\n    };\n\n    /**\n     * Create a paged content region for the calendar events in the given root element.\n     * The content of the root element are replaced with a new paged content section\n     * each time this function is called.\n     *\n     * This function will be called each time the offset or limit values are changed to\n     * reload the event list region.\n     *\n     * @param {object} root The event list container element\n     * @param {int|array} pageLimit A single limit or list of limits as options for the paged content\n     * @param {object} preloadedPages An object of preloaded page data. Page number as key, data promise as value.\n     * @param {string} paginationAriaLabel String to set as the aria label for the pagination bar.\n     * @param {object} additionalConfig Additional config options to pass to pagedContentFactory\n     */\n    var init = function(root, pageLimit, preloadedPages, paginationAriaLabel, additionalConfig) {\n        root = $(root);\n\n        // Create a promise that will be resolved once the first set of page\n        // data has been loaded. This ensures that the loading placeholder isn't\n        // hidden until we have all of the data back to prevent the page elements\n        // jumping around.\n        var firstLoad = $.Deferred();\n        var eventListContent = root.find(SELECTORS.EVENT_LIST_CONTENT);\n        var loadingPlaceholder = root.find(SELECTORS.EVENT_LIST_LOADING_PLACEHOLDER);\n        var courseId = root.attr('data-course-id');\n        var daysOffset = parseInt(root.attr('data-days-offset'), 10);\n        var daysLimit = root.attr('data-days-limit');\n        var midnight = parseInt(root.attr('data-midnight'), 10);\n\n        // Make sure the content area and loading placeholder is visible.\n        // This is because the init function can be called to re-initialise\n        // an existing event list area.\n        emptyContent(root);\n        showContent(root);\n        loadingPlaceholder.removeClass('hidden');\n\n        // Days limit isn't mandatory.\n        if (daysLimit != undefined) {\n            daysLimit = parseInt(daysLimit, 10);\n        }\n\n        // Created the paged content element.\n        return createPagedContent(pageLimit, preloadedPages, midnight, firstLoad, courseId, daysOffset, daysLimit,\n                paginationAriaLabel, additionalConfig)\n            .then(function(html, js) {\n                html = $(html);\n                // Hide the content for now.\n                html.addClass('hidden');\n                // Replace existing elements with the newly created paged content.\n                // If we're reinitialising an existing event list this will replace\n                // the old event list (including removing any event handlers).\n                Templates.replaceNodeContents(eventListContent, html, js);\n\n                firstLoad.then(function(hasContent) {\n                    // Prevent changing page elements too much by only showing the content\n                    // once we've loaded some data for the first time. This allows our\n                    // fancy loading placeholder to shine.\n                    html.removeClass('hidden');\n                    loadingPlaceholder.addClass('hidden');\n\n                    if (!hasContent) {\n                        // If we didn't get any data then show the empty data message.\n                        hideContent(root);\n                    }\n\n                    return hasContent;\n                })\n                .catch(function() {\n                    return false;\n                });\n\n                return html;\n            })\n            .catch(Notification.exception);\n    };\n\n    return {\n        init: init,\n        rootSelector: SELECTORS.ROOT,\n    };\n});\n"],"file":"event_list.min.js"}
\ No newline at end of file
+{"version":3,"sources":["../src/event_list.js"],"names":["define","$","Notification","Templates","PagedContentFactory","Str","UserDate","CalendarEventsRepository","courseview","SELECTORS","EMPTY_MESSAGE","ROOT","EVENT_LIST_CONTENT","EVENT_LIST_LOADING_PLACEHOLDER","TEMPLATES","DEFAULT_PAGED_CONTENT_CONFIG","ignoreControlWhileLoading","controlPlacementBottom","ariaLabels","itemsperpagecomponents","hideContent","root","find","addClass","removeClass","showContent","emptyContent","empty","buildTemplateContext","calendarEvents","midnight","eventsByDay","templateContext","eventsbyday","forEach","calendarEvent","dayTimestamp","timeusermidnight","push","Object","keys","events","past","render","templateName","load","limit","daysOffset","daysLimit","lastId","courseId","endTime","args","starttime","aftereventid","endtime","courseid","queryByCourse","queryByTime","loadEventsFromPageData","pageData","actions","lastIds","preloadedPages","pageNumber","lastPageNumber","hasOwnProperty","eventsPromise","then","result","length","allItemsLoaded","filter","event","eventtype","getUserMidnightForTimestamp","timesort","loadedAll","pop","createPagedContent","pageLimit","firstLoad","paginationAriaLabel","additionalConfig","hasContent","config","extend","get_string","isArray","value","string","itemsperpage","paginationnav","createWithLimit","pagesData","promises","pagePromise","lastEventId","id","catch","exception","when","apply","resolve","init","Deferred","eventListContent","loadingPlaceholder","attr","parseInt","html","js","replaceNodeContents","rootSelector"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Javascript to load and render the list of calendar events for a\n * given day range.\n *\n * @module     block_timeline/event_list\n * @copyright  2016 Ryan Wyllie <ryan@moodle.com>\n * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(\n[\n    'jquery',\n    'core/notification',\n    'core/templates',\n    'core/paged_content_factory',\n    'core/str',\n    'core/user_date',\n    'block_timeline/calendar_events_repository'\n],\nfunction(\n    $,\n    Notification,\n    Templates,\n    PagedContentFactory,\n    Str,\n    UserDate,\n    CalendarEventsRepository\n) {\n\n    var SECONDS_IN_DAY = 60 * 60 * 24;\n    var courseview = false;\n\n    var SELECTORS = {\n        EMPTY_MESSAGE: '[data-region=\"empty-message\"]',\n        ROOT: '[data-region=\"event-list-container\"]',\n        EVENT_LIST_CONTENT: '[data-region=\"event-list-content\"]',\n        EVENT_LIST_LOADING_PLACEHOLDER: '[data-region=\"event-list-loading-placeholder\"]',\n    };\n\n    var TEMPLATES = {\n        EVENT_LIST_CONTENT: 'block_timeline/event-list-content'\n    };\n\n    // We want the paged content controls below the paged content area\n    // and the controls should be ignored while data is loading.\n    var DEFAULT_PAGED_CONTENT_CONFIG = {\n        ignoreControlWhileLoading: true,\n        controlPlacementBottom: true,\n        ariaLabels: {\n            itemsperpagecomponents: 'ariaeventlistpagelimit, block_timeline',\n        }\n    };\n\n    /**\n     * Hide the content area and display the empty content message.\n     *\n     * @param {object} root The container element\n     */\n    var hideContent = function(root) {\n        root.find(SELECTORS.EVENT_LIST_CONTENT).addClass('hidden');\n        root.find(SELECTORS.EMPTY_MESSAGE).removeClass('hidden');\n    };\n\n    /**\n     * Show the content area and hide the empty content message.\n     *\n     * @param {object} root The container element\n     */\n    var showContent = function(root) {\n        root.find(SELECTORS.EVENT_LIST_CONTENT).removeClass('hidden');\n        root.find(SELECTORS.EMPTY_MESSAGE).addClass('hidden');\n    };\n\n    /**\n     * Empty the content area.\n     *\n     * @param {object} root The container element\n     */\n    var emptyContent = function(root) {\n        root.find(SELECTORS.EVENT_LIST_CONTENT).empty();\n    };\n\n    /**\n     * Construct the template context from a list of calendar events. The events\n     * are grouped by which day they are on. The day is calculated from the user's\n     * midnight timestamp to ensure that the calculation is timezone agnostic.\n     *\n     * The return data structure will look like:\n     * {\n     *      eventsbyday: [\n     *          {\n     *              dayTimestamp: 1533744000,\n     *              events: [\n     *                  { ...event 1 data... },\n     *                  { ...event 2 data... }\n     *              ]\n     *          },\n     *          {\n     *              dayTimestamp: 1533830400,\n     *              events: [\n     *                  { ...event 3 data... },\n     *                  { ...event 4 data... }\n     *              ]\n     *          }\n     *      ]\n     * }\n     *\n     * Each day timestamp is the day's midnight in the user's timezone.\n     *\n     * @param {array} calendarEvents List of calendar events\n     * @param {Number} midnight A timestamp representing midnight in the user's timezone\n     * @return {object}\n     */\n    var buildTemplateContext = function(calendarEvents, midnight) {\n        var eventsByDay = {};\n        var templateContext = {\n            courseview,\n            eventsbyday: []\n        };\n\n        calendarEvents.forEach(function(calendarEvent) {\n            var dayTimestamp = calendarEvent.timeusermidnight;\n            if (eventsByDay[dayTimestamp]) {\n                eventsByDay[dayTimestamp].push(calendarEvent);\n            } else {\n                eventsByDay[dayTimestamp] = [calendarEvent];\n            }\n        });\n\n        Object.keys(eventsByDay).forEach(function(dayTimestamp) {\n            var events = eventsByDay[dayTimestamp];\n            templateContext.eventsbyday.push({\n                past: dayTimestamp < midnight,\n                dayTimestamp: dayTimestamp,\n                events: events\n            });\n        });\n\n        return templateContext;\n    };\n\n    /**\n     * Render the HTML for the given calendar events.\n     *\n     * @param {array} calendarEvents  A list of calendar events\n     * @param {Number} midnight A timestamp representing midnight for the user\n     * @return {promise} Resolved with HTML and JS strings.\n     */\n    var render = function(calendarEvents, midnight) {\n        var templateContext = buildTemplateContext(calendarEvents, midnight);\n        var templateName = TEMPLATES.EVENT_LIST_CONTENT;\n\n        return Templates.render(templateName, templateContext);\n    };\n\n    /**\n     * Retrieve a list of calendar events from the server for the given\n     * constraints.\n     *\n     * @param {Number} midnight The user's midnight time in unix timestamp.\n     * @param {Number} limit Limit the result set to this number of items\n     * @param {Number} daysOffset How many days (from midnight) to offset the results from\n     * @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to\n     * @param {int|false} lastId The ID of the last seen event (if any)\n     * @param {int|undefined} courseId Course ID to restrict events to\n     * @return {Promise} A jquery promise\n     */\n    var load = function(midnight, limit, daysOffset, daysLimit, lastId, courseId) {\n        var startTime = midnight + (daysOffset * SECONDS_IN_DAY);\n        var endTime = daysLimit != undefined ? midnight + (daysLimit * SECONDS_IN_DAY) : false;\n\n        var args = {\n            starttime: startTime,\n            limit: limit,\n        };\n\n        if (lastId) {\n            args.aftereventid = lastId;\n        }\n\n        if (endTime) {\n            args.endtime = endTime;\n        }\n\n        if (courseId) {\n            // If we have a course id then we only want events from that course.\n            args.courseid = courseId;\n            return CalendarEventsRepository.queryByCourse(args);\n        } else {\n            // Otherwise we want events from any course.\n            return CalendarEventsRepository.queryByTime(args);\n        }\n    };\n\n    /**\n     * Handle a single page request from the paged content. Uses the given page data to request\n     * the events from the server.\n     *\n     * Checks the given preloadedPages before sending a request to the server to make sure we\n     * don't load data unnecessarily.\n     *\n     * @param {object} pageData A single page data (see core/paged_content_pages for more info).\n     * @param {object} actions Paged content actions (see core/paged_content_pages for more info).\n     * @param {Number} midnight The user's midnight time in unix timestamp.\n     * @param {object} lastIds The last event ID for each loaded page. Page number is key, id is value.\n     * @param {object} preloadedPages An object of preloaded page data. Page number as key, data promise as value.\n     * @param {int|undefined} courseId Course ID to restrict events to\n     * @param {Number} daysOffset How many days (from midnight) to offset the results from\n     * @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to\n     * @return {object} jQuery promise resolved with calendar events.\n     */\n    var loadEventsFromPageData = function(\n        pageData,\n        actions,\n        midnight,\n        lastIds,\n        preloadedPages,\n        courseId,\n        daysOffset,\n        daysLimit\n    ) {\n        var pageNumber = pageData.pageNumber;\n        var limit = pageData.limit;\n        var lastPageNumber = pageNumber;\n\n        // This is here to protect us if, for some reason, the pages\n        // are loaded out of order somehow and we don't have a reference\n        // to the previous page. In that case, scan back to find the most\n        // recent page we've seen.\n        while (!lastIds.hasOwnProperty(lastPageNumber)) {\n            lastPageNumber--;\n        }\n        // Use the last id of the most recent page.\n        var lastId = lastIds[lastPageNumber];\n        var eventsPromise = null;\n\n        if (preloadedPages && preloadedPages.hasOwnProperty(pageNumber)) {\n            // This page has been preloaded so use that rather than load the values\n            // again.\n            eventsPromise = preloadedPages[pageNumber];\n        } else {\n            // Load one more than the given limit so that we can tell if there\n            // is more content to load after this.\n            eventsPromise = load(midnight, limit + 1, daysOffset, daysLimit, lastId, courseId);\n        }\n\n        return eventsPromise.then(function(result) {\n            if (!result.events.length) {\n                // If we didn't get any events back then tell the paged content\n                // that we're done loading.\n                actions.allItemsLoaded(pageNumber);\n                return [];\n            }\n\n            var calendarEvents = result.events.filter(function(event) {\n                if (event.eventtype == \"open\" || event.eventtype == \"opensubmission\") {\n                    var dayTimestamp = UserDate.getUserMidnightForTimestamp(event.timesort, midnight);\n                    return dayTimestamp > midnight;\n                }\n                return true;\n            });\n            // We expect to receive limit + 1 events back from the server.\n            // Any less means there are no more events to load.\n            var loadedAll = calendarEvents.length <= limit;\n\n            if (loadedAll) {\n                // Tell the pagination that everything is loaded.\n                actions.allItemsLoaded(pageNumber);\n            } else {\n                // Remove the last element from the array because it isn't\n                // needed in this result set.\n                calendarEvents.pop();\n            }\n\n            return calendarEvents;\n        });\n    };\n\n    /**\n     * Use the paged content factory to create a paged content element for showing\n     * the event list. We only provide a page limit to the factory because we don't\n     * know exactly how many pages we'll need. This creates a paging bar with just\n     * next/previous buttons.\n     *\n     * This function specifies the callback for loading the event data that the user\n     * is requesting.\n     *\n     * @param {int|array} pageLimit A single limit or list of limits as options for the paged content\n     * @param {object} preloadedPages An object of preloaded page data. Page number as key, data promise as value.\n     * @param {Number} midnight The user's midnight time in unix timestamp.\n     * @param {object} firstLoad A jQuery promise to be resolved after the first set of data is loaded.\n     * @param {int|undefined} courseId Course ID to restrict events to\n     * @param {Number} daysOffset How many days (from midnight) to offset the results from\n     * @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to\n     * @param {string} paginationAriaLabel String to set as the aria label for the pagination bar.\n     * @param {object} additionalConfig Additional config options to pass to pagedContentFactory\n     * @return {object} jQuery promise.\n     */\n    var createPagedContent = function(\n        pageLimit,\n        preloadedPages,\n        midnight,\n        firstLoad,\n        courseId,\n        daysOffset,\n        daysLimit,\n        paginationAriaLabel,\n        additionalConfig\n    ) {\n        // Remember the last event id we loaded on each page because we can't\n        // use the offset value since the backend can skip events if the user doesn't\n        // have the capability to see them. Instead we load the next page of events\n        // based on the last seen event id.\n        var lastIds = {'1': 0};\n        var hasContent = false;\n        var config = $.extend({}, DEFAULT_PAGED_CONTENT_CONFIG, additionalConfig);\n\n        return Str.get_string(\n                'ariaeventlistpagelimit',\n                'block_timeline',\n                $.isArray(pageLimit) ? pageLimit[0].value : pageLimit\n            )\n            .then(function(string) {\n                config.ariaLabels.itemsperpage = string;\n                config.ariaLabels.paginationnav = paginationAriaLabel;\n                return string;\n            })\n            .then(function() {\n                return PagedContentFactory.createWithLimit(\n                    pageLimit,\n                    function(pagesData, actions) {\n                        var promises = [];\n\n                        pagesData.forEach(function(pageData) {\n                            var pageNumber = pageData.pageNumber;\n                            // Load the page data.\n                            var pagePromise = loadEventsFromPageData(\n                                pageData,\n                                actions,\n                                midnight,\n                                lastIds,\n                                preloadedPages,\n                                courseId,\n                                daysOffset,\n                                daysLimit\n                            ).then(function(calendarEvents) {\n                                if (calendarEvents.length) {\n                                    // Remember that we've loaded content.\n                                    hasContent = true;\n                                    // Remember the last id we've seen.\n                                    var lastEventId = calendarEvents[calendarEvents.length - 1].id;\n                                    // Record the id that the next page will need to start from.\n                                    lastIds[pageNumber + 1] = lastEventId;\n                                    // Get the HTML and JS for these calendar events.\n                                    return render(calendarEvents, midnight);\n                                } else {\n                                    return calendarEvents;\n                                }\n                            })\n                            .catch(Notification.exception);\n\n                            promises.push(pagePromise);\n                        });\n\n                        $.when.apply($, promises).then(function() {\n                            // Tell the calling code that the first page has been loaded\n                            // and whether it contains any content.\n                            firstLoad.resolve(hasContent);\n                            return;\n                        })\n                        .catch(function() {\n                            firstLoad.resolve(hasContent);\n                        });\n\n                        return promises;\n                    },\n                    config\n                );\n            });\n    };\n\n    /**\n     * Create a paged content region for the calendar events in the given root element.\n     * The content of the root element are replaced with a new paged content section\n     * each time this function is called.\n     *\n     * This function will be called each time the offset or limit values are changed to\n     * reload the event list region.\n     *\n     * @param {object} root The event list container element\n     * @param {int|array} pageLimit A single limit or list of limits as options for the paged content\n     * @param {object} preloadedPages An object of preloaded page data. Page number as key, data promise as value.\n     * @param {string} paginationAriaLabel String to set as the aria label for the pagination bar.\n     * @param {object} additionalConfig Additional config options to pass to pagedContentFactory\n     */\n    var init = function(root, pageLimit, preloadedPages, paginationAriaLabel, additionalConfig = {}) {\n        root = $(root);\n\n        courseview = !!additionalConfig.courseview;\n\n        // Create a promise that will be resolved once the first set of page\n        // data has been loaded. This ensures that the loading placeholder isn't\n        // hidden until we have all of the data back to prevent the page elements\n        // jumping around.\n        var firstLoad = $.Deferred();\n        var eventListContent = root.find(SELECTORS.EVENT_LIST_CONTENT);\n        var loadingPlaceholder = root.find(SELECTORS.EVENT_LIST_LOADING_PLACEHOLDER);\n        var courseId = root.attr('data-course-id');\n        var daysOffset = parseInt(root.attr('data-days-offset'), 10);\n        var daysLimit = root.attr('data-days-limit');\n        var midnight = parseInt(root.attr('data-midnight'), 10);\n\n        // Make sure the content area and loading placeholder is visible.\n        // This is because the init function can be called to re-initialise\n        // an existing event list area.\n        emptyContent(root);\n        showContent(root);\n        loadingPlaceholder.removeClass('hidden');\n\n        // Days limit isn't mandatory.\n        if (daysLimit != undefined) {\n            daysLimit = parseInt(daysLimit, 10);\n        }\n\n        // Created the paged content element.\n        return createPagedContent(pageLimit, preloadedPages, midnight, firstLoad, courseId, daysOffset, daysLimit,\n                paginationAriaLabel, additionalConfig)\n            .then(function(html, js) {\n                html = $(html);\n                // Hide the content for now.\n                html.addClass('hidden');\n                // Replace existing elements with the newly created paged content.\n                // If we're reinitialising an existing event list this will replace\n                // the old event list (including removing any event handlers).\n                Templates.replaceNodeContents(eventListContent, html, js);\n\n                firstLoad.then(function(hasContent) {\n                    // Prevent changing page elements too much by only showing the content\n                    // once we've loaded some data for the first time. This allows our\n                    // fancy loading placeholder to shine.\n                    html.removeClass('hidden');\n                    loadingPlaceholder.addClass('hidden');\n\n                    if (!hasContent) {\n                        // If we didn't get any data then show the empty data message.\n                        hideContent(root);\n                    }\n\n                    return hasContent;\n                })\n                .catch(function() {\n                    return false;\n                });\n\n                return html;\n            })\n            .catch(Notification.exception);\n    };\n\n    return {\n        init: init,\n        rootSelector: SELECTORS.ROOT,\n    };\n});\n"],"file":"event_list.min.js"}
\ No newline at end of file
diff --git a/blocks/timeline/amd/build/view_courses.min.js b/blocks/timeline/amd/build/view_courses.min.js
index 70345b12123..da21e1a23e4 100644
--- a/blocks/timeline/amd/build/view_courses.min.js
+++ b/blocks/timeline/amd/build/view_courses.min.js
@@ -1,2 +1,2 @@
-define ("block_timeline/view_courses",["jquery","core/notification","core/custom_interaction_events","core/str","core/templates","block_timeline/event_list","core_course/repository","block_timeline/calendar_events_repository"],function(a,b,c,d,e,f,g,h){var i={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"},j={COURSE_ITEMS:"block_timeline/course-items",LOADING_ICON:"core/loading"},k=5,l=86400,m=function(a){a.find(i.COURSE_ITEMS_LOADING_PLACEHOLDER).addClass("hidden")},n=function(a){a.find(i.MORE_COURSES_BUTTON_CONTAINER).addClass("hidden")},o=function(a){a.find(i.MORE_COURSES_BUTTON_CONTAINER).removeClass("hidden")},p=function(a){var b=a.find(i.MORE_COURSES_BUTTON);b.prop("disabled",!0);e.render(j.LOADING_ICON,{}).then(function(a){b.append(a);return a}).catch(function(){return!1})},q=function(a){var b=a.find(i.MORE_COURSES_BUTTON);b.prop("disabled",!1);b.find(i.LOADING_ICON).remove()},r=function(a){a.find(i.NO_COURSES_EMPTY_MESSAGE).removeClass("hidden")},s=function(a,b){var c=a.find(i.COURSES_LIST);e.appendNodeContents(c,b,"")},t=function(a){return 0<a.find(i.COURSE_EVENTS_CONTAINER).length},u=function(a){return parseInt(a.attr("data-offset"),10)},v=function(a,b){a.attr("data-offset",b)},w=function(a){return parseInt(a.attr("data-limit"),10)},x=function(a){return parseInt(a.attr("data-days-offset"),10)},y=function(a){var b=a.attr("data-days-limit");return b!=void 0?parseInt(b,10):void 0},z=function(a){return parseInt(a.attr("data-midnight"),10)},A=function(a){var b=z(a),c=x(a);return b+c*l},B=function(a){var b=z(a),c=y(a);return c!=void 0?b+c*l:!1},C=function(a,b,c,d){var e={courseids:a,starttime:b,limit:c};if(d){e.endtime=d}return h.queryByCourses(e)},D=function(a){return a.data("last-event-load-time")},E=function(a,b){a.data("last-event-load-time",b)},F=function(a,b){return D(a)>b},G=function(a,b,c){var d=a.map(function(a){return a.id});return C(d,b,k+1,c)},H=function(a,b,c,d,f,g){return e.render(j.COURSE_ITEMS,{courses:a,midnight:c,hasdaysoffset:!0,hasdayslimit:f!=void 0,daysoffset:d,dayslimit:f,nodayslimit:f==void 0,urls:{noevents:g}}).then(function(a){m(b);if(a){s(b,a)}else{if(!t(b)){r(b)}}return a}).then(function(c){if(a.length<2){n(b)}else{o(b)}return c}).catch(function(){m(b)})},I=function(c){var e=u(c),h=w(c);return g.getEnrolledCoursesByTimelineClassification("inprogress",h,e,"fullname asc").then(function(b){var e=Date.now(),g=b.courses,h=b.nextoffset,i=x(c),j=y(c),l=z(c),m=A(c),n=B(c),o=c.attr("data-no-events-url");v(c,h);var p=G(g,m,n),q=H(g,c,l,i,j,o);return a.when(p,q).then(function(b){if(F(c,e)){return b}g.forEach(function(e){var g=e.id,h=[],i=c.find("[data-region=\"course-events-container\"][data-course-id=\""+g+"\"]"),j=i.find(f.rootSelector),l=b.groupedbycourse.filter(function(a){return a.courseid==g});if(l.length){h=l[0].events}var m=a.Deferred().resolve({events:h}).promise();d.get_string("ariaeventlistpaginationnavcourses","block_timeline",e.fullnamedisplay).then(function(a){f.init(j,k,{1:m},a);return a}).catch(function(){f.init(j,k,{1:m})})});return b})}).catch(b.exception)},J=function(c){var e=Date.now(),g=A(c),h=B(c),j=c.find(i.COURSE_EVENTS_CONTAINER),l=j.map(function(){return a(this).attr("data-course-id")}).get();E(c,e);return C(l,g,k+1,h).then(function(b){if(F(c,e)){return b}j.each(function(c,e){e=a(e);var g=e.attr("data-course-id"),h=e.find(i.COURSE_NAME).text(),j=e.find(f.rootSelector),l=a.Deferred(),m=[],n=b.groupedbycourse.filter(function(a){return a.courseid==g});if(n.length){m=n[0].events}l.resolve({events:m});d.get_string("ariaeventlistpaginationnavcourses","block_timeline",h).then(function(a){f.init(j,k,{1:l.promise()},a);return a}).catch(function(){f.init(j,k,{1:l.promise()})})});return b}).catch(b.exception)},K=function(a){c.define(a,[c.events.activate]);a.on(c.events.activate,i.MORE_COURSES_BUTTON,function(b,c){p(a);I(a).then(function(){q(a)}).catch(function(){q(a)});if(c){c.originalEvent.preventDefault();c.originalEvent.stopPropagation()}b.stopPropagation()})},L=function(a){if(!a.attr("data-seen")){if(t(a)){J(a)}else{I(a)}a.attr("data-seen",!0)}};return{init:function init(b){b=a(b);E(b,Date.now());if(b.hasClass("active")){I(b);b.attr("data-seen",!0)}K(b)},reset:function reset(a){a.removeAttr("data-seen");if(a.hasClass("active")){L(a)}},shown:L}});
+define ("block_timeline/view_courses",["jquery","core/notification","core/custom_interaction_events","core/str","core/templates","block_timeline/event_list","core_course/repository","block_timeline/calendar_events_repository"],function(a,b,c,d,e,f,g,h){var i={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"},j={COURSE_ITEMS:"block_timeline/course-items",LOADING_ICON:"core/loading"},k=5,l=86400,m=function(a){a.find(i.COURSE_ITEMS_LOADING_PLACEHOLDER).addClass("hidden")},n=function(a){a.find(i.MORE_COURSES_BUTTON_CONTAINER).addClass("hidden")},o=function(a){a.find(i.MORE_COURSES_BUTTON_CONTAINER).removeClass("hidden")},p=function(a){var b=a.find(i.MORE_COURSES_BUTTON);b.prop("disabled",!0);e.render(j.LOADING_ICON,{}).then(function(a){b.append(a);return a}).catch(function(){return!1})},q=function(a){var b=a.find(i.MORE_COURSES_BUTTON);b.prop("disabled",!1);b.find(i.LOADING_ICON).remove()},r=function(a){a.find(i.NO_COURSES_EMPTY_MESSAGE).removeClass("hidden")},s=function(a,b){var c=a.find(i.COURSES_LIST);e.appendNodeContents(c,b,"")},t=function(a){return 0<a.find(i.COURSE_EVENTS_CONTAINER).length},u=function(a){return parseInt(a.attr("data-offset"),10)},v=function(a,b){a.attr("data-offset",b)},w=function(a){return parseInt(a.attr("data-limit"),10)},x=function(a){return parseInt(a.attr("data-days-offset"),10)},y=function(a){var b=a.attr("data-days-limit");return b!=void 0?parseInt(b,10):void 0},z=function(a){return parseInt(a.attr("data-midnight"),10)},A=function(a){var b=z(a),c=x(a);return b+c*l},B=function(a){var b=z(a),c=y(a);return c!=void 0?b+c*l:!1},C=function(a,b,c,d){var e={courseids:a,starttime:b,limit:c};if(d){e.endtime=d}return h.queryByCourses(e)},D=function(a){return a.data("last-event-load-time")},E=function(a,b){a.data("last-event-load-time",b)},F=function(a,b){return D(a)>b},G=function(a,b,c){var d=a.map(function(a){return a.id});return C(d,b,k+1,c)},H=function(a,b,c,d,f,g){return e.render(j.COURSE_ITEMS,{courses:a,midnight:c,hasdaysoffset:!0,hasdayslimit:f!=void 0,daysoffset:d,dayslimit:f,nodayslimit:f==void 0,urls:{noevents:g}}).then(function(a){m(b);if(a){s(b,a)}else{if(!t(b)){r(b)}}return a}).then(function(c){if(a.length<2){n(b)}else{o(b)}return c}).catch(function(){m(b)})},I=function(c){var e=u(c),h=w(c);return g.getEnrolledCoursesByTimelineClassification("inprogress",h,e,"fullname asc").then(function(b){var e=Date.now(),g=b.courses,h=b.nextoffset,i=x(c),j=y(c),l=z(c),m=A(c),n=B(c),o=c.attr("data-no-events-url");v(c,h);var p=G(g,m,n),q=H(g,c,l,i,j,o);return a.when(p,q).then(function(b){if(F(c,e)){return b}g.forEach(function(e){var g=e.id,h=[],i=c.find("[data-region=\"course-events-container\"][data-course-id=\""+g+"\"]"),j=i.find(f.rootSelector),l=b.groupedbycourse.filter(function(a){return a.courseid==g});if(l.length){h=l[0].events}var m=a.Deferred().resolve({events:h}).promise();d.get_string("ariaeventlistpaginationnavcourses","block_timeline",e.fullnamedisplay).then(function(a){f.init(j,k,{1:m},a);return a}).catch(function(){f.init(j,k,{1:m})})});return b})}).catch(b.exception)},J=function(c){var e=Date.now(),g=A(c),h=B(c),j=c.find(i.COURSE_EVENTS_CONTAINER),l=j.map(function(){return a(this).attr("data-course-id")}).get();E(c,e);return C(l,g,k+1,h).then(function(b){if(F(c,e)){return b}j.each(function(c,e){e=a(e);var g=e.attr("data-course-id"),h=e.find(i.COURSE_NAME).text(),j=e.find(f.rootSelector),l=a.Deferred(),m=[],n=b.groupedbycourse.filter(function(a){return a.courseid==g});if(n.length){m=n[0].events}l.resolve({events:m});var o={courseview:!0};d.get_string("ariaeventlistpaginationnavcourses","block_timeline",h).then(function(a){f.init(j,k,{1:l.promise()},a,o);return a}).catch(function(){f.init(j,k,{1:l.promise()},void 0,o)})});return b}).catch(b.exception)},K=function(a){c.define(a,[c.events.activate]);a.on(c.events.activate,i.MORE_COURSES_BUTTON,function(b,c){p(a);I(a).then(function(){q(a)}).catch(function(){q(a)});if(c){c.originalEvent.preventDefault();c.originalEvent.stopPropagation()}b.stopPropagation()})},L=function(a){if(!a.attr("data-seen")){if(t(a)){J(a)}else{I(a)}a.attr("data-seen",!0)}};return{init:function init(b){b=a(b);E(b,Date.now());if(b.hasClass("active")){I(b);b.attr("data-seen",!0)}K(b)},reset:function reset(a){a.removeAttr("data-seen");if(a.hasClass("active")){L(a)}},shown:L}});
 //# sourceMappingURL=view_courses.min.js.map
diff --git a/blocks/timeline/amd/build/view_courses.min.js.map b/blocks/timeline/amd/build/view_courses.min.js.map
index dcf3ab3540b..48c87297bb1 100644
--- a/blocks/timeline/amd/build/view_courses.min.js.map
+++ b/blocks/timeline/amd/build/view_courses.min.js.map
@@ -1 +1 @@
-{"version":3,"sources":["../src/view_courses.js"],"names":["define","$","Notification","CustomEvents","Str","Templates","EventList","CourseRepository","EventsRepository","SELECTORS","MORE_COURSES_BUTTON","MORE_COURSES_BUTTON_CONTAINER","NO_COURSES_EMPTY_MESSAGE","COURSES_LIST","COURSE_ITEMS_LOADING_PLACEHOLDER","COURSE_EVENTS_CONTAINER","COURSE_NAME","LOADING_ICON","TEMPLATES","COURSE_ITEMS","COURSE_EVENT_LIMIT","SECONDS_IN_DAY","hideLoadingPlaceholder","root","find","addClass","hideMoreCoursesButton","showMoreCoursesButton","removeClass","enableMoreCoursesButtonLoading","button","prop","render","then","html","append","catch","disableMoreCoursesButtonLoading","remove","showNoCoursesEmptyMessage","renderCourseItemsHTML","container","appendNodeContents","hasLoadedCourses","length","getOffset","parseInt","attr","setOffset","offset","getLimit","getDaysOffset","getDaysLimit","daysLimit","getMidnight","getStartTime","midnight","daysOffset","getEndTime","getEventsForCourseIds","courseIds","startTime","limit","endTime","args","courseids","starttime","endtime","queryByCourses","getEventReloadTime","data","setEventReloadTime","time","hasReloadedEventsSince","loadEventsForCourses","courses","map","course","id","updateDisplayFromCourses","noEventsURL","hasdaysoffset","hasdayslimit","daysoffset","dayslimit","nodayslimit","urls","noevents","loadMoreCourses","getEnrolledCoursesByTimelineClassification","result","startEventLoadingTime","Date","now","nextOffset","nextoffset","eventsPromise","renderPromise","when","eventsByCourse","forEach","courseId","events","courseEventsContainer","eventListRoot","rootSelector","courseGroups","groupedbycourse","filter","group","courseid","pageOnePreload","Deferred","resolve","promise","get_string","fullnamedisplay","string","init","exception","reloadCourseEvents","startReloadTime","courseEventsContainers","get","each","index","courseName","text","eventListContainer","pageDeferred","registerEventListeners","activate","on","e","originalEvent","preventDefault","stopPropagation","shown","hasClass","reset","removeAttr"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Manage the timeline courses view for the timeline block.\n *\n * @copyright  2018 Ryan Wyllie <ryan@moodle.com>\n * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(\n[\n    'jquery',\n    'core/notification',\n    'core/custom_interaction_events',\n    'core/str',\n    'core/templates',\n    'block_timeline/event_list',\n    'core_course/repository',\n    'block_timeline/calendar_events_repository'\n],\nfunction(\n    $,\n    Notification,\n    CustomEvents,\n    Str,\n    Templates,\n    EventList,\n    CourseRepository,\n    EventsRepository\n) {\n\n    var SELECTORS = {\n        MORE_COURSES_BUTTON: '[data-action=\"more-courses\"]',\n        MORE_COURSES_BUTTON_CONTAINER: '[data-region=\"more-courses-button-container\"]',\n        NO_COURSES_EMPTY_MESSAGE: '[data-region=\"no-courses-empty-message\"]',\n        COURSES_LIST: '[data-region=\"courses-list\"]',\n        COURSE_ITEMS_LOADING_PLACEHOLDER: '[data-region=\"course-items-loading-placeholder\"]',\n        COURSE_EVENTS_CONTAINER: '[data-region=\"course-events-container\"]',\n        COURSE_NAME: '[data-region=\"course-name\"]',\n        LOADING_ICON: '.loading-icon'\n    };\n\n    var TEMPLATES = {\n        COURSE_ITEMS: 'block_timeline/course-items',\n        LOADING_ICON: 'core/loading'\n    };\n\n    var COURSE_CLASSIFICATION = 'inprogress';\n    var COURSE_SORT = 'fullname asc';\n    var COURSE_EVENT_LIMIT = 5;\n    var COURSE_LIMIT = 2;\n    var SECONDS_IN_DAY = 60 * 60 * 24;\n\n    /**\n     * Hide the loading placeholder elements.\n     *\n     * @param {object} root The rool element.\n     */\n    var hideLoadingPlaceholder = function(root) {\n        root.find(SELECTORS.COURSE_ITEMS_LOADING_PLACEHOLDER).addClass('hidden');\n    };\n\n    /**\n     * Hide the \"more courses\" button.\n     *\n     * @param {object} root The rool element.\n     */\n    var hideMoreCoursesButton = function(root) {\n        root.find(SELECTORS.MORE_COURSES_BUTTON_CONTAINER).addClass('hidden');\n    };\n\n    /**\n     * Show the \"more courses\" button.\n     *\n     * @param {object} root The rool element.\n     */\n    var showMoreCoursesButton = function(root) {\n        root.find(SELECTORS.MORE_COURSES_BUTTON_CONTAINER).removeClass('hidden');\n    };\n\n    /**\n     * Disable the \"more courses\" button and show the loading spinner.\n     *\n     * @param {object} root The rool element.\n     */\n    var enableMoreCoursesButtonLoading = function(root) {\n        var button = root.find(SELECTORS.MORE_COURSES_BUTTON);\n        button.prop('disabled', true);\n        Templates.render(TEMPLATES.LOADING_ICON, {})\n            .then(function(html) {\n                button.append(html);\n                return html;\n            })\n            .catch(function() {\n                // It's not important if this false so just do so silently.\n                return false;\n            });\n    };\n\n    /**\n     * Enable the \"more courses\" button and remove the loading spinner.\n     *\n     * @param {object} root The rool element.\n     */\n    var disableMoreCoursesButtonLoading = function(root) {\n        var button = root.find(SELECTORS.MORE_COURSES_BUTTON);\n        button.prop('disabled', false);\n        button.find(SELECTORS.LOADING_ICON).remove();\n    };\n\n    /**\n     * Display the message for when there are no courses available.\n     *\n     * @param {object} root The rool element.\n     */\n    var showNoCoursesEmptyMessage = function(root) {\n        root.find(SELECTORS.NO_COURSES_EMPTY_MESSAGE).removeClass('hidden');\n    };\n\n    /**\n     * Render the course items HTML to the page.\n     *\n     * @param {object} root The rool element.\n     * @param {string} html The course items HTML to render.\n     */\n    var renderCourseItemsHTML = function(root, html) {\n        var container = root.find(SELECTORS.COURSES_LIST);\n        Templates.appendNodeContents(container, html, '');\n    };\n\n    /**\n     * Check if any courses have been loaded.\n     *\n     * @param {object} root The rool element.\n     * @return {bool}\n     */\n    var hasLoadedCourses = function(root) {\n        return root.find(SELECTORS.COURSE_EVENTS_CONTAINER).length > 0;\n    };\n\n    /**\n     * Return the offset value for fetching courses.\n     *\n     * @param {object} root The rool element.\n     * @return {Number}\n     */\n    var getOffset = function(root) {\n        return parseInt(root.attr('data-offset'), 10);\n    };\n\n    /**\n     * Set the offset value for fetching courses.\n     *\n     * @param {object} root The rool element.\n     * @param {Number} offset Offset value.\n     */\n    var setOffset = function(root, offset) {\n        root.attr('data-offset', offset);\n    };\n\n    /**\n     * Return the limit value for fetching courses.\n     *\n     * @param {object} root The rool element.\n     * @return {Number}\n     */\n    var getLimit = function(root) {\n        return parseInt(root.attr('data-limit'), 10);\n    };\n\n    /**\n     * Return the days offset value for fetching events.\n     *\n     * @param {object} root The rool element.\n     * @return {Number}\n     */\n    var getDaysOffset = function(root) {\n        return parseInt(root.attr('data-days-offset'), 10);\n    };\n\n    /**\n     * Return the days limit value for fetching events. The days\n     * limit is optional so undefined will be returned if it isn't\n     * set.\n     *\n     * @param {object} root The rool element.\n     * @return {int|undefined}\n     */\n    var getDaysLimit = function(root) {\n        var daysLimit = root.attr('data-days-limit');\n        return daysLimit != undefined ? parseInt(daysLimit, 10) : undefined;\n    };\n\n    /**\n     * Return the timestamp for the user's midnight.\n     *\n     * @param {object} root The rool element.\n     * @return {Number}\n     */\n    var getMidnight = function(root) {\n        return parseInt(root.attr('data-midnight'), 10);\n    };\n\n    /**\n     * Return the start time for fetching events. This is calculated\n     * based on the user's midnight value so that timezones are\n     * preserved.\n     *\n     * @param {object} root The rool element.\n     * @return {Number}\n     */\n    var getStartTime = function(root) {\n        var midnight = getMidnight(root);\n        var daysOffset = getDaysOffset(root);\n        return midnight + (daysOffset * SECONDS_IN_DAY);\n    };\n\n    /**\n     * Return the end time for fetching events. This is calculated\n     * based on the user's midnight value so that timezones are\n     * preserved.\n     *\n     * @param {object} root The rool element.\n     * @return {Number}\n     */\n    var getEndTime = function(root) {\n        var midnight = getMidnight(root);\n        var daysLimit = getDaysLimit(root);\n        return daysLimit != undefined ? midnight + (daysLimit * SECONDS_IN_DAY) : false;\n    };\n\n    /**\n     * Get a list of events for the given course ids. Returns a promise that will\n     * be resolved with the events.\n     *\n     * @param {array} courseIds The list of course ids to fetch events for.\n     * @param {Number} startTime Timestamp to fetch events from.\n     * @param {Number} limit Limit to the number of events (this applies per course, not total)\n     * @param {Number} endTime Timestamp to fetch events to.\n     * @return {object} jQuery promise.\n     */\n    var getEventsForCourseIds = function(courseIds, startTime, limit, endTime) {\n        var args = {\n            courseids: courseIds,\n            starttime: startTime,\n            limit: limit\n        };\n\n        if (endTime) {\n            args.endtime = endTime;\n        }\n\n        return EventsRepository.queryByCourses(args);\n    };\n\n    /**\n     * Get the last time the events were reloaded.\n     *\n     * @param {object} root The rool element.\n     * @return {Number}\n     */\n    var getEventReloadTime = function(root) {\n        return root.data('last-event-load-time');\n    };\n\n    /**\n     * Set the last time the events were reloaded.\n     *\n     * @param {object} root The rool element.\n     * @param {Number} time Timestamp in milliseconds.\n     */\n    var setEventReloadTime = function(root, time) {\n        root.data('last-event-load-time', time);\n    };\n\n    /**\n     * Check if events have begun reloading since the given\n     * time.\n     *\n     * @param {object} root The rool element.\n     * @param {Number} time Timestamp in milliseconds.\n     * @return {bool}\n     */\n    var hasReloadedEventsSince = function(root, time) {\n        return getEventReloadTime(root) > time;\n    };\n\n    /**\n     * Send a request to the server to load the events for the courses.\n     *\n     * @param {array} courses List of course objects.\n     * @param {Number} startTime Timestamp to load events after.\n     * @param {int|undefined} endTime Timestamp to load events up until.\n     * @return {object} jQuery promise resolved with the events.\n     */\n    var loadEventsForCourses = function(courses, startTime, endTime) {\n        var courseIds = courses.map(function(course) {\n            return course.id;\n        });\n\n        return getEventsForCourseIds(courseIds, startTime, COURSE_EVENT_LIMIT + 1, endTime);\n    };\n\n    /**\n     * Render the courses in the DOM once the server has returned the courses.\n     *\n     * @param {array} courses List of course objects.\n     * @param {object} root The root element\n     * @param {Number} midnight The midnight timestamp in the user's timezone.\n     * @param {Number} daysOffset Number of days from today to offset the events.\n     * @param {Number} daysLimit Number of days from today to limit the events to.\n     * @param {string} noEventsURL URL for the image to display for no events.\n     * @return {object} jQuery promise resolved after rendering is complete.\n     */\n    var updateDisplayFromCourses = function(courses, root, midnight, daysOffset, daysLimit, noEventsURL) {\n        // Render the courses template.\n        return Templates.render(TEMPLATES.COURSE_ITEMS, {\n            courses: courses,\n            midnight: midnight,\n            hasdaysoffset: true,\n            hasdayslimit: daysLimit != undefined,\n            daysoffset: daysOffset,\n            dayslimit: daysLimit,\n            nodayslimit: daysLimit == undefined,\n            urls: {\n                noevents: noEventsURL\n            }\n        }).then(function(html) {\n            hideLoadingPlaceholder(root);\n\n            if (html) {\n                // Template rendering is complete and we have the HTML so we can\n                // add it to the DOM.\n                renderCourseItemsHTML(root, html);\n            } else {\n                if (!hasLoadedCourses(root)) {\n                    // There were no courses to render so show the empty placeholder\n                    // message for the user to tell them.\n                    showNoCoursesEmptyMessage(root);\n                }\n            }\n\n            return html;\n        })\n        .then(function(html) {\n            if (courses.length < COURSE_LIMIT) {\n                // We know there aren't any more courses because we got back less\n                // than we asked for so hide the button to request more.\n                hideMoreCoursesButton(root);\n            } else {\n                // Make sure the button is visible if there are more courses to load.\n                showMoreCoursesButton(root);\n            }\n\n            return html;\n        })\n        .catch(function() {\n            hideLoadingPlaceholder(root);\n        });\n    };\n\n    /**\n     * Find all of the visible course blocks and initialise the event\n     * list module to being loading the events for the course block.\n     *\n     * @param {object} root The root element for the timeline courses view.\n     * @return {object} jQuery promise resolved with courses and events.\n     */\n    var loadMoreCourses = function(root) {\n        var offset = getOffset(root);\n        var limit = getLimit(root);\n\n        // Start loading the next set of courses.\n        return CourseRepository.getEnrolledCoursesByTimelineClassification(\n            COURSE_CLASSIFICATION,\n            limit,\n            offset,\n            COURSE_SORT\n        ).then(function(result) {\n            var startEventLoadingTime = Date.now();\n            var courses = result.courses;\n            var nextOffset = result.nextoffset;\n            var daysOffset = getDaysOffset(root);\n            var daysLimit = getDaysLimit(root);\n            var midnight = getMidnight(root);\n            var startTime = getStartTime(root);\n            var endTime = getEndTime(root);\n            var noEventsURL = root.attr('data-no-events-url');\n            // Record the next offset if we want to request more courses.\n            setOffset(root, nextOffset);\n            // Load the events for these courses.\n            var eventsPromise = loadEventsForCourses(courses, startTime, endTime);\n            // Render the courses in the DOM.\n            var renderPromise = updateDisplayFromCourses(courses, root, midnight, daysOffset, daysLimit, noEventsURL);\n\n            return $.when(eventsPromise, renderPromise)\n                .then(function(eventsByCourse) {\n                    if (hasReloadedEventsSince(root, startEventLoadingTime)) {\n                        // All of the events are being reloaded so ignore our results.\n                        return eventsByCourse;\n                    }\n\n                    // When we've got all of the courses and events we can render the events in the\n                    // correct course event list.\n                    courses.forEach(function(course) {\n                        var courseId = course.id;\n                        var events = [];\n                        var containerSelector = '[data-region=\"course-events-container\"][data-course-id=\"' + courseId + '\"]';\n                        var courseEventsContainer = root.find(containerSelector);\n                        var eventListRoot = courseEventsContainer.find(EventList.rootSelector);\n                        var courseGroups = eventsByCourse.groupedbycourse.filter(function(group) {\n                            return group.courseid == courseId;\n                        });\n\n                        if (courseGroups.length) {\n                            // Get the events for this course.\n                            events = courseGroups[0].events;\n                        }\n\n                        // Create a preloaded page to pass to the event list because we've already\n                        // loaded the first page of events.\n                        var pageOnePreload = $.Deferred().resolve({events: events}).promise();\n                        // Initialise the event list pagination area for this course.\n                        Str.get_string('ariaeventlistpaginationnavcourses', 'block_timeline', course.fullnamedisplay)\n                            .then(function(string) {\n                                EventList.init(eventListRoot, COURSE_EVENT_LIMIT, {'1': pageOnePreload}, string);\n                                return string;\n                            })\n                            .catch(function() {\n                                // An error is ok, just render with the default string.\n                                EventList.init(eventListRoot, COURSE_EVENT_LIMIT, {'1': pageOnePreload});\n                            });\n                    });\n\n                    return eventsByCourse;\n                });\n        }).catch(Notification.exception);\n    };\n\n    /**\n     * Reload the events for all of the visible courses. These events will be loaded\n     * in a single request to the server.\n     *\n     * @param {object} root The root element.\n     * @return {object} jQuery promise resolved with courses and events.\n     */\n    var reloadCourseEvents = function(root) {\n        var startReloadTime = Date.now();\n        var startTime = getStartTime(root);\n        var endTime = getEndTime(root);\n        var courseEventsContainers = root.find(SELECTORS.COURSE_EVENTS_CONTAINER);\n        var courseIds = courseEventsContainers.map(function() {\n            return $(this).attr('data-course-id');\n        }).get();\n\n        // Record when we started our request.\n        setEventReloadTime(root, startReloadTime);\n\n        // Load all of the events for the given courses.\n        return getEventsForCourseIds(courseIds, startTime, COURSE_EVENT_LIMIT + 1, endTime)\n            .then(function(eventsByCourse) {\n                if (hasReloadedEventsSince(root, startReloadTime)) {\n                    // A new reload has begun so ignore our results.\n                    return eventsByCourse;\n                }\n\n                courseEventsContainers.each(function(index, container) {\n                    container = $(container);\n                    var courseId = container.attr('data-course-id');\n                    var courseName = container.find(SELECTORS.COURSE_NAME).text();\n                    var eventListContainer = container.find(EventList.rootSelector);\n                    var pageDeferred = $.Deferred();\n                    var events = [];\n                    var courseGroups = eventsByCourse.groupedbycourse.filter(function(group) {\n                        return group.courseid == courseId;\n                    });\n\n                    if (courseGroups.length) {\n                        // Get the events just for this course.\n                        events = courseGroups[0].events;\n                    }\n\n                    pageDeferred.resolve({events: events});\n\n                    // Re-initialise the events list with the preloaded events we just got from\n                    // the server.\n                    Str.get_string('ariaeventlistpaginationnavcourses', 'block_timeline', courseName)\n                        .then(function(string) {\n                            EventList.init(eventListContainer, COURSE_EVENT_LIMIT, {'1': pageDeferred.promise()}, string);\n                            return string;\n                        })\n                        .catch(function() {\n                            // Ignore a failure to load the string. Just render with the default string.\n                            EventList.init(eventListContainer, COURSE_EVENT_LIMIT, {'1': pageDeferred.promise()});\n                        });\n                });\n\n                return eventsByCourse;\n            }).catch(Notification.exception);\n    };\n\n    /**\n     * Add event listeners to load more courses for the courses view.\n     *\n     * @param {object} root The root element for the timeline courses view.\n     */\n    var registerEventListeners = function(root) {\n        CustomEvents.define(root, [CustomEvents.events.activate]);\n        // Show more courses and load their events when the user clicks the \"more courses\"\n        // button.\n        root.on(CustomEvents.events.activate, SELECTORS.MORE_COURSES_BUTTON, function(e, data) {\n            enableMoreCoursesButtonLoading(root);\n            loadMoreCourses(root)\n                .then(function() {\n                    disableMoreCoursesButtonLoading(root);\n                    return;\n                })\n                .catch(function() {\n                    disableMoreCoursesButtonLoading(root);\n                });\n\n            if (data) {\n                data.originalEvent.preventDefault();\n                data.originalEvent.stopPropagation();\n            }\n            e.stopPropagation();\n        });\n    };\n\n    /**\n     * Initialise the timeline courses view. Begin loading the events\n     * if this view is active. Add the relevant event listeners.\n     *\n     * This function should only be called once per page load because it\n     * is adding event listeners to the page.\n     *\n     * @param {object} root The root element for the timeline courses view.\n     */\n    var init = function(root) {\n        root = $(root);\n\n        setEventReloadTime(root, Date.now());\n\n        if (root.hasClass('active')) {\n            // Only load if this is active otherwise it will be lazy loaded later.\n            loadMoreCourses(root);\n            root.attr('data-seen', true);\n        }\n\n        registerEventListeners(root);\n    };\n\n    /**\n     * Reset the element back to it's initial state. Begin loading the events again\n     * if this view is active.\n     *\n     * @param {object} root The root element for the timeline courses view.\n     */\n    var reset = function(root) {\n        root.removeAttr('data-seen');\n        if (root.hasClass('active')) {\n            shown(root);\n        }\n    };\n\n    /**\n     * If this is the first time this view has been displayed then begin loading\n     * the events.\n     *\n     * @param {object} root The root element for the timeline courses view.\n     */\n    var shown = function(root) {\n        if (!root.attr('data-seen')) {\n            if (hasLoadedCourses(root)) {\n                // This isn't the first time this view is shown so just reload the\n                // events for the courses we've already loaded.\n                reloadCourseEvents(root);\n            } else {\n                // We haven't loaded any courses yet so do that now.\n                loadMoreCourses(root);\n            }\n\n            root.attr('data-seen', true);\n        }\n    };\n\n    return {\n        init: init,\n        reset: reset,\n        shown: shown\n    };\n});\n"],"file":"view_courses.min.js"}
\ No newline at end of file
+{"version":3,"sources":["../src/view_courses.js"],"names":["define","$","Notification","CustomEvents","Str","Templates","EventList","CourseRepository","EventsRepository","SELECTORS","MORE_COURSES_BUTTON","MORE_COURSES_BUTTON_CONTAINER","NO_COURSES_EMPTY_MESSAGE","COURSES_LIST","COURSE_ITEMS_LOADING_PLACEHOLDER","COURSE_EVENTS_CONTAINER","COURSE_NAME","LOADING_ICON","TEMPLATES","COURSE_ITEMS","COURSE_EVENT_LIMIT","SECONDS_IN_DAY","hideLoadingPlaceholder","root","find","addClass","hideMoreCoursesButton","showMoreCoursesButton","removeClass","enableMoreCoursesButtonLoading","button","prop","render","then","html","append","catch","disableMoreCoursesButtonLoading","remove","showNoCoursesEmptyMessage","renderCourseItemsHTML","container","appendNodeContents","hasLoadedCourses","length","getOffset","parseInt","attr","setOffset","offset","getLimit","getDaysOffset","getDaysLimit","daysLimit","getMidnight","getStartTime","midnight","daysOffset","getEndTime","getEventsForCourseIds","courseIds","startTime","limit","endTime","args","courseids","starttime","endtime","queryByCourses","getEventReloadTime","data","setEventReloadTime","time","hasReloadedEventsSince","loadEventsForCourses","courses","map","course","id","updateDisplayFromCourses","noEventsURL","hasdaysoffset","hasdayslimit","daysoffset","dayslimit","nodayslimit","urls","noevents","loadMoreCourses","getEnrolledCoursesByTimelineClassification","result","startEventLoadingTime","Date","now","nextOffset","nextoffset","eventsPromise","renderPromise","when","eventsByCourse","forEach","courseId","events","courseEventsContainer","eventListRoot","rootSelector","courseGroups","groupedbycourse","filter","group","courseid","pageOnePreload","Deferred","resolve","promise","get_string","fullnamedisplay","string","init","exception","reloadCourseEvents","startReloadTime","courseEventsContainers","get","each","index","courseName","text","eventListContainer","pageDeferred","additionalConfig","courseview","registerEventListeners","activate","on","e","originalEvent","preventDefault","stopPropagation","shown","hasClass","reset","removeAttr"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Manage the timeline courses view for the timeline block.\n *\n * @copyright  2018 Ryan Wyllie <ryan@moodle.com>\n * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(\n[\n    'jquery',\n    'core/notification',\n    'core/custom_interaction_events',\n    'core/str',\n    'core/templates',\n    'block_timeline/event_list',\n    'core_course/repository',\n    'block_timeline/calendar_events_repository'\n],\nfunction(\n    $,\n    Notification,\n    CustomEvents,\n    Str,\n    Templates,\n    EventList,\n    CourseRepository,\n    EventsRepository\n) {\n\n    var SELECTORS = {\n        MORE_COURSES_BUTTON: '[data-action=\"more-courses\"]',\n        MORE_COURSES_BUTTON_CONTAINER: '[data-region=\"more-courses-button-container\"]',\n        NO_COURSES_EMPTY_MESSAGE: '[data-region=\"no-courses-empty-message\"]',\n        COURSES_LIST: '[data-region=\"courses-list\"]',\n        COURSE_ITEMS_LOADING_PLACEHOLDER: '[data-region=\"course-items-loading-placeholder\"]',\n        COURSE_EVENTS_CONTAINER: '[data-region=\"course-events-container\"]',\n        COURSE_NAME: '[data-region=\"course-name\"]',\n        LOADING_ICON: '.loading-icon'\n    };\n\n    var TEMPLATES = {\n        COURSE_ITEMS: 'block_timeline/course-items',\n        LOADING_ICON: 'core/loading'\n    };\n\n    var COURSE_CLASSIFICATION = 'inprogress';\n    var COURSE_SORT = 'fullname asc';\n    var COURSE_EVENT_LIMIT = 5;\n    var COURSE_LIMIT = 2;\n    var SECONDS_IN_DAY = 60 * 60 * 24;\n\n    /**\n     * Hide the loading placeholder elements.\n     *\n     * @param {object} root The rool element.\n     */\n    var hideLoadingPlaceholder = function(root) {\n        root.find(SELECTORS.COURSE_ITEMS_LOADING_PLACEHOLDER).addClass('hidden');\n    };\n\n    /**\n     * Hide the \"more courses\" button.\n     *\n     * @param {object} root The rool element.\n     */\n    var hideMoreCoursesButton = function(root) {\n        root.find(SELECTORS.MORE_COURSES_BUTTON_CONTAINER).addClass('hidden');\n    };\n\n    /**\n     * Show the \"more courses\" button.\n     *\n     * @param {object} root The rool element.\n     */\n    var showMoreCoursesButton = function(root) {\n        root.find(SELECTORS.MORE_COURSES_BUTTON_CONTAINER).removeClass('hidden');\n    };\n\n    /**\n     * Disable the \"more courses\" button and show the loading spinner.\n     *\n     * @param {object} root The rool element.\n     */\n    var enableMoreCoursesButtonLoading = function(root) {\n        var button = root.find(SELECTORS.MORE_COURSES_BUTTON);\n        button.prop('disabled', true);\n        Templates.render(TEMPLATES.LOADING_ICON, {})\n            .then(function(html) {\n                button.append(html);\n                return html;\n            })\n            .catch(function() {\n                // It's not important if this false so just do so silently.\n                return false;\n            });\n    };\n\n    /**\n     * Enable the \"more courses\" button and remove the loading spinner.\n     *\n     * @param {object} root The rool element.\n     */\n    var disableMoreCoursesButtonLoading = function(root) {\n        var button = root.find(SELECTORS.MORE_COURSES_BUTTON);\n        button.prop('disabled', false);\n        button.find(SELECTORS.LOADING_ICON).remove();\n    };\n\n    /**\n     * Display the message for when there are no courses available.\n     *\n     * @param {object} root The rool element.\n     */\n    var showNoCoursesEmptyMessage = function(root) {\n        root.find(SELECTORS.NO_COURSES_EMPTY_MESSAGE).removeClass('hidden');\n    };\n\n    /**\n     * Render the course items HTML to the page.\n     *\n     * @param {object} root The rool element.\n     * @param {string} html The course items HTML to render.\n     */\n    var renderCourseItemsHTML = function(root, html) {\n        var container = root.find(SELECTORS.COURSES_LIST);\n        Templates.appendNodeContents(container, html, '');\n    };\n\n    /**\n     * Check if any courses have been loaded.\n     *\n     * @param {object} root The rool element.\n     * @return {bool}\n     */\n    var hasLoadedCourses = function(root) {\n        return root.find(SELECTORS.COURSE_EVENTS_CONTAINER).length > 0;\n    };\n\n    /**\n     * Return the offset value for fetching courses.\n     *\n     * @param {object} root The rool element.\n     * @return {Number}\n     */\n    var getOffset = function(root) {\n        return parseInt(root.attr('data-offset'), 10);\n    };\n\n    /**\n     * Set the offset value for fetching courses.\n     *\n     * @param {object} root The rool element.\n     * @param {Number} offset Offset value.\n     */\n    var setOffset = function(root, offset) {\n        root.attr('data-offset', offset);\n    };\n\n    /**\n     * Return the limit value for fetching courses.\n     *\n     * @param {object} root The rool element.\n     * @return {Number}\n     */\n    var getLimit = function(root) {\n        return parseInt(root.attr('data-limit'), 10);\n    };\n\n    /**\n     * Return the days offset value for fetching events.\n     *\n     * @param {object} root The rool element.\n     * @return {Number}\n     */\n    var getDaysOffset = function(root) {\n        return parseInt(root.attr('data-days-offset'), 10);\n    };\n\n    /**\n     * Return the days limit value for fetching events. The days\n     * limit is optional so undefined will be returned if it isn't\n     * set.\n     *\n     * @param {object} root The rool element.\n     * @return {int|undefined}\n     */\n    var getDaysLimit = function(root) {\n        var daysLimit = root.attr('data-days-limit');\n        return daysLimit != undefined ? parseInt(daysLimit, 10) : undefined;\n    };\n\n    /**\n     * Return the timestamp for the user's midnight.\n     *\n     * @param {object} root The rool element.\n     * @return {Number}\n     */\n    var getMidnight = function(root) {\n        return parseInt(root.attr('data-midnight'), 10);\n    };\n\n    /**\n     * Return the start time for fetching events. This is calculated\n     * based on the user's midnight value so that timezones are\n     * preserved.\n     *\n     * @param {object} root The rool element.\n     * @return {Number}\n     */\n    var getStartTime = function(root) {\n        var midnight = getMidnight(root);\n        var daysOffset = getDaysOffset(root);\n        return midnight + (daysOffset * SECONDS_IN_DAY);\n    };\n\n    /**\n     * Return the end time for fetching events. This is calculated\n     * based on the user's midnight value so that timezones are\n     * preserved.\n     *\n     * @param {object} root The rool element.\n     * @return {Number}\n     */\n    var getEndTime = function(root) {\n        var midnight = getMidnight(root);\n        var daysLimit = getDaysLimit(root);\n        return daysLimit != undefined ? midnight + (daysLimit * SECONDS_IN_DAY) : false;\n    };\n\n    /**\n     * Get a list of events for the given course ids. Returns a promise that will\n     * be resolved with the events.\n     *\n     * @param {array} courseIds The list of course ids to fetch events for.\n     * @param {Number} startTime Timestamp to fetch events from.\n     * @param {Number} limit Limit to the number of events (this applies per course, not total)\n     * @param {Number} endTime Timestamp to fetch events to.\n     * @return {object} jQuery promise.\n     */\n    var getEventsForCourseIds = function(courseIds, startTime, limit, endTime) {\n        var args = {\n            courseids: courseIds,\n            starttime: startTime,\n            limit: limit\n        };\n\n        if (endTime) {\n            args.endtime = endTime;\n        }\n\n        return EventsRepository.queryByCourses(args);\n    };\n\n    /**\n     * Get the last time the events were reloaded.\n     *\n     * @param {object} root The rool element.\n     * @return {Number}\n     */\n    var getEventReloadTime = function(root) {\n        return root.data('last-event-load-time');\n    };\n\n    /**\n     * Set the last time the events were reloaded.\n     *\n     * @param {object} root The rool element.\n     * @param {Number} time Timestamp in milliseconds.\n     */\n    var setEventReloadTime = function(root, time) {\n        root.data('last-event-load-time', time);\n    };\n\n    /**\n     * Check if events have begun reloading since the given\n     * time.\n     *\n     * @param {object} root The rool element.\n     * @param {Number} time Timestamp in milliseconds.\n     * @return {bool}\n     */\n    var hasReloadedEventsSince = function(root, time) {\n        return getEventReloadTime(root) > time;\n    };\n\n    /**\n     * Send a request to the server to load the events for the courses.\n     *\n     * @param {array} courses List of course objects.\n     * @param {Number} startTime Timestamp to load events after.\n     * @param {int|undefined} endTime Timestamp to load events up until.\n     * @return {object} jQuery promise resolved with the events.\n     */\n    var loadEventsForCourses = function(courses, startTime, endTime) {\n        var courseIds = courses.map(function(course) {\n            return course.id;\n        });\n\n        return getEventsForCourseIds(courseIds, startTime, COURSE_EVENT_LIMIT + 1, endTime);\n    };\n\n    /**\n     * Render the courses in the DOM once the server has returned the courses.\n     *\n     * @param {array} courses List of course objects.\n     * @param {object} root The root element\n     * @param {Number} midnight The midnight timestamp in the user's timezone.\n     * @param {Number} daysOffset Number of days from today to offset the events.\n     * @param {Number} daysLimit Number of days from today to limit the events to.\n     * @param {string} noEventsURL URL for the image to display for no events.\n     * @return {object} jQuery promise resolved after rendering is complete.\n     */\n    var updateDisplayFromCourses = function(courses, root, midnight, daysOffset, daysLimit, noEventsURL) {\n        // Render the courses template.\n        return Templates.render(TEMPLATES.COURSE_ITEMS, {\n            courses: courses,\n            midnight: midnight,\n            hasdaysoffset: true,\n            hasdayslimit: daysLimit != undefined,\n            daysoffset: daysOffset,\n            dayslimit: daysLimit,\n            nodayslimit: daysLimit == undefined,\n            urls: {\n                noevents: noEventsURL\n            }\n        }).then(function(html) {\n            hideLoadingPlaceholder(root);\n\n            if (html) {\n                // Template rendering is complete and we have the HTML so we can\n                // add it to the DOM.\n                renderCourseItemsHTML(root, html);\n            } else {\n                if (!hasLoadedCourses(root)) {\n                    // There were no courses to render so show the empty placeholder\n                    // message for the user to tell them.\n                    showNoCoursesEmptyMessage(root);\n                }\n            }\n\n            return html;\n        })\n        .then(function(html) {\n            if (courses.length < COURSE_LIMIT) {\n                // We know there aren't any more courses because we got back less\n                // than we asked for so hide the button to request more.\n                hideMoreCoursesButton(root);\n            } else {\n                // Make sure the button is visible if there are more courses to load.\n                showMoreCoursesButton(root);\n            }\n\n            return html;\n        })\n        .catch(function() {\n            hideLoadingPlaceholder(root);\n        });\n    };\n\n    /**\n     * Find all of the visible course blocks and initialise the event\n     * list module to being loading the events for the course block.\n     *\n     * @param {object} root The root element for the timeline courses view.\n     * @return {object} jQuery promise resolved with courses and events.\n     */\n    var loadMoreCourses = function(root) {\n        var offset = getOffset(root);\n        var limit = getLimit(root);\n\n        // Start loading the next set of courses.\n        return CourseRepository.getEnrolledCoursesByTimelineClassification(\n            COURSE_CLASSIFICATION,\n            limit,\n            offset,\n            COURSE_SORT\n        ).then(function(result) {\n            var startEventLoadingTime = Date.now();\n            var courses = result.courses;\n            var nextOffset = result.nextoffset;\n            var daysOffset = getDaysOffset(root);\n            var daysLimit = getDaysLimit(root);\n            var midnight = getMidnight(root);\n            var startTime = getStartTime(root);\n            var endTime = getEndTime(root);\n            var noEventsURL = root.attr('data-no-events-url');\n            // Record the next offset if we want to request more courses.\n            setOffset(root, nextOffset);\n            // Load the events for these courses.\n            var eventsPromise = loadEventsForCourses(courses, startTime, endTime);\n            // Render the courses in the DOM.\n            var renderPromise = updateDisplayFromCourses(courses, root, midnight, daysOffset, daysLimit, noEventsURL);\n\n            return $.when(eventsPromise, renderPromise)\n                .then(function(eventsByCourse) {\n                    if (hasReloadedEventsSince(root, startEventLoadingTime)) {\n                        // All of the events are being reloaded so ignore our results.\n                        return eventsByCourse;\n                    }\n\n                    // When we've got all of the courses and events we can render the events in the\n                    // correct course event list.\n                    courses.forEach(function(course) {\n                        var courseId = course.id;\n                        var events = [];\n                        var containerSelector = '[data-region=\"course-events-container\"][data-course-id=\"' + courseId + '\"]';\n                        var courseEventsContainer = root.find(containerSelector);\n                        var eventListRoot = courseEventsContainer.find(EventList.rootSelector);\n                        var courseGroups = eventsByCourse.groupedbycourse.filter(function(group) {\n                            return group.courseid == courseId;\n                        });\n\n                        if (courseGroups.length) {\n                            // Get the events for this course.\n                            events = courseGroups[0].events;\n                        }\n\n                        // Create a preloaded page to pass to the event list because we've already\n                        // loaded the first page of events.\n                        var pageOnePreload = $.Deferred().resolve({events: events}).promise();\n                        // Initialise the event list pagination area for this course.\n                        Str.get_string('ariaeventlistpaginationnavcourses', 'block_timeline', course.fullnamedisplay)\n                            .then(function(string) {\n                                EventList.init(eventListRoot, COURSE_EVENT_LIMIT, {'1': pageOnePreload}, string);\n                                return string;\n                            })\n                            .catch(function() {\n                                // An error is ok, just render with the default string.\n                                EventList.init(eventListRoot, COURSE_EVENT_LIMIT, {'1': pageOnePreload});\n                            });\n                    });\n\n                    return eventsByCourse;\n                });\n        }).catch(Notification.exception);\n    };\n\n    /**\n     * Reload the events for all of the visible courses. These events will be loaded\n     * in a single request to the server.\n     *\n     * @param {object} root The root element.\n     * @return {object} jQuery promise resolved with courses and events.\n     */\n    var reloadCourseEvents = function(root) {\n        var startReloadTime = Date.now();\n        var startTime = getStartTime(root);\n        var endTime = getEndTime(root);\n        var courseEventsContainers = root.find(SELECTORS.COURSE_EVENTS_CONTAINER);\n        var courseIds = courseEventsContainers.map(function() {\n            return $(this).attr('data-course-id');\n        }).get();\n\n        // Record when we started our request.\n        setEventReloadTime(root, startReloadTime);\n\n        // Load all of the events for the given courses.\n        return getEventsForCourseIds(courseIds, startTime, COURSE_EVENT_LIMIT + 1, endTime)\n            .then(function(eventsByCourse) {\n                if (hasReloadedEventsSince(root, startReloadTime)) {\n                    // A new reload has begun so ignore our results.\n                    return eventsByCourse;\n                }\n\n                courseEventsContainers.each(function(index, container) {\n                    container = $(container);\n                    var courseId = container.attr('data-course-id');\n                    var courseName = container.find(SELECTORS.COURSE_NAME).text();\n                    var eventListContainer = container.find(EventList.rootSelector);\n                    var pageDeferred = $.Deferred();\n                    var events = [];\n                    var courseGroups = eventsByCourse.groupedbycourse.filter(function(group) {\n                        return group.courseid == courseId;\n                    });\n\n                    if (courseGroups.length) {\n                        // Get the events just for this course.\n                        events = courseGroups[0].events;\n                    }\n\n                    pageDeferred.resolve({events: events});\n\n                    var additionalConfig = {courseview: true};\n                    // Re-initialise the events list with the preloaded events we just got from\n                    // the server.\n                    Str.get_string('ariaeventlistpaginationnavcourses', 'block_timeline', courseName)\n                        .then(function(string) {\n                            EventList.init(eventListContainer, COURSE_EVENT_LIMIT, {'1': pageDeferred.promise()},\n                                string, additionalConfig);\n                            return string;\n                        })\n                        .catch(function() {\n                            // Ignore a failure to load the string. Just render with the default string.\n                            EventList.init(eventListContainer, COURSE_EVENT_LIMIT, {'1': pageDeferred.promise()},\n                                undefined, additionalConfig);\n                        });\n                });\n\n                return eventsByCourse;\n            }).catch(Notification.exception);\n    };\n\n    /**\n     * Add event listeners to load more courses for the courses view.\n     *\n     * @param {object} root The root element for the timeline courses view.\n     */\n    var registerEventListeners = function(root) {\n        CustomEvents.define(root, [CustomEvents.events.activate]);\n        // Show more courses and load their events when the user clicks the \"more courses\"\n        // button.\n        root.on(CustomEvents.events.activate, SELECTORS.MORE_COURSES_BUTTON, function(e, data) {\n            enableMoreCoursesButtonLoading(root);\n            loadMoreCourses(root)\n                .then(function() {\n                    disableMoreCoursesButtonLoading(root);\n                    return;\n                })\n                .catch(function() {\n                    disableMoreCoursesButtonLoading(root);\n                });\n\n            if (data) {\n                data.originalEvent.preventDefault();\n                data.originalEvent.stopPropagation();\n            }\n            e.stopPropagation();\n        });\n    };\n\n    /**\n     * Initialise the timeline courses view. Begin loading the events\n     * if this view is active. Add the relevant event listeners.\n     *\n     * This function should only be called once per page load because it\n     * is adding event listeners to the page.\n     *\n     * @param {object} root The root element for the timeline courses view.\n     */\n    var init = function(root) {\n        root = $(root);\n\n        setEventReloadTime(root, Date.now());\n\n        if (root.hasClass('active')) {\n            // Only load if this is active otherwise it will be lazy loaded later.\n            loadMoreCourses(root);\n            root.attr('data-seen', true);\n        }\n\n        registerEventListeners(root);\n    };\n\n    /**\n     * Reset the element back to it's initial state. Begin loading the events again\n     * if this view is active.\n     *\n     * @param {object} root The root element for the timeline courses view.\n     */\n    var reset = function(root) {\n        root.removeAttr('data-seen');\n        if (root.hasClass('active')) {\n            shown(root);\n        }\n    };\n\n    /**\n     * If this is the first time this view has been displayed then begin loading\n     * the events.\n     *\n     * @param {object} root The root element for the timeline courses view.\n     */\n    var shown = function(root) {\n        if (!root.attr('data-seen')) {\n            if (hasLoadedCourses(root)) {\n                // This isn't the first time this view is shown so just reload the\n                // events for the courses we've already loaded.\n                reloadCourseEvents(root);\n            } else {\n                // We haven't loaded any courses yet so do that now.\n                loadMoreCourses(root);\n            }\n\n            root.attr('data-seen', true);\n        }\n    };\n\n    return {\n        init: init,\n        reset: reset,\n        shown: shown\n    };\n});\n"],"file":"view_courses.min.js"}
\ No newline at end of file
diff --git a/blocks/timeline/amd/src/event_list.js b/blocks/timeline/amd/src/event_list.js
index 785939b6fcb..30f9368a248 100644
--- a/blocks/timeline/amd/src/event_list.js
+++ b/blocks/timeline/amd/src/event_list.js
@@ -42,6 +42,7 @@ function(
 ) {
 
     var SECONDS_IN_DAY = 60 * 60 * 24;
+    var courseview = false;
 
     var SELECTORS = {
         EMPTY_MESSAGE: '[data-region="empty-message"]',
@@ -127,6 +128,7 @@ function(
     var buildTemplateContext = function(calendarEvents, midnight) {
         var eventsByDay = {};
         var templateContext = {
+            courseview,
             eventsbyday: []
         };
 
@@ -405,9 +407,11 @@ function(
      * @param {string} paginationAriaLabel String to set as the aria label for the pagination bar.
      * @param {object} additionalConfig Additional config options to pass to pagedContentFactory
      */
-    var init = function(root, pageLimit, preloadedPages, paginationAriaLabel, additionalConfig) {
+    var init = function(root, pageLimit, preloadedPages, paginationAriaLabel, additionalConfig = {}) {
         root = $(root);
 
+        courseview = !!additionalConfig.courseview;
+
         // 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
diff --git a/blocks/timeline/amd/src/view_courses.js b/blocks/timeline/amd/src/view_courses.js
index a31f49665de..991fc0edd29 100644
--- a/blocks/timeline/amd/src/view_courses.js
+++ b/blocks/timeline/amd/src/view_courses.js
@@ -495,16 +495,19 @@ function(
 
                     pageDeferred.resolve({events: events});
 
+                    var additionalConfig = {courseview: true};
                     // 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);
+                            EventList.init(eventListContainer, COURSE_EVENT_LIMIT, {'1': pageDeferred.promise()},
+                                string, additionalConfig);
                             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()});
+                            EventList.init(eventListContainer, COURSE_EVENT_LIMIT, {'1': pageDeferred.promise()},
+                                undefined, additionalConfig);
                         });
                 });
 
diff --git a/blocks/timeline/styles.css b/blocks/timeline/styles.css
new file mode 100644
index 00000000000..23ebfb11fd9
--- /dev/null
+++ b/blocks/timeline/styles.css
@@ -0,0 +1,9 @@
+#block-region-side-pre .block_timeline h6.event-action {
+    flex-basis: 100%;
+}
+#block-region-side-pre .block_timeline .event-name-container {
+    flex-basis: 50%;
+}
+#block-region-side-pre .block_timeline h6.event-action a.btn {
+    width: auto;
+}
diff --git a/blocks/timeline/templates/course-item.mustache b/blocks/timeline/templates/course-item.mustache
index dafb42a691e..8c8ffda38ea 100644
--- a/blocks/timeline/templates/course-item.mustache
+++ b/blocks/timeline/templates/course-item.mustache
@@ -28,7 +28,7 @@
 }}
 <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>
+        <h4 class="h5 font-weight-bold">{{{fullnamedisplay}}}</h4>
         {{< block_timeline/event-list }}
             {{$courseid}}{{id}}{{/courseid}}
         {{/ block_timeline/event-list }}
diff --git a/blocks/timeline/templates/event-list-content.mustache b/blocks/timeline/templates/event-list-content.mustache
index f138e9c57ca..11c799305cd 100644
--- a/blocks/timeline/templates/event-list-content.mustache
+++ b/blocks/timeline/templates/event-list-content.mustache
@@ -63,9 +63,12 @@
         ]
     }
 }}
-<div class="border-bottom pb-2">
+<div class="pb-2">
     {{#eventsbyday}}
-        <h5 class="h6 mt-3 mb-0 {{#past}}text-danger{{/past}}">{{#userdate}} {{dayTimestamp}}, {{#str}} strftimedaydate, core_langconfig {{/str}}  {{/userdate}}</h5>
+        <div class="mt-3">
+            <h5 class="h6 d-inline {{^courseview}}font-weight-bold{{/courseview}}">{{#userdate}} {{dayTimestamp}}, {{#str}} strftimedaydate, core_langconfig {{/str}}  {{/userdate}}</h5>
+            {{#past}}<span class="badge badge-pill badge-danger">{{#str}} overdue, block_timeline {{/str}}</span>{{/past}}
+        </div>
         {{> block_timeline/event-list-items }}
     {{/eventsbyday}}
-</div>
\ No newline at end of file
+</div>
diff --git a/blocks/timeline/templates/event-list-item.mustache b/blocks/timeline/templates/event-list-item.mustache
index a33a10e1b36..1f83c0d24c7 100644
--- a/blocks/timeline/templates/event-list-item.mustache
+++ b/blocks/timeline/templates/event-list-item.mustache
@@ -41,33 +41,37 @@
         }
     }
 }}
-<div class="list-group-item flex-column py-2 pl-0 pr-0 border-0"
-    data-region="event-list-item"
-    >
-    <div class="d-flex">
-        <div class="icon-size-4 d-flex align-self-top">
+<div class="list-group-item timeline-event-list-item flex-column py-2 pl-0 pr-0 border-bottom"
+        data-region="event-list-item">
+    <div class="d-flex justify-content-between flex-wrap">
+        <small class="text-right text-nowrap align-self-center ml-1">
+            {{#userdate}} {{timesort}}, {{#str}} strftimetime24, core_langconfig {{/str}} {{/userdate}}
+        </small>
+        <div class="icon-size-4 align-self-top align-self-center mx-2">
             {{#icon}}{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}{{/icon}}
         </div>
-        <div class="w-100 event-name-container text-truncate line-height-3">
+        <div class="event-name-container flex-grow-1 text-truncate line-height-3">
             <a href="{{url}}"
                title={{#quote}}{{{name}}}{{/quote}}
-               aria-label='{{#str}} ariaeventlistitem, block_timeline, { "name": {{#quote}}{{{name}}}{{/quote}}, "course": {{#quote}}{{{course.fullnamedisplay}}}{{/quote}}, "date": "{{#userdate}} {{timesort}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}}" } {{/str}}'
-            ><h6 class="event-name text-truncate mb-0">{{{name}}}</h6></a>
+               aria-label='{{#str}} ariaeventlistitem, block_timeline, { "name": {{#quote}}{{{name}}}{{/quote}}, "course": {{#quote}}{{{course.fullnamedisplay}}}{{/quote}}, "date": "{{#userdate}} {{timesort}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}}" } {{/str}}'>
+                <h6 class="event-name text-truncate mb-0">{{{name}}}</h6>
+            </a>
             {{#course.fullnamedisplay}}
             <small class="text-muted text-truncate mb-0">{{{course.fullnamedisplay}}}</small>
             {{/course.fullnamedisplay}}
-            {{#action.actionable}}
-            <h6 class="mb-0 pt-2">
-                <a href="{{action.url}}" aria-label="{{action.name}}" title="{{action.name}}" class="list-group-item-action">{{{action.name}}}</a>
-                {{#action.showitemcount}}
-                <span class="badge badge-secondary">{{action.itemcount}}</span>
-                {{/action.showitemcount}}
-            </h6>
-            {{/action.actionable}}
         </div>
-        <small class="text-right text-nowrap ml-1">
-            {{#userdate}} {{timesort}}, {{#str}} strftimetime24, core_langconfig {{/str}} {{/userdate}}
-        </small>
-
+        {{#action.actionable}}
+        <h6 class="event-action">
+            <a class="list-group-item-action btn btn-outline-secondary btn-sm text-nowrap"
+                   href="{{action.url}}"
+                   aria-label="{{action.name}}"
+                   title="{{action.name}}">
+               {{{action.name}}}
+               {{#action.showitemcount}}
+               <span class="badge badge-secondary">{{action.itemcount}}</span>
+               {{/action.showitemcount}}
+            </a>
+        </h6>
+        {{/action.actionable}}
     </div>
 </div>
diff --git a/blocks/timeline/templates/view-courses.mustache b/blocks/timeline/templates/view-courses.mustache
index 83844f6789e..7073e64d3b6 100644
--- a/blocks/timeline/templates/view-courses.mustache
+++ b/blocks/timeline/templates/view-courses.mustache
@@ -30,7 +30,7 @@
     <div class="bg-pulse-grey mt-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">
+<div class="hidden text-xs-left text-left 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">