From e01f2d514a9277c986bd6a6cacd9abc76b66f118 Mon Sep 17 00:00:00 2001 From: Michael Hawkins Date: Thu, 25 Nov 2021 00:05:56 +0800 Subject: [PATCH] MDL-73068 timeline: Update courses view to omit courses without events Previously, the timeline block courses view included all courses in alphabetical order, whether they contained any events matching the relevant filters or not. This could be tedious and misleading if there were many courses with no events appearing before a course which did have matching events. Now, courses are only included if they contain events matching the currently set time filter and search term. When those values are modified, the block will be refreshed and fetch results for the current values, and also takes these into consideration when determining whether to display the show more courses button. --- blocks/timeline/amd/build/event_list.min.js | 2 +- .../timeline/amd/build/event_list.min.js.map | 2 +- blocks/timeline/amd/build/view_courses.min.js | 2 +- .../amd/build/view_courses.min.js.map | 2 +- blocks/timeline/amd/src/event_list.js | 2 +- blocks/timeline/amd/src/view_courses.js | 207 ++++++++-------- blocks/timeline/templates/event-list.mustache | 16 +- blocks/timeline/templates/no-courses.mustache | 39 ++++ blocks/timeline/templates/no-events.mustache | 38 +++ .../timeline/templates/view-courses.mustache | 26 +-- .../behat/block_timeline_courses.feature | 54 ++++- .../tests/behat/block_timeline_search.feature | 58 ++++- blocks/timeline/upgrade.txt | 5 + course/amd/build/repository.min.js | 2 +- course/amd/build/repository.min.js.map | 2 +- course/amd/src/repository.js | 35 +++ ...tion_events_by_timeline_classification.php | 220 ++++++++++++++++++ course/upgrade.txt | 4 + lib/db/services.php | 9 + version.php | 2 +- 20 files changed, 576 insertions(+), 151 deletions(-) create mode 100644 blocks/timeline/templates/no-courses.mustache create mode 100644 blocks/timeline/templates/no-events.mustache create mode 100644 course/classes/external/get_enrolled_courses_with_action_events_by_timeline_classification.php diff --git a/blocks/timeline/amd/build/event_list.min.js b/blocks/timeline/amd/build/event_list.min.js index 2ef75146806..b97a9ba0924 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/str","core/user_date","block_timeline/calendar_events_repository","core/pending"],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_WRAPPER:"[data-region=\"event-list-wrapper\"]",EVENT_LIST_LOADING_PLACEHOLDER:"[data-region=\"event-list-loading-placeholder\"]",TIMELINE_BLOCK:"[data-region=\"timeline\"]",TIMELINE_SEARCH:"[data-action=\"search\"]",MORE_ACTIVITIES_BUTTON:"[data-action=\"more-events\"]",MORE_ACTIVITIES_BUTTON_CONTAINER:"[data-region=\"more-events-button-container\"]"},j={EVENT_LIST_CONTENT:"block_timeline/event-list-content",MORE_ACTIVITIES_BUTTON:"block_timeline/event-list-loadmore",LOADING_ICON:"core/loading"},k=function(a){a.find(i.EVENT_LIST_CONTENT).addClass("hidden");a.find(i.EMPTY_MESSAGE).removeClass("hidden")},l=function(a){a.find(i.EVENT_LIST_CONTENT).removeClass("hidden");a.find(i.EMPTY_MESSAGE).addClass("hidden")},m=function(a){a.find(i.EVENT_LIST_CONTENT).empty()},n=function(a){var b={},c={courseview:h,eventsbyday:[]};a.forEach(function(a){var c=a.timeusermidnight;if(b[c]){b[c].push(a)}else{b[c]=[a]}});Object.keys(b).forEach(function(a){var d=b[a];c.eventsbyday.push({dayTimestamp:a,events:d})});return c},o=function(a){var b=n(a),d=j.EVENT_LIST_CONTENT;return c.render(d,b)},p=function(a,b,c,d,e,g,h){var i=d!=void 0?a+d*86400:!1,j={starttime:a+c*86400,limit:b};if(e){j.aftereventid=e}if(i){j.endtime=i}if(h){j.searchvalue=h}if(g){j.courseid=g;return f.queryByCourse(j)}else{return f.queryByTime(j)}},q=function(a,c,d,e,f,g,h,i,j){return r(a,d,e,f,g,h,i,j).then(function(a){if(a.calendarEvents.length){var b=a.calendarEvents.at(-1).id,d=a.calendarEvents.at(-1).timeusermidnight;c.resolve({hasContent:!0,lastId:b,lastTimeStamp:d,loadedAll:a.loadedAll});return o(a.calendarEvents,e)}else{c.resolve({hasContent:!1,lastId:0,lastTimeStamp:0,loadedAll:!0});return a.calendarEvents}}).catch(b.exception)},r=function(a,b,c,d,f,g,h,i){var j=p(c,b+1,g,h,d,f,i),k=[],l=!0;return j.then(function(d){if(!d.events.length){return{calendarEvents:k,loadedAll:l}}var f=document.querySelector("[data-filtername='overdue']"),g=f&&f.getAttribute("aria-current");k=d.events.filter(function(a){if("open"==a.eventtype||"opensubmission"==a.eventtype){var b=e.getUserMidnightForTimestamp(a.timesort,c);return b>c}return!g||a.overdue});l=k.length<=b;if(!l){k.pop()}if(k.length){var h=k.at(-1).id;u(a,h)}return{calendarEvents:k,loadedAll:l}})},s=function(d){var e=parseInt(d.attr("data-midnight"),10),f=d.attr("data-course-id"),g=parseInt(d.attr("data-days-offset"),10),h=d.attr("data-days-limit"),k=t(d),l=d.find(i.EVENT_LIST_WRAPPER),m=d.closest(i.TIMELINE_BLOCK).find(i.TIMELINE_SEARCH).val(),n=r(d,10,e,k,f,g,h,m);n.then(function(e){if(e.calendarEvents.length){var f=o(e.calendarEvents),g=v(d);f.then(function(b,f){b=a(b);b.find("[data-timestamp=\"".concat(g,"\"]")).remove();c.appendNodeContents(l,b.html(),f);if(!e.loadedAll){c.render(j.MORE_ACTIVITIES_BUTTON,{}).then(function(a){l.append(a);w(d,e.calendarEvents.at(-1).timeusermidnight);z(d);return a}).catch(function(){return!1})}return b}).catch(b.exception)}return e}).then(function(){return y(d)}).catch(b.exception)},t=function(a){return parseInt(a.attr("data-lazyload-offset"),10)},u=function(a,b){a.attr("data-lazyload-offset",b)},v=function(a){return parseInt(a.attr("data-timestamp"),10)},w=function(a,b){a.attr("data-timestamp",b)},x=function(a){var b=a.find(i.MORE_ACTIVITIES_BUTTON);b.prop("disabled",!0);c.render(j.LOADING_ICON,{}).then(function(a){b.append(a);return a}).catch(function(){return!1})},y=function(a){var b=a.find(i.MORE_ACTIVITIES_BUTTON_CONTAINER);b.remove()},z=function(a){var b=a.find(i.MORE_ACTIVITIES_BUTTON);b.on("click",function(){x(a);s(a)})};return{init:function init(d){var e=1c}return!g||a.overdue});l=k.length<=b;if(!l){k.pop()}if(k.length){var h=k.at(-1).id;u(a,h)}return{calendarEvents:k,loadedAll:l}})},s=function(d){var e=parseInt(d.attr("data-midnight"),10),f=d.attr("data-course-id"),g=parseInt(d.attr("data-days-offset"),10),h=d.attr("data-days-limit"),k=t(d),l=d.find(i.EVENT_LIST_WRAPPER),m=d.closest(i.TIMELINE_BLOCK).find(i.TIMELINE_SEARCH).val(),n=r(d,10,e,k,f,g,h,m);n.then(function(e){if(e.calendarEvents.length){var f=o(e.calendarEvents),g=v(d);f.then(function(b,f){b=a(b);b.find("[data-timestamp=\"".concat(g,"\"]")).remove();c.appendNodeContents(l,b.html(),f);if(!e.loadedAll){c.render(j.MORE_ACTIVITIES_BUTTON,{}).then(function(a){l.append(a);w(d,e.calendarEvents.at(-1).timeusermidnight);z(d);return a}).catch(function(){return!1})}return b}).catch(b.exception)}return e}).then(function(){return y(d)}).catch(b.exception)},t=function(a){return parseInt(a.attr("data-lazyload-offset"),10)},u=function(a,b){a.attr("data-lazyload-offset",b)},v=function(a){return parseInt(a.attr("data-timestamp"),10)},w=function(a,b){a.attr("data-timestamp",b)},x=function(a){var b=a.find(i.MORE_ACTIVITIES_BUTTON);b.prop("disabled",!0);c.render(j.LOADING_ICON,{}).then(function(a){b.append(a);return a}).catch(function(){return!1})},y=function(a){var b=a.find(i.MORE_ACTIVITIES_BUTTON_CONTAINER);b.remove()},z=function(a){var b=a.find(i.MORE_ACTIVITIES_BUTTON);b.on("click",function(){x(a);s(a)})};return{init:function init(d){var e=1.\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 \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/str',\n 'core/user_date',\n 'block_timeline/calendar_events_repository',\n 'core/pending'\n],\nfunction(\n $,\n Notification,\n Templates,\n Str,\n UserDate,\n CalendarEventsRepository,\n Pending\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_WRAPPER: '[data-region=\"event-list-wrapper\"]',\n EVENT_LIST_LOADING_PLACEHOLDER: '[data-region=\"event-list-loading-placeholder\"]',\n TIMELINE_BLOCK: '[data-region=\"timeline\"]',\n TIMELINE_SEARCH: '[data-action=\"search\"]',\n MORE_ACTIVITIES_BUTTON: '[data-action=\"more-events\"]',\n MORE_ACTIVITIES_BUTTON_CONTAINER: '[data-region=\"more-events-button-container\"]'\n };\n\n var TEMPLATES = {\n EVENT_LIST_CONTENT: 'block_timeline/event-list-content',\n MORE_ACTIVITIES_BUTTON: 'block_timeline/event-list-loadmore',\n LOADING_ICON: 'core/loading'\n };\n\n /** @type {number} The total items will be shown on the first load. */\n const DEFAULT_LAZY_LOADING_ITEMS_FIRST_LOAD = 5;\n /** @type {number} The total items will be shown when click on the Show more activities button. */\n const DEFAULT_LAZY_LOADING_ITEMS_OTHER_LOAD = 10;\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 * @return {object}\n */\n var buildTemplateContext = function(calendarEvents) {\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 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 * @return {promise} Resolved with HTML and JS strings.\n */\n var render = function(calendarEvents) {\n var templateContext = buildTemplateContext(calendarEvents);\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 * @param {string|undefined} searchValue Search value\n * @return {Promise} A jquery promise\n */\n var load = function(midnight, limit, daysOffset, daysLimit, lastId, courseId, searchValue) {\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 (searchValue) {\n args.searchvalue = searchValue;\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 * Create a lazy-loading region for the calendar events in the given root element.\n *\n * @param {object} root The event list container element.\n * @param {object} additionalConfig Additional config options to pass to pagedContentFactory.\n */\n var init = function(root, additionalConfig = {}) {\n const pendingPromise = new Pending('block/timeline:event-init');\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 const searchValue = root.closest(SELECTORS.TIMELINE_BLOCK).find(SELECTORS.TIMELINE_SEARCH).val();\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 // Create the lazy loading content element.\n return createLazyLoadingContent(root, firstLoad,\n DEFAULT_LAZY_LOADING_ITEMS_FIRST_LOAD, midnight, 0, courseId, daysOffset, daysLimit, searchValue)\n .then(function(html, js) {\n firstLoad.then(function(data) {\n if (!data.hasContent) {\n loadingPlaceholder.addClass('hidden');\n // If we didn't get any data then show the empty data message.\n return hideContent(root);\n }\n\n html = $(html);\n // Hide the content for now.\n html.addClass('hidden');\n // Replace existing elements with the newly created lazy-loading region.\n Templates.replaceNodeContents(eventListContent, html, js);\n\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 (!data.loadedAll) {\n Templates.render(TEMPLATES.MORE_ACTIVITIES_BUTTON, {courseview}).then(function(html) {\n eventListContent.append(html);\n setLastTimestamp(root, data.lastTimeStamp);\n // Init the event handler.\n initEventListener(root);\n return html;\n }).catch(function() {\n return false;\n });\n }\n\n return data;\n })\n .catch(function() {\n return false;\n });\n\n return html;\n }).then(() => {\n return pendingPromise.resolve();\n })\n .catch(Notification.exception);\n };\n\n /**\n * Create a lazy-loading content element for showing the event list for the initial load.\n *\n * @param {object} root The event list container element.\n * @param {object} firstLoad A jQuery promise to be resolved after the first set of data is loaded.\n * @param {int} itemLimit Limit the number of items.\n * @param {Number} midnight The user's midnight time in unix timestamp.\n * @param {int} lastId The last event ID for each loaded page. Page number is key, id is 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 * @param {string|undefined} searchValue Search value.\n * @return {object} jQuery promise resolved with calendar events.\n */\n const createLazyLoadingContent = (root, firstLoad, itemLimit, midnight, lastId,\n courseId, daysOffset, daysLimit, searchValue) => {\n return loadEventsForLazyLoading(\n root,\n itemLimit,\n midnight,\n lastId,\n courseId,\n daysOffset,\n daysLimit,\n searchValue\n ).then(data => {\n if (data.calendarEvents.length) {\n const lastEventId = data.calendarEvents.at(-1).id;\n const lastTimeStamp = data.calendarEvents.at(-1).timeusermidnight;\n firstLoad.resolve({\n hasContent: true,\n lastId: lastEventId,\n lastTimeStamp: lastTimeStamp,\n loadedAll: data.loadedAll\n });\n return render(data.calendarEvents, midnight);\n } else {\n firstLoad.resolve({\n hasContent: false,\n lastId: 0,\n lastTimeStamp: 0,\n loadedAll: true\n });\n return data.calendarEvents;\n }\n }).catch(Notification.exception);\n };\n\n /**\n * Handle the request from the lazy-loading region.\n * Uses the given data like course id, offset... to request the events from the server.\n *\n * @param {object} root The event list container element.\n * @param {int} itemLimit Limit the number of items.\n * @param {Number} midnight The user's midnight time in unix timestamp.\n * @param {int} lastId The last event ID for each loaded page.\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|undefined} searchValue Search value.\n * @return {object} jQuery promise resolved with calendar events.\n */\n const loadEventsForLazyLoading = (root, itemLimit, midnight, lastId, courseId, daysOffset, daysLimit, searchValue) => {\n // Load one more than the given limit so that we can tell if there\n // is more content to load after this.\n const eventsPromise = load(midnight, itemLimit + 1, daysOffset, daysLimit, lastId, courseId, searchValue);\n let calendarEvents = [];\n let loadedAll = true;\n\n return eventsPromise.then(result => {\n if (!result.events.length) {\n return {calendarEvents, loadedAll};\n }\n\n // Determine if the overdue filter is applied.\n const overdueFilter = document.querySelector(\"[data-filtername='overdue']\");\n const filterByOverdue = (overdueFilter && overdueFilter.getAttribute('aria-current'));\n\n calendarEvents = result.events.filter(event => {\n if (event.eventtype == 'open' || event.eventtype == 'opensubmission') {\n const dayTimestamp = UserDate.getUserMidnightForTimestamp(event.timesort, midnight);\n return dayTimestamp > midnight;\n }\n // When filtering by overdue, we fetch all events due today, in case any have elapsed already and are overdue.\n // This means if filtering by overdue, some events fetched might not be required (eg if due later today).\n return (!filterByOverdue || event.overdue);\n });\n\n loadedAll = calendarEvents.length <= itemLimit;\n\n if (!loadedAll) {\n // Remove the last element from the array because it isn't\n // needed in this result set.\n calendarEvents.pop();\n }\n\n if (calendarEvents.length) {\n const lastEventId = calendarEvents.at(-1).id;\n setOffset(root, lastEventId);\n }\n\n return {calendarEvents, loadedAll};\n });\n };\n\n /**\n * Load new events and append to current list.\n *\n * @param {object} root The event list container element.\n */\n const loadMoreEvents = root => {\n const midnight = parseInt(root.attr('data-midnight'), 10);\n const courseId = root.attr('data-course-id');\n const daysOffset = parseInt(root.attr('data-days-offset'), 10);\n const daysLimit = root.attr('data-days-limit');\n const lastId = getOffset(root);\n const eventListWrapper = root.find(SELECTORS.EVENT_LIST_WRAPPER);\n const searchValue = root.closest(SELECTORS.TIMELINE_BLOCK).find(SELECTORS.TIMELINE_SEARCH).val();\n const eventsPromise = loadEventsForLazyLoading(\n root,\n DEFAULT_LAZY_LOADING_ITEMS_OTHER_LOAD,\n midnight,\n lastId,\n courseId,\n daysOffset,\n daysLimit,\n searchValue\n );\n eventsPromise.then(data => {\n if (data.calendarEvents.length) {\n const renderPromise = render(data.calendarEvents);\n const lastTimestamp = getLastTimestamp(root);\n renderPromise.then((html, js) => {\n html = $(html);\n\n // Remove the date heading if it has the same value as the previous one.\n html.find(`[data-timestamp=\"${lastTimestamp}\"]`).remove();\n Templates.appendNodeContents(eventListWrapper, html.html(), js);\n\n if (!data.loadedAll) {\n Templates.render(TEMPLATES.MORE_ACTIVITIES_BUTTON, {}).then(html => {\n eventListWrapper.append(html);\n setLastTimestamp(root, data.calendarEvents.at(-1).timeusermidnight);\n // Init the event handler.\n initEventListener(root);\n\n return html;\n }).catch(() => {\n return false;\n });\n }\n\n return html;\n }).catch(Notification.exception);\n }\n\n return data;\n }).then(() => {\n return disableMoreActivitiesButtonLoading(root);\n }).catch(Notification.exception);\n };\n\n /**\n * Return the offset value for lazy loading fetching.\n *\n * @param {object} element The event list container element.\n * @return {Number} Offset value.\n */\n const getOffset = element => {\n return parseInt(element.attr('data-lazyload-offset'), 10);\n };\n\n /**\n * Set the offset value for lazy loading fetching.\n *\n * @param {object} element The event list container element.\n * @param {Number} offset Offset value.\n */\n const setOffset = (element, offset) => {\n element.attr('data-lazyload-offset', offset);\n };\n\n /**\n * Return the timestamp value for lazy loading fetching.\n *\n * @param {object} element The event list container element.\n * @return {Number} Timestamp value.\n */\n const getLastTimestamp = element => {\n return parseInt(element.attr('data-timestamp'), 10);\n };\n\n /**\n * Set the timestamp value for lazy loading fetching.\n *\n * @param {object} element The event list container element.\n * @param {Number} timestamp Timestamp value.\n */\n const setLastTimestamp = (element, timestamp) => {\n element.attr('data-timestamp', timestamp);\n };\n\n /**\n * Add the \"Show more activities\" button and remove and loading spinner.\n *\n * @param {object} root The event list container element.\n */\n const enableMoreActivitiesButtonLoading = root => {\n const loadMoreButton = root.find(SELECTORS.MORE_ACTIVITIES_BUTTON);\n loadMoreButton.prop('disabled', true);\n Templates.render(TEMPLATES.LOADING_ICON, {}).then(html => {\n loadMoreButton.append(html);\n return html;\n }).catch(() => {\n // It's not important if this false so just do so silently.\n return false;\n });\n };\n\n /**\n * Remove the \"Show more activities\" button and remove and loading spinner.\n *\n * @param {object} root The event list container element.\n */\n const disableMoreActivitiesButtonLoading = root => {\n const loadMoreButtonContainer = root.find(SELECTORS.MORE_ACTIVITIES_BUTTON_CONTAINER);\n loadMoreButtonContainer.remove();\n };\n\n /**\n * Event initialise.\n *\n * @param {object} root The event list container element.\n */\n const initEventListener = root => {\n const loadMoreButton = root.find(SELECTORS.MORE_ACTIVITIES_BUTTON);\n loadMoreButton.on('click', () => {\n enableMoreActivitiesButtonLoading(root);\n loadMoreEvents(root);\n });\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","Str","UserDate","CalendarEventsRepository","Pending","courseview","SELECTORS","EMPTY_MESSAGE","ROOT","EVENT_LIST_CONTENT","EVENT_LIST_WRAPPER","EVENT_LIST_LOADING_PLACEHOLDER","TIMELINE_BLOCK","TIMELINE_SEARCH","MORE_ACTIVITIES_BUTTON","MORE_ACTIVITIES_BUTTON_CONTAINER","TEMPLATES","LOADING_ICON","hideContent","root","find","addClass","removeClass","showContent","emptyContent","empty","buildTemplateContext","calendarEvents","eventsByDay","templateContext","eventsbyday","forEach","calendarEvent","dayTimestamp","timeusermidnight","push","Object","keys","events","render","templateName","load","midnight","limit","daysOffset","daysLimit","lastId","courseId","searchValue","endTime","args","starttime","aftereventid","endtime","searchvalue","courseid","queryByCourse","queryByTime","createLazyLoadingContent","firstLoad","itemLimit","loadEventsForLazyLoading","then","data","length","lastEventId","at","id","lastTimeStamp","resolve","hasContent","loadedAll","catch","exception","eventsPromise","result","overdueFilter","document","querySelector","filterByOverdue","getAttribute","filter","event","eventtype","getUserMidnightForTimestamp","timesort","overdue","pop","setOffset","loadMoreEvents","parseInt","attr","getOffset","eventListWrapper","closest","val","renderPromise","lastTimestamp","getLastTimestamp","html","js","remove","appendNodeContents","append","setLastTimestamp","initEventListener","disableMoreActivitiesButtonLoading","element","offset","timestamp","enableMoreActivitiesButtonLoading","loadMoreButton","prop","loadMoreButtonContainer","on","init","additionalConfig","pendingPromise","Deferred","eventListContent","loadingPlaceholder","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 .\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 \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/str',\n 'core/user_date',\n 'block_timeline/calendar_events_repository',\n 'core/pending'\n],\nfunction(\n $,\n Notification,\n Templates,\n Str,\n UserDate,\n CalendarEventsRepository,\n Pending\n) {\n\n var SECONDS_IN_DAY = 60 * 60 * 24;\n var courseview = false;\n\n var SELECTORS = {\n EMPTY_MESSAGE: '[data-region=\"no-events-empty-message\"]',\n ROOT: '[data-region=\"event-list-container\"]',\n EVENT_LIST_CONTENT: '[data-region=\"event-list-content\"]',\n EVENT_LIST_WRAPPER: '[data-region=\"event-list-wrapper\"]',\n EVENT_LIST_LOADING_PLACEHOLDER: '[data-region=\"event-list-loading-placeholder\"]',\n TIMELINE_BLOCK: '[data-region=\"timeline\"]',\n TIMELINE_SEARCH: '[data-action=\"search\"]',\n MORE_ACTIVITIES_BUTTON: '[data-action=\"more-events\"]',\n MORE_ACTIVITIES_BUTTON_CONTAINER: '[data-region=\"more-events-button-container\"]'\n };\n\n var TEMPLATES = {\n EVENT_LIST_CONTENT: 'block_timeline/event-list-content',\n MORE_ACTIVITIES_BUTTON: 'block_timeline/event-list-loadmore',\n LOADING_ICON: 'core/loading'\n };\n\n /** @type {number} The total items will be shown on the first load. */\n const DEFAULT_LAZY_LOADING_ITEMS_FIRST_LOAD = 5;\n /** @type {number} The total items will be shown when click on the Show more activities button. */\n const DEFAULT_LAZY_LOADING_ITEMS_OTHER_LOAD = 10;\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 * @return {object}\n */\n var buildTemplateContext = function(calendarEvents) {\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 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 * @return {promise} Resolved with HTML and JS strings.\n */\n var render = function(calendarEvents) {\n var templateContext = buildTemplateContext(calendarEvents);\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 * @param {string|undefined} searchValue Search value\n * @return {Promise} A jquery promise\n */\n var load = function(midnight, limit, daysOffset, daysLimit, lastId, courseId, searchValue) {\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 (searchValue) {\n args.searchvalue = searchValue;\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 * Create a lazy-loading region for the calendar events in the given root element.\n *\n * @param {object} root The event list container element.\n * @param {object} additionalConfig Additional config options to pass to pagedContentFactory.\n */\n var init = function(root, additionalConfig = {}) {\n const pendingPromise = new Pending('block/timeline:event-init');\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 const searchValue = root.closest(SELECTORS.TIMELINE_BLOCK).find(SELECTORS.TIMELINE_SEARCH).val();\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 // Create the lazy loading content element.\n return createLazyLoadingContent(root, firstLoad,\n DEFAULT_LAZY_LOADING_ITEMS_FIRST_LOAD, midnight, 0, courseId, daysOffset, daysLimit, searchValue)\n .then(function(html, js) {\n firstLoad.then(function(data) {\n if (!data.hasContent) {\n loadingPlaceholder.addClass('hidden');\n // If we didn't get any data then show the empty data message.\n return hideContent(root);\n }\n\n html = $(html);\n // Hide the content for now.\n html.addClass('hidden');\n // Replace existing elements with the newly created lazy-loading region.\n Templates.replaceNodeContents(eventListContent, html, js);\n\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 (!data.loadedAll) {\n Templates.render(TEMPLATES.MORE_ACTIVITIES_BUTTON, {courseview}).then(function(html) {\n eventListContent.append(html);\n setLastTimestamp(root, data.lastTimeStamp);\n // Init the event handler.\n initEventListener(root);\n return html;\n }).catch(function() {\n return false;\n });\n }\n\n return data;\n })\n .catch(function() {\n return false;\n });\n\n return html;\n }).then(() => {\n return pendingPromise.resolve();\n })\n .catch(Notification.exception);\n };\n\n /**\n * Create a lazy-loading content element for showing the event list for the initial load.\n *\n * @param {object} root The event list container element.\n * @param {object} firstLoad A jQuery promise to be resolved after the first set of data is loaded.\n * @param {int} itemLimit Limit the number of items.\n * @param {Number} midnight The user's midnight time in unix timestamp.\n * @param {int} lastId The last event ID for each loaded page. Page number is key, id is 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 * @param {string|undefined} searchValue Search value.\n * @return {object} jQuery promise resolved with calendar events.\n */\n const createLazyLoadingContent = (root, firstLoad, itemLimit, midnight, lastId,\n courseId, daysOffset, daysLimit, searchValue) => {\n return loadEventsForLazyLoading(\n root,\n itemLimit,\n midnight,\n lastId,\n courseId,\n daysOffset,\n daysLimit,\n searchValue\n ).then(data => {\n if (data.calendarEvents.length) {\n const lastEventId = data.calendarEvents.at(-1).id;\n const lastTimeStamp = data.calendarEvents.at(-1).timeusermidnight;\n firstLoad.resolve({\n hasContent: true,\n lastId: lastEventId,\n lastTimeStamp: lastTimeStamp,\n loadedAll: data.loadedAll\n });\n return render(data.calendarEvents, midnight);\n } else {\n firstLoad.resolve({\n hasContent: false,\n lastId: 0,\n lastTimeStamp: 0,\n loadedAll: true\n });\n return data.calendarEvents;\n }\n }).catch(Notification.exception);\n };\n\n /**\n * Handle the request from the lazy-loading region.\n * Uses the given data like course id, offset... to request the events from the server.\n *\n * @param {object} root The event list container element.\n * @param {int} itemLimit Limit the number of items.\n * @param {Number} midnight The user's midnight time in unix timestamp.\n * @param {int} lastId The last event ID for each loaded page.\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|undefined} searchValue Search value.\n * @return {object} jQuery promise resolved with calendar events.\n */\n const loadEventsForLazyLoading = (root, itemLimit, midnight, lastId, courseId, daysOffset, daysLimit, searchValue) => {\n // Load one more than the given limit so that we can tell if there\n // is more content to load after this.\n const eventsPromise = load(midnight, itemLimit + 1, daysOffset, daysLimit, lastId, courseId, searchValue);\n let calendarEvents = [];\n let loadedAll = true;\n\n return eventsPromise.then(result => {\n if (!result.events.length) {\n return {calendarEvents, loadedAll};\n }\n\n // Determine if the overdue filter is applied.\n const overdueFilter = document.querySelector(\"[data-filtername='overdue']\");\n const filterByOverdue = (overdueFilter && overdueFilter.getAttribute('aria-current'));\n\n calendarEvents = result.events.filter(event => {\n if (event.eventtype == 'open' || event.eventtype == 'opensubmission') {\n const dayTimestamp = UserDate.getUserMidnightForTimestamp(event.timesort, midnight);\n return dayTimestamp > midnight;\n }\n // When filtering by overdue, we fetch all events due today, in case any have elapsed already and are overdue.\n // This means if filtering by overdue, some events fetched might not be required (eg if due later today).\n return (!filterByOverdue || event.overdue);\n });\n\n loadedAll = calendarEvents.length <= itemLimit;\n\n if (!loadedAll) {\n // Remove the last element from the array because it isn't\n // needed in this result set.\n calendarEvents.pop();\n }\n\n if (calendarEvents.length) {\n const lastEventId = calendarEvents.at(-1).id;\n setOffset(root, lastEventId);\n }\n\n return {calendarEvents, loadedAll};\n });\n };\n\n /**\n * Load new events and append to current list.\n *\n * @param {object} root The event list container element.\n */\n const loadMoreEvents = root => {\n const midnight = parseInt(root.attr('data-midnight'), 10);\n const courseId = root.attr('data-course-id');\n const daysOffset = parseInt(root.attr('data-days-offset'), 10);\n const daysLimit = root.attr('data-days-limit');\n const lastId = getOffset(root);\n const eventListWrapper = root.find(SELECTORS.EVENT_LIST_WRAPPER);\n const searchValue = root.closest(SELECTORS.TIMELINE_BLOCK).find(SELECTORS.TIMELINE_SEARCH).val();\n const eventsPromise = loadEventsForLazyLoading(\n root,\n DEFAULT_LAZY_LOADING_ITEMS_OTHER_LOAD,\n midnight,\n lastId,\n courseId,\n daysOffset,\n daysLimit,\n searchValue\n );\n eventsPromise.then(data => {\n if (data.calendarEvents.length) {\n const renderPromise = render(data.calendarEvents);\n const lastTimestamp = getLastTimestamp(root);\n renderPromise.then((html, js) => {\n html = $(html);\n\n // Remove the date heading if it has the same value as the previous one.\n html.find(`[data-timestamp=\"${lastTimestamp}\"]`).remove();\n Templates.appendNodeContents(eventListWrapper, html.html(), js);\n\n if (!data.loadedAll) {\n Templates.render(TEMPLATES.MORE_ACTIVITIES_BUTTON, {}).then(html => {\n eventListWrapper.append(html);\n setLastTimestamp(root, data.calendarEvents.at(-1).timeusermidnight);\n // Init the event handler.\n initEventListener(root);\n\n return html;\n }).catch(() => {\n return false;\n });\n }\n\n return html;\n }).catch(Notification.exception);\n }\n\n return data;\n }).then(() => {\n return disableMoreActivitiesButtonLoading(root);\n }).catch(Notification.exception);\n };\n\n /**\n * Return the offset value for lazy loading fetching.\n *\n * @param {object} element The event list container element.\n * @return {Number} Offset value.\n */\n const getOffset = element => {\n return parseInt(element.attr('data-lazyload-offset'), 10);\n };\n\n /**\n * Set the offset value for lazy loading fetching.\n *\n * @param {object} element The event list container element.\n * @param {Number} offset Offset value.\n */\n const setOffset = (element, offset) => {\n element.attr('data-lazyload-offset', offset);\n };\n\n /**\n * Return the timestamp value for lazy loading fetching.\n *\n * @param {object} element The event list container element.\n * @return {Number} Timestamp value.\n */\n const getLastTimestamp = element => {\n return parseInt(element.attr('data-timestamp'), 10);\n };\n\n /**\n * Set the timestamp value for lazy loading fetching.\n *\n * @param {object} element The event list container element.\n * @param {Number} timestamp Timestamp value.\n */\n const setLastTimestamp = (element, timestamp) => {\n element.attr('data-timestamp', timestamp);\n };\n\n /**\n * Add the \"Show more activities\" button and remove and loading spinner.\n *\n * @param {object} root The event list container element.\n */\n const enableMoreActivitiesButtonLoading = root => {\n const loadMoreButton = root.find(SELECTORS.MORE_ACTIVITIES_BUTTON);\n loadMoreButton.prop('disabled', true);\n Templates.render(TEMPLATES.LOADING_ICON, {}).then(html => {\n loadMoreButton.append(html);\n return html;\n }).catch(() => {\n // It's not important if this false so just do so silently.\n return false;\n });\n };\n\n /**\n * Remove the \"Show more activities\" button and remove and loading spinner.\n *\n * @param {object} root The event list container element.\n */\n const disableMoreActivitiesButtonLoading = root => {\n const loadMoreButtonContainer = root.find(SELECTORS.MORE_ACTIVITIES_BUTTON_CONTAINER);\n loadMoreButtonContainer.remove();\n };\n\n /**\n * Event initialise.\n *\n * @param {object} root The event list container element.\n */\n const initEventListener = root => {\n const loadMoreButton = root.find(SELECTORS.MORE_ACTIVITIES_BUTTON);\n loadMoreButton.on('click', () => {\n enableMoreActivitiesButtonLoading(root);\n loadMoreEvents(root);\n });\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 e9d907644ee..f184a5f8c79 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/templates","block_timeline/event_list","core_course/repository","block_timeline/calendar_events_repository"],function(a,b,c,d,e,f,g){var h={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",TIMELINE_BLOCK:"[data-region=\"timeline\"]",TIMELINE_SEARCH:"[data-action=\"search\"]"},i={COURSE_ITEMS:"block_timeline/course-items",LOADING_ICON:"core/loading"},j=5,k=86400,l={courseview:!0},m=function(a){a.find(h.COURSE_ITEMS_LOADING_PLACEHOLDER).addClass("hidden")},n=function(a){a.find(h.MORE_COURSES_BUTTON_CONTAINER).addClass("hidden")},o=function(a){a.find(h.MORE_COURSES_BUTTON_CONTAINER).removeClass("hidden")},p=function(a){var b=a.find(h.MORE_COURSES_BUTTON);b.prop("disabled",!0);d.render(i.LOADING_ICON,{}).then(function(a){b.append(a);return a}).catch(function(){return!1})},q=function(a){var b=a.find(h.MORE_COURSES_BUTTON);b.prop("disabled",!1);b.find(h.LOADING_ICON).remove()},r=function(a){a.find(h.NO_COURSES_EMPTY_MESSAGE).removeClass("hidden")},s=function(a,b){var c=a.find(h.COURSES_LIST);d.appendNodeContents(c,b,"")},t=function(a){return 0b},G=function(a,b,c,d){var e=a.map(function(a){return a.id});return C(e,b,j+1,c,d)},H=function(a,b,c,e,f){return d.render(i.COURSE_ITEMS,{courses:a,midnight:c,hasdaysoffset:!0,hasdayslimit:f!=void 0,daysoffset:e,dayslimit:f,nodayslimit:f==void 0,courseview:!0}).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 d=u(c),g=w(c);return f.getEnrolledCoursesByTimelineClassification("inprogress",g,d,"fullname asc").then(function(b){var d=Date.now(),f=b.courses,g=b.nextoffset,i=x(c),j=y(c),k=z(c),m=A(c),n=B(c),o=c.closest(h.TIMELINE_BLOCK).find(h.TIMELINE_SEARCH).val();v(c,g);var p=G(f,m,n,o),q=H(f,c,k,i,j);return a.when(p,q).then(function(a){if(F(c,d)){return a}f.forEach(function(a){var b=a.id,d=c.find("[data-region=\"course-events-container\"][data-course-id=\""+b+"\"]"),f=d.find(e.rootSelector);e.init(f,l)});return a})}).catch(b.exception)},J=function(c){var d=Date.now(),f=A(c),g=B(c),i=c.find(h.COURSE_EVENTS_CONTAINER),k=i.map(function(){return a(this).attr("data-course-id")}).get(),m=c.closest(h.TIMELINE_BLOCK).find(h.TIMELINE_SEARCH).val();E(c,d);return C(k,f,j+1,g,m).then(function(b){if(F(c,d)){return b}i.each(function(b,c){c=a(c);var d=c.find(e.rootSelector);e.init(d,l)});return b}).catch(b.exception)},K=function(a){c.define(a,[c.events.activate]);a.on(c.events.activate,h.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/templates","block_timeline/event_list","core_course/repository","block_timeline/calendar_events_repository","core/pending"],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\"]",NO_COURSES_WITH_EVENTS_MESSAGE:"[data-region=\"no-events-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",TIMELINE_BLOCK:"[data-region=\"timeline\"]",TIMELINE_SEARCH:"[data-action=\"search\"]"},j={COURSE_ITEMS:"block_timeline/course-items",LOADING_ICON:"core/loading"},k=86400,l={courseview:!0},m=function(a){a.find(i.COURSE_ITEMS_LOADING_PLACEHOLDER).addClass("hidden")},n=function(a){a.find(i.COURSE_ITEMS_LOADING_PLACEHOLDER).removeClass("hidden")},o=function(a){a.find(i.MORE_COURSES_BUTTON_CONTAINER).addClass("hidden")},p=function(a){a.find(i.MORE_COURSES_BUTTON_CONTAINER).removeClass("hidden")},q=function(a){var b=a.find(i.MORE_COURSES_BUTTON);b.prop("disabled",!0);d.render(j.LOADING_ICON,{}).then(function(a){b.append(a);return a}).catch(function(){return!1})},r=function(a){var b=a.find(i.MORE_COURSES_BUTTON);b.prop("disabled",!1);b.find(i.LOADING_ICON).remove()},s=function(a){var b=a.find(i.COURSES_LIST);d.replaceNodeContents(b,"","");a.find(i.NO_COURSES_WITH_EVENTS_MESSAGE).removeClass("hidden")},t=function(a){a.find(i.NO_COURSES_WITH_EVENTS_MESSAGE).addClass("hidden")},u=function(a,b){var c=2b},H=function(a,b,c,d){var e=a.map(function(a){return a.id});return D(e,b,5+1,c,d)},I=function(a,b,c,e,f,g){return d.render(j.COURSE_ITEMS,{courses:a,midnight:c,hasdaysoffset:!0,hasdayslimit:f!=void 0,daysoffset:e,dayslimit:f,nodayslimit:f==void 0,courseview:!0,hascourses:!0}).then(function(a){m(b);if(a){u(b,a,g)}return a}).then(function(c){if(a.length<2){o(b)}else{p(b)}return c}).catch(function(){m(b)})},J=function(c){var d=1.\n\n/**\n * Manage the timeline courses view for the timeline block.\n *\n * @copyright 2018 Ryan Wyllie \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/templates',\n 'block_timeline/event_list',\n 'core_course/repository',\n 'block_timeline/calendar_events_repository'\n],\nfunction(\n $,\n Notification,\n CustomEvents,\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 TIMELINE_BLOCK: '[data-region=\"timeline\"]',\n TIMELINE_SEARCH: '[data-action=\"search\"]'\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 const additionalConfig = {courseview: true};\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 * @param {string|undefined} searchValue Search value\n * @return {object} jQuery promise.\n */\n var getEventsForCourseIds = function(courseIds, startTime, limit, endTime, searchValue) {\n var args = {\n courseids: courseIds,\n starttime: startTime,\n limit: limit\n };\n\n if (endTime) {\n args.endtime = endTime;\n }\n\n if (searchValue) {\n args.searchvalue = searchValue;\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 * @param {string|undefined} searchValue Search value\n * @return {object} jQuery promise resolved with the events.\n */\n var loadEventsForCourses = function(courses, startTime, endTime, searchValue) {\n var courseIds = courses.map(function(course) {\n return course.id;\n });\n\n return getEventsForCourseIds(courseIds, startTime, COURSE_EVENT_LIMIT + 1, endTime, searchValue);\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 * @return {object} jQuery promise resolved after rendering is complete.\n */\n var updateDisplayFromCourses = function(courses, root, midnight, daysOffset, daysLimit) {\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 courseview: true,\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 const searchValue = root.closest(SELECTORS.TIMELINE_BLOCK).find(SELECTORS.TIMELINE_SEARCH).val();\n\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, searchValue);\n // Render the courses in the DOM.\n var renderPromise = updateDisplayFromCourses(courses, root, midnight, daysOffset, daysLimit);\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 containerSelector = '[data-region=\"course-events-container\"][data-course-id=\"' + courseId + '\"]';\n var courseEventsContainer = root.find(containerSelector);\n var eventListRoot = courseEventsContainer.find(EventList.rootSelector);\n\n EventList.init(eventListRoot, additionalConfig);\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 const searchValue = root.closest(SELECTORS.TIMELINE_BLOCK).find(SELECTORS.TIMELINE_SEARCH).val();\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, searchValue)\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 eventListContainer = container.find(EventList.rootSelector);\n\n EventList.init(eventListContainer, additionalConfig);\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","Templates","EventList","CourseRepository","EventsRepository","Pending","SELECTORS","MORE_COURSES_BUTTON","MORE_COURSES_BUTTON_CONTAINER","NO_COURSES_EMPTY_MESSAGE","NO_COURSES_WITH_EVENTS_MESSAGE","COURSES_LIST","COURSE_ITEMS_LOADING_PLACEHOLDER","COURSE_EVENTS_CONTAINER","COURSE_NAME","LOADING_ICON","TIMELINE_BLOCK","TIMELINE_SEARCH","TEMPLATES","COURSE_ITEMS","SECONDS_IN_DAY","additionalConfig","courseview","hideLoadingPlaceholder","root","find","addClass","showLoadingPlaceholder","removeClass","hideMoreCoursesButton","showMoreCoursesButton","enableMoreCoursesButtonLoading","button","prop","render","then","html","append","catch","disableMoreCoursesButtonLoading","remove","showNoCoursesWithEventsMessage","container","replaceNodeContents","hideNoCoursesWithEventsMessage","renderCourseItemsHTML","appendNodeContents","getOffset","parseInt","attr","setOffset","offset","getLimit","getDaysOffset","getDaysLimit","daysLimit","getMidnight","getStartTime","midnight","daysOffset","getEndTime","getEventsForCourseIds","courseIds","startTime","limit","endTime","searchValue","args","courseids","starttime","endtime","searchvalue","queryByCourses","getEventReloadTime","data","setEventReloadTime","time","hasReloadedEventsSince","loadEventsForCourses","courses","map","course","id","updateDisplayFromCourses","hasdaysoffset","hasdayslimit","daysoffset","dayslimit","nodayslimit","hascourses","length","loadMoreCourses","pendingPromise","closest","val","getEnrolledCoursesWithEventsByTimelineClassification","result","startEventLoadingTime","Date","now","nextOffset","nextoffset","moreCoursesAvailable","morecoursesavailable","eventsPromise","renderPromise","when","eventsByCourse","forEach","courseId","courseEventsContainer","eventListRoot","rootSelector","init","resolve","exception","registerEventListeners","events","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 .\n\n/**\n * Manage the timeline courses view for the timeline block.\n *\n * @copyright 2018 Ryan Wyllie \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/templates',\n 'block_timeline/event_list',\n 'core_course/repository',\n 'block_timeline/calendar_events_repository',\n 'core/pending'\n],\nfunction(\n $,\n Notification,\n CustomEvents,\n Templates,\n EventList,\n CourseRepository,\n EventsRepository,\n Pending\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 NO_COURSES_WITH_EVENTS_MESSAGE: '[data-region=\"no-events-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 TIMELINE_BLOCK: '[data-region=\"timeline\"]',\n TIMELINE_SEARCH: '[data-action=\"search\"]'\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 const additionalConfig = {courseview: true};\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 * Show the loading placeholder elements.\n *\n * @param {object} root The rool element.\n */\n const showLoadingPlaceholder = function(root) {\n root.find(SELECTORS.COURSE_ITEMS_LOADING_PLACEHOLDER).removeClass('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 courses have no events available (within the current filtering).\n *\n * @param {object} root The rool element.\n */\n const showNoCoursesWithEventsMessage = function(root) {\n // Remove any course list contents, since we will display the no events message.\n const container = root.find(SELECTORS.COURSES_LIST);\n Templates.replaceNodeContents(container, '', '');\n root.find(SELECTORS.NO_COURSES_WITH_EVENTS_MESSAGE).removeClass('hidden');\n };\n\n /**\n * Hide the message for when courses have no events available (within the current filtering).\n *\n * @param {object} root The rool element.\n */\n const hideNoCoursesWithEventsMessage = function(root) {\n root.find(SELECTORS.NO_COURSES_WITH_EVENTS_MESSAGE).addClass('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 * @param {boolean} append Whether the HTML should be appended (eg pressed \"show more courses\").\n * Defaults to false - replaces the existing content (eg when modifying filter values).\n */\n var renderCourseItemsHTML = function(root, html, append = false) {\n var container = root.find(SELECTORS.COURSES_LIST);\n\n if (append) {\n Templates.appendNodeContents(container, html, '');\n } else {\n Templates.replaceNodeContents(container, html, '');\n }\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) : null;\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 * @param {string|undefined} searchValue Search value\n * @return {object} jQuery promise.\n */\n var getEventsForCourseIds = function(courseIds, startTime, limit, endTime, searchValue) {\n var args = {\n courseids: courseIds,\n starttime: startTime,\n limit: limit\n };\n\n if (endTime) {\n args.endtime = endTime;\n }\n\n if (searchValue) {\n args.searchvalue = searchValue;\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 * @param {string|undefined} searchValue Search value\n * @return {object} jQuery promise resolved with the events.\n */\n var loadEventsForCourses = function(courses, startTime, endTime, searchValue) {\n var courseIds = courses.map(function(course) {\n return course.id;\n });\n\n return getEventsForCourseIds(courseIds, startTime, COURSE_EVENT_LIMIT + 1, endTime, searchValue);\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 {boolean} append Whether new content should be appended instead of replaced (eg \"show more courses\").\n * @return {object} jQuery promise resolved after rendering is complete.\n */\n var updateDisplayFromCourses = function(courses, root, midnight, daysOffset, daysLimit, append) {\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 courseview: true,\n hascourses: true\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, append);\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 * @param {boolean} append Whether content should be appended instead of replaced (eg \"show more courses\"). False by default.\n * @return {object} jQuery promise resolved with courses and events.\n */\n var loadMoreCourses = function(root, append = false) {\n const pendingPromise = new Pending('block/timeline:load-more-courses');\n var offset = getOffset(root);\n var limit = getLimit(root);\n const startTime = getStartTime(root);\n const endTime = getEndTime(root);\n const searchValue = root.closest(SELECTORS.TIMELINE_BLOCK).find(SELECTORS.TIMELINE_SEARCH).val();\n\n // Start loading the next set of courses.\n // Fetch up to limit number of courses with at least one action event in the time filtering specified.\n // Courses without events will also be fetched, but hidden in case they have events in other timespans.\n return CourseRepository.getEnrolledCoursesWithEventsByTimelineClassification(\n COURSE_CLASSIFICATION,\n limit,\n offset,\n COURSE_SORT,\n searchValue,\n startTime,\n endTime\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 const moreCoursesAvailable = result.morecoursesavailable;\n\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, searchValue);\n // Render the courses in the DOM.\n var renderPromise = updateDisplayFromCourses(courses, root, midnight, daysOffset, daysLimit, append);\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 if (courses.length > 0) {\n // Render the events in the correct course event list.\n courses.forEach(function(course) {\n const courseId = course.id;\n const containerSelector = '[data-region=\"course-events-container\"][data-course-id=\"' + courseId + '\"]';\n const courseEventsContainer = root.find(containerSelector);\n const eventListRoot = courseEventsContainer.find(EventList.rootSelector);\n\n EventList.init(eventListRoot, additionalConfig);\n });\n\n if (!moreCoursesAvailable) {\n // If no more courses with events matching the current filtering exist, hide the more courses button.\n hideMoreCoursesButton(root);\n } else {\n // If more courses exist with events matching the current filtering, show the more courses button.\n showMoreCoursesButton(root);\n }\n } else {\n // No more courses to load, hide the more courses button.\n hideMoreCoursesButton(root);\n\n // A zero offset means this was not loading \"more courses\", so we need to display the no results message.\n if (offset == 0) {\n showNoCoursesWithEventsMessage(root);\n }\n }\n\n return eventsByCourse;\n });\n }).then(() => {\n return pendingPromise.resolve();\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\" button.\n root.on(CustomEvents.events.activate, SELECTORS.MORE_COURSES_BUTTON, function(e, data) {\n enableMoreCoursesButtonLoading(root);\n loadMoreCourses(root, true)\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 // Only need to handle course loading if the user is actively enrolled in a course.\n if (!root.find(SELECTORS.NO_COURSES_EMPTY_MESSAGE).length) {\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 /**\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\n setOffset(root, 0);\n showLoadingPlaceholder(root);\n hideNoCoursesWithEventsMessage(root);\n root.removeAttr('data-seen');\n\n if (root.hasClass('active')) {\n shown(root);\n }\n };\n\n /**\n * Begin loading the events unless we know there are no actively enrolled courses.\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') && !root.find(SELECTORS.NO_COURSES_EMPTY_MESSAGE).length) {\n loadMoreCourses(root);\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 f2977ef1fd9..f4042dc0b64 100644 --- a/blocks/timeline/amd/src/event_list.js +++ b/blocks/timeline/amd/src/event_list.js @@ -45,7 +45,7 @@ function( var courseview = false; var SELECTORS = { - EMPTY_MESSAGE: '[data-region="empty-message"]', + EMPTY_MESSAGE: '[data-region="no-events-empty-message"]', ROOT: '[data-region="event-list-container"]', EVENT_LIST_CONTENT: '[data-region="event-list-content"]', EVENT_LIST_WRAPPER: '[data-region="event-list-wrapper"]', diff --git a/blocks/timeline/amd/src/view_courses.js b/blocks/timeline/amd/src/view_courses.js index 56d94f9db2c..218e4e8a07a 100644 --- a/blocks/timeline/amd/src/view_courses.js +++ b/blocks/timeline/amd/src/view_courses.js @@ -28,7 +28,8 @@ define( 'core/templates', 'block_timeline/event_list', 'core_course/repository', - 'block_timeline/calendar_events_repository' + 'block_timeline/calendar_events_repository', + 'core/pending' ], function( $, @@ -37,13 +38,15 @@ function( Templates, EventList, CourseRepository, - EventsRepository + EventsRepository, + Pending ) { var SELECTORS = { MORE_COURSES_BUTTON: '[data-action="more-courses"]', MORE_COURSES_BUTTON_CONTAINER: '[data-region="more-courses-button-container"]', NO_COURSES_EMPTY_MESSAGE: '[data-region="no-courses-empty-message"]', + NO_COURSES_WITH_EVENTS_MESSAGE: '[data-region="no-events-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"]', @@ -75,6 +78,15 @@ function( root.find(SELECTORS.COURSE_ITEMS_LOADING_PLACEHOLDER).addClass('hidden'); }; + /** + * Show the loading placeholder elements. + * + * @param {object} root The rool element. + */ + const showLoadingPlaceholder = function(root) { + root.find(SELECTORS.COURSE_ITEMS_LOADING_PLACEHOLDER).removeClass('hidden'); + }; + /** * Hide the "more courses" button. * @@ -124,12 +136,24 @@ function( }; /** - * Display the message for when there are no courses available. + * Display the message for when courses have no events available (within the current filtering). * * @param {object} root The rool element. */ - var showNoCoursesEmptyMessage = function(root) { - root.find(SELECTORS.NO_COURSES_EMPTY_MESSAGE).removeClass('hidden'); + const showNoCoursesWithEventsMessage = function(root) { + // Remove any course list contents, since we will display the no events message. + const container = root.find(SELECTORS.COURSES_LIST); + Templates.replaceNodeContents(container, '', ''); + root.find(SELECTORS.NO_COURSES_WITH_EVENTS_MESSAGE).removeClass('hidden'); + }; + + /** + * Hide the message for when courses have no events available (within the current filtering). + * + * @param {object} root The rool element. + */ + const hideNoCoursesWithEventsMessage = function(root) { + root.find(SELECTORS.NO_COURSES_WITH_EVENTS_MESSAGE).addClass('hidden'); }; /** @@ -137,20 +161,17 @@ function( * * @param {object} root The rool element. * @param {string} html The course items HTML to render. + * @param {boolean} append Whether the HTML should be appended (eg pressed "show more courses"). + * Defaults to false - replaces the existing content (eg when modifying filter values). */ - var renderCourseItemsHTML = function(root, html) { + var renderCourseItemsHTML = function(root, html, append = false) { var container = root.find(SELECTORS.COURSES_LIST); - Templates.appendNodeContents(container, html, ''); - }; - /** - * Check if any courses have been loaded. - * - * @param {object} root The rool element. - * @return {bool} - */ - var hasLoadedCourses = function(root) { - return root.find(SELECTORS.COURSE_EVENTS_CONTAINER).length > 0; + if (append) { + Templates.appendNodeContents(container, html, ''); + } else { + Templates.replaceNodeContents(container, html, ''); + } }; /** @@ -241,7 +262,7 @@ function( var getEndTime = function(root) { var midnight = getMidnight(root); var daysLimit = getDaysLimit(root); - return daysLimit != undefined ? midnight + (daysLimit * SECONDS_IN_DAY) : false; + return daysLimit != undefined ? midnight + (daysLimit * SECONDS_IN_DAY) : null; }; /** @@ -330,9 +351,10 @@ function( * @param {Number} midnight The midnight timestamp in the user's timezone. * @param {Number} daysOffset Number of days from today to offset the events. * @param {Number} daysLimit Number of days from today to limit the events to. + * @param {boolean} append Whether new content should be appended instead of replaced (eg "show more courses"). * @return {object} jQuery promise resolved after rendering is complete. */ - var updateDisplayFromCourses = function(courses, root, midnight, daysOffset, daysLimit) { + var updateDisplayFromCourses = function(courses, root, midnight, daysOffset, daysLimit, append) { // Render the courses template. return Templates.render(TEMPLATES.COURSE_ITEMS, { courses: courses, @@ -343,19 +365,14 @@ function( dayslimit: daysLimit, nodayslimit: daysLimit == undefined, courseview: true, + hascourses: true }).then(function(html) { hideLoadingPlaceholder(root); if (html) { // Template rendering is complete and we have the HTML so we can // add it to the DOM. - renderCourseItemsHTML(root, html); - } else { - if (!hasLoadedCourses(root)) { - // There were no courses to render so show the empty placeholder - // message for the user to tell them. - showNoCoursesEmptyMessage(root); - } + renderCourseItemsHTML(root, html, append); } return html; @@ -382,18 +399,28 @@ function( * list module to being loading the events for the course block. * * @param {object} root The root element for the timeline courses view. + * @param {boolean} append Whether content should be appended instead of replaced (eg "show more courses"). False by default. * @return {object} jQuery promise resolved with courses and events. */ - var loadMoreCourses = function(root) { + var loadMoreCourses = function(root, append = false) { + const pendingPromise = new Pending('block/timeline:load-more-courses'); var offset = getOffset(root); var limit = getLimit(root); + const startTime = getStartTime(root); + const endTime = getEndTime(root); + const searchValue = root.closest(SELECTORS.TIMELINE_BLOCK).find(SELECTORS.TIMELINE_SEARCH).val(); // Start loading the next set of courses. - return CourseRepository.getEnrolledCoursesByTimelineClassification( + // Fetch up to limit number of courses with at least one action event in the time filtering specified. + // Courses without events will also be fetched, but hidden in case they have events in other timespans. + return CourseRepository.getEnrolledCoursesWithEventsByTimelineClassification( COURSE_CLASSIFICATION, limit, offset, - COURSE_SORT + COURSE_SORT, + searchValue, + startTime, + endTime ).then(function(result) { var startEventLoadingTime = Date.now(); var courses = result.courses; @@ -401,16 +428,14 @@ function( var daysOffset = getDaysOffset(root); var daysLimit = getDaysLimit(root); var midnight = getMidnight(root); - var startTime = getStartTime(root); - var endTime = getEndTime(root); - const searchValue = root.closest(SELECTORS.TIMELINE_BLOCK).find(SELECTORS.TIMELINE_SEARCH).val(); + const moreCoursesAvailable = result.morecoursesavailable; // Record the next offset if we want to request more courses. setOffset(root, nextOffset); // Load the events for these courses. var eventsPromise = loadEventsForCourses(courses, startTime, endTime, searchValue); // Render the courses in the DOM. - var renderPromise = updateDisplayFromCourses(courses, root, midnight, daysOffset, daysLimit); + var renderPromise = updateDisplayFromCourses(courses, root, midnight, daysOffset, daysLimit, append); return $.when(eventsPromise, renderPromise) .then(function(eventsByCourse) { @@ -419,61 +444,41 @@ function( return eventsByCourse; } - // When we've got all of the courses and events we can render the events in the - // correct course event list. - courses.forEach(function(course) { - var courseId = course.id; - var containerSelector = '[data-region="course-events-container"][data-course-id="' + courseId + '"]'; - var courseEventsContainer = root.find(containerSelector); - var eventListRoot = courseEventsContainer.find(EventList.rootSelector); + if (courses.length > 0) { + // Render the events in the correct course event list. + courses.forEach(function(course) { + const courseId = course.id; + const containerSelector = '[data-region="course-events-container"][data-course-id="' + courseId + '"]'; + const courseEventsContainer = root.find(containerSelector); + const eventListRoot = courseEventsContainer.find(EventList.rootSelector); - EventList.init(eventListRoot, additionalConfig); - }); + EventList.init(eventListRoot, additionalConfig); + }); + + if (!moreCoursesAvailable) { + // If no more courses with events matching the current filtering exist, hide the more courses button. + hideMoreCoursesButton(root); + } else { + // If more courses exist with events matching the current filtering, show the more courses button. + showMoreCoursesButton(root); + } + } else { + // No more courses to load, hide the more courses button. + hideMoreCoursesButton(root); + + // A zero offset means this was not loading "more courses", so we need to display the no results message. + if (offset == 0) { + showNoCoursesWithEventsMessage(root); + } + } return eventsByCourse; }); + }).then(() => { + return pendingPromise.resolve(); }).catch(Notification.exception); }; - /** - * Reload the events for all of the visible courses. These events will be loaded - * in a single request to the server. - * - * @param {object} root The root element. - * @return {object} jQuery promise resolved with courses and events. - */ - var reloadCourseEvents = function(root) { - var startReloadTime = Date.now(); - var startTime = getStartTime(root); - var endTime = getEndTime(root); - var courseEventsContainers = root.find(SELECTORS.COURSE_EVENTS_CONTAINER); - var courseIds = courseEventsContainers.map(function() { - return $(this).attr('data-course-id'); - }).get(); - const searchValue = root.closest(SELECTORS.TIMELINE_BLOCK).find(SELECTORS.TIMELINE_SEARCH).val(); - - // Record when we started our request. - setEventReloadTime(root, startReloadTime); - - // Load all of the events for the given courses. - return getEventsForCourseIds(courseIds, startTime, COURSE_EVENT_LIMIT + 1, endTime, searchValue) - .then(function(eventsByCourse) { - if (hasReloadedEventsSince(root, startReloadTime)) { - // A new reload has begun so ignore our results. - return eventsByCourse; - } - - courseEventsContainers.each(function(index, container) { - container = $(container); - var eventListContainer = container.find(EventList.rootSelector); - - EventList.init(eventListContainer, additionalConfig); - }); - - return eventsByCourse; - }).catch(Notification.exception); - }; - /** * Add event listeners to load more courses for the courses view. * @@ -481,11 +486,10 @@ function( */ var registerEventListeners = function(root) { CustomEvents.define(root, [CustomEvents.events.activate]); - // Show more courses and load their events when the user clicks the "more courses" - // button. + // Show more courses and load their events when the user clicks the "more courses" button. root.on(CustomEvents.events.activate, SELECTORS.MORE_COURSES_BUTTON, function(e, data) { enableMoreCoursesButtonLoading(root); - loadMoreCourses(root) + loadMoreCourses(root, true) .then(function() { disableMoreCoursesButtonLoading(root); return; @@ -514,15 +518,18 @@ function( var init = function(root) { root = $(root); - setEventReloadTime(root, Date.now()); + // Only need to handle course loading if the user is actively enrolled in a course. + if (!root.find(SELECTORS.NO_COURSES_EMPTY_MESSAGE).length) { + setEventReloadTime(root, Date.now()); - if (root.hasClass('active')) { - // Only load if this is active otherwise it will be lazy loaded later. - loadMoreCourses(root); - root.attr('data-seen', true); + if (root.hasClass('active')) { + // Only load if this is active otherwise it will be lazy loaded later. + loadMoreCourses(root); + root.attr('data-seen', true); + } + + registerEventListeners(root); } - - registerEventListeners(root); }; /** @@ -532,29 +539,25 @@ function( * @param {object} root The root element for the timeline courses view. */ var reset = function(root) { + + setOffset(root, 0); + showLoadingPlaceholder(root); + hideNoCoursesWithEventsMessage(root); root.removeAttr('data-seen'); + if (root.hasClass('active')) { shown(root); } }; /** - * If this is the first time this view has been displayed then begin loading - * the events. + * Begin loading the events unless we know there are no actively enrolled courses. * * @param {object} root The root element for the timeline courses view. */ var shown = function(root) { - if (!root.attr('data-seen')) { - if (hasLoadedCourses(root)) { - // This isn't the first time this view is shown so just reload the - // events for the courses we've already loaded. - reloadCourseEvents(root); - } else { - // We haven't loaded any courses yet so do that now. - loadMoreCourses(root); - } - + if (!root.attr('data-seen') && !root.find(SELECTORS.NO_COURSES_EMPTY_MESSAGE).length) { + loadMoreCourses(root); root.attr('data-seen', true); } }; diff --git a/blocks/timeline/templates/event-list.mustache b/blocks/timeline/templates/event-list.mustache index 26d25d3d78d..b10f5cc3932 100644 --- a/blocks/timeline/templates/event-list.mustache +++ b/blocks/timeline/templates/event-list.mustache @@ -42,19 +42,5 @@
- {{#urls.noevents}} - - {{/urls.noevents}} - {{^urls.noevents}} - - {{/urls.noevents}} + {{> block_timeline/no-events }} diff --git a/blocks/timeline/templates/no-courses.mustache b/blocks/timeline/templates/no-courses.mustache new file mode 100644 index 00000000000..f7bb7dfc1b3 --- /dev/null +++ b/blocks/timeline/templates/no-courses.mustache @@ -0,0 +1,39 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/no-courses + + This template renders a notice that the user has no active courses. + + Example context (json): + { + "hascourses": false, + "urls": { + "noevents": "#" + } + } +}} +{{^hascourses}} +
+ +

{{#str}} nocoursesinprogress, block_timeline {{/str}}

+
+{{/hascourses}} diff --git a/blocks/timeline/templates/no-events.mustache b/blocks/timeline/templates/no-events.mustache new file mode 100644 index 00000000000..61cca4e3fa0 --- /dev/null +++ b/blocks/timeline/templates/no-events.mustache @@ -0,0 +1,38 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/no-events + + This template renders confirmation that no events require action. + + Example context (json): + { + "urls": { + "noevents": "#" + } + } +}} +{{#urls.noevents}} + +{{/urls.noevents}} diff --git a/blocks/timeline/templates/view-courses.mustache b/blocks/timeline/templates/view-courses.mustache index 1749d4018ca..b1c2b9c3d3e 100644 --- a/blocks/timeline/templates/view-courses.mustache +++ b/blocks/timeline/templates/view-courses.mustache @@ -22,13 +22,15 @@ Example context (json): {} }} -
-
    - {{> block_timeline/course-item-loading-placeholder }} - {{> block_timeline/course-item-loading-placeholder }} -
-
-
+{{#hascourses}} +
+
    + {{> block_timeline/course-item-loading-placeholder }} + {{> block_timeline/course-item-loading-placeholder }} +
+
+
+{{/hascourses}}
    - +{{> block_timeline/no-events }} +{{> block_timeline/no-courses }} diff --git a/blocks/timeline/tests/behat/block_timeline_courses.feature b/blocks/timeline/tests/behat/block_timeline_courses.feature index 94d17ad209e..76b6032edae 100644 --- a/blocks/timeline/tests/behat/block_timeline_courses.feature +++ b/blocks/timeline/tests/behat/block_timeline_courses.feature @@ -44,7 +44,7 @@ Feature: The timeline block allows users to see upcoming courses When I click on "Sort by courses" "link" in the "Timeline" "block" Then I should see "Course 1" in the ".block_timeline [data-region='view-courses']" "css_element" And I should see "Course 2" in the ".block_timeline [data-region='view-courses']" "css_element" - And I should see "Show more courses" in the "Timeline" "block" + And I should not see "Show more courses" in the "Timeline" "block" And "Test choice 1" "link" should exist in the "Timeline" "block" And I should see "Choice closes" in the "Timeline" "block" And "Test feedback 1" "link" should exist in the "Timeline" "block" @@ -92,7 +92,7 @@ Feature: The timeline block allows users to see upcoming courses When I reload the page Then I should see "Course 1" in the "Timeline" "block" And I should see "Course 2" in the "Timeline" "block" - And I should see "Show more courses" in the "Timeline" "block" + And I should not see "Show more courses" in the "Timeline" "block" And "Test choice 1" "link" should exist in the "Timeline" "block" And "Test feedback 1" "link" should exist in the "Timeline" "block" And "Test feedback 5" "link" should exist in the "Timeline" "block" @@ -111,6 +111,9 @@ Feature: The timeline block allows users to see upcoming courses And I click on "Filter timeline by date" "button" in the "Timeline" "block" And I click on "All" "link" in the "Timeline" "block" When I reload the page + And I should not see "Course 3" in the "Timeline" "block" + And I should see "Course 2" in the "Timeline" "block" + And I should see "Course 1" in the "Timeline" "block" And I click on "Show more courses" "button" in the "Timeline" "block" Then I should see "Course 3" in the "Timeline" "block" And I should see "Course 2" in the "Timeline" "block" @@ -180,3 +183,50 @@ Feature: The timeline block allows users to see upcoming courses And "Test choice 1" "link" should not exist in the "Timeline" "block" And "Test choice 2" "link" should not exist in the "Timeline" "block" And "Test choice 3" "link" should not exist in the "Timeline" "block" + + Scenario: Student not enrolled in any courses sees a message + Given I log in as "student2" + When I click on "Sort timeline items" "button" in the "Timeline" "block" + And I click on "Sort by courses" "link" in the "Timeline" "block" + Then I should see "No in-progress courses" in the "Timeline" "block" + And I should not see "Test choice 1" + + Scenario: Only courses with matching events are displayed and are refreshed when filtering changes + Given I log in as "student1" + And I click on "Sort timeline items" "button" in the "Timeline" "block" + And I click on "Sort by courses" "link" in the "Timeline" "block" + And I click on "Filter timeline by date" "button" in the "Timeline" "block" + And I click on "All" "link" in the "Timeline" "block" + And I click on "Show more courses" "button" in the "Timeline" "block" + And I should see "Course 1" in the ".block-timeline [data-region='view-courses']" "css_element" + And I should see "Course 2" in the ".block-timeline [data-region='view-courses']" "css_element" + And I should see "Course 3" in the ".block-timeline [data-region='view-courses']" "css_element" + And "Test assign 1" "link" should exist in the ".block-timeline [data-region='view-courses']" "css_element" + And "Test assign 2" "link" should exist in the ".block-timeline [data-region='view-courses']" "css_element" + And "Test choice 1" "link" should exist in the ".block-timeline [data-region='view-courses']" "css_element" + And "Test choice 3" "link" should exist in the ".block-timeline [data-region='view-courses']" "css_element" + And "Test feedback 5" "link" should exist in the ".block-timeline [data-region='view-courses']" "css_element" + And I click on "Filter timeline by date" "button" in the "Timeline" "block" + When I click on "Overdue" "link" in the "Timeline" "block" + Then I should see "Course 1" in the ".block-timeline [data-region='view-courses']" "css_element" + And I should see "Course 2" in the ".block-timeline [data-region='view-courses']" "css_element" + And I should not see "Course 3" in the "Timeline" "block" + And I should not see "Show more courses" in the "Timeline" "block" + And "Test assign 1" "link" should exist in the "Timeline" "block" + And "Test assign 2" "link" should exist in the "Timeline" "block" + And "Test choice 1" "link" should not exist in the ".block-timeline [data-region='view-courses']" "css_element" + And "Test choice 2" "link" should not exist in the ".block-timeline [data-region='view-courses']" "css_element" + And "Test choice 3" "link" should not exist in the ".block-timeline [data-region='view-courses']" "css_element" + And "Test feedback 5" "link" should not exist in the ".block-timeline [data-region='view-courses']" "css_element" + And I click on "Filter timeline by date" "button" in the "Timeline" "block" + And I click on "Next 7 days" "link" in the "Timeline" "block" + And I should see "Course 1" in the ".block-timeline [data-region='view-courses']" "css_element" + And I should see "Course 2" in the ".block-timeline [data-region='view-courses']" "css_element" + And I should not see "Course 3" in the "Timeline" "block" + And I should not see "Show more courses" in the "Timeline" "block" + And "Test assign 2" "link" should exist in the ".block-timeline [data-region='view-courses']" "css_element" + And "Test choice 1" "link" should exist in the ".block-timeline [data-region='view-courses']" "css_element" + And "Test feedback 5" "link" should exist in the ".block-timeline [data-region='view-courses']" "css_element" + And "Test assign 1" "link" should not exist in the ".block-timeline [data-region='view-courses']" "css_element" + And "Test choice 2" "link" should not exist in the ".block-timeline [data-region='view-courses']" "css_element" + And "Test choice 3" "link" should not exist in the ".block-timeline [data-region='view-courses']" "css_element" diff --git a/blocks/timeline/tests/behat/block_timeline_search.feature b/blocks/timeline/tests/behat/block_timeline_search.feature index 1b364dde6a2..5fa95d82dca 100644 --- a/blocks/timeline/tests/behat/block_timeline_search.feature +++ b/blocks/timeline/tests/behat/block_timeline_search.feature @@ -5,22 +5,26 @@ Feature: The timeline block allows users to search for upcoming activities Background: Given the following "users" exist: - | username | firstname | lastname | email | idnumber | + | username | firstname | lastname | email | idnumber | | student1 | Student | 1 | student1@example.com | S1 | And the following "courses" exist: - | fullname | shortname | category | startdate | enddate | - | Course 1 | C1 | 0 | ##1 month ago## | ##15 days ago## | - | Course 2 | C2 | 0 | ##yesterday## | ##tomorrow## | + | fullname | shortname | category | startdate | enddate | + | Course 1 | C1 | 0 | ##1 month ago## | ##15 days ago## | + | Course 2 | C2 | 0 | ##yesterday## | ##tomorrow## | | Course 3 | C3 | 0 | ##first day of next month## | ##last day of next month## | + | Course 4 | C4 | 0 | ##1 month ago## | ##tomorrow## | + | Course 5 | C5 | 0 | ##first day of last month## | ##last day of next month## | And the following "activities" exist: - | activity | course | idnumber | name | intro | timeopen | timeclose | - | choice | C2 | choice1 | Test choice 1 | Test choice description | ##yesterday## | ##tomorrow## | + | activity | course | idnumber | name | intro | timeopen | timeclose | + | choice | C2 | choice1 | Test choice 1 | Test choice description | ##yesterday## | ##tomorrow## | | choice | C1 | choice2 | Test choice 2 | Test choice description | ##first day of +5 months## | ##last day of +5 months## | | choice | C3 | choice3 | Test choice 3 | Test choice description | ##first day of +5 months## | ##last day of +10 months## | | choice | C2 | choice4 | Test choice 4 | Test choice description | ##first day of +5 months## | ##last day of +15 months## | | choice | C1 | choice5 | Test choice 5 | Test choice description | ##first day of +5 months## | ##last day of +20 months## | | choice | C3 | choice6 | Test choice 6 | Test choice description | ##first day of +5 months## | ##last day of +25 months## | - | feedback | C2 | feedback1 | Test feedback 1 | Test feedback description | ##yesterday## | ##tomorrow## | + | choice | C4 | choice7 | Test choice 7 | Test choice description | ##first day of +5 months## | ##last day of +5 months## | + | choice | C5 | choice8 | Test choice 8 | Test choice description | ##first day of +5 months## | ##last day of +10 months## | + | feedback | C2 | feedback1 | Test feedback 1 | Test feedback description | ##yesterday## | ##tomorrow## | | feedback | C1 | feedback2 | Test feedback 2 | Test feedback description | ##first day of +5 months## | ##last day of +5 months## | | feedback | C3 | feedback3 | Test feedback 3 | Test feedback description | ##first day of +5 months## | ##last day of +10 months## | And the following "course enrolments" exist: @@ -28,6 +32,8 @@ Feature: The timeline block allows users to search for upcoming activities | student1 | C1 | student | | student1 | C2 | student | | student1 | C3 | student | + | student1 | C4 | student | + | student1 | C5 | student | Scenario: The search should return no events if I enter the wrong value Given I log in as "student1" @@ -88,8 +94,42 @@ Feature: The timeline block allows users to search for upcoming activities Then I should see "Test choice 1" in the "Timeline" "block" And I should see "Test choice 2" in the "Timeline" "block" And I should see "Test choice 3" in the "Timeline" "block" - And I should see "Test choice 4" in the "Timeline" "block" - And I should see "Test choice 5" in the "Timeline" "block" + And I should see "Test choice 7" in the "Timeline" "block" + And I should see "Test choice 8" in the "Timeline" "block" + And I should not see "Test choice 4" in the "Timeline" "block" + And I should not see "Test choice 5" in the "Timeline" "block" And I should not see "Test choice 6" in the "Timeline" "block" And I click on "Show more activities" "button" + And I should see "Test choice 4" in the "Timeline" "block" + And I should see "Test choice 5" in the "Timeline" "block" And I should see "Test choice 6" in the "Timeline" "block" + + Scenario: Courses view is refreshed when search changes + Given I log in as "student1" + And I click on "Sort timeline items" "button" in the "Timeline" "block" + And I click on "Sort by courses" "link" in the "Timeline" "block" + And I click on "Filter timeline by date" "button" in the "Timeline" "block" + And I click on "All" "link" in the "Timeline" "block" + And I click on "Show more courses" "button" in the "Timeline" "block" + And I should see "Course 2" in the ".block-timeline [data-region='view-courses']" "css_element" + And I should see "Course 4" in the ".block-timeline [data-region='view-courses']" "css_element" + And I should see "Course 5" in the ".block-timeline [data-region='view-courses']" "css_element" + And I should see "Test choice 1" in the ".block-timeline [data-region='view-courses']" "css_element" + And I should see "Test choice 7" in the ".block-timeline [data-region='view-courses']" "css_element" + And I should see "Test choice 8" in the ".block-timeline [data-region='view-courses']" "css_element" + And I should not see "Course 1" in the ".block-timeline [data-region='view-courses']" "css_element" + And I should not see "Course 3" in the ".block-timeline [data-region='view-courses']" "css_element" + And I should not see "Test choice 2" in the ".block-timeline [data-region='view-courses']" "css_element" + And I should not see "Test choice 3" in the ".block-timeline [data-region='view-courses']" "css_element" + When I set the field "Search by activity type or name" to "choice 1" + And I wait until "Course 4" "text" does not exist + Then I should see "Test choice 1" in the "Timeline" "block" + And I should see "Course 2" in the ".block-timeline [data-region='view-courses']" "css_element" + And I should not see "Course 1" in the ".block-timeline [data-region='view-courses']" "css_element" + And I should not see "Course 3" in the ".block-timeline [data-region='view-courses']" "css_element" + And I should not see "Course 4" in the ".block-timeline [data-region='view-courses']" "css_element" + And I should not see "Course 5" in the ".block-timeline [data-region='view-courses']" "css_element" + And I should not see "Test choice 2" in the ".block-timeline [data-region='view-courses']" "css_element" + And I should not see "Test choice 3" in the ".block-timeline [data-region='view-courses']" "css_element" + And I should not see "Test choice 7" in the ".block-timeline [data-region='view-courses']" "css_element" + And I should not see "Test choice 8" in the ".block-timeline [data-region='view-courses']" "css_element" diff --git a/blocks/timeline/upgrade.txt b/blocks/timeline/upgrade.txt index 3331950df33..b64f9a5af9f 100644 --- a/blocks/timeline/upgrade.txt +++ b/blocks/timeline/upgrade.txt @@ -1,5 +1,10 @@ This file describes API changes in the timeline block code. +=== 4.0 === +* The timeline block courses view has been updated to only list courses which contain at least one action event within the chosen + filter, so now uses \core_course\external\get_enrolled_courses_with_action_events_by_timeline_classification to fetch courses + instead of core_course_external::get_enrolled_courses_by_timeline_classification, which fetches all courses within the limit. + === 3.7 === * The 'block/timeline:addinstance' capability has been removed. It has never been used in code. \ No newline at end of file diff --git a/course/amd/build/repository.min.js b/course/amd/build/repository.min.js index 9b34369a925..3052e5b377c 100644 --- a/course/amd/build/repository.min.js +++ b/course/amd/build/repository.min.js @@ -1,2 +1,2 @@ -define ("core_course/repository",["exports","core/ajax"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);var c=function(a,c,d,e){var f={classification:a};if("undefined"!=typeof c){f.limit=c}if("undefined"!=typeof d){f.offset=d}if("undefined"!=typeof e){f.sort=e}return b.default.call([{methodname:"core_course_get_enrolled_courses_by_timeline_classification",args:f}])[0]},d=function(a,c,d,e){var f={};if("undefined"!=typeof a){f.userid=a}if("undefined"!=typeof c){f.limit=c}if("undefined"!=typeof d){f.offset=d}if("undefined"!=typeof e){f.sort=e}return b.default.call([{methodname:"core_course_get_recent_courses",args:f}])[0]},e=function(a,c){return b.default.call([{methodname:"core_course_get_enrolled_users_by_cmid",args:{cmid:a,groupid:c}}])[0]},f=function(a,c){return b.default.call([{methodname:"core_completion_update_activity_completion_status_manually",args:{cmid:a,completed:c}}])[0]};a.default={getEnrolledCoursesByTimelineClassification:c,getLastAccessedCourses:d,getUsersFromCourseModuleID:e,toggleManualCompletion:f};return a.default}); +define ("core_course/repository",["exports","core/ajax"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);var c=function(a,c,d,e){var f={classification:a};if("undefined"!=typeof c){f.limit=c}if("undefined"!=typeof d){f.offset=d}if("undefined"!=typeof e){f.sort=e}return b.default.call([{methodname:"core_course_get_enrolled_courses_by_timeline_classification",args:f}])[0]},d=function(a){var c=1.\n\n/**\n * A javascript module to handle course ajax actions.\n *\n * @module core_course/repository\n * @copyright 2018 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\n\n/**\n * Get the list of courses that the logged in user is enrolled in for a given\n * timeline classification.\n *\n * @param {string} classification past, inprogress, or future\n * @param {int} limit Only return this many results\n * @param {int} offset Skip this many results from the start of the result set\n * @param {string} sort Column to sort by and direction, e.g. 'shortname asc'\n * @return {object} jQuery promise resolved with courses.\n */\nconst getEnrolledCoursesByTimelineClassification = (classification, limit, offset, sort) => {\n const args = {\n classification: classification\n };\n\n if (typeof limit !== 'undefined') {\n args.limit = limit;\n }\n\n if (typeof offset !== 'undefined') {\n args.offset = offset;\n }\n\n if (typeof sort !== 'undefined') {\n args.sort = sort;\n }\n\n const request = {\n methodname: 'core_course_get_enrolled_courses_by_timeline_classification',\n args: args\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Get the list of courses that the user has most recently accessed.\n *\n * @method getLastAccessedCourses\n * @param {int} userid User from which the courses will be obtained\n * @param {int} limit Only return this many results\n * @param {int} offset Skip this many results from the start of the result set\n * @param {string} sort Column to sort by and direction, e.g. 'shortname asc'\n * @return {promise} Resolved with an array of courses\n */\nconst getLastAccessedCourses = (userid, limit, offset, sort) => {\n const args = {};\n\n if (typeof userid !== 'undefined') {\n args.userid = userid;\n }\n\n if (typeof limit !== 'undefined') {\n args.limit = limit;\n }\n\n if (typeof offset !== 'undefined') {\n args.offset = offset;\n }\n\n if (typeof sort !== 'undefined') {\n args.sort = sort;\n }\n\n const request = {\n methodname: 'core_course_get_recent_courses',\n args: args\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Get the list of users enrolled in this cmid.\n *\n * @param {Number} cmid Course Module from which the users will be obtained\n * @param {Number} groupID Group ID from which the users will be obtained\n * @returns {Promise} Promise containing a list of users\n */\nconst getEnrolledUsersFromCourseModuleID = (cmid, groupID) => {\n var request = {\n methodname: 'core_course_get_enrolled_users_by_cmid',\n args: {\n cmid: cmid,\n groupid: groupID,\n },\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Toggle the completion state of an activity with manual completion.\n *\n * @param {Number} cmid The course module ID.\n * @param {Boolean} completed Whether to set as complete or not.\n * @returns {object} jQuery promise\n */\nconst toggleManualCompletion = (cmid, completed) => {\n const request = {\n methodname: 'core_completion_update_activity_completion_status_manually',\n args: {\n cmid,\n completed,\n }\n };\n return Ajax.call([request])[0];\n};\n\nexport default {\n getEnrolledCoursesByTimelineClassification,\n getLastAccessedCourses,\n getUsersFromCourseModuleID: getEnrolledUsersFromCourseModuleID,\n toggleManualCompletion,\n};\n"],"file":"repository.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/repository.js"],"names":["getEnrolledCoursesByTimelineClassification","classification","limit","offset","sort","args","Ajax","call","methodname","getEnrolledCoursesWithEventsByTimelineClassification","searchValue","eventsFrom","eventsTo","eventsfrom","eventsto","searchvalue","getLastAccessedCourses","userid","getEnrolledUsersFromCourseModuleID","cmid","groupID","groupid","toggleManualCompletion","completed","getUsersFromCourseModuleID"],"mappings":"sJAuBA,uD,GAYMA,CAAAA,CAA0C,CAAG,SAACC,CAAD,CAAiBC,CAAjB,CAAwBC,CAAxB,CAAgCC,CAAhC,CAAyC,CACxF,GAAMC,CAAAA,CAAI,CAAG,CACTJ,cAAc,CAAEA,CADP,CAAb,CAIA,GAAqB,WAAjB,QAAOC,CAAAA,CAAX,CAAkC,CAC9BG,CAAI,CAACH,KAAL,CAAaA,CAChB,CAED,GAAsB,WAAlB,QAAOC,CAAAA,CAAX,CAAmC,CAC/BE,CAAI,CAACF,MAAL,CAAcA,CACjB,CAED,GAAoB,WAAhB,QAAOC,CAAAA,CAAX,CAAiC,CAC7BC,CAAI,CAACD,IAAL,CAAYA,CACf,CAOD,MAAOE,WAAKC,IAAL,CAAU,CALD,CACZC,UAAU,CAAE,6DADA,CAEZH,IAAI,CAAEA,CAFM,CAKC,CAAV,EAAqB,CAArB,CACV,C,CAeMI,CAAoD,CAAG,SAACR,CAAD,CACK,IADYC,CAAAA,CACZ,wDADoB,CACpB,CADuBC,CACvB,wDADgC,CAChC,CADmCC,CACnC,wDAD0C,IAC1C,CAA3DM,CAA2D,wDAA7C,IAA6C,CAAvCC,CAAuC,wDAA1B,IAA0B,CAApBC,CAAoB,wDAAT,IAAS,CAiB/D,MAAON,WAAKC,IAAL,CAAU,CALD,CACZC,UAAU,CAAE,gFADA,CAEZH,IAAI,CAZK,CACTJ,cAAc,CAAEA,CADP,CAETC,KAAK,CAAEA,CAFE,CAGTC,MAAM,CAAEA,CAHC,CAITC,IAAI,CAAEA,CAJG,CAKTS,UAAU,CAAEF,CALH,CAMTG,QAAQ,CAAEF,CAND,CAOTG,WAAW,CAAEL,CAPJ,CAUG,CAKC,CAAV,EAAqB,CAArB,CACV,C,CAYKM,CAAsB,CAAG,SAACC,CAAD,CAASf,CAAT,CAAgBC,CAAhB,CAAwBC,CAAxB,CAAiC,CAC5D,GAAMC,CAAAA,CAAI,CAAG,EAAb,CAEA,GAAsB,WAAlB,QAAOY,CAAAA,CAAX,CAAmC,CAC/BZ,CAAI,CAACY,MAAL,CAAcA,CACjB,CAED,GAAqB,WAAjB,QAAOf,CAAAA,CAAX,CAAkC,CAC9BG,CAAI,CAACH,KAAL,CAAaA,CAChB,CAED,GAAsB,WAAlB,QAAOC,CAAAA,CAAX,CAAmC,CAC/BE,CAAI,CAACF,MAAL,CAAcA,CACjB,CAED,GAAoB,WAAhB,QAAOC,CAAAA,CAAX,CAAiC,CAC7BC,CAAI,CAACD,IAAL,CAAYA,CACf,CAOD,MAAOE,WAAKC,IAAL,CAAU,CALD,CACZC,UAAU,CAAE,gCADA,CAEZH,IAAI,CAAEA,CAFM,CAKC,CAAV,EAAqB,CAArB,CACV,C,CASKa,CAAkC,CAAG,SAACC,CAAD,CAAOC,CAAP,CAAmB,CAS1D,MAAOd,WAAKC,IAAL,CAAU,CARH,CACVC,UAAU,CAAE,wCADF,CAEVH,IAAI,CAAE,CACFc,IAAI,CAAEA,CADJ,CAEFE,OAAO,CAAED,CAFP,CAFI,CAQG,CAAV,EAAqB,CAArB,CACV,C,CASKE,CAAsB,CAAG,SAACH,CAAD,CAAOI,CAAP,CAAqB,CAQhD,MAAOjB,WAAKC,IAAL,CAAU,CAPD,CACZC,UAAU,CAAE,4DADA,CAEZH,IAAI,CAAE,CACFc,IAAI,CAAJA,CADE,CAEFI,SAAS,CAATA,CAFE,CAFM,CAOC,CAAV,EAAqB,CAArB,CACV,C,WAEc,CACXvB,0CAA0C,CAA1CA,CADW,CAEXgB,sBAAsB,CAAtBA,CAFW,CAGXQ,0BAA0B,CAAEN,CAHjB,CAIXI,sBAAsB,CAAtBA,CAJW,CAKXb,oDAAoD,CAApDA,CALW,C","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 .\n\n/**\n * A javascript module to handle course ajax actions.\n *\n * @module core_course/repository\n * @copyright 2018 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\n\n/**\n * Get the list of courses that the logged in user is enrolled in for a given\n * timeline classification.\n *\n * @param {string} classification past, inprogress, or future\n * @param {int} limit Only return this many results\n * @param {int} offset Skip this many results from the start of the result set\n * @param {string} sort Column to sort by and direction, e.g. 'shortname asc'\n * @return {object} jQuery promise resolved with courses.\n */\nconst getEnrolledCoursesByTimelineClassification = (classification, limit, offset, sort) => {\n const args = {\n classification: classification\n };\n\n if (typeof limit !== 'undefined') {\n args.limit = limit;\n }\n\n if (typeof offset !== 'undefined') {\n args.offset = offset;\n }\n\n if (typeof sort !== 'undefined') {\n args.sort = sort;\n }\n\n const request = {\n methodname: 'core_course_get_enrolled_courses_by_timeline_classification',\n args: args\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Get a list of courses that the logged in user is enrolled in, where they have at least one action event,\n * for a given timeline classification.\n *\n * @param {string} classification past, inprogress, or future\n * @param {int} limit The maximum number of courses to return\n * @param {int} offset Skip this many results from the start of the result set\n * @param {string} sort Column to sort by and direction, e.g. 'shortname asc'\n * @param {string} searchValue Optional text search value\n * @param {int} eventsFrom Optional start timestamp (inclusive) that the course should have event(s) in\n * @param {int} eventsTo Optional end timestamp (inclusive) that the course should have event(s) in\n * @return {object} jQuery promise resolved with courses.\n */\n const getEnrolledCoursesWithEventsByTimelineClassification = (classification, limit = 0, offset = 0, sort = null,\n searchValue = null, eventsFrom = null, eventsTo = null) => {\n\n const args = {\n classification: classification,\n limit: limit,\n offset: offset,\n sort: sort,\n eventsfrom: eventsFrom,\n eventsto: eventsTo,\n searchvalue: searchValue,\n };\n\n const request = {\n methodname: 'core_course_get_enrolled_courses_with_action_events_by_timeline_classification',\n args: args\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Get the list of courses that the user has most recently accessed.\n *\n * @method getLastAccessedCourses\n * @param {int} userid User from which the courses will be obtained\n * @param {int} limit Only return this many results\n * @param {int} offset Skip this many results from the start of the result set\n * @param {string} sort Column to sort by and direction, e.g. 'shortname asc'\n * @return {promise} Resolved with an array of courses\n */\nconst getLastAccessedCourses = (userid, limit, offset, sort) => {\n const args = {};\n\n if (typeof userid !== 'undefined') {\n args.userid = userid;\n }\n\n if (typeof limit !== 'undefined') {\n args.limit = limit;\n }\n\n if (typeof offset !== 'undefined') {\n args.offset = offset;\n }\n\n if (typeof sort !== 'undefined') {\n args.sort = sort;\n }\n\n const request = {\n methodname: 'core_course_get_recent_courses',\n args: args\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Get the list of users enrolled in this cmid.\n *\n * @param {Number} cmid Course Module from which the users will be obtained\n * @param {Number} groupID Group ID from which the users will be obtained\n * @returns {Promise} Promise containing a list of users\n */\nconst getEnrolledUsersFromCourseModuleID = (cmid, groupID) => {\n var request = {\n methodname: 'core_course_get_enrolled_users_by_cmid',\n args: {\n cmid: cmid,\n groupid: groupID,\n },\n };\n\n return Ajax.call([request])[0];\n};\n\n/**\n * Toggle the completion state of an activity with manual completion.\n *\n * @param {Number} cmid The course module ID.\n * @param {Boolean} completed Whether to set as complete or not.\n * @returns {object} jQuery promise\n */\nconst toggleManualCompletion = (cmid, completed) => {\n const request = {\n methodname: 'core_completion_update_activity_completion_status_manually',\n args: {\n cmid,\n completed,\n }\n };\n return Ajax.call([request])[0];\n};\n\nexport default {\n getEnrolledCoursesByTimelineClassification,\n getLastAccessedCourses,\n getUsersFromCourseModuleID: getEnrolledUsersFromCourseModuleID,\n toggleManualCompletion,\n getEnrolledCoursesWithEventsByTimelineClassification,\n};\n"],"file":"repository.min.js"} \ No newline at end of file diff --git a/course/amd/src/repository.js b/course/amd/src/repository.js index 47f37c7fd85..7e08a725a60 100644 --- a/course/amd/src/repository.js +++ b/course/amd/src/repository.js @@ -58,6 +58,40 @@ const getEnrolledCoursesByTimelineClassification = (classification, limit, offse return Ajax.call([request])[0]; }; +/** + * Get a list of courses that the logged in user is enrolled in, where they have at least one action event, + * for a given timeline classification. + * + * @param {string} classification past, inprogress, or future + * @param {int} limit The maximum number of courses to return + * @param {int} offset Skip this many results from the start of the result set + * @param {string} sort Column to sort by and direction, e.g. 'shortname asc' + * @param {string} searchValue Optional text search value + * @param {int} eventsFrom Optional start timestamp (inclusive) that the course should have event(s) in + * @param {int} eventsTo Optional end timestamp (inclusive) that the course should have event(s) in + * @return {object} jQuery promise resolved with courses. + */ + const getEnrolledCoursesWithEventsByTimelineClassification = (classification, limit = 0, offset = 0, sort = null, + searchValue = null, eventsFrom = null, eventsTo = null) => { + + const args = { + classification: classification, + limit: limit, + offset: offset, + sort: sort, + eventsfrom: eventsFrom, + eventsto: eventsTo, + searchvalue: searchValue, + }; + + const request = { + methodname: 'core_course_get_enrolled_courses_with_action_events_by_timeline_classification', + args: args + }; + + return Ajax.call([request])[0]; +}; + /** * Get the list of courses that the user has most recently accessed. * @@ -137,4 +171,5 @@ export default { getLastAccessedCourses, getUsersFromCourseModuleID: getEnrolledUsersFromCourseModuleID, toggleManualCompletion, + getEnrolledCoursesWithEventsByTimelineClassification, }; diff --git a/course/classes/external/get_enrolled_courses_with_action_events_by_timeline_classification.php b/course/classes/external/get_enrolled_courses_with_action_events_by_timeline_classification.php new file mode 100644 index 00000000000..580d8d6b601 --- /dev/null +++ b/course/classes/external/get_enrolled_courses_with_action_events_by_timeline_classification.php @@ -0,0 +1,220 @@ +. + +namespace core_course\external; + +defined('MOODLE_INTERNAL') || die(); + +use context_user; +use core_calendar_external; +use core_course_external; +use external_api; +use external_function_parameters; +use external_multiple_structure; +use external_single_structure; +use external_value; + +require_once("{$CFG->dirroot}/calendar/externallib.php"); +require_once("{$CFG->dirroot}/course/externallib.php"); + +/** + * Class for fetching courses which have action event(s) and match given filter parameters. + * + * @package core_course + * @copyright 2022 Michael Hawkins + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_enrolled_courses_with_action_events_by_timeline_classification extends external_api { + + /** + * Returns the description of method parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters( + [ + 'classification' => new external_value(PARAM_ALPHA, 'future, inprogress, or past'), + 'limit' => new external_value(PARAM_INT, 'Result set limit', VALUE_DEFAULT, 0), + 'offset' => new external_value(PARAM_INT, 'Result set offset', VALUE_DEFAULT, 0), + 'sort' => new external_value(PARAM_TEXT, 'Sort string', VALUE_DEFAULT, null), + 'customfieldname' => new external_value(PARAM_ALPHANUMEXT, 'Used when classification = customfield', + VALUE_DEFAULT, null), + 'customfieldvalue' => new external_value(PARAM_RAW, 'Used when classification = customfield', + VALUE_DEFAULT, null), + 'searchvalue' => new external_value(PARAM_TEXT, 'The value a user wishes to search against', + VALUE_DEFAULT, null), + 'eventsfrom' => new external_value(PARAM_INT, 'Optional starting timestamp for action events', + VALUE_DEFAULT, null), + 'eventsto' => new external_value(PARAM_INT, 'Optional ending timestamp for action events', + VALUE_DEFAULT, null), + ] + ); + } + + /** + * Get courses matching the given timeline classification which have action event(s). + * + * Fetches courses by timeline classification which have at least one action event within the specified filtering. + * + * @param string $classification past, inprogress, or future + * @param int $limit Number of courses with events to attempt to fetch + * @param int $offset Offset the full course set before timeline classification is applied + * @param string $sort SQL sort string for results + * @param string $customfieldname Custom field name used when when classification is customfield + * @param string $customfieldvalue Custom field value used when when classification is customfield + * @param string $searchvalue Text search being applied + * @param int $eventsfrom The start timestamp (inclusive) to search from for action events in the course + * @param int $eventsto The end timestamp (inclusive) to search to for action events in the course + * @return array list of courses and any warnings + */ + public static function execute( + string $classification, + int $limit = 0, + int $offset = 0, + string $sort = null, + string $customfieldname = null, + string $customfieldvalue = null, + string $searchvalue = null, + int $eventsfrom = null, + int $eventsto = null + ): array { + global $USER; + + self::validate_context(context_user::instance($USER->id)); + + $params = self::validate_parameters( + self::execute_parameters(), + [ + 'classification' => $classification, + 'limit' => $limit, + 'offset' => $offset, + 'sort' => $sort, + 'customfieldname' => $customfieldname, + 'customfieldvalue' => $customfieldvalue, + 'searchvalue' => $searchvalue, + 'eventsfrom' => $eventsfrom, + 'eventsto' => $eventsto, + ] + ); + + $classification = $params['classification']; + $limit = $params['limit']; + $offset = $params['offset']; + $sort = $params['sort']; + $customfieldname = $params['customfieldname']; + $customfieldvalue = $params['customfieldvalue']; + $searchvalue = $params['searchvalue']; + $eventsfrom = $params['eventsfrom']; + $eventsto = $params['eventsto']; + $morecoursestofetch = true; + $morecoursespossible = true; + $coursesfinal = []; + + do { + // Fetch courses. + [ + 'courses' => $coursesfetched, + 'nextoffset' => $offset, + ] = core_course_external::get_enrolled_courses_by_timeline_classification($classification, $limit, + $offset, $sort, $customfieldname, $customfieldvalue, $searchvalue); + + $courseids = array_column($coursesfetched, 'id'); + $coursesfetched = array_combine($courseids, $coursesfetched); + + if (!empty($courseids)) { + // Need to check this to know how many are expected (since it is possible for this to be less than the limit). + $numcoursesfetched = count($courseids); + $numfetchedwithevents = 0; + + // If less courses are fetched than we requested, we know it is not possible for more courses to be available. + if ($numcoursesfetched < $limit) { + $morecoursestofetch = false; + $morecoursespossible = false; + } + + // Try to fetch one action event within the time/search parameters for each course. + $events = core_calendar_external::get_calendar_action_events_by_courses($courseids, $eventsfrom, $eventsto, 1, + $searchvalue); + + foreach ($events->groupedbycourse as $courseevents) { + $courseid = $courseevents->courseid; + + // Only include courses which contain at least one event. + if (empty($courseevents->events)) { + unset($coursesfetched[$courseid]); + } else { + $numfetchedwithevents++; + } + } + + // Add courses with events to the final course list in order. + $coursesfinal = array_merge($coursesfinal, $coursesfetched); + + // If any courses did not have events, adjust the limit so we can attempt to fetch as many as are still required. + if ($numfetchedwithevents < $numcoursesfetched) { + $limit -= $numfetchedwithevents; + } else { + // If we have found as many courses as required or are available, no need to attempt fetching more. + $morecoursestofetch = false; + } + } else { + $morecoursestofetch = false; + $morecoursespossible = false; + } + + } while ($morecoursestofetch); + + static $isrecursivecall = false; + $morecoursesavailable = false; + + // Recursively call this method to check if at least one more course is available if we know that is a possibility. + if (!$isrecursivecall && $morecoursespossible) { + // Prevent infinite recursion. + $isrecursivecall = true; + + $additionalcourses = self::execute( + $classification, 1, $offset, $sort, $customfieldname, $customfieldvalue, $searchvalue, $eventsfrom, $eventsto + ); + + if (!empty($additionalcourses['courses'])) { + $morecoursesavailable = true; + } + } + + return [ + 'courses' => $coursesfinal, + 'nextoffset' => $offset, + 'morecoursesavailable' => $morecoursesavailable, + ]; + } + + /** + * Returns description of method result value. + * + * @return external_description + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure( + [ + 'courses' => new external_multiple_structure(course_summary_exporter::get_read_structure(), 'Course'), + 'nextoffset' => new external_value(PARAM_INT, 'Offset for the next request'), + 'morecoursesavailable' => new external_value(PARAM_BOOL, + 'Whether more courses with events exist within the provided parameters'), + ] + ); + } +} diff --git a/course/upgrade.txt b/course/upgrade.txt index 5304cb31005..9f7c4a37496 100644 --- a/course/upgrade.txt +++ b/course/upgrade.txt @@ -77,6 +77,10 @@ course formats don't have their own renderer. * Calendar menu entry is now moved to User menu items, so the following methods will not return the calendar item anymore: - course_get_user_navigation_options - core_course_externalcore_course_external::get_user_navigation_options +* New external \core_course\external\get_enrolled_courses_with_action_events_by_timeline_classification: + - Returns courses the user is enrolled in which contain at least one action event in the supplied time/text filtering parameters. + - Provides a similar response to get_enrolled_courses_by_timeline_classification, but omits courses which have no matching + action events. === 3.11 === * A new callback xxx_coursemodule_definition_after_data that allows plugins to extend activity forms after the data is set. diff --git a/lib/db/services.php b/lib/db/services.php index ee62cce76cc..e59ae638be6 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -648,6 +648,15 @@ $functions = array( 'ajax' => true, 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), + 'core_course_get_enrolled_courses_with_action_events_by_timeline_classification' => array( + 'classname' => '\core_course\external\get_enrolled_courses_with_action_events_by_timeline_classification', + 'methodname' => 'execute', + 'classpath' => '', + 'description' => 'List of enrolled courses with action events in a given timeframe, for the given timeline classification.', + 'type' => 'read', + 'ajax' => true, + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), + ), 'core_course_get_recent_courses' => array( 'classname' => 'core_course_external', 'methodname' => 'get_recent_courses', diff --git a/version.php b/version.php index 37ade91c5ea..6f5e449c944 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2022011400.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2022011400.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '4.0dev+ (Build: 20220114)'; // Human-friendly version name